Now that we have our Swig account set up with proper authorities from the previous tutorials, let’s explore how to sign transactions. If you haven’t completed the previous tutorials, please go back and complete them first as we’ll be building on those concepts.

Create a new file called tutorial-3.ts in your examples/transfer/tutorial directory:

touch tutorial-3.ts

Example

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

import {
  Actions,
  createSwig,
  Ed25519Authority,
  findSwigPda,
  addAuthorityInstruction,
  fetchSwig,
  createEd25519AuthorityInfo,
  signInstruction,
} from "@swig-wallet/classic";

import {
  TOKEN_PROGRAM_ID,
  ASSOCIATED_TOKEN_PROGRAM_ID,
  createMint,
  createAssociatedTokenAccount,
  mintTo,
  getAssociatedTokenAddress,
  getAccount,
  createTransferInstruction,
} from "@solana/spl-token";

import chalk from 'chalk';

async function createSwigAccount(
  connection: Connection,
  user: Keypair,
) {
  try {
    const id = new Uint8Array(32);
    crypto.getRandomValues(id);
    const [swigAddress] = findSwigPda(id);
    const rootAuthority = Ed25519Authority.fromPublicKey(user.publicKey);
    const rootActions = Actions.set().manageAuthority().get();
    const tx = await createSwig(
      connection,
      id,
      rootAuthority,
      rootActions,
      user.publicKey,
      [user]
    );

    console.log(chalk.green("✓ Swig account created at:"), chalk.cyan(swigAddress.toBase58()));
    return swigAddress;
  } catch (error) {
    console.error(chalk.red("✗ Error creating Swig account:"), chalk.red(error));
    throw error;
  }
}

async function addNewAuthority(
  connection: Connection,
  rootUser: Keypair,
  newAuthority: Keypair,
  swigAddress: PublicKey,
  actions: any,
  description: string
) {
  try {
    const swig = await fetchSwig(connection, swigAddress);
    const rootRole = swig.findRolesByEd25519SignerPk(rootUser.publicKey)[0];
    if (!rootRole) {
      throw new Error('Root role not found for authority');
    }

    const addAuthorityIx = await addAuthorityInstruction(
      rootRole,
      rootUser.publicKey,
      createEd25519AuthorityInfo(newAuthority.publicKey),
      actions,
    );

    const transaction = new Transaction().add(addAuthorityIx);
    await sendAndConfirmTransaction(connection, transaction, [rootUser]);
    console.log(chalk.green(`✓ New ${description} authority added:`), chalk.cyan(newAuthority.publicKey.toBase58()));
  } catch (error) {
    console.error(chalk.red(`✗ Error adding ${description} authority:`), chalk.red(error));
    throw error;
  }
}

async function displayTokenBalance(
  connection: Connection,
  tokenAccount: PublicKey,
  label: string
) {
  const account = await getAccount(connection, tokenAccount);
  console.log(chalk.yellow(`${label} token balance:`), chalk.cyan(account.amount.toString()));
}

