Skip to main content

Overview

This guide shows how to sign Solana transactions using Dynamic’s Node SDK and broadcast them to the network.
signTransaction returns the raw signature, not the full signed transaction.DynamicSvmWalletClient.signTransaction() returns a base58-encoded Ed25519 signature: exactly 88 characters as a string, which decodes to 64 bytes. It is not a serialized transaction — calling VersionedTransaction.deserialize() on the decoded bytes throws immediately.You must decode the signature and add it back to the original transaction before broadcasting. See Step 3 below.

Prerequisites

npm install @solana/web3.js

Step 1: Set Up the Client

import { DynamicSvmWalletClient } from '@dynamic-labs-wallet/node-svm';

const client = new DynamicSvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);

Step 2: Build the Transaction

import {
  Connection,
  PublicKey,
  Transaction,
  SystemProgram,
  LAMPORTS_PER_SOL,
} from '@solana/web3.js';

const connection = new Connection(
  process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com',
  'confirmed'
);
const { blockhash } = await connection.getLatestBlockhash();

const transaction = new Transaction().add(
  SystemProgram.transfer({
    fromPubkey: new PublicKey(walletMetadata.accountAddress),
    toPubkey:   new PublicKey('RecipientAddress'),
    lamports:   LAMPORTS_PER_SOL * 0.001,
  })
);
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(walletMetadata.accountAddress);

Step 3: Sign and Broadcast

signTransaction returns the 64-byte Ed25519 signature as a base58 string. Add it to the transaction before sending.

Legacy Transaction

import { decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';
import { PublicKey } from '@solana/web3.js';

// 1. Sign — returns base58 signature (88 chars), NOT the full signed tx
const signatureBase58 = await client.signTransaction({
  walletMetadata,
  transaction,
});

// 2. Decode signature from base58 → 64 bytes
const signatureBytes = decodeBase58(signatureBase58);

// 3. Add signature to the transaction
const signedTx = addSignatureToTransaction({
  transaction,
  signature: signatureBytes,
  signerPublicKey: new PublicKey(walletMetadata.accountAddress),
});

// 4. Broadcast
const txid = await connection.sendRawTransaction(signedTx.serialize());
await connection.confirmTransaction(txid, 'confirmed');
console.log('Transaction confirmed:', txid);

VersionedTransaction (e.g. from Checkout API)

If you receive a pre-built VersionedTransaction — for example the base64-encoded serializedTransaction from the Checkout API /prepare endpoint — decode it first:
import { VersionedTransaction } from '@solana/web3.js';
import { decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';

// Decode base64 → VersionedTransaction
const vtx = VersionedTransaction.deserialize(
  Buffer.from(payload.serializedTransaction, 'base64')
);

// Sign — returns base58 signature
const signatureBase58 = await client.signTransaction({
  walletMetadata,
  transaction: vtx,
});

// Decode, add signature, broadcast
const sigBytes = decodeBase58(signatureBase58);
const signedVtx = addSignatureToTransaction({
  transaction: vtx,
  signature: sigBytes,
  signerPublicKey: new PublicKey(walletMetadata.accountAddress),
});

const { lastValidBlockHeight } = await connection.getLatestBlockhash();
const txid = await connection.sendRawTransaction(signedVtx.serialize(), {
  skipPreflight: true, // recommended for pre-built txs with oracle dependencies
});
await connection.confirmTransaction(
  { signature: txid, blockhash: vtx.message.recentBlockhash, lastValidBlockHeight },
  'confirmed'
);
Why skipPreflight: true for VersionedTransaction?Pre-built transactions from swap/bridge protocols may include oracle update instructions that simulation rejects with errors like “PythOracleOutdated” — even though the transaction would succeed on validators. Use skipPreflight: true to bypass local simulation for these cases.
Do not pass signTransaction() output directly to connection.sendRawTransaction() or the SDK’s sendTransaction() export.signTransaction() returns a base58 string. Calling Buffer.from(base58string) interprets it as UTF-8 (not decoded bytes), producing a garbled payload that fails with “failed to deserialize VersionedTransaction”. Always follow the decode → add signature → serialize → sendRawTransaction pattern.

Manual Backup (externalServerKeyShares)

If the wallet was created with backUpToDynamic: false, supply key shares explicitly:
const keyShares = await vault.read(`wallet:${walletMetadata.accountAddress}/shares`);

const signatureBase58 = await client.signTransaction({
  walletMetadata,
  externalServerKeyShares: keyShares,
  transaction,
  password: 'user-password', // only if wallet is password-protected
});

Complete Example: Send SOL

import { DynamicSvmWalletClient, decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';
import {
  Connection, PublicKey, Transaction, SystemProgram, LAMPORTS_PER_SOL,
} from '@solana/web3.js';

async function sendSol({
  walletMetadata,
  toAddress,
  amountSol,
}: {
  walletMetadata: WalletMetadata;
  toAddress: string;
  amountSol: number;
}) {
  const client = new DynamicSvmWalletClient({
    environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
  });
  await client.authenticateApiToken(process.env.DYNAMIC_AUTH_TOKEN!);

  const connection = new Connection(
    process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com',
    'confirmed'
  );
  const { blockhash } = await connection.getLatestBlockhash();

  const transaction = new Transaction().add(
    SystemProgram.transfer({
      fromPubkey: new PublicKey(walletMetadata.accountAddress),
      toPubkey:   new PublicKey(toAddress),
      lamports:   LAMPORTS_PER_SOL * amountSol,
    })
  );
  transaction.recentBlockhash = blockhash;
  transaction.feePayer = new PublicKey(walletMetadata.accountAddress);

  const signatureBase58 = await client.signTransaction({ walletMetadata, transaction });
  const signatureBytes = decodeBase58(signatureBase58);
  const signedTx = addSignatureToTransaction({
    transaction,
    signature: signatureBytes,
    signerPublicKey: new PublicKey(walletMetadata.accountAddress),
  });

  const txid = await connection.sendRawTransaction(signedTx.serialize());
  await connection.confirmTransaction(txid, 'confirmed');
  return txid;
}

Next Steps

Last modified on June 15, 2026