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 calledDocumentation Index
Fetch the complete documentation index at: https://build.onswig.com/llms.txt
Use this file to discover all available pages before exploring further.
tutorial-3.ts in your examples/tutorial/classic directory for the classic version or examples/tutorial/kit for kit:
touch tutorial/tutorial-3.ts
Example
- Classic
- Kit
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
PublicKey,
Transaction,
sendAndConfirmTransaction,
} from "@solana/web3.js";
import {
Actions,
createEd25519AuthorityInfo,
fetchSwig,
findSwigPda,
getAddAuthorityInstructions,
getCreateSwigInstruction,
getSignInstructions,
} from "@swig-wallet/classic";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
createAssociatedTokenAccount,
createMint,
createTransferInstruction,
getAccount,
mintTo,
} 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 rootAuthorityInfo = createEd25519AuthorityInfo(user.publicKey);
const rootActions = Actions.set().manageAuthority().get();
const createSwigIx = await getCreateSwigInstruction({
payer: user.publicKey,
id,
actions: rootActions,
authorityInfo: rootAuthorityInfo,
});
const transaction = new Transaction().add(createSwigIx);
const signature = await sendAndConfirmTransaction(connection, transaction, [
user,
]);
console.log(
chalk.green("✓ Swig account created at:"),
chalk.cyan(swigAddress.toBase58()),
);
console.log(chalk.blue("Transaction signature:"), chalk.cyan(signature));
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 addAuthorityInstructions = await getAddAuthorityInstructions(
swig,
rootRole.id,
createEd25519AuthorityInfo(newAuthority.publicKey),
actions,
);
const transaction = new Transaction().add(...addAuthorityInstructions);
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()),
);
const airdrop = await connection.requestAirdrop(
rootUser.publicKey,
100 * LAMPORTS_PER_SOL,
);
const 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 {
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`,
),
);
})();
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
generateKeyPairSigner,
getSignatureFromTransaction,
lamports,
pipe,
sendAndConfirmTransactionFactory,
signTransactionMessageWithSigners,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
addSignersToTransactionMessage,
type IInstruction,
type KeyPairSigner,
} from '@solana/kit';
import {
Actions,
createEd25519AuthorityInfo,
fetchSwig,
findSwigPda,
getAddAuthorityInstructions,
getCreateSwigInstruction,
getSignInstructions,
} from '@swig-wallet/kit';
import {
TOKEN_2022_PROGRAM_ADDRESS,
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getInitializeMintInstruction,
getMintSize,
getMintToCheckedInstruction,
getTransferCheckedInstruction,
} from '@solana-program/token-2022';
import { getCreateAccountInstruction } from '@solana-program/system';
import chalk from 'chalk';
import { sleepSync } from 'bun';
function randomBytes(length: number): Uint8Array {
const arr = new Uint8Array(length);
crypto.getRandomValues(arr);
return arr;
}
async function sendTransaction(
instructions: IInstruction[],
payer: KeyPairSigner,
signers: KeyPairSigner[] = []
): Promise<string> {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const txMsg = pipe(
createTransactionMessage({ version: 0 }),
(tx) => setTransactionMessageFeePayerSigner(payer, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => addSignersToTransactionMessage(signers, tx)
);
const signedTx = await signTransactionMessageWithSigners(txMsg);
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTx, {
commitment: 'confirmed',
});
return getSignatureFromTransaction(signedTx).toString();
}
const rpc = createSolanaRpc('http://localhost:8899');
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://localhost:8900');
(async () => {
console.log(chalk.blue('🚀 Starting tutorial - Token Authority and Transfers (Kit Style)'));
const rootUser = await generateKeyPairSigner();
await rpc.requestAirdrop(rootUser.address, lamports(100n * 1_000_000_000n)).send();
sleepSync(2000);
console.log(chalk.green('👤 Root user public key:'), chalk.cyan(rootUser.address.toString()));
const id = randomBytes(32);
const swigAddress = await findSwigPda(id);
const rootAuthorityInfo = createEd25519AuthorityInfo(rootUser.address);
const rootActions = Actions.set().manageAuthority().get();
const createSwigIx = await getCreateSwigInstruction({
payer: rootUser.address,
id,
actions: rootActions,
authorityInfo: rootAuthorityInfo,
});
await sendTransaction([createSwigIx], rootUser);
console.log(chalk.green('✓ Swig account created at:'), chalk.cyan(swigAddress.toString()));
await rpc.requestAirdrop(swigAddress, lamports(1n)).send();
sleepSync(2000);
console.log(chalk.green('✓ Airdropped 1 SOL to Swig account'));
const mint = await generateKeyPairSigner();
const mintAuthority = rootUser;
const decimals = 0;
const rent = await rpc.getMinimumBalanceForRentExemption(BigInt(getMintSize())).send();
const createMintAccountIx = getCreateAccountInstruction({
payer: mintAuthority,
newAccount: mint,
lamports: rent,
space: BigInt(getMintSize()),
programAddress: TOKEN_2022_PROGRAM_ADDRESS,
});
const initMintIx = getInitializeMintInstruction({
mint: mint.address,
decimals,
mintAuthority: mintAuthority.address,
});
const [swigATA] = await findAssociatedTokenPda({
mint: mint.address,
owner: swigAddress,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
});
const createSwigATAIx = await getCreateAssociatedTokenInstructionAsync({
payer: mintAuthority,
mint: mint.address,
owner: swigAddress,
});
const mintToSwigIx = await getMintToCheckedInstruction({
mint: mint.address,
token: swigATA,
mintAuthority,
amount: 10n,
decimals,
});
await sendTransaction(
[createMintAccountIx, initMintIx, createSwigATAIx, mintToSwigIx],
mintAuthority,
[mint]
);
console.log(chalk.green('✓ Token mint and Swig ATA created and funded'));
const recipient = await generateKeyPairSigner();
const [recipientATA] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipient.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS,
});
const createRecipientATAIx = await getCreateAssociatedTokenInstructionAsync({
payer: rootUser,
mint: mint.address,
owner: recipient.address,
});
await sendTransaction([createRecipientATAIx], rootUser);
console.log(chalk.green('✓ Recipient ATA created'));
const tokenAuthority = await generateKeyPairSigner();
await rpc.requestAirdrop(tokenAuthority.address, lamports(1n)).send();
sleepSync(2000);
const tokenActions = Actions.set()
.tokenLimit({ mint: mint.address, amount: 10n })
.get();
const swig = await fetchSwig(rpc, swigAddress);
const rootRole = swig.findRolesByEd25519SignerPk(rootUser.address)[0];
const addAuthorityIxs = await getAddAuthorityInstructions(
swig,
rootRole.id,
createEd25519AuthorityInfo(tokenAuthority.address),
tokenActions
);
await sendTransaction(addAuthorityIxs, rootUser);
console.log(chalk.green('✓ Token authority added'));
//check how much sol the swig has
const swigBalance = await rpc.getBalance(swigAddress).send();
console.log(chalk.green('✓ Swig account balance:'), chalk.cyan(swigBalance.value.toString()));
// Add balance checks for Swig ATA and Recipient ATA
const swigATABalance = await rpc.getTokenAccountBalance(swigATA).send();
console.log(chalk.green('✓ Swig ATA balance:'), chalk.cyan(swigATABalance.value.amount));
const recipientATABalance = await rpc.getTokenAccountBalance(recipientATA).send();
console.log(chalk.green('✓ Recipient ATA balance:'), chalk.cyan(recipientATABalance.value.amount));
// ❌ Second transfer (should fail)
try {
const swigLatest = await fetchSwig(rpc, swigAddress);
const role = swigLatest.findRolesByEd25519SignerPk(tokenAuthority.address)[0];
const transferAgain = getTransferCheckedInstruction({
source: swigATA,
destination: recipientATA,
mint: mint.address,
amount: 10n,
decimals,
authority: swigAddress,
});
const signed = await getSignInstructions(swigLatest, role.id, [transferAgain]);
await sendTransaction(signed, tokenAuthority);
console.error(chalk.red('✗ Second transfer unexpectedly succeeded!'));
} catch {
console.log(chalk.green('✓ Second transfer failed as expected (no allowance left)'));
}
console.log(chalk.green('\n✨ Tutorial completed successfully!'));
console.log(
chalk.yellow('🔍 View your Swig on Explorer:'),
chalk.cyan(
`https://explorer.solana.com/address/${swigAddress}?cluster=custom&customUrl=http%3A%2F%2Flocalhost%3A8899`
)
);
})();
1. Fetch the Swig Account
- Classic
- Kit
const swig = await fetchSwig(connection, swigAddress);
const swig = await fetchSwig(rpc, swigAddress);
2. Find the Appropriate Role
- Classic
- Kit
const tokenRole = swig.findRolesByEd25519SignerPk(tokenAuthority.publicKey)[0];
const role = swig.findRolesByEd25519SignerPk(tokenAuthority.address)[0];
💡 A single authority can have multiple roles. Always make sure you’re using the correct role for your intended action.
3. Create the Instruction
- Classic
- Kit
const transferIx = createTransferInstruction(
swigTokenAccount,
recipientTokenAccount,
swigAddress,
BigInt(10),
);
const transferAgain = getTransferCheckedInstruction({
source: swigATA,
destination: recipientATA,
mint: mint.address,
amount: 10n,
decimals,
authority: swigAddress,
});
4. Sign the Instruction
- Classic
- Kit
const signedTransferInstructions = await getSignInstructions(
swig,
tokenRole.id,
[transferIx],
);
const signed = await getSignInstructions(swigLatest, role.id, [transferAgain]);
getSignInstructions function:
- Verifies that the role has permission to sign this type of instruction
- Creates the necessary signing instructions
- 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
- Classic
- Kit
const transaction = new Transaction().add(...signedTransferInstructions);
await sendAndConfirmTransaction(connection, transaction, [tokenAuthority]);
await sendTransaction(signed, tokenAuthority);
Understanding Signature Verification
When the transaction reaches the Solana network, the Swig program will:- Extract the original instruction
- Verify the authority’s signature
- Check that the role’s permissions allow this action
- 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:- Completed the previous tutorials
- Have a running validator (
bun start-validator) - Created a Swig account with appropriate authorities
bun run ./tutorial/tutorial-3.ts