(async () => {
  console.log(chalk.blue("🚀 Starting tutorial - Token Authority and Transfers"));

  // Connect to local Solana network
  const connection = new Connection('http://localhost:8899', 'confirmed');

  // Create and fund the root user
  const rootUser = Keypair.generate();
  console.log(chalk.green("👤 Root user public key:"), chalk.cyan(rootUser.publicKey.toBase58()));

  let airdrop = await connection.requestAirdrop(rootUser.publicKey, 100 * LAMPORTS_PER_SOL);
  let blockhash = await connection.getLatestBlockhash();
  await connection.confirmTransaction({
    signature: airdrop,
    blockhash: blockhash.blockhash,
    lastValidBlockHeight: blockhash.lastValidBlockHeight,
  });

  // Create the Swig account
  console.log(chalk.yellow("\n📝 Creating Swig account..."));
  const swigAddress = await createSwigAccount(connection, rootUser);

  // Create token mint
  console.log(chalk.yellow("\n💎 Creating token mint..."));
  const mintAuthority = Keypair.generate();
  const tokenMint = await createMint(
    connection,
    rootUser,
    mintAuthority.publicKey,
    null,
    0
  );
  console.log(chalk.green("✓ Token mint created:"), chalk.cyan(tokenMint.toBase58()));

  // Create token accounts
  console.log(chalk.yellow("\n💰 Creating token accounts..."));
  const swigTokenAccount = await createAssociatedTokenAccount(
    connection,
    rootUser,
    tokenMint,
    swigAddress,
    {},
    TOKEN_PROGRAM_ID,
    ASSOCIATED_TOKEN_PROGRAM_ID,
    true
  );
  console.log(chalk.green("✓ Swig token account created:"), chalk.cyan(swigTokenAccount.toBase58()));

  const recipientKeypair = Keypair.generate();
  const recipientTokenAccount = await createAssociatedTokenAccount(
    connection,
    rootUser,
    tokenMint,
    recipientKeypair.publicKey
  );
  console.log(chalk.green("✓ Recipient token account created:"), chalk.cyan(recipientTokenAccount.toBase58()));

  // Mint initial tokens to Swig account
  console.log(chalk.yellow("\n🏦 Minting tokens to Swig account..."));
  await mintTo(
    connection,
    rootUser,
    tokenMint,
    swigTokenAccount,
    mintAuthority,
    10
  );
  await displayTokenBalance(connection, swigTokenAccount, "Swig");
  await displayTokenBalance(connection, recipientTokenAccount, "Recipient");

  // Create token authority with permission to send exactly 10 tokens
  const tokenAuthority = Keypair.generate();
  console.log(chalk.green("\n👥 Token authority public key:"), chalk.cyan(tokenAuthority.publicKey.toBase58()));
  await connection.requestAirdrop(tokenAuthority.publicKey, 100 * LAMPORTS_PER_SOL);
  console.log(chalk.yellow("\n🔑 Adding token authority..."));
  const tokenActions = Actions.set()
    .tokenLimit({ mint: tokenMint, amount: BigInt(10) })
    .get();
  await addNewAuthority(connection, rootUser, tokenAuthority, swigAddress, tokenActions, "token");

  // Add some delay to ensure the authority is added
  await new Promise(resolve => setTimeout(resolve, 2000));

  // First transfer - should succeed
  console.log(chalk.yellow("\n💸 Attempting first transfer of 10 tokens..."));
  try {
    const swig = await fetchSwig(connection, swigAddress);
    const tokenRole = swig.findRolesByEd25519SignerPk(tokenAuthority.publicKey)[0];

    const transferIx = createTransferInstruction(
      swigTokenAccount,
      recipientTokenAccount,
      swigAddress,
      BigInt(10)
    );

    const signedTransfer = await signInstruction(
      tokenRole,
      tokenAuthority.publicKey,
      [transferIx]
    );

    const transaction = new Transaction().add(signedTransfer);
    await sendAndConfirmTransaction(connection, transaction, [tokenAuthority]);
    console.log(chalk.green("✓ First transfer successful!"));
    await displayTokenBalance(connection, swigTokenAccount, "Swig");
    await displayTokenBalance(connection, recipientTokenAccount, "Recipient");
  } catch (error) {
    console.error(chalk.red("✗ First transfer failed:"), chalk.red(error));
  }

  // Second transfer - should fail
  console.log(chalk.yellow("\n💸 Attempting second transfer (should fail)..."));
  try {
    const swig = await fetchSwig(connection, swigAddress);
    const tokenRole = swig.findRolesByEd25519SignerPk(tokenAuthority.publicKey)[0];

    const transferIx = createTransferInstruction(
      swigTokenAccount,
      recipientTokenAccount,
      swigAddress,
      BigInt(10)
    );

    const signedTransfer = await signInstruction(
      tokenRole,
      tokenAuthority.publicKey,
      [transferIx]
    );

    const transaction = new Transaction().add(signedTransfer);
    await sendAndConfirmTransaction(connection, transaction, [tokenAuthority], {
      skipPreflight: true,
    });
    console.error(chalk.red("✗ Second transfer unexpectedly succeeded!"));
  } catch (error) {
    console.log(chalk.green("✓ Second transfer failed as expected:"), "Authority has no remaining token allowance");
  }

  await displayTokenBalance(connection, swigTokenAccount, "Final Swig");
  await displayTokenBalance(connection, recipientTokenAccount, "Final Recipient");

  console.log(chalk.green("\n✨ Tutorial completed successfully!"));
  console.log(chalk.yellow("🔍 Check out your transaction on Solana Explorer:"));
  console.log(chalk.cyan(`https://explorer.solana.com/address/${swigAddress}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`));
})();

Let’s break down how signing works with Swig.

1. Fetch the Swig Account

const swig = await fetchSwig(connection, swigAddress);

First, we need to fetch the current state of our Swig account. This contains all the roles and their permissions that we set up in the previous tutorials.

2. Find the Appropriate Role

const tokenRole = swig.findRolesByEd25519SignerPk(tokenAuthority.publicKey)[0];

We locate the role associated with our token authority. Remember from the previous tutorial that this role was created with specific token transfer limits.

💡 A single authority can have multiple roles. Always make sure you’re using the correct role for your intended action.

3. Create the Instruction

const transferIx = createTransferInstruction(
  swigTokenAccount,
  recipientTokenAccount,
  swigAddress,
  BigInt(10)
);

Create the instruction that needs to be signed. In this case, it’s a token transfer instruction, but Swig can sign any type of instruction as long as the role has the appropriate permissions.

4. Sign the Instruction

const signedTransfer = await signInstruction(
  tokenRole,
  tokenAuthority.publicKey,
  [transferIx]
);

This is where the magic happens. The signInstruction function:

  1. Verifies that the role has permission to sign this type of instruction
  2. Creates a signature using the authority’s credentials
  3. Wraps the original instruction with Swig’s signing logic

🔒 Swig validates both the authority type and action permissions at this stage

5. Build and Send the Transaction

const transaction = new Transaction().add(signedTransfer);
await sendAndConfirmTransaction(connection, transaction, [tokenAuthority]);

Finally, we build the transaction with our signed instruction and send it to the network. Note that the transaction still needs to be signed by the authority’s keypair because they’re the one paying for the transaction fees.

Understanding Signature Verification

When the transaction reaches the Solana network, the Swig program will:

  1. Extract the original instruction
  2. Verify the authority’s signature
  3. Check that the role’s permissions allow this action
  4. Execute the instruction if all checks pass

🧠 This multi-step verification ensures that only authorized actions are performed

Ready to try it?

Make sure you have:

  1. Completed the previous tutorials
  2. Have a running validator (bun start-validator)
  3. Created a Swig account with appropriate authorities

Then run:

bun run ./tutorial-3.ts

The link at the end of the tutorial will take you to the swig on the Solana Explorer. You will see multiple transactions and there should be one failed which shows the negative case.