diff --git a/programs/cardinal-token-manager/src/instructions/invalidate.rs b/programs/cardinal-token-manager/src/instructions/invalidate.rs index 4db1fe0bf..da7c833c1 100644 --- a/programs/cardinal-token-manager/src/instructions/invalidate.rs +++ b/programs/cardinal-token-manager/src/instructions/invalidate.rs @@ -166,6 +166,36 @@ pub fn handler<'key, 'accounts, 'remaining, 'info>(ctx: Context<'key, 'accounts, }; } t if t == InvalidationType::Release as u8 => { + // https://github.com/solana-labs/solana-program-library/pull/2872 + // remove delegate + // let cpi_accounts = Revoke { + // source: ctx.accounts.recipient_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::revoke(cpi_context)?; + + // transfer 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)?; + + // transfer back to receipient unlocked + let cpi_accounts = Transfer { + from: ctx.accounts.token_manager_token_account.to_account_info(), + to: ctx.accounts.recipient_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)?; + // close token_manager_token_account let cpi_accounts = CloseAccount { account: ctx.accounts.token_manager_token_account.to_account_info(), diff --git a/tests/timeInvalidationRelease.spec.ts b/tests/timeInvalidationRelease.spec.ts new file mode 100644 index 000000000..60f8c71f5 --- /dev/null +++ b/tests/timeInvalidationRelease.spec.ts @@ -0,0 +1,191 @@ +import { BN } from "@project-serum/anchor"; +import { expectTXTable } from "@saberhq/chai-solana"; +import { + SignerWallet, + SolanaProvider, + TransactionEnvelope, +} from "@saberhq/solana-contrib"; +import type { Token } from "@solana/spl-token"; +import type { PublicKey } from "@solana/web3.js"; +import { Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js"; +import { expect } from "chai"; + +import { findAta, invalidate, rentals, tryGetAccount } from "../src"; +import { tokenManager } from "../src/programs"; +import { + InvalidationType, + TokenManagerState, +} from "../src/programs/tokenManager"; +import { createMint } from "./utils"; +import { getProvider } from "./workspace"; + +describe("Time invalidation release", () => { + const recipient = Keypair.generate(); + const tokenCreator = Keypair.generate(); + let issuerTokenAccountId: PublicKey; + let rentalMint: Token; + + before(async () => { + const provider = getProvider(); + const airdropCreator = await provider.connection.requestAirdrop( + tokenCreator.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); + + // create rental mint + [issuerTokenAccountId, rentalMint] = await createMint( + provider.connection, + tokenCreator, + provider.wallet.publicKey, + 1, + provider.wallet.publicKey + ); + }); + + it("Create rental", async () => { + const provider = getProvider(); + const [transaction, tokenManagerId] = await rentals.createRental( + provider.connection, + provider.wallet, + { + timeInvalidation: { maxExpiration: Date.now() / 1000 + 1 }, + mint: rentalMint.publicKey, + issuerTokenAccountId: issuerTokenAccountId, + amount: new BN(1), + invalidationType: InvalidationType.Release, + } + ); + const txEnvelope = new TransactionEnvelope( + SolanaProvider.init({ + connection: provider.connection, + wallet: provider.wallet, + opts: provider.opts, + }), + [...transaction.instructions] + ); + await expectTXTable(txEnvelope, "create", { + verbosity: "error", + formatLogs: true, + }).to.be.fulfilled; + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).to.eq(TokenManagerState.Issued); + expect(tokenManagerData.parsed.amount.toNumber()).to.eq(1); + expect(tokenManagerData.parsed.mint).to.eqAddress(rentalMint.publicKey); + expect(tokenManagerData.parsed.invalidators).length.greaterThanOrEqual(1); + expect(tokenManagerData.parsed.issuer).to.eqAddress( + provider.wallet.publicKey + ); + + const checkIssuerTokenAccount = await rentalMint.getAccountInfo( + issuerTokenAccountId + ); + expect(checkIssuerTokenAccount.amount.toNumber()).to.eq(0); + }); + + it("Claim rental", async () => { + const provider = getProvider(); + + const tokenManagerId = await tokenManager.pda.tokenManagerAddressFromMint( + provider.connection, + rentalMint.publicKey + ); + + const transaction = await rentals.claimRental( + provider.connection, + new SignerWallet(recipient), + tokenManagerId + ); + + const txEnvelope = new TransactionEnvelope( + SolanaProvider.init({ + connection: provider.connection, + wallet: new SignerWallet(recipient), + opts: provider.opts, + }), + [...transaction.instructions] + ); + + await expectTXTable(txEnvelope, "claim", { + verbosity: "error", + formatLogs: true, + }).to.be.fulfilled; + + const tokenManagerData = await tokenManager.accounts.getTokenManager( + provider.connection, + tokenManagerId + ); + expect(tokenManagerData.parsed.state).to.eq(TokenManagerState.Claimed); + expect(tokenManagerData.parsed.amount.toNumber()).to.eq(1); + + const checkIssuerTokenAccount = await rentalMint.getAccountInfo( + issuerTokenAccountId + ); + expect(checkIssuerTokenAccount.amount.toNumber()).to.eq(0); + + const checkRecipientTokenAccount = await rentalMint.getAccountInfo( + await findAta(rentalMint.publicKey, recipient.publicKey) + ); + expect(checkRecipientTokenAccount.amount.toNumber()).to.eq(1); + }); + + it("Invalidate", async () => { + await new Promise((r) => setTimeout(r, 2000)); + + const provider = getProvider(); + const transaction = await invalidate( + provider.connection, + new SignerWallet(recipient), + rentalMint.publicKey + ); + + const txEnvelope = new TransactionEnvelope( + SolanaProvider.init({ + connection: provider.connection, + wallet: new SignerWallet(recipient), + opts: provider.opts, + }), + [...transaction.instructions] + ); + + await expectTXTable(txEnvelope, "use", { + verbosity: "error", + formatLogs: true, + }).to.be.fulfilled; + + const tokenManagerId = await tokenManager.pda.tokenManagerAddressFromMint( + provider.connection, + rentalMint.publicKey + ); + + const tokenManagerData = await tryGetAccount(() => + tokenManager.accounts.getTokenManager(provider.connection, tokenManagerId) + ); + expect(tokenManagerData).to.eq(null); + + const checkIssuerTokenAccount = await rentalMint.getAccountInfo( + issuerTokenAccountId + ); + expect(checkIssuerTokenAccount.amount.toNumber()).to.eq(0); + console.log(checkIssuerTokenAccount); + + const checkRecipientTokenAccount = await rentalMint.getAccountInfo( + await findAta(rentalMint.publicKey, recipient.publicKey) + ); + console.log(checkRecipientTokenAccount); + expect(checkRecipientTokenAccount.amount.toNumber()).to.eq(1); + expect(checkRecipientTokenAccount.isFrozen).to.eq(false); + expect(checkRecipientTokenAccount.delegatedAmount.toNumber()).to.eq(0); + expect(checkRecipientTokenAccount.delegate).to.eq(null); + }); +}); diff --git a/tools/getPaymentManager.ts b/tools/getPaymentManager.ts index 31d392f01..e5636c003 100644 --- a/tools/getPaymentManager.ts +++ b/tools/getPaymentManager.ts @@ -1,12 +1,9 @@ -import { connectionFor } from "./utils"; import { getPaymentManager } from "../src/programs/paymentManager/accounts"; import { findPaymentManagerAddress } from "../src/programs/paymentManager/pda"; import { tryGetAccount } from "../src"; +import { connectionFor } from "./connection"; -const main = async ( - paymentManagerName: string, - cluster = "mainnet" -) => { +const main = async (paymentManagerName: string, cluster = "mainnet") => { const connection = connectionFor(cluster); const [paymentManagerId] = await findPaymentManagerAddress( paymentManagerName @@ -17,7 +14,10 @@ const main = async ( if (!paymentManagerData) { console.log("Error: Failed to create payment manager"); } else { - console.log(`Created payment manager ${paymentManagerName} (${paymentManagerId.toString()})`, paymentManagerData.parsed.feeCollector.toString()); + console.log( + `Created payment manager ${paymentManagerName} (${paymentManagerId.toString()})`, + paymentManagerData.parsed.feeCollector.toString() + ); } };