diff --git a/Cargo.lock b/Cargo.lock index c6e34c1a9..bf3c7cfcb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -407,7 +407,7 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "cardinal-paid-claim-approver" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anchor-lang", "anchor-spl", @@ -421,7 +421,7 @@ dependencies = [ [[package]] name = "cardinal-payment-manager" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anchor-lang", "anchor-spl", @@ -434,7 +434,7 @@ dependencies = [ [[package]] name = "cardinal-rent-receipt" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anchor-lang", "anchor-spl", @@ -447,7 +447,7 @@ dependencies = [ [[package]] name = "cardinal-rental-counter" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anchor-lang", "anchor-spl", @@ -460,7 +460,7 @@ dependencies = [ [[package]] name = "cardinal-time-invalidator" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anchor-lang", "anchor-spl", @@ -473,7 +473,7 @@ dependencies = [ [[package]] name = "cardinal-token-manager" -version = "0.0.2" +version = "0.0.3" dependencies = [ "anchor-lang", "anchor-spl", diff --git a/package.json b/package.json index 9485cde9e..09f37cfb5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@cardinal/token-manager", - "version": "0.0.2", + "version": "0.0.3", "description": "Cardinal token manager SDK", "keywords": [ "solana", diff --git a/programs/cardinal-paid-claim-approver/Cargo.toml b/programs/cardinal-paid-claim-approver/Cargo.toml index 15bba0f6c..2b49a28a2 100644 --- a/programs/cardinal-paid-claim-approver/Cargo.toml +++ b/programs/cardinal-paid-claim-approver/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardinal-paid-claim-approver" -version = "0.0.2" +version = "0.0.3" description = "Cardinal paid claim approver" edition = "2021" homepage = "https://cardinal.so" @@ -25,8 +25,8 @@ anchor-spl = "0.20.1" spl-associated-token-account = "1.0.2" spl-token = { version = "3.1.1", features = ["no-entrypoint"] } solana-program = "1.8.1" -cardinal-token-manager = { version = "^0.0.2", path = "../cardinal-token-manager", features = ["cpi"] } -cardinal-payment-manager = { version = "^0.0.2", path = "../cardinal-payment-manager", features = ["cpi"] } +cardinal-token-manager = { version = "^0.0.3", path = "../cardinal-token-manager", features = ["cpi"] } +cardinal-payment-manager = { version = "^0.0.3", path = "../cardinal-payment-manager", features = ["cpi"] } [dev-dependencies] proptest = { version = "1.0" } \ No newline at end of file diff --git a/programs/cardinal-payment-manager/Cargo.toml b/programs/cardinal-payment-manager/Cargo.toml index f0fdbf42b..c6ee19722 100644 --- a/programs/cardinal-payment-manager/Cargo.toml +++ b/programs/cardinal-payment-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardinal-payment-manager" -version = "0.0.2" +version = "0.0.3" description = "Cardinal paid claim approver" edition = "2021" homepage = "https://cardinal.so" @@ -25,7 +25,7 @@ anchor-spl = "0.20.1" spl-associated-token-account = "1.0.2" spl-token = { version = "3.1.1", features = ["no-entrypoint"] } solana-program = "1.8.1" -cardinal-token-manager = { version = "^0.0.2", path = "../cardinal-token-manager", features = ["cpi"] } +cardinal-token-manager = { version = "^0.0.3", path = "../cardinal-token-manager", features = ["cpi"] } [dev-dependencies] proptest = { version = "1.0" } \ No newline at end of file diff --git a/programs/cardinal-rent-receipt/Cargo.toml b/programs/cardinal-rent-receipt/Cargo.toml index 37b404389..618b6f1d4 100644 --- a/programs/cardinal-rent-receipt/Cargo.toml +++ b/programs/cardinal-rent-receipt/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardinal-rent-receipt" -version = "0.0.2" +version = "0.0.3" description = "Cardinal paid claim approver" edition = "2021" homepage = "https://cardinal.so" @@ -25,7 +25,7 @@ anchor-spl = "0.20.1" spl-associated-token-account = "1.0.2" spl-token = { version = "3.1.1", features = ["no-entrypoint"] } solana-program = "1.8.1" -cardinal-token-manager = { version = "^0.0.2", path = "../cardinal-token-manager", features = ["cpi"] } +cardinal-token-manager = { version = "^0.0.3", path = "../cardinal-token-manager", features = ["cpi"] } [dev-dependencies] proptest = { version = "1.0" } \ No newline at end of file diff --git a/programs/cardinal-rental-counter/Cargo.toml b/programs/cardinal-rental-counter/Cargo.toml index e90f2ca1d..332347e9b 100644 --- a/programs/cardinal-rental-counter/Cargo.toml +++ b/programs/cardinal-rental-counter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardinal-rental-counter" -version = "0.0.2" +version = "0.0.3" description = "Cardinal paid claim approver" edition = "2021" homepage = "https://cardinal.so" @@ -25,7 +25,7 @@ anchor-spl = "0.20.1" spl-associated-token-account = "1.0.2" spl-token = { version = "3.1.1", features = ["no-entrypoint"] } solana-program = "1.8.1" -cardinal-token-manager = { version = "^0.0.2", path = "../cardinal-token-manager", features = ["cpi"] } +cardinal-token-manager = { version = "^0.0.3", path = "../cardinal-token-manager", features = ["cpi"] } [dev-dependencies] proptest = { version = "1.0" } \ No newline at end of file diff --git a/programs/cardinal-time-invalidator/Cargo.toml b/programs/cardinal-time-invalidator/Cargo.toml index ffdd47ecf..2cd05e931 100644 --- a/programs/cardinal-time-invalidator/Cargo.toml +++ b/programs/cardinal-time-invalidator/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardinal-time-invalidator" -version = "0.0.2" +version = "0.0.3" description = "Cardinal paid claim approver" edition = "2021" homepage = "https://cardinal.so" @@ -25,7 +25,7 @@ anchor-spl = "0.20.1" spl-associated-token-account = "1.0.2" spl-token = { version = "3.1.1", features = ["no-entrypoint"] } solana-program = "1.8.1" -cardinal-token-manager = { version = "^0.0.2", path = "../cardinal-token-manager", features = ["cpi"] } +cardinal-token-manager = { version = "^0.0.3", path = "../cardinal-token-manager", features = ["cpi"] } [dev-dependencies] proptest = { version = "1.0" } \ No newline at end of file diff --git a/programs/cardinal-token-manager/Cargo.toml b/programs/cardinal-token-manager/Cargo.toml index bd609f365..58cf1c345 100644 --- a/programs/cardinal-token-manager/Cargo.toml +++ b/programs/cardinal-token-manager/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cardinal-token-manager" -version = "0.0.2" +version = "0.0.3" description = "Cardinal token manager" edition = "2021" homepage = "https://cardinal.so" diff --git a/src/claimLinks.ts b/src/claimLinks.ts new file mode 100644 index 000000000..fb30272c6 --- /dev/null +++ b/src/claimLinks.ts @@ -0,0 +1,168 @@ +import { BN, utils, web3 } from "@project-serum/anchor"; +import type { Wallet } from "@saberhq/solana-contrib"; +import { SPLToken } from "@saberhq/token-utils"; +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + Token, + TOKEN_PROGRAM_ID, +} from "@solana/spl-token"; +import type { Connection, PublicKey } from "@solana/web3.js"; +import { Keypair, Transaction } from "@solana/web3.js"; + +import { tokenManager } from "./programs"; +import { TokenManagerKind } from "./programs/tokenManager"; +import { findTokenManagerAddress } from "./programs/tokenManager/pda"; +import { withFindOrInitAssociatedTokenAccount } from "./utils"; + +export const getLink = (mintId: PublicKey, otp: Keypair): string => { + return `https://claim.cardinal.so/${mintId.toString()}?otp=${utils.bytes.bs58.encode( + otp.secretKey + )}`; +}; + +export const fromLink = (link: string): [PublicKey, Keypair] => { + try { + const [_, mintId, otp] = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + /https:\/\/claim\.cardinal\.so\/(.*)\?otp=(.*)/.exec(link)!; + return [ + new web3.PublicKey(mintId as string), + Keypair.fromSecretKey(utils.bytes.bs58.decode(otp as string)), + ]; + } catch (e) { + console.log("Error decoding link: ", e, link); + throw e; + } +}; + +export const issueToken = async ( + connection: Connection, + wallet: Wallet, + { + rentalMint, + issuerTokenAccountId, + amount = new BN(1), + kind = TokenManagerKind.Managed, + }: { + rentalMint: PublicKey; + issuerTokenAccountId: PublicKey; + amount?: BN; + kind?: TokenManagerKind; + } +): Promise<[Transaction, PublicKey, Keypair]> => { + const otp = Keypair.generate(); + const transaction = new Transaction(); + + // init token manager + const [tokenManagerIx, tokenManagerId] = await tokenManager.instruction.init( + connection, + wallet, + rentalMint + ); + transaction.add(tokenManagerIx); + + transaction.add( + tokenManager.instruction.setClaimApprover( + connection, + wallet, + tokenManagerId, + otp.publicKey + ) + ); + + if (kind === TokenManagerKind.Managed) { + transaction.add( + SPLToken.createSetAuthorityInstruction( + TOKEN_PROGRAM_ID, + rentalMint, + tokenManagerId, + "FreezeAccount", + wallet.publicKey, + [] + ) + ); + } + + // issuer + const tokenManagerTokenAccountId = await withFindOrInitAssociatedTokenAccount( + transaction, + connection, + rentalMint, + tokenManagerId, + wallet.publicKey, + true + ); + + transaction.add( + tokenManager.instruction.issue( + connection, + wallet, + tokenManagerId, + amount, + rentalMint, + tokenManagerTokenAccountId, + issuerTokenAccountId, + kind + ) + ); + + return [transaction, tokenManagerId, otp]; +}; + +export const claimFromLink = async ( + connection: Connection, + wallet: Wallet, + mintId: PublicKey, + otpKeypair: Keypair +): Promise => { + const transaction = new Transaction(); + // const otp = utils.bytes.bs58.decode(otpString); + // const keypair = Keypair.fromSecretKey(otp); + + const [tokenManagerId] = await findTokenManagerAddress(mintId); + const tokenManagerData = await tokenManager.accounts.getTokenManager( + connection, + tokenManagerId + ); + + // approve claim request + const [createClaimReceiptIx, claimReceiptId] = + await tokenManager.instruction.createClaimReceipt( + connection, + wallet, + tokenManagerId, + otpKeypair.publicKey + ); + transaction.add(createClaimReceiptIx); + + const tokenManagerTokenAccountId = await Token.getAssociatedTokenAddress( + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + tokenManagerData.parsed.mint, + tokenManagerId, + true + ); + + const recipientTokenAccountId = await withFindOrInitAssociatedTokenAccount( + transaction, + connection, + tokenManagerData.parsed.mint, + wallet.publicKey, + wallet.publicKey + ); + + // claim + transaction.add( + tokenManager.instruction.claim( + connection, + wallet, + tokenManagerId, + tokenManagerData.parsed.mint, + tokenManagerTokenAccountId, + recipientTokenAccountId, + claimReceiptId + ) + ); + + return transaction; +}; diff --git a/src/index.ts b/src/index.ts index 1f5590957..4ee34cce9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,3 @@ export * from "./api"; +export * as claimLinks from "./claimLinks"; export * from "./utils"; diff --git a/src/programs/tokenManager/instruction.ts b/src/programs/tokenManager/instruction.ts index 770d5c709..7c81b209d 100644 --- a/src/programs/tokenManager/instruction.ts +++ b/src/programs/tokenManager/instruction.ts @@ -11,7 +11,7 @@ import { SystemProgram } from "@solana/web3.js"; import type { TOKEN_MANAGER_PROGRAM, TokenManagerKind } from "./constants"; import { TOKEN_MANAGER_ADDRESS, TOKEN_MANAGER_IDL } from "./constants"; -import { findTokenManagerAddress } from "./pda"; +import { findClaimReceiptId, findTokenManagerAddress } from "./pda"; export const init = async ( connection: Connection, @@ -201,3 +201,39 @@ export const claim = ( : [], }); }; + +export const createClaimReceipt = async ( + connection: Connection, + wallet: Wallet, + tokenManagerId: PublicKey, + claimApproverId: PublicKey +): Promise<[TransactionInstruction, PublicKey]> => { + const provider = new Provider(connection, wallet, {}); + const tokenManagerProgram = new Program( + TOKEN_MANAGER_IDL, + TOKEN_MANAGER_ADDRESS, + provider + ); + + const [claimReceiptId, claimReceiptBump] = await findClaimReceiptId( + tokenManagerId, + wallet.publicKey + ); + + return [ + tokenManagerProgram.instruction.createClaimReceipt( + claimReceiptBump, + wallet.publicKey, + { + accounts: { + tokenManager: tokenManagerId, + claimApprover: claimApproverId, + claimReceipt: claimReceiptId, + payer: wallet.publicKey, + systemProgram: SystemProgram.programId, + }, + } + ), + claimReceiptId, + ]; +}; diff --git a/src/programs/tokenManager/pda.ts b/src/programs/tokenManager/pda.ts index 3a8e1684b..3ceb795ec 100644 --- a/src/programs/tokenManager/pda.ts +++ b/src/programs/tokenManager/pda.ts @@ -28,7 +28,7 @@ export const findClaimReceiptId = async ( tokenManagerKey: PublicKey, recipientKey: PublicKey ): Promise<[PublicKey, number]> => { - return await PublicKey.findProgramAddress( + return PublicKey.findProgramAddress( [ utils.bytes.utf8.encode(CLAIM_RECEIPT_SEED), tokenManagerKey.toBuffer(), diff --git a/tests/claimLinks.spec.ts b/tests/claimLinks.spec.ts new file mode 100644 index 000000000..8097eac46 --- /dev/null +++ b/tests/claimLinks.spec.ts @@ -0,0 +1,147 @@ +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 { claimLinks, findAta } from "../src"; +import { fromLink } from "../src/claimLinks"; +import { tokenManager } from "../src/programs"; +import { + TokenManagerKind, + TokenManagerState, +} from "../src/programs/tokenManager"; +import { createMint } from "./utils"; +import { getProvider } from "./workspace"; + +describe("Claim links", () => { + const recipient = Keypair.generate(); + const tokenCreator = Keypair.generate(); + let issuerTokenAccountId: PublicKey; + let rentalMint: Token; + let claimLink: string; + + 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 link", async () => { + const provider = getProvider(); + const [transaction, tokenManagerId, otp] = await claimLinks.issueToken( + provider.connection, + provider.wallet, + { + rentalMint: rentalMint.publicKey, + issuerTokenAccountId, + kind: TokenManagerKind.Unmanaged, + } + ); + + const txEnvelope = new TransactionEnvelope( + SolanaProvider.init({ + connection: provider.connection, + wallet: provider.wallet, + opts: provider.opts, + }), + [...transaction.instructions] + ); + await expectTXTable(txEnvelope, "test", { + verbosity: "always", + formatLogs: true, + }).to.be.fulfilled; + + claimLink = claimLinks.getLink(rentalMint.publicKey, otp); + + 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(0); + expect(tokenManagerData.parsed.issuer).to.eqAddress( + provider.wallet.publicKey + ); + expect(tokenManagerData.parsed.claimApprover).to.eqAddress(otp.publicKey); + + const checkIssuerTokenAccount = await rentalMint.getAccountInfo( + issuerTokenAccountId + ); + expect(checkIssuerTokenAccount.amount.toNumber()).to.eq(0); + + console.log("Link created: ", claimLink); + }); + + it("Claim from link", async () => { + const provider = getProvider(); + + const [mintId, otpKeypair] = fromLink(claimLink); + + const transaction = await claimLinks.claimFromLink( + provider.connection, + new SignerWallet(recipient), + mintId, + otpKeypair + ); + + const txEnvelope = new TransactionEnvelope( + SolanaProvider.init({ + connection: provider.connection, + wallet: new SignerWallet(recipient), + opts: provider.opts, + }), + [...transaction.instructions], + [otpKeypair] + ); + await expectTXTable(txEnvelope, "test", { + verbosity: "always", + formatLogs: true, + }).to.be.fulfilled; + + const [tokenManagerId] = await tokenManager.pda.findTokenManagerAddress( + rentalMint.publicKey + ); + 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); + }); +});