diff --git a/src/actions/addTokensToVault.ts b/src/actions/addTokensToVault.ts new file mode 100644 index 0000000..6f6c4d4 --- /dev/null +++ b/src/actions/addTokensToVault.ts @@ -0,0 +1,101 @@ +import BN from 'bn.js'; +import { AccountLayout } from '@solana/spl-token'; +import { + Vault, + SafetyDepositBox, + AddTokenToInactiveVault, +} from '@metaplex-foundation/mpl-token-vault'; +import { Connection, TransactionSignature, PublicKey, Keypair } from '@solana/web3.js'; + +import { Wallet } from '../wallet'; +import { createApproveTxs } from './shared'; +import { sendTransaction } from './transactions'; +import { CreateTokenAccount } from '../programs'; +import { TransactionsBatch } from '../utils/transactions-batch'; + +interface Token2Add { + tokenAccount: PublicKey; + tokenMint: PublicKey; + amount: BN; +} + +interface SafetyDepositTokenStore { + txId: TransactionSignature; + tokenAccount: PublicKey; + tokenStoreAccount: PublicKey; + tokenMint: PublicKey; +} + +interface AddTokensToVaultParams { + connection: Connection; + wallet: Wallet; + vault: PublicKey; + nfts: Token2Add[]; +} + +interface AddTokensToVaultResponse { + safetyDepositTokenStores: SafetyDepositTokenStore[]; +} + +export const addTokensToVault = async ({ + connection, + wallet, + vault, + nfts, +}: AddTokensToVaultParams): Promise => { + const txOptions = { feePayer: wallet.publicKey }; + const safetyDepositTokenStores: SafetyDepositTokenStore[] = []; + + const vaultAuthority = await Vault.getPDA(vault); + const accountRent = await connection.getMinimumBalanceForRentExemption(AccountLayout.span); + + for (const nft of nfts) { + const tokenTxBatch = new TransactionsBatch({ transactions: [] }); + const safetyDepositBox = await SafetyDepositBox.getPDA(vault, nft.tokenMint); + + const tokenStoreAccount = Keypair.generate(); + const tokenStoreAccountTx = new CreateTokenAccount(txOptions, { + newAccountPubkey: tokenStoreAccount.publicKey, + lamports: accountRent, + mint: nft.tokenMint, + owner: vaultAuthority, + }); + tokenTxBatch.addTransaction(tokenStoreAccountTx); + tokenTxBatch.addSigner(tokenStoreAccount); + + const { authority: transferAuthority, createApproveTx } = createApproveTxs({ + account: nft.tokenAccount, + owner: wallet.publicKey, + amount: nft.amount.toNumber(), + }); + tokenTxBatch.addTransaction(createApproveTx); + tokenTxBatch.addSigner(transferAuthority); + + const addTokenTx = new AddTokenToInactiveVault(txOptions, { + vault, + vaultAuthority: wallet.publicKey, + tokenAccount: nft.tokenAccount, + tokenStoreAccount: tokenStoreAccount.publicKey, + transferAuthority: transferAuthority.publicKey, + safetyDepositBox: safetyDepositBox, + amount: nft.amount, + }); + tokenTxBatch.addTransaction(addTokenTx); + + const txId = await sendTransaction({ + connection, + wallet, + txs: tokenTxBatch.transactions, + signers: tokenTxBatch.signers, + }); + + safetyDepositTokenStores.push({ + txId, + tokenStoreAccount: tokenStoreAccount.publicKey, + tokenMint: nft.tokenMint, + tokenAccount: nft.tokenAccount, + }); + } + + return { safetyDepositTokenStores }; +}; diff --git a/src/actions/index.ts b/src/actions/index.ts index 8aa2633..1d554fb 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -1,3 +1,4 @@ +export * from './addTokensToVault'; export * from './transactions'; export * from './initStore'; export * from './initStoreV2'; diff --git a/test/actions/addTokensToVault.test.ts b/test/actions/addTokensToVault.test.ts new file mode 100644 index 0000000..5d99107 --- /dev/null +++ b/test/actions/addTokensToVault.test.ts @@ -0,0 +1,66 @@ +import BN from 'bn.js'; +import { Transaction } from '@metaplex-foundation/mpl-core'; +import { airdrop, LOCALHOST } from '@metaplex-foundation/amman'; +import { Keypair, sendAndConfirmTransaction } from '@solana/web3.js'; + +import { Connection, NodeWallet } from '../../src'; +import { + addTokensToVault, + createExternalPriceAccount, + createVault, + prepareTokenAccountAndMintTxs, +} from '../../src/actions'; + +describe('addTokensToVault action', () => { + test('creation and adding of multiple mint tokens to newly created vault', async () => { + const payer = Keypair.generate(); + const wallet = new NodeWallet(payer); + const connection = new Connection(LOCALHOST, 'confirmed'); + await airdrop(connection, payer.publicKey, 10); + + const TOKEN_AMOUNT = 2; + const externalPriceAccountData = await createExternalPriceAccount({ connection, wallet }); + + const { vault } = await createVault({ + connection, + wallet, + ...externalPriceAccountData, + }); + + const testNfts = []; + + for (let i = 0; i < TOKEN_AMOUNT; i++) { + const { + mint, + recipient: tokenAccount, + createAssociatedTokenAccountTx, + createMintTx, + mintToTx, + } = await prepareTokenAccountAndMintTxs(connection, wallet.publicKey); + + await sendAndConfirmTransaction( + connection, + Transaction.fromCombined([createMintTx, createAssociatedTokenAccountTx, mintToTx]), + [payer, mint, wallet.payer], + ); + + testNfts.push({ + tokenAccount, + tokenMint: mint.publicKey, + amount: new BN(1), + }); + } + + const { safetyDepositTokenStores } = await addTokensToVault({ + connection, + wallet, + vault, + nfts: testNfts, + }); + + expect(safetyDepositTokenStores.length).toEqual(testNfts.length); + expect(safetyDepositTokenStores.map(({ tokenMint }) => tokenMint).join(',')).toEqual( + testNfts.map(({ tokenMint }) => tokenMint).join(','), + ); + }); +});