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:
// 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:
// 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

// 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

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