diff --git a/.gitmodules b/.gitmodules index ce4eb81..0cad343 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "programs/anchor/cp-swap-reference"] path = programs/anchor/cp-swap-reference url = https://github.com/Lightprotocol/cp-swap-reference.git +[submodule "vendor/smart-account-program"] + path = vendor/smart-account-program + url = https://github.com/klausundklaus/smart-account-program.git diff --git a/package.json b/package.json index 5dd0512..267ccd5 100644 --- a/package.json +++ b/package.json @@ -7,10 +7,13 @@ "toolkits/sign-with-privy/react", "toolkits/sign-with-privy/nodejs", "toolkits/sign-with-privy/scripts", - "toolkits/sponsor-rent-top-ups/typescript" + "toolkits/sponsor-rent-top-ups/typescript", + "toolkits/squads-smart-wallet", + "vendor/smart-account-program/sdk/smart-account" ], "scripts": { - "toolkit:payments": "npm run -w toolkits/payments-and-wallets" + "toolkit:payments": "npm run -w toolkits/payments-and-wallets", + "toolkit:squads": "npm run -w toolkits/squads-smart-wallet" }, "dependencies": { "@lightprotocol/compressed-token": "beta", diff --git a/toolkits/squads-smart-wallet/fund-wallet.ts b/toolkits/squads-smart-wallet/fund-wallet.ts new file mode 100644 index 0000000..31f98da --- /dev/null +++ b/toolkits/squads-smart-wallet/fund-wallet.ts @@ -0,0 +1,93 @@ +import "dotenv/config"; +import { + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + createRpc, + buildAndSignTx, + sendAndConfirmTx, +} from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + getAssociatedTokenAddressInterface, + mintToInterface, + createLightTokenTransferInstruction, +} from "@lightprotocol/compressed-token"; +import * as smartAccount from "@sqds/smart-account"; +import { homedir } from "os"; +import { readFileSync } from "fs"; + +const RPC_URL = process.env.RPC_URL || "http://127.0.0.1:8899"; +const rpc = createRpc(RPC_URL); + +const payer = Keypair.fromSecretKey( + new Uint8Array( + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) + ) +); + +(async function () { + // 1. Create Light Token mint and mint tokens to payer + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const payerAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, payerAta, payer, 1_000_000); + + // 2. Create a 1-of-1 smart account + const programConfig = + await smartAccount.accounts.ProgramConfig.fromAccountAddress( + rpc, + smartAccount.getProgramConfigPda({})[0] + ); + const accountIndex = + BigInt(programConfig.smartAccountIndex.toString()) + 1n; + const [settingsPda] = smartAccount.getSettingsPda({ accountIndex }); + const [walletPda] = smartAccount.getSmartAccountPda({ + settingsPda, + accountIndex: 0, + }); + + const createSig = await smartAccount.rpc.createSmartAccount({ + connection: rpc, + treasury: programConfig.treasury, + creator: payer, + settings: settingsPda, + settingsAuthority: null, + threshold: 1, + signers: [ + { + key: payer.publicKey, + permissions: smartAccount.types.Permissions.all(), + }, + ], + timeLock: 0, + rentCollector: null, + sendOptions: { skipPreflight: true }, + }); + await rpc.confirmTransaction(createSig, "confirmed"); + + // 3. Create Light Token ATA for the wallet (off-curve PDA) + await createAtaInterface(rpc, payer, mint, walletPda, true); + const walletAta = getAssociatedTokenAddressInterface(mint, walletPda, true); + + // 4. Transfer Light Tokens to the wallet — no approval needed + const ix = createLightTokenTransferInstruction( + payerAta, + walletAta, + payer.publicKey, + 500_000 + ); + const { blockhash } = await rpc.getLatestBlockhash(); + const tx = buildAndSignTx([ix], payer, blockhash, []); + const sig = await sendAndConfirmTx(rpc, tx); + + console.log("Settings PDA:", settingsPda.toBase58()); + console.log("Wallet PDA:", walletPda.toBase58()); + console.log("Wallet ATA:", walletAta.toBase58()); + console.log("Fund tx:", sig); +})(); diff --git a/toolkits/squads-smart-wallet/guide.md b/toolkits/squads-smart-wallet/guide.md new file mode 100644 index 0000000..285df59 --- /dev/null +++ b/toolkits/squads-smart-wallet/guide.md @@ -0,0 +1,221 @@ +# Squads Smart Account + Light Token Integration + +This toolkit demonstrates how to use rent-free Light Tokens with Squads Smart Accounts — programmable wallets with configurable access control on Solana. + +## Overview + +A Squads Smart Account is a wallet (PDA) with rules: who can sign, at what threshold, with what time lock. Light Tokens are real Solana ATAs with protocol-sponsored rent. Combined, you get a programmable wallet that holds rent-free tokens. + +Two execution modes: + +- **Sync** — Immediate execution in a single transaction. Requires all signers present and `timeLock=0`. No proposal overhead. +- **Async** — Full proposal lifecycle: create → propose → approve → execute. For multi-party governance. + +## Smart Account Setup + +```typescript +import * as smartAccount from "@sqds/smart-account"; + +// Read ProgramConfig to get the next available account index +const programConfig = + await smartAccount.accounts.ProgramConfig.fromAccountAddress( + rpc, + smartAccount.getProgramConfigPda({})[0] + ); +const accountIndex = + BigInt(programConfig.smartAccountIndex.toString()) + 1n; + +// Derive PDAs +const [settingsPda] = smartAccount.getSettingsPda({ accountIndex }); +const [walletPda] = smartAccount.getSmartAccountPda({ + settingsPda, + accountIndex: 0, +}); + +// Create 1-of-1 smart account (timeLock=0 enables sync execution) +await smartAccount.rpc.createSmartAccount({ + connection: rpc, + treasury: programConfig.treasury, + creator: payer, + settings: settingsPda, + settingsAuthority: null, + threshold: 1, + signers: [ + { key: payer.publicKey, permissions: smartAccount.types.Permissions.all() }, + ], + timeLock: 0, + rentCollector: null, +}); +``` + +For multi-party governance, add more signers and increase the threshold: + +```typescript +const { Permission, Permissions } = smartAccount.types; + +signers: [ + { key: admin.publicKey, permissions: Permissions.all() }, + { key: signer2.publicKey, permissions: Permissions.fromPermissions([Permission.Vote]) }, + { key: signer3.publicKey, permissions: Permissions.fromPermissions([Permission.Vote]) }, +], +threshold: 2, +``` + +## Off-Curve PDA Transfers + +The wallet PDA is off-curve (not a valid keypair). The high-level SDK functions `transferInterface()` and `createTransferInterfaceInstructions()` reject off-curve addresses. + +Use `createLightTokenTransferInstruction()` instead — it accepts any `PublicKey`: + +```typescript +import { createLightTokenTransferInstruction } from "@lightprotocol/compressed-token"; + +const ix = createLightTokenTransferInstruction( + sourceAta, // source Light Token ATA + destAta, // destination Light Token ATA + ownerPubkey, // owner of source ATA (can be off-curve PDA) + amount, + feePayer // optional, defaults to owner +); +``` + +## Fund the Smart Wallet + +Anyone can send Light Tokens to a smart wallet — no approval needed. + +```typescript +// Create wallet's Light Token ATA (allowOwnerOffCurve=true for PDAs) +await createAtaInterface(rpc, payer, mint, walletPda, true); +const walletAta = getAssociatedTokenAddressInterface(mint, walletPda, true); + +// Transfer +const ix = createLightTokenTransferInstruction( + payerAta, walletAta, payer.publicKey, amount +); +const { blockhash } = await rpc.getLatestBlockhash(); +const tx = buildAndSignTx([ix], payer, blockhash, []); +await sendAndConfirmTx(rpc, tx); +``` + +See `fund-wallet.ts` for a complete example. + +## Smart Wallet Sends — Sync Execution + +Single transaction, immediate execution. The smart account program executes the inner instruction via CPI, signing with the wallet PDA's seeds. + +```typescript +// Build Light Token transfer instruction +const transferIx = createLightTokenTransferInstruction( + walletAta, recipientAta, walletPda, amount, walletPda +); + +// Compile for synchronous execution +const { instructions, accounts } = + smartAccount.utils.instructionsToSynchronousTransactionDetails({ + vaultPda: walletPda, + members: [payer.publicKey], + transaction_instructions: [transferIx], + }); + +// Build sync execution instruction +const syncIx = smartAccount.instructions.executeTransactionSync({ + settingsPda, + numSigners: 1, + accountIndex: 0, + instructions, + instruction_accounts: accounts, +}); + +// Send as a single transaction +const msg = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: blockhash, + instructions: [syncIx], +}).compileToV0Message(); +const tx = new VersionedTransaction(msg); +tx.sign([payer]); +await rpc.sendRawTransaction(tx.serialize()); +``` + +See `wallet-send-sync.ts` for a complete example. + +## Smart Wallet Sends — Async Proposal Flow + +Multi-step governance flow. Each step must be confirmed before the next. + +```typescript +// Read current transaction index +const settings = await smartAccount.accounts.Settings.fromAccountAddress( + rpc, settingsPda +); +const txIndex = BigInt(settings.transactionIndex.toString()) + 1n; + +// 1. Create transaction +await smartAccount.rpc.createTransaction({ + connection: rpc, feePayer: payer, settingsPda, + transactionIndex: txIndex, creator: payer.publicKey, + accountIndex: 0, ephemeralSigners: 0, + transactionMessage: new TransactionMessage({ + payerKey: walletPda, + recentBlockhash: blockhash, + instructions: [transferIx], + }), +}); + +// 2. Create proposal +await smartAccount.rpc.createProposal({ + connection: rpc, feePayer: payer, settingsPda, + transactionIndex: txIndex, creator: payer, +}); + +// 3. Approve (repeat for each signer up to threshold) +await smartAccount.rpc.approveProposal({ + connection: rpc, feePayer: payer, settingsPda, + transactionIndex: txIndex, signer: payer, +}); + +// 4. Execute +await smartAccount.rpc.executeTransaction({ + connection: rpc, feePayer: payer, settingsPda, + transactionIndex: txIndex, signer: payer.publicKey, + signers: [payer], +}); +``` + +See `wallet-send-async.ts` for a complete example. + +## Running the Examples + +```bash +npm install + +# Set RPC endpoint (defaults to localhost) +export RPC_URL="https://devnet.helius-rpc.com?api-key=YOUR_KEY" + +# Fund a smart wallet with Light Tokens +npx tsx fund-wallet.ts + +# Smart wallet sends LTs (sync — single transaction) +npx tsx wallet-send-sync.ts + +# Smart wallet sends LTs (async — proposal flow) +npx tsx wallet-send-async.ts + +# Run full integration test (all 3 flows) +npx tsx squads-light-token.test.ts +``` + +## Dependencies + +- `@lightprotocol/compressed-token` — Light Token SDK +- `@lightprotocol/stateless.js` — Light Protocol RPC client +- `@sqds/smart-account` — Squads Smart Account SDK +- `@solana/web3.js` — Solana web3 (peer dependency) + +## Program IDs + +| Program | ID | +|---------|-----| +| Squads Smart Account | `SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG` | +| Light Compressed Token | `cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m` | +| Light System Program | `SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7` | diff --git a/toolkits/squads-smart-wallet/package.json b/toolkits/squads-smart-wallet/package.json new file mode 100644 index 0000000..bdd8b83 --- /dev/null +++ b/toolkits/squads-smart-wallet/package.json @@ -0,0 +1,17 @@ +{ + "name": "light-token-toolkit-squads", + "version": "1.0.0", + "description": "Light Token Squads Smart Wallet Integration", + "type": "module", + "scripts": { + "fund-wallet": "tsx fund-wallet.ts", + "wallet-send-sync": "tsx wallet-send-sync.ts", + "wallet-send-async": "tsx wallet-send-async.ts", + "test": "tsx squads-light-token.test.ts" + }, + "dependencies": { + "@lightprotocol/compressed-token": "^0.23.0-beta.9", + "@lightprotocol/stateless.js": "^0.23.0-beta.9", + "@sqds/smart-account": "*" + } +} diff --git a/toolkits/squads-smart-wallet/squads-light-token.test.ts b/toolkits/squads-smart-wallet/squads-light-token.test.ts new file mode 100644 index 0000000..2170dc7 --- /dev/null +++ b/toolkits/squads-smart-wallet/squads-light-token.test.ts @@ -0,0 +1,295 @@ +import "dotenv/config"; +import { + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + createRpc, + buildAndSignTx, + sendAndConfirmTx, +} from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + getAssociatedTokenAddressInterface, + mintToInterface, + createLightTokenTransferInstruction, +} from "@lightprotocol/compressed-token"; +import * as smartAccount from "@sqds/smart-account"; +import { homedir } from "os"; +import { readFileSync } from "fs"; + +const { Permission, Permissions } = smartAccount.types; + +const RPC_URL = process.env.RPC_URL || "http://127.0.0.1:8899"; +const rpc = createRpc(RPC_URL); + +const payer = Keypair.fromSecretKey( + new Uint8Array( + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) + ) +); + +function assert(condition: boolean, message: string) { + if (!condition) throw new Error(`FAIL: ${message}`); + console.log(`PASS: ${message}`); +} + +async function accountExists(pubkey: PublicKey): Promise { + const info = await rpc.getAccountInfo(pubkey); + return info !== null && info.value !== null; +} + +async function confirmTx(sig: string) { + await rpc.confirmTransaction(sig, "confirmed"); +} + +(async function () { + console.log("=== Squads Smart Account + Light Token Integration Test ===\n"); + + // ── Setup: Create Light Token mint and fund payer ── + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + console.log("Mint:", mint.toBase58()); + + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const payerAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, payerAta, payer, 1_000_000); + console.log("Payer ATA funded with 1,000,000 tokens\n"); + + // ── Step 1: Create Smart Account (1-of-1, timeLock=0) ── + console.log("--- Step 1: Create smart account ---"); + + const programConfigPda = smartAccount.getProgramConfigPda({})[0]; + const programConfig = + await smartAccount.accounts.ProgramConfig.fromAccountAddress( + rpc, + programConfigPda + ); + const accountIndex = + BigInt(programConfig.smartAccountIndex.toString()) + 1n; + + const [settingsPda] = smartAccount.getSettingsPda({ accountIndex }); + const [walletPda] = smartAccount.getSmartAccountPda({ + settingsPda, + accountIndex: 0, + }); + + const createSig = await smartAccount.rpc.createSmartAccount({ + connection: rpc, + treasury: programConfig.treasury, + creator: payer, + settings: settingsPda, + settingsAuthority: null, + threshold: 1, + signers: [ + { key: payer.publicKey, permissions: Permissions.all() }, + ], + timeLock: 0, + rentCollector: null, + sendOptions: { skipPreflight: true }, + }); + await confirmTx(createSig); + + console.log("Settings PDA:", settingsPda.toBase58()); + console.log("Wallet PDA:", walletPda.toBase58()); + assert(await accountExists(settingsPda), "Smart account created"); + + // ── Step 2: Fund smart wallet with Light Tokens ── + console.log("\n--- Step 2: Fund smart wallet with Light Tokens ---"); + + // Create Light Token ATA for the wallet PDA (off-curve, so allowOwnerOffCurve=true) + await createAtaInterface(rpc, payer, mint, walletPda, true); + const walletAta = getAssociatedTokenAddressInterface(mint, walletPda, true); + console.log("Wallet ATA:", walletAta.toBase58()); + + // Transfer LTs from payer to wallet (no approval needed — anyone can fund) + const fundIx = createLightTokenTransferInstruction( + payerAta, + walletAta, + payer.publicKey, + 500_000 + ); + const { blockhash: bh1 } = await rpc.getLatestBlockhash(); + const fundTx = buildAndSignTx([fundIx], payer, bh1, []); + const fundSig = await sendAndConfirmTx(rpc, fundTx); + console.log("Fund tx:", fundSig); + assert(await accountExists(walletAta), "Wallet ATA exists on-chain"); + + // Fund wallet with SOL for inner transaction fees + const solFundIx = SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: walletPda, + lamports: 10_000_000, + }); + const { blockhash: bh2 } = await rpc.getLatestBlockhash(); + const solFundMsg = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: bh2, + instructions: [solFundIx], + }).compileToV0Message(); + const solFundTx = new VersionedTransaction(solFundMsg); + solFundTx.sign([payer]); + const solFundSig = await rpc.sendRawTransaction(solFundTx.serialize()); + await confirmTx(solFundSig); + console.log("Wallet funded with 0.01 SOL"); + + // ── Step 3: Smart wallet sends LTs — sync execution ── + console.log("\n--- Step 3: Smart wallet sends LTs (sync) ---"); + + const recipientA = Keypair.generate(); + await createAtaInterface(rpc, payer, mint, recipientA.publicKey); + const recipientAtaA = getAssociatedTokenAddressInterface( + mint, + recipientA.publicKey + ); + + // Build Light Token transfer instruction (wallet → recipient A) + const transferSyncIx = createLightTokenTransferInstruction( + walletAta, + recipientAtaA, + walletPda, // owner of source ATA + 100_000, + walletPda // fee payer = wallet PDA + ); + + // Compile for synchronous execution + const { instructions: syncInstructions, accounts: syncAccounts } = + smartAccount.utils.instructionsToSynchronousTransactionDetails({ + vaultPda: walletPda, + members: [payer.publicKey], + transaction_instructions: [transferSyncIx], + }); + + // Build the sync execution instruction + const syncExecIx = smartAccount.instructions.executeTransactionSync({ + settingsPda, + numSigners: 1, + accountIndex: 0, + instructions: syncInstructions, + instruction_accounts: syncAccounts, + }); + + // Send as a single transaction — immediate execution, no proposal needed + const { blockhash: bh3 } = await rpc.getLatestBlockhash(); + const syncMsg = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: bh3, + instructions: [syncExecIx], + }).compileToV0Message(); + const syncTx = new VersionedTransaction(syncMsg); + syncTx.sign([payer]); + const syncSig = await rpc.sendRawTransaction(syncTx.serialize(), { + skipPreflight: true, + }); + await confirmTx(syncSig); + console.log("Sync transfer tx:", syncSig); + assert( + await accountExists(recipientAtaA), + "Recipient A ATA exists (sync transfer)" + ); + + // ── Step 4: Smart wallet sends LTs — async proposal flow ── + console.log("\n--- Step 4: Smart wallet sends LTs (async) ---"); + + const recipientB = Keypair.generate(); + await createAtaInterface(rpc, payer, mint, recipientB.publicKey); + const recipientAtaB = getAssociatedTokenAddressInterface( + mint, + recipientB.publicKey + ); + + // Build Light Token transfer instruction (wallet → recipient B) + const transferAsyncIx = createLightTokenTransferInstruction( + walletAta, + recipientAtaB, + walletPda, + 100_000, + walletPda + ); + + // Read current transaction index + const settings = await smartAccount.accounts.Settings.fromAccountAddress( + rpc, + settingsPda + ); + const txIndex = BigInt(settings.transactionIndex.toString()) + 1n; + + // 4a. Create transaction + const { blockhash: bh4 } = await rpc.getLatestBlockhash(); + const createTxSig = await smartAccount.rpc.createTransaction({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + creator: payer.publicKey, + accountIndex: 0, + ephemeralSigners: 0, + transactionMessage: new TransactionMessage({ + payerKey: walletPda, + recentBlockhash: bh4, + instructions: [transferAsyncIx], + }), + sendOptions: { skipPreflight: true }, + }); + await confirmTx(createTxSig); + console.log("Transaction created:", createTxSig); + + // 4b. Create proposal + const proposalSig = await smartAccount.rpc.createProposal({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + creator: payer, + sendOptions: { skipPreflight: true }, + }); + await confirmTx(proposalSig); + console.log("Proposal created:", proposalSig); + + // 4c. Approve proposal + const approveSig = await smartAccount.rpc.approveProposal({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + signer: payer, + sendOptions: { skipPreflight: true }, + }); + await confirmTx(approveSig); + console.log("Proposal approved:", approveSig); + + // 4d. Execute transaction + const executeSig = await smartAccount.rpc.executeTransaction({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + signer: payer.publicKey, + signers: [payer], + sendOptions: { skipPreflight: true }, + }); + await confirmTx(executeSig); + console.log("Transaction executed:", executeSig); + + assert( + await accountExists(recipientAtaB), + "Recipient B ATA exists (async transfer)" + ); + + // ── Step 5: Verify ── + console.log("\n--- Step 5: Verify ---"); + assert(await accountExists(walletAta), "Wallet ATA still exists"); + assert( + await accountExists(recipientAtaA), + "Recipient A received tokens (sync)" + ); + assert( + await accountExists(recipientAtaB), + "Recipient B received tokens (async)" + ); + + console.log("\n=== All tests passed ==="); +})(); diff --git a/toolkits/squads-smart-wallet/tsconfig.json b/toolkits/squads-smart-wallet/tsconfig.json new file mode 100644 index 0000000..de8ab73 --- /dev/null +++ b/toolkits/squads-smart-wallet/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "./dist" + }, + "include": ["**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/toolkits/squads-smart-wallet/wallet-send-async.ts b/toolkits/squads-smart-wallet/wallet-send-async.ts new file mode 100644 index 0000000..969c04a --- /dev/null +++ b/toolkits/squads-smart-wallet/wallet-send-async.ts @@ -0,0 +1,188 @@ +import "dotenv/config"; +import { + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + createRpc, + buildAndSignTx, + sendAndConfirmTx, +} from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + getAssociatedTokenAddressInterface, + mintToInterface, + createLightTokenTransferInstruction, +} from "@lightprotocol/compressed-token"; +import * as smartAccount from "@sqds/smart-account"; +import { homedir } from "os"; +import { readFileSync } from "fs"; + +const RPC_URL = process.env.RPC_URL || "http://127.0.0.1:8899"; +const rpc = createRpc(RPC_URL); + +const payer = Keypair.fromSecretKey( + new Uint8Array( + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) + ) +); + +async function confirmTx(sig: string) { + await rpc.confirmTransaction(sig, "confirmed"); +} + +(async function () { + // 1. Setup: Create mint, fund payer, create smart account, fund wallet + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const payerAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, payerAta, payer, 1_000_000); + + const programConfig = + await smartAccount.accounts.ProgramConfig.fromAccountAddress( + rpc, + smartAccount.getProgramConfigPda({})[0] + ); + const accountIndex = + BigInt(programConfig.smartAccountIndex.toString()) + 1n; + const [settingsPda] = smartAccount.getSettingsPda({ accountIndex }); + const [walletPda] = smartAccount.getSmartAccountPda({ + settingsPda, + accountIndex: 0, + }); + + const createSig = await smartAccount.rpc.createSmartAccount({ + connection: rpc, + treasury: programConfig.treasury, + creator: payer, + settings: settingsPda, + settingsAuthority: null, + threshold: 1, + signers: [ + { + key: payer.publicKey, + permissions: smartAccount.types.Permissions.all(), + }, + ], + timeLock: 0, + rentCollector: null, + sendOptions: { skipPreflight: true }, + }); + await confirmTx(createSig); + + // Fund wallet with LTs + await createAtaInterface(rpc, payer, mint, walletPda, true); + const walletAta = getAssociatedTokenAddressInterface(mint, walletPda, true); + const fundIx = createLightTokenTransferInstruction( + payerAta, + walletAta, + payer.publicKey, + 500_000 + ); + const { blockhash: bh1 } = await rpc.getLatestBlockhash(); + await sendAndConfirmTx(rpc, buildAndSignTx([fundIx], payer, bh1, [])); + + // Fund wallet with SOL + const { blockhash: bh2 } = await rpc.getLatestBlockhash(); + const solMsg = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: bh2, + instructions: [ + SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: walletPda, + lamports: 10_000_000, + }), + ], + }).compileToV0Message(); + const solTx = new VersionedTransaction(solMsg); + solTx.sign([payer]); + await confirmTx(await rpc.sendRawTransaction(solTx.serialize())); + + // 2. Smart wallet sends LTs via async proposal flow + const recipient = Keypair.generate(); + await createAtaInterface(rpc, payer, mint, recipient.publicKey); + const recipientAta = getAssociatedTokenAddressInterface( + mint, + recipient.publicKey + ); + + const transferIx = createLightTokenTransferInstruction( + walletAta, + recipientAta, + walletPda, + 100_000, + walletPda + ); + + // Read current transaction index + const settings = await smartAccount.accounts.Settings.fromAccountAddress( + rpc, + settingsPda + ); + const txIndex = BigInt(settings.transactionIndex.toString()) + 1n; + + // Create transaction + const { blockhash: bh3 } = await rpc.getLatestBlockhash(); + const createTxSig = await smartAccount.rpc.createTransaction({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + creator: payer.publicKey, + accountIndex: 0, + ephemeralSigners: 0, + transactionMessage: new TransactionMessage({ + payerKey: walletPda, + recentBlockhash: bh3, + instructions: [transferIx], + }), + sendOptions: { skipPreflight: true }, + }); + await confirmTx(createTxSig); + console.log("Transaction created"); + + // Create proposal + const proposalSig = await smartAccount.rpc.createProposal({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + creator: payer, + sendOptions: { skipPreflight: true }, + }); + await confirmTx(proposalSig); + console.log("Proposal created"); + + // Approve proposal + const approveSig = await smartAccount.rpc.approveProposal({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + signer: payer, + sendOptions: { skipPreflight: true }, + }); + await confirmTx(approveSig); + console.log("Proposal approved"); + + // Execute + const execSig = await smartAccount.rpc.executeTransaction({ + connection: rpc, + feePayer: payer, + settingsPda, + transactionIndex: txIndex, + signer: payer.publicKey, + signers: [payer], + sendOptions: { skipPreflight: true }, + }); + await confirmTx(execSig); + + console.log("Wallet:", walletPda.toBase58()); + console.log("Recipient:", recipient.publicKey.toBase58()); + console.log("Async transfer tx:", execSig); +})(); diff --git a/toolkits/squads-smart-wallet/wallet-send-sync.ts b/toolkits/squads-smart-wallet/wallet-send-sync.ts new file mode 100644 index 0000000..4d91feb --- /dev/null +++ b/toolkits/squads-smart-wallet/wallet-send-sync.ts @@ -0,0 +1,152 @@ +import "dotenv/config"; +import { + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + createRpc, + buildAndSignTx, + sendAndConfirmTx, +} from "@lightprotocol/stateless.js"; +import { + createMintInterface, + createAtaInterface, + getAssociatedTokenAddressInterface, + mintToInterface, + createLightTokenTransferInstruction, +} from "@lightprotocol/compressed-token"; +import * as smartAccount from "@sqds/smart-account"; +import { homedir } from "os"; +import { readFileSync } from "fs"; + +const RPC_URL = process.env.RPC_URL || "http://127.0.0.1:8899"; +const rpc = createRpc(RPC_URL); + +const payer = Keypair.fromSecretKey( + new Uint8Array( + JSON.parse(readFileSync(`${homedir()}/.config/solana/id.json`, "utf8")) + ) +); + +(async function () { + // 1. Setup: Create mint, fund payer, create smart account, fund wallet + const { mint } = await createMintInterface(rpc, payer, payer, null, 9); + await createAtaInterface(rpc, payer, mint, payer.publicKey); + const payerAta = getAssociatedTokenAddressInterface(mint, payer.publicKey); + await mintToInterface(rpc, payer, mint, payerAta, payer, 1_000_000); + + const programConfig = + await smartAccount.accounts.ProgramConfig.fromAccountAddress( + rpc, + smartAccount.getProgramConfigPda({})[0] + ); + const accountIndex = + BigInt(programConfig.smartAccountIndex.toString()) + 1n; + const [settingsPda] = smartAccount.getSettingsPda({ accountIndex }); + const [walletPda] = smartAccount.getSmartAccountPda({ + settingsPda, + accountIndex: 0, + }); + + const createSig = await smartAccount.rpc.createSmartAccount({ + connection: rpc, + treasury: programConfig.treasury, + creator: payer, + settings: settingsPda, + settingsAuthority: null, + threshold: 1, + signers: [ + { + key: payer.publicKey, + permissions: smartAccount.types.Permissions.all(), + }, + ], + timeLock: 0, + rentCollector: null, + sendOptions: { skipPreflight: true }, + }); + await rpc.confirmTransaction(createSig, "confirmed"); + + // Fund wallet with LTs + await createAtaInterface(rpc, payer, mint, walletPda, true); + const walletAta = getAssociatedTokenAddressInterface(mint, walletPda, true); + const fundIx = createLightTokenTransferInstruction( + payerAta, + walletAta, + payer.publicKey, + 500_000 + ); + const { blockhash: bh1 } = await rpc.getLatestBlockhash(); + await sendAndConfirmTx(rpc, buildAndSignTx([fundIx], payer, bh1, [])); + + // Fund wallet with SOL + const { blockhash: bh2 } = await rpc.getLatestBlockhash(); + const solMsg = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: bh2, + instructions: [ + SystemProgram.transfer({ + fromPubkey: payer.publicKey, + toPubkey: walletPda, + lamports: 10_000_000, + }), + ], + }).compileToV0Message(); + const solTx = new VersionedTransaction(solMsg); + solTx.sign([payer]); + const solSig = await rpc.sendRawTransaction(solTx.serialize()); + await rpc.confirmTransaction(solSig, "confirmed"); + + // 2. Smart wallet sends LTs via sync execution + const recipient = Keypair.generate(); + await createAtaInterface(rpc, payer, mint, recipient.publicKey); + const recipientAta = getAssociatedTokenAddressInterface( + mint, + recipient.publicKey + ); + + const transferIx = createLightTokenTransferInstruction( + walletAta, + recipientAta, + walletPda, + 100_000, + walletPda + ); + + // Compile for sync execution + const { instructions, accounts } = + smartAccount.utils.instructionsToSynchronousTransactionDetails({ + vaultPda: walletPda, + members: [payer.publicKey], + transaction_instructions: [transferIx], + }); + + const syncIx = smartAccount.instructions.executeTransactionSync({ + settingsPda, + numSigners: 1, + accountIndex: 0, + instructions, + instruction_accounts: accounts, + }); + + // Execute in a single transaction — no proposal needed + const { blockhash: bh3 } = await rpc.getLatestBlockhash(); + const msg = new TransactionMessage({ + payerKey: payer.publicKey, + recentBlockhash: bh3, + instructions: [syncIx], + }).compileToV0Message(); + const tx = new VersionedTransaction(msg); + tx.sign([payer]); + const sig = await rpc.sendRawTransaction(tx.serialize(), { + skipPreflight: true, + }); + await rpc.confirmTransaction(sig, "confirmed"); + + console.log("Wallet:", walletPda.toBase58()); + console.log("Recipient:", recipient.publicKey.toBase58()); + console.log("Sync transfer tx:", sig); +})(); diff --git a/vendor/smart-account-program b/vendor/smart-account-program new file mode 160000 index 0000000..fc50fce --- /dev/null +++ b/vendor/smart-account-program @@ -0,0 +1 @@ +Subproject commit fc50fce07375b2a1f3fa5ab08238d491c3227324