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 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 {
  return bytesToHex(bs58.decode(solanaAddress)); // 64-char hex string
}

Sign a Message

Sign the raw UTF-8 bytes of the message using the Solana wallet:
async function signNearMessage(message: string): Promise<string> {
  const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "SOL")!;
  const messageBytes = new TextEncoder().encode(message);
  const { signedMessage } = await dynamicClient.wallets.signMessage({
    wallet,
    message: bytesToHex(messageBytes),
  });
  return signedMessage;
}

Sign a Transaction

Build a Borsh-serialized transaction, sign with the Solana wallet, and submit via JSON-RPC:
import { sha256 } from "@noble/hashes/sha2";
import bs58 from "bs58";

async function sendNearTransfer(
  recipient: string,
  amountNear: number,
  nearAddress: string,
): Promise<string> {
  const wallet = dynamicClient.wallets.userWallets.find(w => w.chain === "SOL")!;
  const pubkey = bs58.decode(wallet.address);

  const [wholePart, fracPart = ""] = amountNear.toString().split(".");
  const paddedFrac = fracPart.padEnd(24, "0").slice(0, 24);
  const amountYocto = BigInt(wholePart + paddedFrac);

  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);

  const txBytes = concatBytes(
    borshString(nearAddress),
    borshU8(0),   // PublicKey variant: ed25519
    pubkey,       // 32-byte pubkey
    borshU64(nonce),
    borshString(recipient),
    blockHash,    // 32-byte block hash
    borshU32(1),  // number of actions
    borshU8(3),   // Transfer action variant
    borshU128(amountYocto),
  );

  const txHash = sha256(txBytes);
  const { signedMessage } = await dynamicClient.wallets.signMessage({
    wallet,
    message: bytesToHex(txHash),
  });
  const sigBytes = decodeSig(signedMessage);

  const signedTxBytes = concatBytes(txBytes, borshU8(0), sigBytes);
  const signedTxBase64 = btoa(String.fromCharCode(...signedTxBytes));
  const result = await nearRpc("broadcast_tx_commit", [signedTxBase64]);

  return result.transaction.hash;
}
For the Borsh serialization primitives (borshU8, borshU32, borshU64, borshU128, borshString) and nearRpc helper, see the JavaScript NEAR guide. The only difference in React Native is the signing call.

Verify a Signature

import { ed25519 } from "@noble/curves/ed25519";
import bs58 from "bs58";

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

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).toString().padStart(24, "0").replace(/0+$/, "");
  return frac.length === 0 ? whole.toString() : `${whole}.${frac}`;
}