diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json index 9f36d81..92ac8f5 100644 --- a/docs/content/docs/meta.json +++ b/docs/content/docs/meta.json @@ -6,6 +6,7 @@ "associated-token-account", "confidential-balances", "transfer-hook-interface", + "token-wrap", "---Staking---", "stake-pool", "single-pool", diff --git a/docs/content/docs/token-wrap.mdx b/docs/content/docs/token-wrap.mdx new file mode 100644 index 0000000..e80584a --- /dev/null +++ b/docs/content/docs/token-wrap.mdx @@ -0,0 +1,958 @@ +--- +title: Token Wrap +description: A program for wrapping SPL tokens to enable interoperability between token standards. If you are building an app with a mint/token and find yourself wishing you could take advantage of some of the latest features of a specific token program, this might be for you! +--- + +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; + +## Features + +* **Bidirectional Wrapping:** Convert tokens between SPL Token and SPL Token 2022 standards in either direction, +including conversions between different SPL Token 2022 mints. +* **SPL Token 2022 Extension Support:** Preserve or add SPL Token 2022 extensions (like transfer fees, confidential +transfers, etc.) during the wrapping process. Note: this requires forking and updating the `CreateMint` instruction. +* **Transfer Hook Compatibility:** Integrates with tokens that implement the SPL Transfer Hook interface, +enabling custom logic on token transfers. +* **Multisignature Support:** Compatible with multisig signers for both wrapping and unwrapping operations. + +## How It Works + +It supports three primary operations: + +1. **`CreateMint`:** This operation initializes a new wrapped token mint and its associated backpointer account. Note, +the caller must pre-fund this account with lamports. This is to avoid requiring writer+signer privileges on this +instruction. + * **Wrapped Mint:** An SPL Token or SPL Token 2022 mint account is created. The address of this mint is a +PDA derived from the *unwrapped* token's mint address and the *wrapped* token program ID. This ensures a unique, +deterministic relationship between the wrapped and unwrapped tokens. The wrapped mint's authority is also a PDA, +controlled by the Token Wrap program. + * **Backpointer:** An account (also a PDA, derived from the *wrapped* mint address) is created to store the +address of the original *unwrapped* token mint. This allows anyone to easily determine the unwrapped token +corresponding to a wrapped token, facilitating unwrapping. + +2. **`Wrap`:** This operation accepts deposits of unwrapped tokens and mints wrapped tokens. + * Unwrapped tokens are transferred from the user's account to an escrow account owned by the wrapped mint authority (different for every mint). Any unwrapped token account whose owner is a PDA controlled by the Token Wrap program can be used. + * An equivalent amount of wrapped tokens is minted to the user's wrapped token account. + +3. **`Unwrap`:** This operation burns wrapped tokens and releases unwrapped token deposits. + * Wrapped tokens are burned from the user's wrapped token account. + * An equivalent amount of unwrapped tokens is transferred from the escrow account to the user's unwrapped token +account. + +The 1:1 relationship between wrapped and unwrapped tokens is maintained through the escrow mechanism, ensuring that +wrapped tokens are always fully backed by their unwrapped counterparts. + +## Permissionless design + +The SPL Token Wrap program is designed to be **permissionless**. This means: + +* **Anyone can create a wrapped mint:** No special permissions or whitelisting is required to create a wrapped +version of an existing mint. The `CreateMint` instruction is open to all users, provided they can +pay the required rent for the new accounts. +* **Anyone can wrap and unwrap tokens:** Once a wrapped mint has been created, any user holding the underlying +unwrapped tokens can use the `Wrap` and `Unwrap` instructions. All transfers are controlled by PDAs owned by the Token +Wrap program itself. However, it is important to note that if the *unwrapped* token has a freeze authority, +that freeze authority is *preserved* in the wrapped token. + +## Source + +The Token Wrap Program's source is available on +[GitHub](https://github.com/solana-program/token-wrap). + +## Security Audits + +The Token Wrap program is currently undergoing an audit with Zellic & Runtime Verification. + +## SDK + +* **Rust Crate:** The program is written in Rust and available as the `spl-token-wrap` crate on [crates.io](https://crates.io/crates/spl-token-wrap) and [docs.rs](https://docs.rs/spl-token-wrap). +* **JavaScript bindings** for web development: [@solana-program/token-wrap](https://www.npmjs.com/package/@solana-program/token-wrap) ([source](https://github.com/solana-program/token-wrap/tree/main/clients/js)). +* **Command-Line Interface (CLI):** The `spl-token-wrap-cli` utility allows direct interaction with the program via the command line for testing, scripting, or manual operations. + +## Reference Guide + +### Setup + + + + The spl-token-wrap command-line utility can be used to interact with the Token Wrap program. + + Install from crates.io + ```console + $ cargo install spl-token-wrap-cli + ``` + + or, build the CLI from source: + + ```console + $ git clone https://github.com/solana-program/token-wrap.git + $ cd token-wrap + $ cargo build --bin spl-token-wrap + ``` + Run `spl-token-wrap --help` for a full description of available commands. + + The spl-token-wrap configuration is shared with the solana command-line tool. + + + npm + ```console + npm install @solana-program/token-wrap + ``` + Yarn + ```console + yarn add @solana-program/token-wrap + ``` + pnpm + ```console + pnpm add @solana-program/token-wrap + ``` + + + +### Create a wrapped token mint +To create a new wrapped token mint, first you need to identify the unwrapped token mint address you want to wrap and the to/from token programs. + + + + ```console + $ UNWRAPPED_MINT_ADDRESS=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap create-mint $UNWRAPPED_MINT_ADDRESS $WRAPPED_TOKEN_PROGRAM + + Creating wrapped mint for BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Funding wrapped_mint_account B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM with 1461600 lamports for rent + Funding backpointer_account CNjr898vsBdzWxrJApMSAQac4A7o7qLRcSseTb56X7C9 with 1113600 lamports for rent + Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM + Wrapped backpointer address: CNjr898vsBdzWxrJApMSAQac4A7o7qLRcSseTb56X7C9 + Funded wrapped mint lamports: 1461600 + Funded backpointer lamports: 1113600 + Signature: 2UAPjhDogs8aTTfynWRi36KWez6jzmFJhAHPTBpYsamDvKRQ5Uqn2BXoz1mKfRwBPV8p1j1MSXLN7yZHLwb1wdnT + ``` + + + + ```typescript + import { + address, + createKeyPairSignerFromBytes, + createSolanaRpc, + createSolanaRpcSubscriptions, + getSignatureFromTransaction, + sendAndConfirmTransactionFactory, + signTransactionMessageWithSigners, + } from '@solana/kit'; + import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; + import { createMintTx } from '@solana-program/token-wrap'; + + // Replace these consts with your own + const PRIVATE_KEY_PAIR = new Uint8Array([242, 30, 38, 177, 152, 71, ... ]); + const UNWRAPPED_MINT_ADDRESS = address('Cp3dvsXSiWJiA5AyfNdDdJ1Drw91yWdQwx5nnmcHKVi6'); + + async function main() { + const rpc = createSolanaRpc('http://127.0.0.1:8899'); + const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + const payer = await createKeyPairSignerFromBytes(PRIVATE_KEY_PAIR); + const { value: blockhash } = await rpc.getLatestBlockhash().send(); + + const createMintMessage = await createMintTx({ + rpc, + blockhash, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + payer, + idempotent: true, + }); + const signedCreateMintTx = await signTransactionMessageWithSigners(createMintMessage.tx); + await sendAndConfirm(signedCreateMintTx, { commitment: 'confirmed' }); + const createMintSignature = getSignatureFromTransaction(signedCreateMintTx); + + console.log('======== Create Mint Successful ========'); + console.log('Wrapped Mint:', createMintMessage.wrappedMint); + console.log('Backpointer:', createMintMessage.backpointer); + console.log('Funded wrapped mint lamports:', createMintMessage.fundedWrappedMintLamports); + console.log('Funded backpointer lamports:', createMintMessage.fundedBackpointerLamports); + console.log('Signature:', createMintSignature); + } + + void main(); + + ``` + + + +### Find PDAs for a wrapped token + +To interact with wrapped tokens, you need to know the PDAs (Program Derived Addresses) associated with them: + + + + ```console + $ UNWRAPPED_MINT_ADDRESS=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap find-pdas $UNWRAPPED_MINT_ADDRESS $WRAPPED_TOKEN_PROGRAM + + Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM + Wrapped mint authority: 8WdYPmtq8c6ZfmHMZUwCQL2E8qVHEV8rG9MXkyax3joR + Wrapped backpointer address: CNjr898vsBdzWxrJApMSAQac4A7o7qLRcSseTb56X7C9 + ``` + + + + ```typescript + import { address } from '@solana/kit'; + import { + findBackpointerPda, + findWrappedMintAuthorityPda, + findWrappedMintPda, + } from '@solana-program/token-wrap'; + import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; + + const UNWRAPPED_MINT_ADDRESS = address('5StBUZ2w8ShDN9iF7NkGpDNNH2wv9jK7zhArmVRpwrCt'); + + async function main() { + const [wrappedMint] = await findWrappedMintPda({ + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + }); + const [backpointer] = await findBackpointerPda({ wrappedMint }); + const [wrappedMintAuthority] = await findWrappedMintAuthorityPda({ wrappedMint }); + + console.log('WRAPPED_MINT_ADDRESS', wrappedMint); + console.log('BACKPOINTER', backpointer); + console.log('WRAPPED_MINT_AUTHORITY', wrappedMintAuthority); + } + + void main(); + ``` + + + +### Create escrow account + +Before wrapping tokens, you need to create an account to hold the unwrapped tokens. The escrow account's owner must be the correct PDA (see `find-pdas` command above). There is also a helper to create this account: + + + + ```console + $ UNWRAPPED_MINT_ADDRESS=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap create-escrow-account $UNWRAPPED_MINT_ADDRESS $WRAPPED_TOKEN_PROGRAM + + Creating escrow account under program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA for unwrapped mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e owned by PDA 8WdYPmtq8c6ZfmHMZUwCQL2E8qVHEV8rG9MXkyax3joR + Escrow Account Address: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + Escrow Account Owner (PDA): 8WdYPmtq8c6ZfmHMZUwCQL2E8qVHEV8rG9MXkyax3joR + Escrow Token Program ID: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + Signature: 3ysN6YEQcsYQBjnCPMas9xEGP53CSSvoL6CSJ1vJS1S5ZvtN5NbsUtDKQMs6hwCQHhsctcrEhLQBLTEBuQWEKqNE + ``` + + + + ```typescript + import { + address, + createKeyPairSignerFromBytes, + createSolanaRpc, + createSolanaRpcSubscriptions, + sendAndConfirmTransactionFactory, + signTransactionMessageWithSigners, + } from '@solana/kit'; + import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; + import { createEscrowAccountTx } from '@solana-program/token-wrap'; + + // Replace these consts with your own + const PRIVATE_KEY_PAIR = new Uint8Array([242, 30, 38, 177, 152, 71, ... ]); + const UNWRAPPED_MINT_ADDRESS = address('Cp3dvsXSiWJiA5AyfNdDdJ1Drw91yWdQwx5nnmcHKVi6'); + + async function main() { + const rpc = createSolanaRpc('http://127.0.0.1:8899'); + const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + const payer = await createKeyPairSignerFromBytes(PRIVATE_KEY_PAIR); + const { value: blockhash } = await rpc.getLatestBlockhash().send(); + + const createEscrowMessage = await createEscrowAccountTx({ + rpc, + blockhash, + payer, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + }); + const signedCreateEscrowTx = await signTransactionMessageWithSigners(createEscrowMessage.tx); + await sendAndConfirm(signedCreateEscrowTx, { commitment: 'confirmed' }); + + console.log('ESCROW_ADDRESS', createEscrowMessage.keyPair.address); + } + + void main(); + ``` + + + + +### Wrap tokens (single signer) + +Escrows unwrapped tokens and mints wrapped tokens to recipient account. + + + + ```console + $ UNWRAPPED_TOKEN_ACCOUNT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $WRAPPED_TOKEN_PROGRAM 100 + + Wrapping 100 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM + Unwrapped token account: DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + Recipient wrapped token account: HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt + Escrow account: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + Amount: 100 + Signers: + 26xTNzcurTuXQfHSCCuamxmrDXbkbA38JtGC9GhEcKgVZwxnyvXBD5AMH8TXmkfpNw64noDPaS4Ezm4RLMvfq3nF + ``` + + You can specify a recipient token account with the `--recipient-token-account` option. If not provided, the associated token account of the fee payer will be used or created if it doesn't exist. + + ```console + $ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $WRAPPED_TOKEN_PROGRAM 100 \ + --recipient-token-account $RECIPIENT_WRAPPED_TOKEN_ACCOUNT + ``` + + + + ```typescript + import { + address, + createKeyPairSignerFromBytes, + createSolanaRpc, + createSolanaRpcSubscriptions, + getSignatureFromTransaction, + sendAndConfirmTransactionFactory, + signTransactionMessageWithSigners, + } from '@solana/kit'; + import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; + import { singleSignerWrapTx } from '@solana-program/token-wrap'; + + // Replace these consts with your own + const PRIVATE_KEY_PAIR = new Uint8Array([242, 30, 38, 177, 152, 71, ... ]); + const ESCROW_ACCOUNT = address('4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3'); + const UNWRAPPED_TOKEN_ACCOUNT = address('CbuRmvG3frMoPFnsKfC2t8jTUHFjtnrKZBt2aqdqH4PG'); + const RECIPIENT = address('HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt'); + const AMOUNT_TO_WRAP = 100n; + + async function main() { + const rpc = createSolanaRpc('http://127.0.0.1:8899'); + const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + const payer = await createKeyPairSignerFromBytes(PRIVATE_KEY_PAIR); + const { value: blockhash } = await rpc.getLatestBlockhash().send(); + + const wrapMessage = await singleSignerWrapTx({ + rpc, + blockhash, + payer, + unwrappedTokenAccount: UNWRAPPED_TOKEN_ACCOUNT, + escrowAccount: ESCROW_ACCOUNT, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + recipientWrappedTokenAccount: RECIPIENT, + amount: AMOUNT_TO_WRAP, + }); + + const signedWrapTx = await signTransactionMessageWithSigners(wrapMessage.tx); + await sendAndConfirm(signedWrapTx, { commitment: 'confirmed' }); + const wrapSignature = getSignatureFromTransaction(signedWrapTx); + + console.log('======== Wrap Successful ========'); + console.log('Wrap amount:', wrapMessage.amount); + console.log('Recipient account:', wrapMessage.recipientWrappedTokenAccount); + console.log('Escrow Account:', wrapMessage.escrowAccount); + console.log('Signature:', wrapSignature); + } + + void main(); + ``` + + + +### Wrap tokens (SPL Token Multisig) + +An example wrapping tokens whose origin is a token account owned by an SPL Token multisig. + + + + There are two parts to this. The first is having the multisig members sign the message independently. + The second is the broadcaster collecting those signatures and sending the transaction to the network. + + Let's pretend we have a 2 of 3 multisig and the broadcaster will be the fee payer. Here's what that would look like: + + Get a recent blockhash. This will need to be the same for all signers. + ```console + $ solana block + Blockhash: E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + ⬆️ send this to all signers + ``` + + First signer runs this command with their keypair: + + ```console + Different for each signer + $ SIGNER_1=signer-1.json + $ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK + $ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5 + + Same for everyone + $ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + $ UNWRAPPED_TOKEN_ACCOUNT=4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL # owned by multisig + $ MULTISIG_ADDRESS=mgnqjedikMKaRtS5wrhVttuA12JaPXiqY619Gfef5eh + $ RECIPIENT_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt + $ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $WRAPPED_TOKEN_PROGRAM 23 \ + --transfer-authority $MULTISIG_ADDRESS \ + --recipient-token-account $RECIPIENT_ACCOUNT \ + --unwrapped-mint $UNWRAPPED_MINT \ + --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \ + --fee-payer $FEE_PAYER \ + --multisig-signer $SIGNER_1 \ + --multisig-signer $SIGNER_2 \ + --blockhash $BLOCKHASH \ + --sign-only + + Signers (Pubkey=Signature): + DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS=4sQFJg338zP9bxX4Gw4KS58eXkpBB2pwjwo4szxCEVQZxrApzgYMN7riBYUnbvZPb84tsThPE1aHApiCCC9PSSP7 + Absent Signers (Pubkey): + 2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5 + 42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK + ``` + + Second signer uses their own keypair (note the change at the top): + + ```console + Signer 2 uses their keypair and puts the pubkey for signer 1 + $ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS + $ SIGNER_2=signer-2.json + $ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5 + + $ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + $ UNWRAPPED_TOKEN_ACCOUNT=4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL + $ MULTISIG_ADDRESS=mgnqjedikMKaRtS5wrhVttuA12JaPXiqY619Gfef5eh + $ RECIPIENT_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt + $ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $WRAPPED_TOKEN_PROGRAM 23 \ + --transfer-authority $MULTISIG_ADDRESS \ + --recipient-token-account $RECIPIENT_ACCOUNT \ + --unwrapped-mint $UNWRAPPED_MINT \ + --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \ + --fee-payer $FEE_PAYER \ + --multisig-signer $SIGNER_1 \ + --multisig-signer $SIGNER_2 \ + --blockhash $BLOCKHASH \ + --sign-only + + Signers (Pubkey=Signature): + 42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK=4UPUAV9USLFp8CKJ9u6gXhvUUFkpL2FTMbu3eJyyZ8DonjHJBEjUchuaM7j7tTaNWWF7zaRFfK5TkYvBytbV5vUR + Absent Signers (Pubkey): + 2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5 + DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS + ``` + + Now the broadcaster (and in this case, the fee payer as well) sends the last message with the `Pubkey=Signature` they have collected from Signer 1 and Signer 2: + + ```console + $ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS + $ SIGNATURE_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS=4sQFJg338zP9bxX4Gw4KS58eXkpBB2pwjwo4szxCEVQZxrApzgYMN7riBYUnbvZPb84tsThPE1aHApiCCC9PSSP7 + $ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK + $ SIGNATURE_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK=4UPUAV9USLFp8CKJ9u6gXhvUUFkpL2FTMbu3eJyyZ8DonjHJBEjUchuaM7j7tTaNWWF7zaRFfK5TkYvBytbV5vUR + $ FEE_PAYER="$HOME/.config/solana/id.json" + + $ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + $ UNWRAPPED_TOKEN_ACCOUNT=4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL + $ MULTISIG_ADDRESS=mgnqjedikMKaRtS5wrhVttuA12JaPXiqY619Gfef5eh + $ RECIPIENT_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt + $ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap wrap $UNWRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $WRAPPED_TOKEN_PROGRAM 23 \ + --transfer-authority $MULTISIG_ADDRESS \ + --recipient-token-account $RECIPIENT_ACCOUNT \ + --unwrapped-mint $UNWRAPPED_MINT \ + --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \ + --fee-payer $FEE_PAYER \ + --multisig-signer $SIGNER_1 \ + --multisig-signer $SIGNER_2 \ + --blockhash $BLOCKHASH \ + --signer $SIGNATURE_1 \ + --signer $SIGNATURE_2 + + Wrapping 23 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Wrapped mint address: B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM + Unwrapped token account: 4jFsvSDhp9J67An6DUGwezTiunud11RXiaf2zqtG2yUL + Recipient wrapped token account: HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt + Escrow account: 4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + Amount: 23 + Signers: + 5pBReBRzy8yWLbz5j5GNBVFTmwGy65d4BvzigUPRTWWRYx4SUceNWDb78h1ufaRdzyi7yNmKpdLHv2eNS7ziaH7L + 3KdzhMYjFxBFZtQzEqfeugg6LVGhApSRj8pAx8HpgqCRU7C3gA2Wm5Hvx55taMAcpDWaKSJdtpgUJ8ksBVo4PDJU + 259kbWfYYhhe4FjTWZCeCXhPN4q3VSKN4dRHMcb42i85jptfh82TocrEf13aj5qMMDux9btzL5RCV55AxCWJbu5Q + ``` + + Note all three needed signers in final broadcasted message. + + + + ```typescript + import { + address, + createKeyPairSignerFromBytes, + createNoopSigner, + createSolanaRpc, + createSolanaRpcSubscriptions, + getBase58Decoder, + partiallySignTransactionMessageWithSigners, + sendAndConfirmTransactionFactory, + } from '@solana/kit'; + import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; + import { + findWrappedMintAuthorityPda, + multisigOfflineSignWrapTx, + combinedMultisigTx, + } from '@solana-program/token-wrap'; + + // Replace these consts with your own + const PAYER_KEYPAIR_BYTES = new Uint8Array([242, 30, 38, 177, 152, 71, ... ]); + const MULTISIG_SPL_TOKEN = address('2XBevFsu4pnZpB9PewYKAJHNyx9dFQf3MaiGBszF5fm8'); + const SIGNER_A_KEYPAIR_BYTES = new Uint8Array([210, 190, 232, 169, 113, 107, ... ]); + const SIGNER_B_KEYPAIR_BYTES = new Uint8Array([37, 161, 191, 225, 59, 192, ... ]); + const WRAPPED_MINT_ADDRESS = address('B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM'); + const UNWRAPPED_MINT_ADDRESS = address('E8r9ixwg7QYr6xCh4tSdHErZ6CUxQhVGHqF5bRoZXyyV'); + const UNWRAPPED_TOKEN_ACCOUNT = address('DGNyuKAWP3susy6XMbVsYHy2AMrrKmh8pXM3WpQUeyL2'); // Must be owned by multisig account + const UNWRAPPED_TOKEN_PROGRAM = address('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'); + const ESCROW = address('4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3'); + const RECIPIENT = address('HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt'); + const AMOUNT_TO_WRAP = 100n; + + async function main() { + const rpc = createSolanaRpc('http://127.0.0.1:8899'); + const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + const payer = await createKeyPairSignerFromBytes(PAYER_KEYPAIR_BYTES); + const { value: blockhash } = await rpc.getLatestBlockhash().send(); + + const [wrappedMintAuthority] = await findWrappedMintAuthorityPda({ wrappedMint: WRAPPED_MINT_ADDRESS }); + + const signerA = await createKeyPairSignerFromBytes(SIGNER_A_KEYPAIR_BYTES); + const signerB = await createKeyPairSignerFromBytes(SIGNER_B_KEYPAIR_BYTES); + + // Two signers and the payer sign the transaction independently + + const wrapTxA = multisigOfflineSignWrapTx({ + payer: createNoopSigner(payer.address), + unwrappedTokenAccount: UNWRAPPED_TOKEN_ACCOUNT, + escrowAccount: ESCROW, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + amount: AMOUNT_TO_WRAP, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + recipientWrappedTokenAccount: RECIPIENT, + transferAuthority: MULTISIG_SPL_TOKEN, + wrappedMint: WRAPPED_MINT_ADDRESS, + wrappedMintAuthority, + unwrappedTokenProgram: UNWRAPPED_TOKEN_PROGRAM, + multiSigners: [signerA, createNoopSigner(signerB.address)], + blockhash, + }); + const signedWrapTxA = await partiallySignTransactionMessageWithSigners(wrapTxA); + + const wrapTxB = multisigOfflineSignWrapTx({ + payer: createNoopSigner(payer.address), + unwrappedTokenAccount: UNWRAPPED_TOKEN_ACCOUNT, + escrowAccount: ESCROW, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + amount: AMOUNT_TO_WRAP, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + recipientWrappedTokenAccount: RECIPIENT, + transferAuthority: MULTISIG_SPL_TOKEN, + wrappedMint: WRAPPED_MINT_ADDRESS, + wrappedMintAuthority, + unwrappedTokenProgram: UNWRAPPED_TOKEN_PROGRAM, + multiSigners: [createNoopSigner(signerA.address), signerB], + blockhash, + }); + const signedWrapTxB = await partiallySignTransactionMessageWithSigners(wrapTxB); + + const wrapTxC = multisigOfflineSignWrapTx({ + payer, + unwrappedTokenAccount: UNWRAPPED_TOKEN_ACCOUNT, + escrowAccount: ESCROW, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + amount: AMOUNT_TO_WRAP, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + recipientWrappedTokenAccount: RECIPIENT, + transferAuthority: MULTISIG_SPL_TOKEN, + wrappedMint: WRAPPED_MINT_ADDRESS, + wrappedMintAuthority, + unwrappedTokenProgram: UNWRAPPED_TOKEN_PROGRAM, + multiSigners: [createNoopSigner(signerA.address), createNoopSigner(signerB.address)], + blockhash, + }); + const signedWrapTxC = await partiallySignTransactionMessageWithSigners(wrapTxC); + + // Lastly, all signatures are combined together and broadcast + + const combinedWrapTx = combinedMultisigTx({ + signedTxs: [signedWrapTxA, signedWrapTxB, signedWrapTxC], + blockhash, + }); + await sendAndConfirm(combinedWrapTx, { commitment: 'confirmed' }); + + console.log('======== Multisig Wrap Successful ========'); + for (const [pubkey, signature] of Object.entries(combinedWrapTx.signatures)) { + if (signature) { + const base58Sig = getBase58Decoder().decode(signature); + console.log(`pubkey: ${pubkey}`); + console.log(`signature: ${base58Sig}`); + console.log('-----'); + } + } + } + + void main(); + ``` + + + +### Unwrap tokens (single signer) +Burns wrapped tokens and releases unwrapped tokens from escrow. + + + + ```console + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ WRAPPED_TOKEN_ACCOUNT=HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt + $ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + + $ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 50 + + Unwrapping 50 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Unwrapped token program: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Recipient unwrapped token account: DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + Amount unwrapped: 50 + Signers: + 4HjHkjpjZztvoYT95mKHy2wH7z7iAFqpxSMeeMdUpPTzsjZN3vKg1KXvTMV7VT3jK6CaePYYXYDCTm52KTWz6du + ``` + + + + ```typescript + import { + address, + createKeyPairSignerFromBytes, + createSolanaRpc, + createSolanaRpcSubscriptions, + getSignatureFromTransaction, + sendAndConfirmTransactionFactory, + signTransactionMessageWithSigners, + } from '@solana/kit'; + import { singleSignerUnwrapTx } from '@solana-program/token-wrap'; + + // Replace these consts with your own + const PRIVATE_KEY_PAIR = new Uint8Array([242, 30, 38, 177, 152, 71, ... ]); + const WRAPPED_TOKEN_ACCOUNT = address('HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt'); + const ESCROW = address('4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3'); + const RECIPIENT = address('DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14'); + const AMOUNT_TO_WRAP = 100n; + + async function main() { + const rpc = createSolanaRpc('http://127.0.0.1:8899'); + const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + const payer = await createKeyPairSignerFromBytes(PRIVATE_KEY_PAIR); + const { value: blockhash } = await rpc.getLatestBlockhash().send(); + + const unwrapMessage = await singleSignerUnwrapTx({ + rpc, + blockhash, + payer, + wrappedTokenAccount: WRAPPED_TOKEN_ACCOUNT, + unwrappedEscrow: ESCROW, + amount: AMOUNT_TO_WRAP, + recipientUnwrappedToken: RECIPIENT, + }); + + const signedUnwrapTx = await signTransactionMessageWithSigners(unwrapMessage.tx); + await sendAndConfirm(signedUnwrapTx, { commitment: 'confirmed' }); + const unwrapSignature = getSignatureFromTransaction(signedUnwrapTx); + + console.log('======== Unwrap Successful ========'); + console.log('Unwrapped amount:', unwrapMessage.amount); + console.log('Recipient account:', unwrapMessage.recipientUnwrappedToken); + console.log('Signature:', unwrapSignature); + } + + void main(); + ``` + + + +### Unwrap tokens (SPL Token Multisig) + +An example unwrapping tokens whose origin is a token account owned by an SPL Token multisig. + + + + There are two parts to this. The first is having the multisig members sign the message independently. + The second is the broadcaster collecting those signatures and sending the transaction to the network. + + Let's pretend we have a 2 of 3 multisig and the broadcaster will be the fee payer. Here's what that would look like: + + Get a recent blockhash. This will need to be the same for all signers. + ```console + $ solana block + Blockhash: E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + ⬆️ send this to all signers + ``` + + First signer runs this command with their keypair: + + ```console + Different for each signer + $ SIGNER_1=signer-1.json + $ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK + $ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5 + + Same for everyone + $ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + $ WRAPPED_TOKEN_ACCOUNT=3FzdqSEo32BcFgTUqWL5QakZGQBRX91yBAQFo1vGsCji + $ MULTISIG_ADDRESS=FFQvYvhaWnHeGsCMfixccUMdnXPgDrkG3KkGzpfBHFPb # note this should have the same program-id as wrapped token account + $ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + $ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 5 \ + --transfer-authority $MULTISIG_ADDRESS \ + --fee-payer $FEE_PAYER \ + --unwrapped-mint $UNWRAPPED_MINT \ + --wrapped-token-program $WRAPPED_TOKEN_PROGRAM \ + --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \ + --multisig-signer $SIGNER_1 \ + --multisig-signer $SIGNER_2 \ + --blockhash $BLOCKHASH \ + --sign-only + ``` + + Second signer uses their own keypair (note the change at the top): + + ```console + Signer 2 uses their keypair and puts the pubkey for signer 1 + $ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS + $ SIGNER_2=signer-2.json + $ FEE_PAYER=2cQ3SDmgHxMGU1Uabj7RZ35vtuLk3ZU1afnqEo5zoYk5 + + $ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + $ WRAPPED_TOKEN_ACCOUNT=3FzdqSEo32BcFgTUqWL5QakZGQBRX91yBAQFo1vGsCji + $ MULTISIG_ADDRESS=FFQvYvhaWnHeGsCMfixccUMdnXPgDrkG3KkGzpfBHFPb + $ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + $ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 5 \ + --transfer-authority $MULTISIG_ADDRESS \ + --fee-payer $FEE_PAYER \ + --unwrapped-mint $UNWRAPPED_MINT \ + --wrapped-token-program $WRAPPED_TOKEN_PROGRAM \ + --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \ + --multisig-signer $SIGNER_1 \ + --multisig-signer $SIGNER_2 \ + --blockhash $BLOCKHASH \ + --sign-only + ``` + + Now the broadcaster (and in this case, the fee payer as well) sends the last message with the `Pubkey=Signature` they have collected from Signer 1 and Signer 2: + + ```console + $ SIGNER_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS + $ SIGNATURE_1=DXj2Mn5FFQCZ5Hx5XsMX1UHGaGJtYYVLKfEYJng99JWS=4sQFJg338zP9bxX4Gw4KS58eXkpBB2pwjwo4szxCEVQZxrApzgYMN7riBYUnbvZPb84tsThPE1aHApiCCC9PSSP7 + $ SIGNER_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK + $ SIGNATURE_2=42uzyxAMNRFhvwd1jjFE7Fts693bDi7QKu1hTXxhmpAK=4UPUAV9USLFp8CKJ9u6gXhvUUFkpL2FTMbu3eJyyZ8DonjHJBEjUchuaM7j7tTaNWWF7zaRFfK5TkYvBytbV5vUR + $ FEE_PAYER="$HOME/.config/solana/id.json" + + $ BLOCKHASH=E12VZaDq99G7Tg38Jr7U2VWRCmxjzWzsow8dPMhA47Rm + $ WRAPPED_TOKEN_ACCOUNT=3FzdqSEo32BcFgTUqWL5QakZGQBRX91yBAQFo1vGsCji + $ MULTISIG_ADDRESS=FFQvYvhaWnHeGsCMfixccUMdnXPgDrkG3KkGzpfBHFPb + $ UNWRAPPED_TOKEN_RECIPIENT=DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + $ UNWRAPPED_MINT=BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + $ ESCROW_ACCOUNT=4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3 + $ UNWRAPPED_TOKEN_PROGRAM=TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + $ WRAPPED_TOKEN_PROGRAM=TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb + + $ spl-token-wrap unwrap $WRAPPED_TOKEN_ACCOUNT $ESCROW_ACCOUNT $UNWRAPPED_TOKEN_RECIPIENT 5 \ + --transfer-authority $MULTISIG_ADDRESS \ + --fee-payer $FEE_PAYER \ + --unwrapped-mint $UNWRAPPED_MINT \ + --wrapped-token-program $WRAPPED_TOKEN_PROGRAM \ + --unwrapped-token-program $UNWRAPPED_TOKEN_PROGRAM \ + --multisig-signer $SIGNER_1 \ + --multisig-signer $SIGNER_2 \ + --blockhash $BLOCKHASH \ + --signer $SIGNATURE_1 \ + --signer $SIGNATURE_2 + + Unwrapping 5 tokens from mint BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Unwrapped token program: TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA + Unwrapped mint address: BVpjjYmSgSPZbFGTXe52NYXApsDNQJRe2qQF1hQft85e + Recipient unwrapped token account: DKFjYKEFS4tkXjamwkuiGf555Lww3eRSWwNTbue9x14 + Amount unwrapped: 5 + Signers: + 5s2gNCExGchZJcnDTHMubyRsjuNprhyzaNeSbc34sJSLt5AB8N6V6uBoAegnBF1zvm1s65CPtjVLNH7Eb2hFYLsM + eEcQFqdsDZzKL5CAm43kofVvnXCy7ZTQFm3RS6iYg4nckrSUzWZctebDYqcUNqtxxBgnLHyeDZiYMxNABAYYt2x + 5fxWE9KQYFQHk2u9ienicDtuaRf9XWBvrmM48CBTxwtmpJXuxHxDzAYSM5atHe77rFTVsezbLCbuzirN1o5XdZTf + ``` + + Note all three needed signers in final broadcasted message. + + + + ```typescript + import { + address, + createKeyPairSignerFromBytes, + createNoopSigner, + createSolanaRpc, + createSolanaRpcSubscriptions, + getBase58Decoder, + partiallySignTransactionMessageWithSigners, + sendAndConfirmTransactionFactory, + } from '@solana/kit'; + import { TOKEN_2022_PROGRAM_ADDRESS } from '@solana-program/token-2022'; + import { + findWrappedMintAuthorityPda, + combinedMultisigTx, + multisigOfflineSignUnwrap + } from '@solana-program/token-wrap'; + + // Replace these consts with your own + const PAYER_KEYPAIR_BYTES = new Uint8Array([242, 30, 38, 177, 152, 71, ... ]); + const MULTISIG_SPL_TOKEN_2022 = address('BSdexGFqwmDGeXe4pBXVbQnqrEH5trmo9W3wqoXUQY5Y'); + const SIGNER_A_KEYPAIR_BYTES = new Uint8Array([210, 190, 232, 169, 113, 107, ... ]); + const SIGNER_B_KEYPAIR_BYTES = new Uint8Array([37, 161, 191, 225, 59, 192, ... ]); + const WRAPPED_MINT_ADDRESS = address('B8HbxGU4npjgjMX5xJFR2FYkgvAHdZqyVb8MyFvdsuNM'); + const UNWRAPPED_MINT_ADDRESS = address('E8r9ixwg7QYr6xCh4tSdHErZ6CUxQhVGHqF5bRoZXyyV'); + const UNWRAPPED_TOKEN_ACCOUNT = address('DGNyuKAWP3susy6XMbVsYHy2AMrrKmh8pXM3WpQUeyL2'); // Must be owned by multisig account + const UNWRAPPED_TOKEN_PROGRAM = address('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'); + const ESCROW = address('4NoeQJKuH8fu1Pqk5k8BJpNu4wA7T8K6QABJxjTWoHs3'); + const RECIPIENT = address('HKHfad5Rx7Vv1iWzPiQhx3cnXpbVfDonYRRo1e16x5Bt'); + const AMOUNT_TO_WRAP = 100n; + + async function main() { + const rpc = createSolanaRpc('http://127.0.0.1:8899'); + const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900'); + const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions }); + + const payer = await createKeyPairSignerFromBytes(PAYER_KEYPAIR_BYTES); + const { value: blockhash } = await rpc.getLatestBlockhash().send(); + + const [wrappedMintAuthority] = await findWrappedMintAuthorityPda({ wrappedMint: WRAPPED_MINT_ADDRESS }); + + const signerA = await createKeyPairSignerFromBytes(SIGNER_A_KEYPAIR_BYTES); + const signerB = await createKeyPairSignerFromBytes(SIGNER_B_KEYPAIR_BYTES); + + const { value: unwrapBlockhash } = await rpc.getLatestBlockhash().send(); + + const unwrapTxA = multisigOfflineSignUnwrap({ + payer: createNoopSigner(payer.address), + unwrappedEscrow: ESCROW, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + amount: AMOUNT_TO_WRAP, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + wrappedTokenAccount: RECIPIENT, + recipientUnwrappedToken: UNWRAPPED_TOKEN_ACCOUNT, + transferAuthority: MULTISIG_SPL_TOKEN_2022, + wrappedMint: WRAPPED_MINT_ADDRESS, + wrappedMintAuthority, + unwrappedTokenProgram: UNWRAPPED_TOKEN_PROGRAM, + multiSigners: [signerA, createNoopSigner(signerB.address)], + blockhash: unwrapBlockhash, + }); + const signedUnwrapTxA = await partiallySignTransactionMessageWithSigners(unwrapTxA); + + const unwrapTxB = multisigOfflineSignUnwrap({ + payer: createNoopSigner(payer.address), + unwrappedEscrow: ESCROW, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + amount: AMOUNT_TO_WRAP, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + wrappedTokenAccount: RECIPIENT, + recipientUnwrappedToken: UNWRAPPED_TOKEN_ACCOUNT, + transferAuthority: MULTISIG_SPL_TOKEN_2022, + wrappedMint: WRAPPED_MINT_ADDRESS, + wrappedMintAuthority, + unwrappedTokenProgram: UNWRAPPED_TOKEN_PROGRAM, + multiSigners: [createNoopSigner(signerA.address), signerB], + blockhash: unwrapBlockhash, + }); + const signedUnwrapTxB = await partiallySignTransactionMessageWithSigners(unwrapTxB); + + const unwrapTxC = multisigOfflineSignUnwrap({ + payer: payer, + unwrappedEscrow: ESCROW, + wrappedTokenProgram: TOKEN_2022_PROGRAM_ADDRESS, + amount: AMOUNT_TO_WRAP, + unwrappedMint: UNWRAPPED_MINT_ADDRESS, + wrappedTokenAccount: RECIPIENT, + recipientUnwrappedToken: UNWRAPPED_TOKEN_ACCOUNT, + transferAuthority: MULTISIG_SPL_TOKEN_2022, + wrappedMint: WRAPPED_MINT_ADDRESS, + wrappedMintAuthority, + unwrappedTokenProgram: UNWRAPPED_TOKEN_PROGRAM, + multiSigners: [createNoopSigner(signerA.address), createNoopSigner(signerB.address)], + blockhash: unwrapBlockhash, + }); + const signedUnwrapTxC = await partiallySignTransactionMessageWithSigners(unwrapTxC); + + const combinedUnwrapTx = combinedMultisigTx({ + signedTxs: [signedUnwrapTxA, signedUnwrapTxB, signedUnwrapTxC], + blockhash, + }); + await sendAndConfirm(combinedUnwrapTx, { commitment: 'confirmed' }); + + console.log('======== Multisig Unwrap Successful ========'); + for (const [pubkey, signature] of Object.entries(combinedUnwrapTx.signatures)) { + if (signature) { + const base58Sig = getBase58Decoder().decode(signature); + console.log(`pubkey: ${pubkey}`); + console.log(`signature: ${base58Sig}`); + console.log('-----'); + } + } + } + + void main(); + ``` + + \ No newline at end of file