diff --git a/bun.lock b/bun.lock index 266e59c..5ad256d 100644 --- a/bun.lock +++ b/bun.lock @@ -91,6 +91,16 @@ "typescript": "^5.5.3", }, }, + "examples/migrate-from-squads": { + "name": "swig-migrate-from-squads-example", + "version": "1.0.0", + "dependencies": { + "@solana/web3.js": "^1.87.0", + "@sqds/multisig": "^2.1.4", + "@swig-wallet/classic": "^1.4.0", + "chalk": "^5.3.0", + }, + }, "examples/secp-ui": { "name": "secp-ui", "dependencies": { @@ -145,7 +155,7 @@ }, "packages/classic": { "name": "@swig-wallet/classic", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "@noble/curves": "^1.8.2", "@solana-program/token": "^0.5.1", @@ -173,7 +183,7 @@ }, "packages/coder": { "name": "@swig-wallet/coder", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "@solana/kit": "^2.1.0", }, @@ -191,7 +201,7 @@ }, "packages/kit": { "name": "@swig-wallet/kit", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "@noble/curves": "^1.8.2", "@solana-program/token": "^0.5.1", @@ -217,7 +227,7 @@ }, "packages/lib": { "name": "@swig-wallet/lib", - "version": "1.3.0", + "version": "1.4.0", "dependencies": { "@noble/curves": "^1.8.2", "@solana-program/token": "^0.5.1", @@ -566,6 +576,12 @@ "@metamask/utils": ["@metamask/utils@8.5.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.0.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ=="], + "@metaplex-foundation/beet": ["@metaplex-foundation/beet@0.7.1", "", { "dependencies": { "ansicolors": "^0.3.2", "bn.js": "^5.2.0", "debug": "^4.3.3" } }, "sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA=="], + + "@metaplex-foundation/beet-solana": ["@metaplex-foundation/beet-solana@0.4.0", "", { "dependencies": { "@metaplex-foundation/beet": ">=0.1.0", "@solana/web3.js": "^1.56.2", "bs58": "^5.0.0", "debug": "^4.3.4" } }, "sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ=="], + + "@metaplex-foundation/cusper": ["@metaplex-foundation/cusper@0.0.2", "", {}, "sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.4", "", { "dependencies": { "@emnapi/core": "^1.1.0", "@emnapi/runtime": "^1.1.0", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9zESzOO5aDByvhIAsOy9TbpZ0Ur2AJbUI7UT73kcUTS2mxAMHOBaa1st/jAymNoCtvrit99kkzT1FZuXVcgfIQ=="], "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], @@ -848,6 +864,8 @@ "@solana/web3.js": ["@solana/web3.js@1.98.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A=="], + "@sqds/multisig": ["@sqds/multisig@2.1.4", "", { "dependencies": { "@metaplex-foundation/beet": "0.7.1", "@metaplex-foundation/beet-solana": "0.4.0", "@metaplex-foundation/cusper": "^0.0.2", "@solana/spl-token": "^0.3.6", "@solana/web3.js": "^1.70.3", "@types/bn.js": "^5.1.1", "assert": "^2.0.0", "bn.js": "^5.2.1", "buffer": "6.0.3", "invariant": "2.2.4" } }, "sha512-5w+NmwHOzl96nI50R/fjSD6uFydRLNUquhoEmmWbGepS4D9DnQyF2TKcUBfTyxV3sgJt00ypBt7SXB3y8WOzUQ=="], + "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], "@swig-wallet/classic": ["@swig-wallet/classic@workspace:packages/classic"], @@ -1128,6 +1146,8 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansicolors": ["ansicolors@0.3.2", "", {}, "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg=="], + "ansis": ["ansis@4.1.0", "", {}, "sha512-BGcItUBWSMRgOCe+SVZJ+S7yTRG0eGt9cXAHev72yuGcY23hnLA7Bky5L/xLyPINoSN95geovfBkqoTlNZYa7w=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], @@ -2422,6 +2442,8 @@ "swig-jupiter-swap-example": ["swig-jupiter-swap-example@workspace:examples/swap"], + "swig-migrate-from-squads-example": ["swig-migrate-from-squads-example@workspace:examples/migrate-from-squads"], + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], "tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="], @@ -2756,6 +2778,8 @@ "@metamask/sdk-communication-layer/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "@metaplex-foundation/beet-solana/bs58": ["bs58@5.0.0", "", { "dependencies": { "base-x": "^4.0.0" } }, "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ=="], + "@react-native/codegen/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "@react-native/community-cli-plugin/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -2798,6 +2822,8 @@ "@solana/web3.js/@noble/curves": ["@noble/curves@1.9.2", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="], + "@sqds/multisig/@solana/spl-token": ["@solana/spl-token@0.3.11", "", { "dependencies": { "@solana/buffer-layout": "^4.0.0", "@solana/buffer-layout-utils": "^0.2.0", "@solana/spl-token-metadata": "^0.1.2", "buffer": "^6.0.3" }, "peerDependencies": { "@solana/web3.js": "^1.88.0" } }, "sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ=="], + "@swc/helpers/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@swig-wallet/kit/@noble/curves": ["@noble/curves@1.9.2", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="], @@ -3188,6 +3214,8 @@ "@metamask/eth-json-rpc-provider/@metamask/utils/superstruct": ["superstruct@1.0.4", "", {}, "sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ=="], + "@metaplex-foundation/beet-solana/bs58/base-x": ["base-x@4.0.1", "", {}, "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw=="], + "@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/sign-client": ["@walletconnect/sign-client@2.21.0", "", { "dependencies": { "@walletconnect/core": "2.21.0", "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-utils": "1.0.8", "@walletconnect/logger": "2.1.2", "@walletconnect/time": "1.0.2", "@walletconnect/types": "2.21.0", "@walletconnect/utils": "2.21.0", "events": "3.3.0" } }, "sha512-z7h+PeLa5Au2R591d/8ZlziE0stJvdzP9jNFzFolf2RG/OiXulgFKum8PrIyXy+Rg2q95U9nRVUF9fWcn78yBA=="], "@reown/appkit-controllers/@walletconnect/universal-provider/@walletconnect/types": ["@walletconnect/types@2.21.0", "", { "dependencies": { "@walletconnect/events": "1.0.1", "@walletconnect/heartbeat": "1.2.2", "@walletconnect/jsonrpc-types": "1.0.4", "@walletconnect/keyvaluestorage": "1.1.1", "@walletconnect/logger": "2.1.2", "events": "3.3.0" } }, "sha512-ll+9upzqt95ZBWcfkOszXZkfnpbJJ2CmxMfGgE5GmhdxxxCcO5bGhXkI+x8OpiS555RJ/v/sXJYMSOLkmu4fFw=="], diff --git a/examples/migrate-from-squads/index.ts b/examples/migrate-from-squads/index.ts new file mode 100644 index 0000000..ceb344a --- /dev/null +++ b/examples/migrate-from-squads/index.ts @@ -0,0 +1,161 @@ +import { + Connection, + Keypair, + PublicKey, + Transaction, + TransactionInstruction, +} from '@solana/web3.js'; + +import { + Actions, + createEd25519AuthorityInfo, + fetchSwig, + findSwigPda, + getCreateSwigInstructionBuilder, + getSwigWalletAddress, +} from '@swig-wallet/classic'; + +import * as multisig from '@sqds/multisig'; +import chalk from 'chalk'; +import * as fs from 'fs'; + +async function sendTransaction( + connection: Connection, + instructions: TransactionInstruction[], + payer: Keypair, +): Promise { + const tx = new Transaction().add(...instructions); + tx.feePayer = payer.publicKey; + tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + tx.sign(payer); + const sig = await connection.sendRawTransaction(tx.serialize()); + await connection.confirmTransaction(sig); + return sig; +} + +async function main() { + // Check for keypair file argument and squads address + if (process.argv.length < 4) { + console.error( + chalk.red( + 'Please provide the path to your keypair file and squads address', + ), + ); + console.error( + chalk.yellow( + 'Usage: bun run index.ts ', + ), + ); + process.exit(1); + } + + const keypairPath = process.argv[2]; + if (!fs.existsSync(keypairPath)) { + console.error(chalk.red(`Keypair file not found at ${keypairPath}`)); + process.exit(1); + } + + // keypair should be the config authority on the squads + const squadsMemberKeypair = Keypair.fromSecretKey( + new Uint8Array(JSON.parse(fs.readFileSync(keypairPath, 'utf-8'))), + ); + + const rpcUrl = process.env.RPC_URL; + if (!rpcUrl) { + console.error(chalk.red('Please set RPC_URL in your environment.')); + process.exit(1); + } + + const connection = new Connection(rpcUrl, 'confirmed'); + console.log(chalk.cyan(`🌐 Connected to Solana RPC: ${rpcUrl}`)); + console.log( + chalk.green('👤 Squads config authority public key:'), + chalk.cyan(squadsMemberKeypair.publicKey.toBase58()), + ); + + // Squad multisig publickey + let squadAddress: PublicKey; + + try { + squadAddress = new PublicKey(process.argv[3]); + } catch { + throw new Error( + `Invalid base58 encoded publickey provided as Squads multisig publickey`, + ); + } + + const squadsRawAccountInfo = await connection.getAccountInfo(squadAddress); + if (!squadsRawAccountInfo) + throw new Error( + `No squad multisig found for address ${squadAddress.toBase58()}`, + ); + + const squads = + multisig.accounts.Multisig.fromAccountInfo(squadsRawAccountInfo)[0]; + + // we use the same seeds for creating the squads mulitsig to create the swig + const swigId = squads.createKey.toBytes(); + + // we check if the keypair is a member on the squads multisig + if ( + !squads.members.find( + (member) => + member.key.toBase58() === squadsMemberKeypair.publicKey.toBase58(), + ) + ) { + throw new Error( + `The provided keypair ${squadsMemberKeypair.publicKey.toBase58()} is not a member on the multisig.`, + ); + } + + // we make sure we the config authority excluded from the members' list to eliminate duplicate when adding authorities + const otherMembers = squads.members + .map((s) => s.key) + .filter( + (key) => key.toBase58() !== squadsMemberKeypair.publicKey.toBase58(), + ); + + const swigAddress = findSwigPda(swigId); + + const createIxBuilder = getCreateSwigInstructionBuilder({ + payer: squadsMemberKeypair.publicKey, + // we set the action of the config authority on swig to `All` + // to grant it root access as it has on the multisig + actions: Actions.set().all().get(), + authorityInfo: createEd25519AuthorityInfo(squads.configAuthority), + id: swigId, + swigAddress, + options: {}, + }); + + // we include instructions for adding the other members as authorites on the swig + otherMembers.forEach((authority) => + createIxBuilder.addAuthority( + createEd25519AuthorityInfo(authority), + // we limit the actions on these authorites, + // these authorities can be updated as needed the config authority (root authority) + Actions.set().programCurated().get(), + ), + ); + + const createIxs = await createIxBuilder.getInstructions(); + + const sig = await sendTransaction(connection, createIxs, squadsMemberKeypair); + + console.log(chalk.green('🎉 Swig creation successful!')); + console.log(chalk.gray(`Signature: ${sig}`)); + + const swig = await fetchSwig(connection, swigAddress); + + const swigWalletAddress = await getSwigWalletAddress(swig); + + console.log( + chalk.green('swig wallet address:'), + chalk.yellow(swigWalletAddress.toBase58()), + ); +} + +main().catch((err) => { + console.error(chalk.red('Fatal Error:'), err); + process.exit(1); +}); diff --git a/examples/migrate-from-squads/package.json b/examples/migrate-from-squads/package.json new file mode 100644 index 0000000..4c19dee --- /dev/null +++ b/examples/migrate-from-squads/package.json @@ -0,0 +1,17 @@ +{ + "name": "swig-migrate-from-squads-example", + "version": "1.0.0", + "description": "Example of migrating from Squads multisig to Swig", + "private": true, + "module": "index.ts", + "type": "module", + "scripts": { + "start": "bun run index.ts" + }, + "dependencies": { + "@sqds/multisig": "^2.1.4", + "@swig-wallet/classic": "^1.4.0", + "@solana/web3.js": "^1.87.0", + "chalk": "^5.3.0" + } +} \ No newline at end of file