Skip to main content
This guide walks through different ways to perform transfers using Swig, progressing from basic to more advanced patterns. You can find all example code in the swig-ts/examples/classic/transfer directory or for kit refer to this directory swig-ts/examples/kit/transfer.
Examples with svm in the file name use LiteSVM for testing. Those without svm in the file name use a local validator. Which you can run with the bun start-validator command.

Basic Transfer with Ed25519 Keys

The simplest way to use Swig is with standard Ed25519 keypairs. Here are the key parts:
  • Classic
  • Kit
// Key Setup - Using standard Solana keypairs
const userRootKeypair = Keypair.generate();
const dappAuthorityKeypair = Keypair.generate();

// Create a new Swig account with root authority
const rootActions = Actions.set().all().get();
const createSwigInstruction = await getCreateSwigInstruction({
  payer: userRootKeypair.publicKey,
  authorityInfo: createEd25519AuthorityInfo(userRootKeypair.publicKey),
  id,
  actions: rootActions,
});

// Add a new authority with a spending limit
const dappActions = Actions.set().solLimit({ amount: BigInt(0.1 * LAMPORTS_PER_SOL) }).get();
const addAuthorityInstructions = await getAddAuthorityInstructions(
  swig,
  rootRole.id,
  createEd25519AuthorityInfo(dappAuthorityKeypair.publicKey),
  dappActions,
);

// Transfer SOL with Swig authority
const transferIx = SystemProgram.transfer({
  fromPubkey: swigAddress,
  toPubkey: dappAuthorityKeypair.publicKey,
  lamports: 0.1 * LAMPORTS_PER_SOL,
});

const signedInstructions = await getSignInstructions(
  swig,
  dappRole.id,
  [transferIx]
);

await sendTransaction(connection, signedInstructions, dappAuthorityKeypair);
You can find the complete example in transfer-local.ts and transfer-svm.ts for local and LiteSVM environments respectively.

Using Secp256k1 Keys (EVM Compatible)

For EVM compatibility, Swig supports Secp256k1 keys. Note the key differences from the Ed25519 example:
  • Classic
  • Kit
// Key Setup - Using Ethereum wallet
const userWallet = Wallet.generate(); // 👈 EVM wallet instead of Solana Keypair

// Connect to network and create payers
const connection = new Connection('http://localhost:8899', 'confirmed');
const payer = Keypair.generate();
await connection.requestAirdrop(payer.publicKey, LAMPORTS_PER_SOL);
const signer = Keypair.generate();
await connection.requestAirdrop(signer.publicKey, LAMPORTS_PER_SOL);

// Find Swig PDA by ID
const id = Uint8Array.from(Array(32).fill(1)); // use any 32-byte identifier
const swigAddress = findSwigPda(id);

// Create Swig with Secp256k1 authority
const rootActions = Actions.set().all().get();
const createSwigInstruction = await getCreateSwigInstruction({
  authorityInfo: createSecp256k1AuthorityInfo(userWallet.getPublicKey()),
  id,
  payer: payer.publicKey,
  actions: rootActions,
});

// Prepare transfer
const transfer = SystemProgram.transfer({
  fromPubkey: swigAddress,
  toPubkey: dappTreasury,
  lamports: 0.1 * LAMPORTS_PER_SOL,
});

// Sign using the Secp256k1 private key
const signingFn = getSigningFnForSecp256k1PrivateKey(
  userWallet.getPrivateKey(),
);
const slot = await connection.getSlot('finalized');

const signedTransfer = await getSignInstructions(
  swig,
  rootRole.id,
  [transfer],
  false,
  {
    currentSlot: BigInt(slot),
    signingFn,
    payer: signer.publicKey,
  },
);
Check out transfer-local-secp.ts and transfer-svm-secp.ts for complete examples.

Using Viem for Ethereum Wallet Integration

For a more complete Ethereum wallet integration using Viem:
// Using Viem account
const privateKeyAccount = privateKeyToAccount(userWallet.getPrivateKeyString());

const swigAddress = findSwigPda(id);
const rootActions = Actions.set().all().get();

const createSwigInstruction = await getCreateSwigInstruction({
  authorityInfo: createSecp256k1AuthorityInfo(privateKeyAccount.publicKey),
  id,
  payer: transactionPayer.publicKey,
  actions: rootActions,
});

sendSVMTransaction(svm, [createSwigInstruction], transactionPayer);

let swig = fetchSwig(svm, swigAddress);
svm.airdrop(swigAddress, BigInt(LAMPORTS_PER_SOL));

let rootRole = swig.findRolesBySecp256k1SignerAddress(privateKeyAccount.address)[0];
if (!rootRole) throw new Error('Role not found for authority');

const viemSign: SigningFn = async (message: Uint8Array) => {
  const sig = await privateKeyAccount.sign({ hash: keccak256(message) });
  return { signature: hexToBytes(sig) };
};

const transfer = SystemProgram.transfer({
  fromPubkey: swigAddress,
  toPubkey: dappTreasury,
  lamports: 0.1 * LAMPORTS_PER_SOL,
});

let signTransfer = await getSignInstructions(swig, rootRole.id, [transfer], false, {
  currentSlot: svm.getClock().slot,
  signingFn: viemSign,
  payer: transactionPayer.publicKey,
});

