Function Signature
signTransaction(params: {
walletMetadata: WalletMetadata;
transaction: VersionedTransaction | Transaction | string;
password?: string;
externalServerKeyShares?: ServerKeyShare[];
sponsor?: boolean;
}): Promise<string>
Description
Signs an SVM transaction using the wallet identified by the supplied walletMetadata. The SDK is stateless — every call requires walletMetadata. Accepts a Transaction, VersionedTransaction, or hex string. Optionally requests gas sponsorship from Dynamic before signing.
When you pass externalServerKeyShares (the caller-supplied path), walletMetadata.externalServerKeySharesBackupInfo must also be present — signTransaction throws if shares are supplied but backup metadata is missing. The full walletMetadata returned from createWalletAccount / importPrivateKey already includes it; identity-only metadata from fetchWalletMetadata will be rejected.
Parameters
Required Parameters
walletMetadata (WalletMetadata) - Non-sensitive wallet metadata persisted from createWalletAccount() / importPrivateKey(). The signer address comes from walletMetadata.accountAddress.
transaction (VersionedTransaction | Transaction | string) - The transaction to sign
Optional Parameters
password (string) - Required if the wallet was created with backUpToDynamic: true.
externalServerKeyShares (ServerKeyShare[]) - Caller-supplied plaintext shares.
sponsor (boolean) - If true, requests gas sponsorship from Dynamic before signing. Requires a Transaction or VersionedTransaction (not a hex string).
Returns
Promise<string> - A base58-encoded Ed25519 signature — exactly 88 characters, which decodes to 64 bytes.
This is the raw signature only, not the full signed transaction.Verified return format:typeof: string
length: 88 characters
decoded bytes: 64 (Ed25519 signature)
example: "5pkf7k6nDCtoohetqdWoMCJS9Vd..."
Calling VersionedTransaction.deserialize() on the decoded bytes will throw “Reached end of buffer unexpectedly” — confirming the 64 bytes are a signature, not a serialized transaction.To broadcast: decode the signature and add it back to the transaction using decodeBase58 and addSignatureToTransaction from @dynamic-labs-wallet/node-svm, then call connection.sendRawTransaction(signedTx.serialize()). See the example below.Passing the string directly to connection.sendRawTransaction() or the SDK’s sendTransaction() will fail — Buffer.from(base58string) interprets the characters as UTF-8 bytes (not the decoded signature), producing a garbled payload.
Example
import { authenticatedSvmClient } from './client';
import { decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';
import {
Connection, Transaction, SystemProgram, LAMPORTS_PER_SOL, PublicKey,
} from '@solana/web3.js';
const svmClient = await authenticatedSvmClient();
const walletMetadata = JSON.parse(await redis.get(`wallet:${accountAddress}`));
const externalServerKeyShares = await vault.read(`wallet:${accountAddress}/shares`);
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('11111111111111111111111111111112'),
lamports: LAMPORTS_PER_SOL * 0.001,
})
);
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(walletMetadata.accountAddress);
// Returns base58 signature (64 bytes), NOT the full signed transaction
const signatureBase58 = await svmClient.signTransaction({
walletMetadata,
externalServerKeyShares,
transaction,
password: 'user-password',
});
// Decode and attach the signature before broadcasting
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');
console.log('Transaction confirmed:', txid);
Pass sponsor: true to have Dynamic sponsor the transaction fees. Gas sponsorship must be enabled in the Dynamic dashboard.
import { authenticatedSvmClient } from './client';
import { decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';
import {
Connection, Transaction, SystemProgram, LAMPORTS_PER_SOL, PublicKey,
} from '@solana/web3.js';
const svmClient = await authenticatedSvmClient();
const walletMetadata = JSON.parse(await redis.get(`wallet:${accountAddress}`));
const externalServerKeyShares = await vault.read(`wallet:${accountAddress}/shares`);
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('11111111111111111111111111111112'),
lamports: LAMPORTS_PER_SOL * 0.001,
})
);
transaction.recentBlockhash = blockhash;
transaction.feePayer = new PublicKey(walletMetadata.accountAddress);
// sponsor: true replaces the fee payer with the Dynamic sponsor
const signatureBase58 = await svmClient.signTransaction({
walletMetadata,
externalServerKeyShares,
transaction,
password: 'user-password',
sponsor: true,
});
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');
console.log('Sponsored transaction confirmed:', txid);