> ## Documentation Index
> Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Sign SVM Transactions

> Learn how to sign and broadcast Solana transactions using Dynamic's Node SDK

## Overview

This guide shows how to sign Solana transactions using Dynamic's Node SDK and broadcast them to the network.

<Warning>
  **`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](#step-3-broadcast) below.
</Warning>

## Prerequisites

* [Created a Solana wallet](/node/svm/create-wallet)
* Installed `@solana/web3.js`

```bash theme={"system"}
npm install @solana/web3.js
```

## Step 1: Set Up the Client

```typescript theme={"system"}
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

```typescript theme={"system"}
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);
```

<h2 id="step-3-broadcast">
  Step 3: Sign and Broadcast
</h2>

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

### Legacy Transaction

```typescript theme={"system"}
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:

```typescript theme={"system"}
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'
);
```

<Tip>
  **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.
</Tip>

<Warning>
  **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.
</Warning>

## Manual Backup (externalServerKeyShares)

If the wallet was created with `backUpToDynamic: false`, supply key shares explicitly:

```typescript theme={"system"}
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

```typescript theme={"system"}
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

* [Sign Solana messages](/node/svm/sign-messages)
* [Import existing Solana private keys](/node/svm/import-private-keys)
* [Full cross-chain checkout flow](/recipes/integrations/checkouts/checkout-api)
