diff --git a/programs/cardinal-token-manager/src/instructions/invalidate.rs b/programs/cardinal-token-manager/src/instructions/invalidate.rs index 40c10daba..884e4c69c 100644 --- a/programs/cardinal-token-manager/src/instructions/invalidate.rs +++ b/programs/cardinal-token-manager/src/instructions/invalidate.rs @@ -766,15 +766,157 @@ pub fn handler<'key, 'accounts, 'remaining, 'info>(ctx: Context<'key, 'accounts, token_manager.close(ctx.accounts.collector.to_account_info())?; } t if t == InvalidationType::Reissue as u8 => { - // transfer back to token_manager - let cpi_accounts = Transfer { - from: ctx.accounts.recipient_token_account.to_account_info(), - to: ctx.accounts.token_manager_token_account.to_account_info(), - authority: token_manager.to_account_info(), - }; - let cpi_program = ctx.accounts.token_program.to_account_info(); - let cpi_context = CpiContext::new(cpi_program, cpi_accounts).with_signer(token_manager_signer); - token::transfer(cpi_context, token_manager.amount)?; + match token_manager.kind { + k if k == TokenManagerKind::Programmable as u8 => { + // get PDA seeds to sign with + let token_manager_seeds = &[TOKEN_MANAGER_SEED.as_bytes(), token_manager.mint.as_ref(), &[token_manager.bump]]; + let token_manager_signer = &[&token_manager_seeds[..]]; + + let recipient_token_account_owner_info = next_account_info(remaining_accs)?; + let payer_info = next_account_info(remaining_accs)?; + let system_program_info = next_account_info(remaining_accs)?; + let token_manager_token_record = next_account_info(remaining_accs)?; + let mint_info = next_account_info(remaining_accs)?; + let mint_metadata_info = next_account_info(remaining_accs)?; + let mint_edition_info = next_account_info(remaining_accs)?; + let from_token_record = next_account_info(remaining_accs)?; + let sysvar_instructions_info = next_account_info(remaining_accs)?; + let associated_token_program_info = next_account_info(remaining_accs)?; + let authorization_rules_program_info = next_account_info(remaining_accs)?; + let authorization_rules_info = next_account_info(remaining_accs)?; + + invoke_signed( + &Instruction { + program_id: mpl_token_metadata::id(), + accounts: vec![ + // 0. `[signer]` Delegate + AccountMeta::new_readonly(token_manager.key(), true), + // 1. `[optional]` Token owner + AccountMeta::new_readonly(recipient_token_account_owner_info.key(), false), + // 2. `[writable]` Token account + AccountMeta::new(ctx.accounts.recipient_token_account.key(), false), + // 3. `[]` Mint account + AccountMeta::new_readonly(mint_info.key(), false), + // 4. `[writable]` Metadata account + AccountMeta::new(mint_metadata_info.key(), false), + // 5. `[optional]` Edition account + AccountMeta::new_readonly(mint_edition_info.key(), false), + // 6. `[optional, writable]` Token record account + AccountMeta::new(from_token_record.key(), false), + // 7. `[signer, writable]` Payer + AccountMeta::new(payer_info.key(), true), + // 8. `[]` System Program + AccountMeta::new_readonly(system_program_info.key(), false), + // 9. `[]` Instructions sysvar account + AccountMeta::new_readonly(sysvar_instructions_info.key(), false), + // 10. `[optional]` SPL Token Program + AccountMeta::new_readonly(ctx.accounts.token_program.key(), false), + // 11. `[optional]` Token Authorization Rules program + AccountMeta::new_readonly(authorization_rules_program_info.key(), false), + // 12. `[optional]` Token Authorization Rules account + AccountMeta::new_readonly(authorization_rules_info.key(), false), + ], + data: MetadataInstruction::Unlock(UnlockArgs::V1 { authorization_data: None }).try_to_vec().unwrap(), + }, + &[ + token_manager.to_account_info(), + recipient_token_account_owner_info.to_account_info(), + ctx.accounts.recipient_token_account.to_account_info(), + mint_info.to_account_info(), + ctx.accounts.recipient_token_account.to_account_info(), + mint_info.to_account_info(), + mint_metadata_info.to_account_info(), + mint_edition_info.to_account_info(), + from_token_record.to_account_info(), + payer_info.to_account_info(), + system_program_info.to_account_info(), + sysvar_instructions_info.to_account_info(), + ctx.accounts.token_program.to_account_info(), + authorization_rules_program_info.to_account_info(), + authorization_rules_info.to_account_info(), + ], + token_manager_signer, + )?; + + invoke_signed( + &Instruction { + program_id: mpl_token_metadata::id(), + accounts: vec![ + // #[account(0, writable, name="token", desc="Token account")] + AccountMeta::new(ctx.accounts.recipient_token_account.key(), false), + // #[account(1, name="token_owner", desc="Token account owner")] + AccountMeta::new_readonly(ctx.accounts.recipient_token_account.owner.key(), false), + // #[account(2, writable, name="destination", desc="Destination token account")] + AccountMeta::new(ctx.accounts.token_manager_token_account.key(), false), + // #[account(3, name="destination_owner", desc="Destination token account owner")] + AccountMeta::new_readonly(token_manager.key(), false), + // #[account(4, name="mint", desc="Mint of token asset")] + AccountMeta::new_readonly(mint_info.key(), false), + // #[account(5, writable, name="metadata", desc="Metadata (pda of ['metadata', program id, mint id])")] + AccountMeta::new(mint_metadata_info.key(), false), + // #[account(6, optional, name="edition", desc="Edition of token asset")] + AccountMeta::new_readonly(mint_edition_info.key(), false), + // #[account(7, optional, writable, name="recipient_token_record", desc="Owner token record account")] + AccountMeta::new(from_token_record.key(), false), + // #[account(8, optional, writable, name="destination_token_record", desc="Destination token record account")] + AccountMeta::new(token_manager_token_record.key(), false), + // #[account(9, signer, name="authority", desc="Transfer authority (token owner or delegate)")] + AccountMeta::new_readonly(token_manager.key(), true), + // #[account(10, signer, writable, name="payer", desc="Payer")] + AccountMeta::new(payer_info.key(), true), + // #[account(11, name="system_program", desc="System Program")] + AccountMeta::new_readonly(system_program_info.key(), false), + // #[account(12, name="sysvar_instructions", desc="Instructions sysvar account")] + AccountMeta::new_readonly(sysvar_instructions_info.key(), false), + // #[account(13, name="spl_token_program", desc="SPL Token Program")] + AccountMeta::new_readonly(ctx.accounts.token_program.key(), false), + // #[account(14, name="spl_ata_program", desc="SPL Associated Token Account program")] + AccountMeta::new_readonly(associated_token_program_info.key(), false), + // #[account(15, optional, name="authorization_rules_program", desc="Token Authorization Rules Program")] + AccountMeta::new_readonly(authorization_rules_program_info.key(), false), + // #[account(16, optional, name="authorization_rules", desc="Token Authorization Rules account")] + AccountMeta::new_readonly(authorization_rules_info.key(), false), + ], + data: MetadataInstruction::Transfer(TransferArgs::V1 { + amount: token_manager.amount, + authorization_data: None, + }) + .try_to_vec() + .unwrap(), + }, + &[ + ctx.accounts.recipient_token_account.to_account_info(), + recipient_token_account_owner_info.to_account_info(), + ctx.accounts.token_manager_token_account.to_account_info(), + token_manager.to_account_info(), + mint_info.to_account_info(), + mint_metadata_info.to_account_info(), + mint_edition_info.to_account_info(), + from_token_record.to_account_info(), + token_manager_token_record.to_account_info(), + payer_info.to_account_info(), + system_program_info.to_account_info(), + sysvar_instructions_info.to_account_info(), + ctx.accounts.token_program.to_account_info(), + associated_token_program_info.to_account_info(), + authorization_rules_program_info.to_account_info(), + authorization_rules_info.to_account_info(), + ], + token_manager_signer, + )?; + } + _ => { + // transfer back to token_manager + let cpi_accounts = Transfer { + from: ctx.accounts.recipient_token_account.to_account_info(), + to: ctx.accounts.token_manager_token_account.to_account_info(), + authority: token_manager.to_account_info(), + }; + let cpi_program = ctx.accounts.token_program.to_account_info(); + let cpi_context = CpiContext::new(cpi_program, cpi_accounts).with_signer(token_manager_signer); + token::transfer(cpi_context, token_manager.amount)?; + } + } token_manager.state = TokenManagerState::Issued as u8; token_manager.recipient_token_account = ctx.accounts.token_manager_token_account.key(); diff --git a/src/programs/tokenManager/utils.ts b/src/programs/tokenManager/utils.ts index d375485b7..22e54f115 100644 --- a/src/programs/tokenManager/utils.ts +++ b/src/programs/tokenManager/utils.ts @@ -187,11 +187,12 @@ export const withRemainingAccountsForInvalidate = async ( } if ( - tokenManagerData.parsed.invalidationType === InvalidationType.Release && - tokenManagerData.parsed.kind === TokenManagerKind.Programmable + tokenManagerData.parsed.kind === TokenManagerKind.Programmable && + (tokenManagerData.parsed.invalidationType === InvalidationType.Release || + tokenManagerData.parsed.invalidationType === InvalidationType.Reissue) ) { if (!metadata?.programmableConfig?.ruleSet) throw "Ruleset not specified"; - const releaseAccounts = remainingAccountForProgrammableRelease( + const releaseAccounts = remainingAccountForProgrammableUnlockAndTransfer( recipientTokenAccountOwnerId, wallet.publicKey, mintId, @@ -491,7 +492,7 @@ export const remainingAccountForProgrammable = ( ]; }; -export const remainingAccountForProgrammableRelease = ( +export const remainingAccountForProgrammableUnlockAndTransfer = ( recipient: PublicKey, payer: PublicKey, mintId: PublicKey, diff --git a/src/transaction.ts b/src/transaction.ts index f67e1200b..6654b1772 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -561,6 +561,18 @@ export const withClaimToken = async ( tokenManagerId, wallet.publicKey ); + + if ( + tokenManagerData.parsed.kind === TokenManagerKind.Programmable || + metadata?.tokenStandard === TokenStandard.ProgrammableNonFungible + ) { + transaction.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: 400000, + }) + ); + } + // pay claim approver if ( claimApproverData?.parsed && diff --git a/tests/programmable/programmableRentalReissue.test.ts b/tests/programmable/programmableRentalReissue.test.ts new file mode 100644 index 000000000..6e7322553 --- /dev/null +++ b/tests/programmable/programmableRentalReissue.test.ts @@ -0,0 +1,288 @@ +import type { CardinalProvider } from "@cardinal/common"; +import { + executeTransaction, + findAta, + getTestProvider, + newAccountWithLamports, +} from "@cardinal/common"; +import { beforeAll, expect } from "@jest/globals"; +import { Wallet } from "@project-serum/anchor"; +import { getAccount, getAssociatedTokenAddressSync } from "@solana/spl-token"; +import type { Keypair, PublicKey } from "@solana/web3.js"; +import { LAMPORTS_PER_SOL } from "@solana/web3.js"; + +import { claimToken, invalidate, issueToken } from "../../src"; +import { tokenManager } from "../../src/programs"; +import { + InvalidationType, + TokenManagerKind, + TokenManagerState, +} from "../../src/programs/tokenManager"; +import { findTokenManagerAddress } from "../../src/programs/tokenManager/pda"; +import { createProgrammableAsset } from "../utils"; + +describe("Programmable rental reissue", () => { + let provider: CardinalProvider; + let recipient: Keypair; + let issuer: Keypair; + let invalidator: Keypair; + let issuerTokenAccountId: PublicKey; + let mintId: PublicKey; + let rulesetId: PublicKey; + + beforeAll(async () => { + provider = await getTestProvider(); + recipient = await newAccountWithLamports(provider.connection); + issuer = await newAccountWithLamports(provider.connection); + invalidator = await newAccountWithLamports(provider.connection); + const airdropCreator = await provider.connection.requestAirdrop( + issuer.publicKey, + LAMPORTS_PER_SOL + ); + await provider.connection.confirmTransaction(airdropCreator); + + const airdropRecipient = await provider.connection.requestAirdrop( + recipient.publicKey, + LAMPORTS_PER_SOL + ); + await provider.connection.confirmTransaction(airdropRecipient); + [issuerTokenAccountId, mintId, rulesetId] = await createProgrammableAsset( + provider.connection, + new Wallet(issuer) + ); + }); + + it("Issue token", async () => { + const [transaction, tokenManagerId] = await issueToken( + provider.connection, + new Wallet(issuer), + { + mint: mintId, + issuerTokenAccountId: issuerTokenAccountId, + kind: TokenManagerKind.Programmable, + rulesetId: rulesetId, + invalidationType: InvalidationType.Reissue, + customInvalidators: [invalidator.publicKey], + } + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(issuer) + ); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Issued); + expect(tokenManagerData.parsed.invalidationType).toEqual( + InvalidationType.Reissue + ); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual(mintId.toString()); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + expect(tokenManagerData.parsed.issuer.toString()).toEqual( + issuer.publicKey.toString() + ); + + const checkIssuerTokenAccount = await getAccount( + provider.connection, + issuerTokenAccountId + ); + expect(checkIssuerTokenAccount.amount.toString()).toEqual("0"); + + // check receipt-index + const tokenManagers = await tokenManager.accounts.getTokenManagersForIssuer( + provider.connection, + issuer.publicKey + ); + expect(tokenManagers.map((i) => i.pubkey.toString())).toContain( + tokenManagerId.toString() + ); + }); + + it("Claim token", async () => { + const tokenManagerId = findTokenManagerAddress(mintId); + const transaction = await claimToken( + provider.connection, + new Wallet(recipient), + tokenManagerId + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(recipient) + ); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Claimed); + expect(tokenManagerData.parsed.invalidationType).toEqual( + InvalidationType.Reissue + ); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual(mintId.toString()); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + expect(tokenManagerData.parsed.issuer.toString()).toEqual( + issuer.publicKey.toString() + ); + + const recipientTokenAccountId = getAssociatedTokenAddressSync( + mintId, + recipient.publicKey + ); + const receipientTokenAccount = await getAccount( + provider.connection, + recipientTokenAccountId + ); + expect(receipientTokenAccount.amount.toString()).toEqual("1"); + }); + + it("Invalidate", async () => { + const tokenManagerId = findTokenManagerAddress(mintId); + const tokenManagerAccountBefore = await provider.connection.getAccountInfo( + tokenManagerId + ); + + const transaction = await invalidate( + provider.connection, + new Wallet(invalidator), + mintId + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(invalidator) + ); + + const tokenManagerAccountAfter = await provider.connection.getAccountInfo( + tokenManagerId + ); + expect( + (tokenManagerAccountBefore?.lamports || 0) - + (tokenManagerAccountAfter?.lamports || 0) + ).toEqual(5000000); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Issued); + expect(tokenManagerData.parsed.invalidationType).toEqual( + InvalidationType.Reissue + ); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual(mintId.toString()); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + expect(tokenManagerData.parsed.issuer.toString()).toEqual( + issuer.publicKey.toString() + ); + + const recipientAtaId = await findAta(mintId, recipient.publicKey); + const checkRecipientTokenAccount = await getAccount( + provider.connection, + recipientAtaId + ); + expect(checkRecipientTokenAccount.amount.toString()).toEqual("0"); + }); + + it("Claim token again", async () => { + const tokenManagerId = findTokenManagerAddress(mintId); + const transaction = await claimToken( + provider.connection, + new Wallet(recipient), + tokenManagerId + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(recipient) + ); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Claimed); + expect(tokenManagerData.parsed.invalidationType).toEqual( + InvalidationType.Reissue + ); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual(mintId.toString()); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + expect(tokenManagerData.parsed.issuer.toString()).toEqual( + issuer.publicKey.toString() + ); + + const recipientTokenAccountId = getAssociatedTokenAddressSync( + mintId, + recipient.publicKey + ); + const receipientTokenAccount = await getAccount( + provider.connection, + recipientTokenAccountId + ); + expect(receipientTokenAccount.amount.toString()).toEqual("1"); + }); + + it("Invalidate", async () => { + const tokenManagerId = findTokenManagerAddress(mintId); + const tokenManagerAccountBefore = await provider.connection.getAccountInfo( + tokenManagerId + ); + + const transaction = await invalidate( + provider.connection, + new Wallet(invalidator), + mintId + ); + await executeTransaction( + provider.connection, + transaction, + new Wallet(invalidator) + ); + + const tokenManagerAccountAfter = await provider.connection.getAccountInfo( + tokenManagerId + ); + expect( + (tokenManagerAccountBefore?.lamports || 0) - + (tokenManagerAccountAfter?.lamports || 0) + ).toEqual(5000000); + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).toEqual(TokenManagerState.Issued); + expect(tokenManagerData.parsed.invalidationType).toEqual( + InvalidationType.Reissue + ); + expect(tokenManagerData.parsed.amount.toNumber()).toEqual(1); + expect(tokenManagerData.parsed.mint.toString()).toEqual(mintId.toString()); + expect(tokenManagerData.parsed.invalidators.length).toBeGreaterThanOrEqual( + 1 + ); + expect(tokenManagerData.parsed.issuer.toString()).toEqual( + issuer.publicKey.toString() + ); + + const recipientAtaId = await findAta(mintId, recipient.publicKey); + const checkRecipientTokenAccount = await getAccount( + provider.connection, + recipientAtaId + ); + expect(checkRecipientTokenAccount.amount.toString()).toEqual("0"); + }); +});