sendSVMTransaction(svm, signTransfer, transactionPayer);

const viemSignWithPrefix: SigningFn = async (message: Uint8Array) => {
  const prefix = getEvmPersonalSignPrefix(message.length);
  const prefixedMessage = new Uint8Array(prefix.length + message.length);

  prefixedMessage.set(prefix);
  prefixedMessage.set(message, prefix.length);

  const sig = await privateKeyAccount.sign({ hash: keccak256(prefixedMessage) });
  return { signature: hexToBytes(sig), prefix };
};

signTransfer = await getSignInstructions(swig, rootRole.id, [transfer], false, {
  currentSlot: svm.getClock().slot,
  signingFn: viemSignWithPrefix,
  payer: transactionPayer.publicKey,
});

sendSVMTransaction(svm, signTransfer, transactionPayer);

const viemSignMessage: SigningFn = async (message: Uint8Array) => {
  const sig = await privateKeyAccount.signMessage({ message: { raw: message } });
  return {
    signature: hexToBytes(sig),
    prefix: getEvmPersonalSignPrefix(message.length),
  };
};

signTransfer = await getSignInstructions(swig, rootRole.id, [transfer], false, {
  currentSlot: svm.getClock().slot,
  signingFn: viemSignMessage,
  payer: transactionPayer.publicKey,
});

sendSVMTransaction(svm, signTransfer, transactionPayer);
See transfer-svm-viem.ts for the full working code.

Using Sessions for Enhanced Security

Sessions allow limited, temporary authority:

Ed25519 Sessions

  • Classic
  • Kit
// Key Setup
const userRootKeypair = Keypair.generate();
const dappSessionKeypair = Keypair.generate();

// Create Swig with Session Authority
const createSwigIx = await getCreateSwigInstruction({
  id,
  authorityInfo: createEd25519SessionAuthorityInfo(
    userRootKeypair.publicKey,
    100n, // session duration in slots
  ),
  actions: rootActions,
  payer: userRootKeypair.publicKey,
});

// Create Session for dApp
const createSessionIx = await getCreateSessionInstructions(
  swig,
  rootRole.id,
  dappSessionKeypair.publicKey,
  50n, // session duration
);

// Find and use session role
const sessionRole = swig.findRoleBySessionKey(dappSessionKeypair.publicKey);
if (!sessionRole || !sessionRole.isSessionBased()) {
  throw new Error('Invalid session role');
}

// Sign with session key
const signedTransfer = await getSignInstructions(
  swig,
  sessionRole.id,
  [transfer],
  false,
  { payer: dappSessionKeypair.publicKey },
);

Secp256k1 Sessions

  • Classic
  • Kit
// Key Setup - EVM wallet with session capability
const userWallet = Wallet.generate();
const userRootKeypair = Keypair.generate();
const dappSessionKeypair = Keypair.generate();

// Create Swig with Secp256k1 Session Authority
const createSwigInstruction = await getCreateSwigInstruction({
  authorityInfo: createSecp256k1SessionAuthorityInfo(
    userWallet.getPublicKey(),
    100n, // max session duration in slots
  ),
  id,
  payer: userRootKeypair.publicKey,
  actions: rootActions,
});

// Create Session with EVM signing
const signingFn = getSigningFnForSecp256k1PrivateKey(
  userWallet.getPrivateKeyString(),
);
const currentSlot = await connection.getSlot('confirmed');

const sessionInstructions = await getCreateSessionInstructions(
  swig,
  rootRole.id,
  dappSessionKeypair.publicKey,
  50n, // session duration
  {
    currentSlot: BigInt(currentSlot),
    signingFn,
    payer: userRootKeypair.publicKey,
  },
);

// Find and use session role
const sessionRole = swig.findRoleBySessionKey(dappSessionKeypair.publicKey);
if (!sessionRole) throw new Error('Session role not found');

// Sign with session key
const signTransfer = await getSignInstructions(
  swig,
  sessionRole.id,
  [transferIx],
  false,
  {
    currentSlot: BigInt(currentSlot),
    signingFn,
    payer: dappSessionKeypair.publicKey,
  },
);

Key aspects of sessions:

  • Can be created with any supported authority type (Ed25519, Secp256k1)
  • Limited duration specified in slots
  • Restricted set of actions (defined by sessionActions)
  • Perfect for dApp integrations where temporary access is needed
You can find complete session examples in (we have just mentioned a few you can find all of them labelled a specific way but it will contain the word “sessions” in the file name):
  • transfer-local-session.ts (Ed25519)
  • transfer-svm-session.ts (Ed25519)
  • transfer-svm-secp-session.ts (Secp256k1)
For more detailed information about sessions, check out the Creating Sessions Guide.

Testing Environment Options

These examples can be run in different environments: Local Validator: Use the transfer-local-* examples
  • Requires running a local Solana validator
  • Real blockchain environment
  • Good for final testing
LiteSVM: Use the transfer-svm-* examples
  • No validator required
  • Instant transaction confirmation
  • Perfect for rapid development
  • Simulates the Solana runtime
Devnet: All examples work with devnet
  • Change connection URL to https://api.devnet.solana.com
  • Real network environment
  • Free to use with airdropped SOL
LiteSVM is recommended for initial development as it provides the fastest feedback loop. Move to local validator or devnet for more thorough testing.
I