Skip to main content

Overview

NEAR uses the Ed25519 elliptic curve — the same as Solana. NEAR implicit accounts are simply the hex-encoded 32-byte public key. Transactions use Borsh (Binary Object Representation Serializer for Hashing) serialization and are submitted via JSON-RPC.
PropertyValue
CurveEd25519
Root WalletSolana
Address Format64-char hex string (implicit account)
HashingSHA-256
SerializationBorsh
Smallest UnityoctoNEAR (1 NEAR = 10^24 yoctoNEAR)

Dependencies

npm install @noble/curves @noble/hashes bs58

Derive Address

NEAR implicit accounts are the hex-encoded Ed25519 public key — the simplest derivation of all chains:
import bs58 from "bs58";

function deriveNearAddress(solanaAddress: string): string {
  const pubkey = bs58.decode(solanaAddress);
  return bytesToHex(pubkey); // 64-char hex string
}

Sign a Message

Sign the raw UTF-8 bytes of the message using the Solana wallet:
import { signMessage } from "@dynamic-labs-sdk/client";
import type { WalletAccount } from "@dynamic-labs-sdk/client";

async function signNearMessage(
  message: string,
  solWallet: WalletAccount,
): Promise<string> {
  const messageBytes = new TextEncoder().encode(message);
  const result = await signMessage({
    walletAccount: solWallet,
    message: bytesToHex(messageBytes),
  });
  return result.signature;
}

Verify a Signature

Verify the Ed25519 signature against the raw message bytes:
import { ed25519 } from "@noble/curves/ed25519";

function verifyNearSignature(
  message: string,
  signature: string,
  solanaAddress: string,
): boolean {
  const pubkey = bs58.decode(solanaAddress);
  const messageBytes = new TextEncoder().encode(message);
  const sigBytes = decodeSig(signature);
  return ed25519.verify(sigBytes, messageBytes, pubkey);
}

Check Balance

Query the NEAR balance via JSON-RPC:
async function getNearBalance(accountId: string): Promise<string> {
  const res = await fetch("https://rpc.testnet.near.org", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      jsonrpc: "2.0",
      id: "1",
      method: "query",
      params: {
        request_type: "view_account",
        finality: "final",
        account_id: accountId,
      },
    }),
  });

  const data = await res.json();
  if (data.error) return "0";

  const yocto = BigInt(data.result.amount);
  const divisor = BigInt(10) ** BigInt(24);
  const whole = yocto / divisor;
  const frac = yocto % divisor;
  const fracStr = frac.toString().padStart(24, "0").replace(/0+$/, "");
  if (fracStr.length === 0) return whole.toString();
  return `${whole}.${fracStr}`;
}

Send a Transfer

Build a Borsh-serialized transaction, sign with the Solana wallet, and submit via JSON-RPC.

Borsh Serialization Primitives

function borshU8(value: number): Uint8Array {
  return new Uint8Array([value & 0xff]);
}

function borshU32(value: number): Uint8Array {
  const buf = new Uint8Array(4);
  buf[0] = value & 0xff;
  buf[1] = (value >>> 8) & 0xff;
  buf[2] = (value >>> 16) & 0xff;
  buf[3] = (value >>> 24) & 0xff;
  return buf;
}

function borshU64(value: bigint): Uint8Array {
  const buf = new Uint8Array(8);
  for (let i = 0; i < 8; i++) {
    buf[i] = Number((value >> BigInt(i * 8)) & BigInt(0xff));
  }
  return buf;
}

function borshU128(value: bigint): Uint8Array {
  const buf = new Uint8Array(16);
  for (let i = 0; i < 16; i++) {
    buf[i] = Number((value >> BigInt(i * 8)) & BigInt(0xff));
  }
  return buf;
}

function borshString(s: string): Uint8Array {
  const encoded = new TextEncoder().encode(s);
  const len = borshU32(encoded.length);
  return concatBytes(len, encoded);
}

Full Transfer Implementation

import { sha256 } from "@noble/hashes/sha2";
import { signMessage } from "@dynamic-labs-sdk/client";
import bs58 from "bs58";

async function sendNearTransfer(
  recipient: string,
  amountNear: number,
  nearAddress: string,
  solWallet: WalletAccount,
): Promise<string> {
  const pubkey = bs58.decode(solWallet.address);

  // Convert NEAR to yoctoNEAR (1 NEAR = 10^24 yoctoNEAR)
  const [wholePart, fracPart = ""] = amountNear.toString().split(".");
  const paddedFrac = fracPart.padEnd(24, "0").slice(0, 24);
  const amountYocto = BigInt(wholePart + paddedFrac);

  // Fetch access key and recent block hash
  const [accessKeyResult, blockResult] = await Promise.all([
    nearRpc("query", {
      request_type: "view_access_key",
      finality: "final",
      account_id: nearAddress,
      public_key: `ed25519:${bs58.encode(pubkey)}`,
    }),
    nearRpc("block", { finality: "final" }),
  ]);

  const nonce = BigInt(accessKeyResult.nonce) + BigInt(1);
  const blockHash = bs58.decode(blockResult.header.hash);

  // Build transfer action: variant 3 (Transfer) + u128 amount
  const transferAction = concatBytes(
    borshU8(3), // Transfer action variant
    borshU128(amountYocto),
  );

  // Serialize the transaction
  const txBytes = concatBytes(
    borshString(nearAddress),       // signer_id
    borshU8(0),                     // PublicKey variant: ed25519
    pubkey,                         // 32-byte pubkey
    borshU64(nonce),
    borshString(recipient),         // receiver_id
    blockHash,                      // 32-byte block hash
    borshU32(1),                    // number of actions
    transferAction,
  );

  // Hash and sign
  const txHash = sha256(txBytes);
  const signResult = await signMessage({
    walletAccount: solWallet,
    message: bytesToHex(txHash),
  });
  const sigBytes = decodeSig(signResult.signature);

  // Build signed transaction
  const signedTxBytes = concatBytes(
    txBytes,
    borshU8(0),   // Signature variant: ed25519
    sigBytes,     // 64-byte signature
  );

  // Base64 encode and submit
  const signedTxBase64 = btoa(String.fromCharCode(...signedTxBytes));
  const result = await nearRpc("broadcast_tx_commit", [signedTxBase64]);

  return result.transaction.hash;
}