From 7487bf9ba160ef16f5355d5c3f941f6f92871e5d Mon Sep 17 00:00:00 2001 From: matias martinez Date: Thu, 28 Mar 2024 19:13:03 -0300 Subject: [PATCH] simplify test --- backend/src/handlers/fund-transactions.ts | 5 +- backend/src/token-dispenser.json | 653 -------- backend/src/token-dispenser.ts | 1310 +++++++++++++++++ backend/src/utils/discord.ts | 7 +- backend/src/utils/fund-transactions.ts | 18 +- .../test/handlers/fund-transactions.test.ts | 93 +- 6 files changed, 1345 insertions(+), 741 deletions(-) delete mode 100644 backend/src/token-dispenser.json create mode 100644 backend/src/token-dispenser.ts diff --git a/backend/src/handlers/fund-transactions.ts b/backend/src/handlers/fund-transactions.ts index 58d7d8d0..ece415dc 100644 --- a/backend/src/handlers/fund-transactions.ts +++ b/backend/src/handlers/fund-transactions.ts @@ -82,7 +82,10 @@ function getSignature(tx: VersionedTransaction): string { } function logSignatures(signedTransactions: VersionedTransaction[]) { - const sigs: { sig: string; instruction?: Instruction | null }[] = [] + const sigs: { + sig: string + instruction?: ReturnType + }[] = [] signedTransactions.forEach((tx) => { sigs.push({ sig: getSignature(tx), instruction: extractCallData(tx) }) }) diff --git a/backend/src/token-dispenser.json b/backend/src/token-dispenser.json deleted file mode 100644 index 277238a0..00000000 --- a/backend/src/token-dispenser.json +++ /dev/null @@ -1,653 +0,0 @@ -{ - "version": "0.1.0", - "name": "token_dispenser", - "instructions": [ - { - "name": "initialize", - "docs": [ - "This can only be called once and should be called right after the program is deployed." - ], - "accounts": [ - { - "name": "payer", - "isMut": true, - "isSigner": true - }, - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": false, - "isSigner": false, - "docs": ["Mint of the treasury"] - }, - { - "name": "treasury", - "isMut": false, - "isSigner": false, - "docs": [ - "Treasury token account. This is an externally owned token account and", - "the owner of this account will approve the config as a delegate using the", - "solana CLI command `spl-token approve `" - ] - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "addressLookupTable", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "merkleRoot", - "type": { - "array": ["u8", 20] - } - }, - { - "name": "dispenserGuard", - "type": "publicKey" - }, - { - "name": "funder", - "type": "publicKey" - }, - { - "name": "maxTransfer", - "type": "u64" - } - ] - }, - { - "name": "claim", - "docs": [ - "* Claim a claimant's tokens. This instructions needs to enforce :\n * - The dispenser guard has signed the transaction - DONE\n * - The claimant is claiming no more than once per ecosystem - DONE\n * - The claimant has provided a valid proof of identity (is the owner of the wallet\n * entitled to the tokens)\n * - The claimant has provided a valid proof of inclusion (this confirm that the claimant --\n * DONE\n * - The claimant has not already claimed tokens -- DONE" - ], - "accounts": [ - { - "name": "funder", - "isMut": true, - "isSigner": true - }, - { - "name": "claimant", - "isMut": false, - "isSigner": true - }, - { - "name": "claimantFund", - "isMut": true, - "isSigner": false, - "docs": [ - "Claimant's associated token account to receive the tokens", - "Should be initialized outside of this program." - ] - }, - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "mint", - "isMut": false, - "isSigner": false - }, - { - "name": "treasury", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "sysvarInstruction", - "isMut": false, - "isSigner": false, - "docs": [ - "CHECK : Anchor wants me to write this comment because I'm using AccountInfo which doesn't check for ownership and doesn't deserialize the account automatically. But it's fine because I check the address and I load it using load_instruction_at_checked." - ] - }, - { - "name": "associatedTokenProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "claimCertificate", - "type": { - "defined": "ClaimCertificate" - } - } - ] - } - ], - "accounts": [ - { - "name": "Config", - "type": { - "kind": "struct", - "fields": [ - { - "name": "bump", - "type": "u8" - }, - { - "name": "merkleRoot", - "type": { - "array": ["u8", 20] - } - }, - { - "name": "dispenserGuard", - "type": "publicKey" - }, - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "treasury", - "type": "publicKey" - }, - { - "name": "addressLookupTable", - "type": "publicKey" - }, - { - "name": "funder", - "type": "publicKey" - }, - { - "name": "maxTransfer", - "type": "u64" - } - ] - } - }, - { - "name": "Receipt", - "type": { - "kind": "struct", - "fields": [] - } - } - ], - "types": [ - { - "name": "CosmosMessage", - "docs": [ - "* An ADR036 message used in Cosmos. ADR036 is a standard for signing arbitrary data.\n* Only the message payload is stored in this struct.\n* The message signed for Cosmos is a JSON serialized CosmosStdSignDoc containing the payload and ADR036 compliant parameters.\n* The message also contains the bech32 address of the signer. We check that the signer corresponds to the public key." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "payload", - "type": "bytes" - }, - { - "name": "signer", - "type": "string" - } - ] - } - }, - { - "name": "DiscordMessage", - "docs": [ - "* This message (borsh-serialized) needs to be signed by the dispenser guard after\n * verifying the claimant's pubkey controls the discord account.\n * The dispenser guard key should not be used for anything else." - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "username", - "type": "string" - }, - { - "name": "claimant", - "type": "publicKey" - } - ] - } - }, - { - "name": "Ed25519InstructionHeader", - "type": { - "kind": "struct", - "fields": [ - { - "name": "numSignatures", - "type": "u8" - }, - { - "name": "padding", - "type": "u8" - }, - { - "name": "signatureOffset", - "type": "u16" - }, - { - "name": "signatureInstructionIndex", - "type": "u16" - }, - { - "name": "publicKeyOffset", - "type": "u16" - }, - { - "name": "publicKeyInstructionIndex", - "type": "u16" - }, - { - "name": "messageDataOffset", - "type": "u16" - }, - { - "name": "messageDataSize", - "type": "u16" - }, - { - "name": "messageInstructionIndex", - "type": "u16" - } - ] - } - }, - { - "name": "Secp256k1InstructionHeader", - "type": { - "kind": "struct", - "fields": [ - { - "name": "numSignatures", - "type": "u8" - }, - { - "name": "signatureOffset", - "type": "u16" - }, - { - "name": "signatureInstructionIndex", - "type": "u8" - }, - { - "name": "ethAddressOffset", - "type": "u16" - }, - { - "name": "ethAddressInstructionIndex", - "type": "u8" - }, - { - "name": "messageDataOffset", - "type": "u16" - }, - { - "name": "messageDataSize", - "type": "u16" - }, - { - "name": "messageInstructionIndex", - "type": "u8" - } - ] - } - }, - { - "name": "ClaimInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "identity", - "type": { - "defined": "Identity" - } - }, - { - "name": "amount", - "type": "u64" - } - ] - } - }, - { - "name": "ClaimCertificate", - "type": { - "kind": "struct", - "fields": [ - { - "name": "amount", - "type": "u64" - }, - { - "name": "proofOfIdentity", - "type": { - "defined": "IdentityCertificate" - } - }, - { - "name": "proofOfInclusion", - "type": { - "vec": { - "array": ["u8", 20] - } - } - } - ] - } - }, - { - "name": "Identity", - "docs": [ - "* This is the identity that the claimant will use to claim tokens.\n * A claimant can claim tokens for 1 identity on each ecosystem.\n * Typically for a blockchain it is a public key in the blockchain's address space." - ], - "type": { - "kind": "enum", - "variants": [ - { - "name": "Discord", - "fields": [ - { - "name": "username", - "type": "string" - } - ] - }, - { - "name": "Solana", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 32] - } - } - ] - }, - { - "name": "Evm", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 20] - } - } - ] - }, - { - "name": "Sui", - "fields": [ - { - "name": "address", - "type": { - "array": ["u8", 32] - } - } - ] - }, - { - "name": "Aptos", - "fields": [ - { - "name": "address", - "type": { - "array": ["u8", 32] - } - } - ] - }, - { - "name": "Cosmwasm", - "fields": [ - { - "name": "address", - "type": "string" - } - ] - }, - { - "name": "Injective", - "fields": [ - { - "name": "address", - "type": "string" - } - ] - }, - { - "name": "Algorand", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 32] - } - } - ] - } - ] - } - }, - { - "name": "IdentityCertificate", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Discord", - "fields": [ - { - "name": "username", - "type": "string" - }, - { - "name": "verification_instruction_index", - "type": "u8" - } - ] - }, - { - "name": "Evm", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 20] - } - }, - { - "name": "verification_instruction_index", - "type": "u8" - } - ] - }, - { - "name": "Solana" - }, - { - "name": "Sui", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 32] - } - }, - { - "name": "verification_instruction_index", - "type": "u8" - } - ] - }, - { - "name": "Aptos", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 32] - } - }, - { - "name": "verification_instruction_index", - "type": "u8" - } - ] - }, - { - "name": "Cosmwasm", - "fields": [ - { - "name": "chain_id", - "type": "string" - }, - { - "name": "signature", - "type": { - "array": ["u8", 64] - } - }, - { - "name": "recovery_id", - "type": "u8" - }, - { - "name": "pubkey", - "type": { - "array": ["u8", 65] - } - }, - { - "name": "message", - "type": "bytes" - } - ] - }, - { - "name": "Injective", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 20] - } - }, - { - "name": "verification_instruction_index", - "type": "u8" - } - ] - }, - { - "name": "Algorand", - "fields": [ - { - "name": "pubkey", - "type": { - "array": ["u8", 32] - } - }, - { - "name": "verification_instruction_index", - "type": "u8" - } - ] - } - ] - } - } - ], - "events": [ - { - "name": "ClaimEvent", - "fields": [ - { - "name": "remainingBalance", - "type": "u64", - "index": false - }, - { - "name": "claimant", - "type": "publicKey", - "index": false - }, - { - "name": "claimInfo", - "type": { - "defined": "ClaimInfo" - }, - "index": false - } - ] - } - ], - "errors": [ - { - "code": 6000, - "name": "AlreadyClaimed" - }, - { - "code": 6001, - "name": "InvalidInclusionProof" - }, - { - "code": 6002, - "name": "WrongPda" - }, - { - "code": 6003, - "name": "SignatureVerificationWrongProgram" - }, - { - "code": 6004, - "name": "SignatureVerificationWrongAccounts" - }, - { - "code": 6005, - "name": "SignatureVerificationWrongHeader" - }, - { - "code": 6006, - "name": "SignatureVerificationWrongPayload" - }, - { - "code": 6007, - "name": "SignatureVerificationWrongPayloadMetadata" - }, - { - "code": 6008, - "name": "SignatureVerificationWrongSigner" - }, - { - "code": 6009, - "name": "UnauthorizedCosmosChainId" - }, - { - "code": 6010, - "name": "TransferExceedsMax" - } - ] -} diff --git a/backend/src/token-dispenser.ts b/backend/src/token-dispenser.ts new file mode 100644 index 00000000..5a2f2a4b --- /dev/null +++ b/backend/src/token-dispenser.ts @@ -0,0 +1,1310 @@ +import { BorshCoder } from '@coral-xyz/anchor' + +export type TokenDispenser = { + version: '0.1.0' + name: 'token_dispenser' + instructions: [ + { + name: 'initialize' + docs: [ + 'This can only be called once and should be called right after the program is deployed.' + ] + accounts: [ + { + name: 'payer' + isMut: true + isSigner: true + }, + { + name: 'config' + isMut: true + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + docs: ['Mint of the treasury'] + }, + { + name: 'treasury' + isMut: false + isSigner: false + docs: [ + 'Treasury token account. This is an externally owned token account and', + 'the owner of this account will approve the config as a delegate using the', + 'solana CLI command `spl-token approve `' + ] + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + { + name: 'addressLookupTable' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'merkleRoot' + type: { + array: ['u8', 20] + } + }, + { + name: 'dispenserGuard' + type: 'publicKey' + }, + { + name: 'funder' + type: 'publicKey' + }, + { + name: 'maxTransfer' + type: 'u64' + } + ] + }, + { + name: 'claim' + docs: [ + "* Claim a claimant's tokens. This instructions needs to enforce :\n * - The dispenser guard has signed the transaction - DONE\n * - The claimant is claiming no more than once per ecosystem - DONE\n * - The claimant has provided a valid proof of identity (is the owner of the wallet\n * entitled to the tokens)\n * - The claimant has provided a valid proof of inclusion (this confirm that the claimant --\n * DONE\n * - The claimant has not already claimed tokens -- DONE" + ] + accounts: [ + { + name: 'funder' + isMut: true + isSigner: true + }, + { + name: 'claimant' + isMut: false + isSigner: true + }, + { + name: 'claimantFund' + isMut: true + isSigner: false + docs: [ + "Claimant's associated token account to receive the tokens", + 'Should be initialized outside of this program.' + ] + }, + { + name: 'config' + isMut: false + isSigner: false + }, + { + name: 'mint' + isMut: false + isSigner: false + }, + { + name: 'treasury' + isMut: true + isSigner: false + }, + { + name: 'tokenProgram' + isMut: false + isSigner: false + }, + { + name: 'systemProgram' + isMut: false + isSigner: false + }, + { + name: 'sysvarInstruction' + isMut: false + isSigner: false + docs: [ + "CHECK : Anchor wants me to write this comment because I'm using AccountInfo which doesn't check for ownership and doesn't deserialize the account automatically. But it's fine because I check the address and I load it using load_instruction_at_checked." + ] + }, + { + name: 'associatedTokenProgram' + isMut: false + isSigner: false + } + ] + args: [ + { + name: 'claimCertificate' + type: { + defined: 'ClaimCertificate' + } + } + ] + } + ] + accounts: [ + { + name: 'Config' + type: { + kind: 'struct' + fields: [ + { + name: 'bump' + type: 'u8' + }, + { + name: 'merkleRoot' + type: { + array: ['u8', 20] + } + }, + { + name: 'dispenserGuard' + type: 'publicKey' + }, + { + name: 'mint' + type: 'publicKey' + }, + { + name: 'treasury' + type: 'publicKey' + }, + { + name: 'addressLookupTable' + type: 'publicKey' + }, + { + name: 'funder' + type: 'publicKey' + }, + { + name: 'maxTransfer' + type: 'u64' + } + ] + } + }, + { + name: 'Receipt' + type: { + kind: 'struct' + fields: [] + } + } + ] + types: [ + { + name: 'CosmosMessage' + docs: [ + '* An ADR036 message used in Cosmos. ADR036 is a standard for signing arbitrary data.\n* Only the message payload is stored in this struct.\n* The message signed for Cosmos is a JSON serialized CosmosStdSignDoc containing the payload and ADR036 compliant parameters.\n* The message also contains the bech32 address of the signer. We check that the signer corresponds to the public key.' + ] + type: { + kind: 'struct' + fields: [ + { + name: 'payload' + type: 'bytes' + }, + { + name: 'signer' + type: 'string' + } + ] + } + }, + { + name: 'DiscordMessage' + docs: [ + "* This message (borsh-serialized) needs to be signed by the dispenser guard after\n * verifying the claimant's pubkey controls the discord account.\n * The dispenser guard key should not be used for anything else." + ] + type: { + kind: 'struct' + fields: [ + { + name: 'username' + type: 'string' + }, + { + name: 'claimant' + type: 'publicKey' + } + ] + } + }, + { + name: 'Ed25519InstructionHeader' + type: { + kind: 'struct' + fields: [ + { + name: 'numSignatures' + type: 'u8' + }, + { + name: 'padding' + type: 'u8' + }, + { + name: 'signatureOffset' + type: 'u16' + }, + { + name: 'signatureInstructionIndex' + type: 'u16' + }, + { + name: 'publicKeyOffset' + type: 'u16' + }, + { + name: 'publicKeyInstructionIndex' + type: 'u16' + }, + { + name: 'messageDataOffset' + type: 'u16' + }, + { + name: 'messageDataSize' + type: 'u16' + }, + { + name: 'messageInstructionIndex' + type: 'u16' + } + ] + } + }, + { + name: 'Secp256k1InstructionHeader' + type: { + kind: 'struct' + fields: [ + { + name: 'numSignatures' + type: 'u8' + }, + { + name: 'signatureOffset' + type: 'u16' + }, + { + name: 'signatureInstructionIndex' + type: 'u8' + }, + { + name: 'ethAddressOffset' + type: 'u16' + }, + { + name: 'ethAddressInstructionIndex' + type: 'u8' + }, + { + name: 'messageDataOffset' + type: 'u16' + }, + { + name: 'messageDataSize' + type: 'u16' + }, + { + name: 'messageInstructionIndex' + type: 'u8' + } + ] + } + }, + { + name: 'ClaimInfo' + type: { + kind: 'struct' + fields: [ + { + name: 'identity' + type: { + defined: 'Identity' + } + }, + { + name: 'amount' + type: 'u64' + } + ] + } + }, + { + name: 'ClaimCertificate' + type: { + kind: 'struct' + fields: [ + { + name: 'amount' + type: 'u64' + }, + { + name: 'proofOfIdentity' + type: { + defined: 'IdentityCertificate' + } + }, + { + name: 'proofOfInclusion' + type: { + vec: { + array: ['u8', 20] + } + } + } + ] + } + }, + { + name: 'Identity' + docs: [ + "* This is the identity that the claimant will use to claim tokens.\n * A claimant can claim tokens for 1 identity on each ecosystem.\n * Typically for a blockchain it is a public key in the blockchain's address space." + ] + type: { + kind: 'enum' + variants: [ + { + name: 'Discord' + fields: [ + { + name: 'username' + type: 'string' + } + ] + }, + { + name: 'Solana' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 32] + } + } + ] + }, + { + name: 'Evm' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 20] + } + } + ] + }, + { + name: 'Sui' + fields: [ + { + name: 'address' + type: { + array: ['u8', 32] + } + } + ] + }, + { + name: 'Aptos' + fields: [ + { + name: 'address' + type: { + array: ['u8', 32] + } + } + ] + }, + { + name: 'Cosmwasm' + fields: [ + { + name: 'address' + type: 'string' + } + ] + }, + { + name: 'Injective' + fields: [ + { + name: 'address' + type: 'string' + } + ] + }, + { + name: 'Algorand' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 32] + } + } + ] + } + ] + } + }, + { + name: 'IdentityCertificate' + type: { + kind: 'enum' + variants: [ + { + name: 'Discord' + fields: [ + { + name: 'username' + type: 'string' + }, + { + name: 'verification_instruction_index' + type: 'u8' + } + ] + }, + { + name: 'Evm' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 20] + } + }, + { + name: 'verification_instruction_index' + type: 'u8' + } + ] + }, + { + name: 'Solana' + }, + { + name: 'Sui' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 32] + } + }, + { + name: 'verification_instruction_index' + type: 'u8' + } + ] + }, + { + name: 'Aptos' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 32] + } + }, + { + name: 'verification_instruction_index' + type: 'u8' + } + ] + }, + { + name: 'Cosmwasm' + fields: [ + { + name: 'chain_id' + type: 'string' + }, + { + name: 'signature' + type: { + array: ['u8', 64] + } + }, + { + name: 'recovery_id' + type: 'u8' + }, + { + name: 'pubkey' + type: { + array: ['u8', 65] + } + }, + { + name: 'message' + type: 'bytes' + } + ] + }, + { + name: 'Injective' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 20] + } + }, + { + name: 'verification_instruction_index' + type: 'u8' + } + ] + }, + { + name: 'Algorand' + fields: [ + { + name: 'pubkey' + type: { + array: ['u8', 32] + } + }, + { + name: 'verification_instruction_index' + type: 'u8' + } + ] + } + ] + } + } + ] + events: [ + { + name: 'ClaimEvent' + fields: [ + { + name: 'remainingBalance' + type: 'u64' + index: false + }, + { + name: 'claimant' + type: 'publicKey' + index: false + }, + { + name: 'claimInfo' + type: { + defined: 'ClaimInfo' + } + index: false + } + ] + } + ] + errors: [ + { + code: 6000 + name: 'AlreadyClaimed' + }, + { + code: 6001 + name: 'InvalidInclusionProof' + }, + { + code: 6002 + name: 'WrongPda' + }, + { + code: 6003 + name: 'SignatureVerificationWrongProgram' + }, + { + code: 6004 + name: 'SignatureVerificationWrongAccounts' + }, + { + code: 6005 + name: 'SignatureVerificationWrongHeader' + }, + { + code: 6006 + name: 'SignatureVerificationWrongPayload' + }, + { + code: 6007 + name: 'SignatureVerificationWrongPayloadMetadata' + }, + { + code: 6008 + name: 'SignatureVerificationWrongSigner' + }, + { + code: 6009 + name: 'UnauthorizedCosmosChainId' + }, + { + code: 6010 + name: 'TransferExceedsMax' + } + ] +} +export const IDL: TokenDispenser = { + version: '0.1.0', + name: 'token_dispenser', + instructions: [ + { + name: 'initialize', + docs: [ + 'This can only be called once and should be called right after the program is deployed.' + ], + accounts: [ + { + name: 'payer', + isMut: true, + isSigner: true + }, + { + name: 'config', + isMut: true, + isSigner: false + }, + { + name: 'mint', + isMut: false, + isSigner: false, + docs: ['Mint of the treasury'] + }, + { + name: 'treasury', + isMut: false, + isSigner: false, + docs: [ + 'Treasury token account. This is an externally owned token account and', + 'the owner of this account will approve the config as a delegate using the', + 'solana CLI command `spl-token approve `' + ] + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false + }, + { + name: 'addressLookupTable', + isMut: false, + isSigner: false + } + ], + args: [ + { + name: 'merkleRoot', + type: { + array: ['u8', 20] + } + }, + { + name: 'dispenserGuard', + type: 'publicKey' + }, + { + name: 'funder', + type: 'publicKey' + }, + { + name: 'maxTransfer', + type: 'u64' + } + ] + }, + { + name: 'claim', + docs: [ + "* Claim a claimant's tokens. This instructions needs to enforce :\n * - The dispenser guard has signed the transaction - DONE\n * - The claimant is claiming no more than once per ecosystem - DONE\n * - The claimant has provided a valid proof of identity (is the owner of the wallet\n * entitled to the tokens)\n * - The claimant has provided a valid proof of inclusion (this confirm that the claimant --\n * DONE\n * - The claimant has not already claimed tokens -- DONE" + ], + accounts: [ + { + name: 'funder', + isMut: true, + isSigner: true + }, + { + name: 'claimant', + isMut: false, + isSigner: true + }, + { + name: 'claimantFund', + isMut: true, + isSigner: false, + docs: [ + "Claimant's associated token account to receive the tokens", + 'Should be initialized outside of this program.' + ] + }, + { + name: 'config', + isMut: false, + isSigner: false + }, + { + name: 'mint', + isMut: false, + isSigner: false + }, + { + name: 'treasury', + isMut: true, + isSigner: false + }, + { + name: 'tokenProgram', + isMut: false, + isSigner: false + }, + { + name: 'systemProgram', + isMut: false, + isSigner: false + }, + { + name: 'sysvarInstruction', + isMut: false, + isSigner: false, + docs: [ + "CHECK : Anchor wants me to write this comment because I'm using AccountInfo which doesn't check for ownership and doesn't deserialize the account automatically. But it's fine because I check the address and I load it using load_instruction_at_checked." + ] + }, + { + name: 'associatedTokenProgram', + isMut: false, + isSigner: false + } + ], + args: [ + { + name: 'claimCertificate', + type: { + defined: 'ClaimCertificate' + } + } + ] + } + ], + accounts: [ + { + name: 'Config', + type: { + kind: 'struct', + fields: [ + { + name: 'bump', + type: 'u8' + }, + { + name: 'merkleRoot', + type: { + array: ['u8', 20] + } + }, + { + name: 'dispenserGuard', + type: 'publicKey' + }, + { + name: 'mint', + type: 'publicKey' + }, + { + name: 'treasury', + type: 'publicKey' + }, + { + name: 'addressLookupTable', + type: 'publicKey' + }, + { + name: 'funder', + type: 'publicKey' + }, + { + name: 'maxTransfer', + type: 'u64' + } + ] + } + }, + { + name: 'Receipt', + type: { + kind: 'struct', + fields: [] + } + } + ], + types: [ + { + name: 'CosmosMessage', + docs: [ + '* An ADR036 message used in Cosmos. ADR036 is a standard for signing arbitrary data.\n* Only the message payload is stored in this struct.\n* The message signed for Cosmos is a JSON serialized CosmosStdSignDoc containing the payload and ADR036 compliant parameters.\n* The message also contains the bech32 address of the signer. We check that the signer corresponds to the public key.' + ], + type: { + kind: 'struct', + fields: [ + { + name: 'payload', + type: 'bytes' + }, + { + name: 'signer', + type: 'string' + } + ] + } + }, + { + name: 'DiscordMessage', + docs: [ + "* This message (borsh-serialized) needs to be signed by the dispenser guard after\n * verifying the claimant's pubkey controls the discord account.\n * The dispenser guard key should not be used for anything else." + ], + type: { + kind: 'struct', + fields: [ + { + name: 'username', + type: 'string' + }, + { + name: 'claimant', + type: 'publicKey' + } + ] + } + }, + { + name: 'Ed25519InstructionHeader', + type: { + kind: 'struct', + fields: [ + { + name: 'numSignatures', + type: 'u8' + }, + { + name: 'padding', + type: 'u8' + }, + { + name: 'signatureOffset', + type: 'u16' + }, + { + name: 'signatureInstructionIndex', + type: 'u16' + }, + { + name: 'publicKeyOffset', + type: 'u16' + }, + { + name: 'publicKeyInstructionIndex', + type: 'u16' + }, + { + name: 'messageDataOffset', + type: 'u16' + }, + { + name: 'messageDataSize', + type: 'u16' + }, + { + name: 'messageInstructionIndex', + type: 'u16' + } + ] + } + }, + { + name: 'Secp256k1InstructionHeader', + type: { + kind: 'struct', + fields: [ + { + name: 'numSignatures', + type: 'u8' + }, + { + name: 'signatureOffset', + type: 'u16' + }, + { + name: 'signatureInstructionIndex', + type: 'u8' + }, + { + name: 'ethAddressOffset', + type: 'u16' + }, + { + name: 'ethAddressInstructionIndex', + type: 'u8' + }, + { + name: 'messageDataOffset', + type: 'u16' + }, + { + name: 'messageDataSize', + type: 'u16' + }, + { + name: 'messageInstructionIndex', + type: 'u8' + } + ] + } + }, + { + name: 'ClaimInfo', + type: { + kind: 'struct', + fields: [ + { + name: 'identity', + type: { + defined: 'Identity' + } + }, + { + name: 'amount', + type: 'u64' + } + ] + } + }, + { + name: 'ClaimCertificate', + type: { + kind: 'struct', + fields: [ + { + name: 'amount', + type: 'u64' + }, + { + name: 'proofOfIdentity', + type: { + defined: 'IdentityCertificate' + } + }, + { + name: 'proofOfInclusion', + type: { + vec: { + array: ['u8', 20] + } + } + } + ] + } + }, + { + name: 'Identity', + docs: [ + "* This is the identity that the claimant will use to claim tokens.\n * A claimant can claim tokens for 1 identity on each ecosystem.\n * Typically for a blockchain it is a public key in the blockchain's address space." + ], + type: { + kind: 'enum', + variants: [ + { + name: 'Discord', + fields: [ + { + name: 'username', + type: 'string' + } + ] + }, + { + name: 'Solana', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 32] + } + } + ] + }, + { + name: 'Evm', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 20] + } + } + ] + }, + { + name: 'Sui', + fields: [ + { + name: 'address', + type: { + array: ['u8', 32] + } + } + ] + }, + { + name: 'Aptos', + fields: [ + { + name: 'address', + type: { + array: ['u8', 32] + } + } + ] + }, + { + name: 'Cosmwasm', + fields: [ + { + name: 'address', + type: 'string' + } + ] + }, + { + name: 'Injective', + fields: [ + { + name: 'address', + type: 'string' + } + ] + }, + { + name: 'Algorand', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 32] + } + } + ] + } + ] + } + }, + { + name: 'IdentityCertificate', + type: { + kind: 'enum', + variants: [ + { + name: 'Discord', + fields: [ + { + name: 'username', + type: 'string' + }, + { + name: 'verification_instruction_index', + type: 'u8' + } + ] + }, + { + name: 'Evm', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 20] + } + }, + { + name: 'verification_instruction_index', + type: 'u8' + } + ] + }, + { + name: 'Solana' + }, + { + name: 'Sui', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 32] + } + }, + { + name: 'verification_instruction_index', + type: 'u8' + } + ] + }, + { + name: 'Aptos', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 32] + } + }, + { + name: 'verification_instruction_index', + type: 'u8' + } + ] + }, + { + name: 'Cosmwasm', + fields: [ + { + name: 'chain_id', + type: 'string' + }, + { + name: 'signature', + type: { + array: ['u8', 64] + } + }, + { + name: 'recovery_id', + type: 'u8' + }, + { + name: 'pubkey', + type: { + array: ['u8', 65] + } + }, + { + name: 'message', + type: 'bytes' + } + ] + }, + { + name: 'Injective', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 20] + } + }, + { + name: 'verification_instruction_index', + type: 'u8' + } + ] + }, + { + name: 'Algorand', + fields: [ + { + name: 'pubkey', + type: { + array: ['u8', 32] + } + }, + { + name: 'verification_instruction_index', + type: 'u8' + } + ] + } + ] + } + } + ], + events: [ + { + name: 'ClaimEvent', + fields: [ + { + name: 'remainingBalance', + type: 'u64', + index: false + }, + { + name: 'claimant', + type: 'publicKey', + index: false + }, + { + name: 'claimInfo', + type: { + defined: 'ClaimInfo' + }, + index: false + } + ] + } + ], + errors: [ + { + code: 6000, + name: 'AlreadyClaimed' + }, + { + code: 6001, + name: 'InvalidInclusionProof' + }, + { + code: 6002, + name: 'WrongPda' + }, + { + code: 6003, + name: 'SignatureVerificationWrongProgram' + }, + { + code: 6004, + name: 'SignatureVerificationWrongAccounts' + }, + { + code: 6005, + name: 'SignatureVerificationWrongHeader' + }, + { + code: 6006, + name: 'SignatureVerificationWrongPayload' + }, + { + code: 6007, + name: 'SignatureVerificationWrongPayloadMetadata' + }, + { + code: 6008, + name: 'SignatureVerificationWrongSigner' + }, + { + code: 6009, + name: 'UnauthorizedCosmosChainId' + }, + { + code: 6010, + name: 'TransferExceedsMax' + } + ] +} + +export const coder = new BorshCoder(IDL) diff --git a/backend/src/utils/discord.ts b/backend/src/utils/discord.ts index 72fa88bd..d8e1df3e 100644 --- a/backend/src/utils/discord.ts +++ b/backend/src/utils/discord.ts @@ -1,8 +1,7 @@ import { Keypair, PublicKey } from '@solana/web3.js' import { SignedMessage } from '../types' import nacl from 'tweetnacl' -import IDL from '../token-dispenser.json' -import * as anchor from '@coral-xyz/anchor' +import { coder } from '../token-dispenser' import config from '../config' export async function getDiscordUser( @@ -28,10 +27,6 @@ export async function getDiscordUser( } } -// TODO: Update IDL with wormhole token dispenser program IDL -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const coder = new anchor.BorshCoder(IDL as any) - function hardDriveSignDigest( fullMessage: Uint8Array, keypair: Keypair diff --git a/backend/src/utils/fund-transactions.ts b/backend/src/utils/fund-transactions.ts index 11122df6..614d4b3d 100644 --- a/backend/src/utils/fund-transactions.ts +++ b/backend/src/utils/fund-transactions.ts @@ -7,19 +7,16 @@ import { TransactionInstruction, VersionedTransaction } from '@solana/web3.js' -import IDL from '../token-dispenser.json' -import * as anchor from '@coral-xyz/anchor' +import { TokenDispenser, coder } from '../token-dispenser' import config from '../config' +import { IdlTypes } from '@coral-xyz/anchor' const SET_COMPUTE_UNIT_LIMIT_DISCRIMINANT = 2 const SET_COMPUTE_UNIT_PRICE_DISCRIMINANT = 3 const MAX_COMPUTE_UNIT_PRICE = BigInt(1_000_000) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const coder = new anchor.BorshCoder(IDL as any) - export function deserializeTransactions( transactions: unknown ): VersionedTransaction[] { @@ -214,10 +211,12 @@ export async function checkTransactions( } } +type ClaimCertificate = IdlTypes['ClaimCertificate'] + export function extractCallData( versionedTx: VersionedTransaction, programId?: string -) { +): ClaimCertificate | null { const tokenDispenserPid = programId || config.tokenDispenserProgramId() if (!tokenDispenserPid) { console.error('Token dispenser program ID not set') @@ -235,7 +234,12 @@ export function extractCallData( return null } - return coder.instruction.decode(Buffer.from(instruction.data), 'base58') + const decoded = coder.instruction.decode( + Buffer.from(instruction.data), + 'base58' + )?.data as { claimCertificate: ClaimCertificate } + + return decoded?.claimCertificate as ClaimCertificate } catch (err) { console.error('Failed to extract call data', err) return null diff --git a/backend/test/handlers/fund-transactions.test.ts b/backend/test/handlers/fund-transactions.test.ts index 72ae704a..40180497 100644 --- a/backend/test/handlers/fund-transactions.test.ts +++ b/backend/test/handlers/fund-transactions.test.ts @@ -15,9 +15,9 @@ import { VersionedTransaction } from '@solana/web3.js' import { extractCallData } from '../../src/utils/fund-transactions' -import { AnchorProvider, Program } from '@coral-xyz/anchor' +import { AnchorProvider, IdlTypes, Program, BN } from '@coral-xyz/anchor' import NodeWallet from '@coral-xyz/anchor/dist/cjs/nodewallet' -import IDL from '../../src/token-dispenser.json' +import { IDL, TokenDispenser } from '../../src/token-dispenser' import { fundTransactions } from '../../src/handlers/fund-transactions' const RANDOM_BLOCKHASH = 'HXq5QPm883r7834LWwDpcmEM8G8uQ9Hqm1xakCHGxprV' @@ -223,20 +223,16 @@ describe('fundTransactions integration test', () => { }) test('should extract claim info', async () => { - const versionedTx = VersionedTransaction.deserialize( - serialedSignedOnceEvmClaimTx - ) + const versionedTx = createTestTransactionFromInstructions([ + await createTokenDispenserProgramInstruction() + ]) + const callData = extractCallData(versionedTx, PROGRAM_ID.toBase58()) expect(callData).not.toBeNull() - expect(callData.name).toBe('claim') - expect(callData.data.claimCertificate.amount.toNumber()).toBe(3000000) - expect(callData.data.claimCertificate.proofOfInclusion).toBeDefined() - expect( - Buffer.from( - callData.data.claimCertificate.proofOfIdentity.evm.pubkey - ).toString('hex') - ).toBe('b80eb09f118ca9df95b2df575f68e41ac7b9e2f8') + expect(callData?.amount.toNumber()).toBe(3000000) + expect(callData?.proofOfInclusion).toBeDefined() + expect(callData?.proofOfIdentity.discord?.username).toBe('username') }) }) @@ -295,7 +291,7 @@ const createTestLegacyTransactionFromInstructions = ( const createTokenDispenserProgramInstruction = async () => { const tokenDispenser = new Program( - IDL as any, + IDL, PROGRAM_ID, new AnchorProvider( new Connection('http://localhost:8899'), @@ -304,8 +300,16 @@ const createTokenDispenserProgramInstruction = async () => { ) ) + const claimCert: IdlTypes['ClaimCertificate'] = { + amount: new BN(3000000), + proofOfIdentity: { + discord: { username: 'username', verificationInstructionIndex: 0 } + }, + proofOfInclusion: [] + } + const tokenDispenserInstruction = await tokenDispenser.methods - .claim([]) + .claim(claimCert) .accounts({ funder: FUNDER_KEY.publicKey, claimant: PublicKey.unique(), @@ -354,62 +358,3 @@ const createEd25519ProgramInstruction = () => { message: Buffer.from('hello') }) } - -/** - * - * JSON.stringify(claimInfo) - * '{"ecosystem":"evm","identity":"0xb80eb09f118ca9df95b2df575f68e41ac7b9e2f8","amount":"2dc6c0"}' - * JSON.stringify(proofOfInclusion) - * '[[0,85,165,191,92,119,130,34,61,26,158,210,214,153,226,137,155,183,3,99],[229,163,97,168,48,117,76,185,244,168,62,129,73,137,137,90,173,216,206,210],[140,236,51,115,81,124,212,64,246,44,113,251,122,212,33,53,42,92,222,112],[28,69,23,62,87,211,157,249,83,121,168,178,199,53,205,162,18,193,8,6]]' - */ -const serialedSignedOnceEvmClaimTx = Uint8Array.from([ - 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 34, 1, 197, 79, 108, 160, 223, 45, - 236, 34, 216, 129, 237, 101, 15, 109, 180, 204, 76, 99, 87, 142, 98, 93, 87, - 117, 101, 163, 146, 161, 43, 180, 191, 224, 251, 187, 232, 87, 46, 153, 228, - 175, 247, 125, 255, 254, 96, 136, 5, 177, 86, 33, 163, 36, 213, 149, 130, 72, - 4, 252, 63, 34, 5, 128, 2, 1, 3, 7, 30, 119, 201, 38, 51, 176, 207, 221, 193, - 222, 235, 244, 163, 250, 125, 66, 68, 196, 45, 208, 212, 201, 232, 178, 100, - 163, 24, 21, 106, 83, 66, 174, 155, 123, 158, 253, 193, 104, 240, 64, 146, 99, - 29, 11, 36, 179, 197, 27, 230, 135, 92, 254, 214, 38, 39, 148, 146, 117, 160, - 142, 58, 36, 50, 145, 136, 129, 220, 63, 156, 186, 207, 151, 103, 32, 111, - 158, 28, 225, 44, 198, 161, 39, 21, 240, 31, 100, 125, 105, 73, 48, 65, 140, - 82, 216, 96, 205, 229, 4, 138, 111, 250, 79, 206, 121, 204, 172, 251, 189, - 226, 13, 40, 248, 204, 203, 161, 244, 46, 143, 70, 195, 149, 243, 156, 179, - 22, 201, 44, 160, 4, 198, 252, 32, 240, 80, 204, 240, 85, 132, 215, 33, 28, - 159, 140, 245, 158, 193, 71, 133, 187, 22, 106, 30, 40, 48, 232, 18, 32, 0, 0, - 0, 218, 7, 92, 178, 255, 94, 198, 129, 118, 19, 222, 83, 11, 105, 42, 135, 53, - 71, 119, 105, 218, 71, 67, 12, 189, 129, 84, 51, 92, 74, 131, 39, 3, 6, 70, - 111, 229, 33, 23, 50, 255, 236, 173, 186, 114, 195, 155, 231, 188, 140, 229, - 187, 197, 247, 18, 107, 44, 67, 155, 58, 64, 0, 0, 0, 39, 230, 229, 205, 37, - 151, 231, 95, 84, 49, 60, 71, 233, 161, 128, 19, 206, 255, 26, 207, 228, 53, - 179, 118, 102, 49, 206, 99, 213, 230, 34, 141, 3, 4, 0, 151, 2, 1, 32, 0, 0, - 12, 0, 0, 97, 0, 182, 0, 0, 184, 14, 176, 159, 17, 140, 169, 223, 149, 178, - 223, 87, 95, 104, 228, 26, 199, 185, 226, 248, 156, 153, 118, 241, 244, 168, - 39, 253, 26, 71, 201, 208, 176, 76, 148, 46, 129, 155, 107, 68, 2, 232, 95, 6, - 177, 218, 161, 194, 228, 3, 131, 88, 66, 19, 183, 133, 200, 71, 240, 101, 97, - 107, 73, 0, 41, 107, 222, 192, 115, 89, 181, 52, 3, 193, 170, 59, 222, 1, 97, - 151, 205, 125, 11, 157, 0, 25, 69, 116, 104, 101, 114, 101, 117, 109, 32, 83, - 105, 103, 110, 101, 100, 32, 77, 101, 115, 115, 97, 103, 101, 58, 10, 49, 53, - 51, 87, 32, 65, 105, 114, 100, 114, 111, 112, 32, 80, 73, 68, 58, 10, 70, 103, - 54, 80, 97, 70, 112, 111, 71, 88, 107, 89, 115, 105, 100, 77, 112, 87, 84, 75, - 54, 87, 50, 66, 101, 90, 55, 70, 69, 102, 99, 89, 107, 103, 52, 55, 54, 122, - 80, 70, 115, 76, 110, 83, 10, 73, 32, 97, 117, 116, 104, 111, 114, 105, 122, - 101, 32, 83, 111, 108, 97, 110, 97, 32, 119, 97, 108, 108, 101, 116, 10, 66, - 84, 119, 88, 81, 90, 83, 51, 69, 122, 102, 120, 66, 107, 118, 50, 65, 53, 52, - 101, 115, 116, 109, 110, 57, 89, 98, 109, 99, 112, 109, 82, 87, 101, 70, 80, - 52, 102, 51, 97, 118, 76, 105, 52, 10, 116, 111, 32, 99, 108, 97, 105, 109, - 32, 109, 121, 32, 87, 32, 116, 111, 107, 101, 110, 115, 46, 10, 5, 11, 0, 1, - 2, 8, 9, 7, 10, 11, 12, 13, 3, 122, 62, 198, 214, 193, 213, 159, 108, 210, - 192, 198, 45, 0, 0, 0, 0, 0, 1, 184, 14, 176, 159, 17, 140, 169, 223, 149, - 178, 223, 87, 95, 104, 228, 26, 199, 185, 226, 248, 0, 4, 0, 0, 0, 0, 85, 165, - 191, 92, 119, 130, 34, 61, 26, 158, 210, 214, 153, 226, 137, 155, 183, 3, 99, - 229, 163, 97, 168, 48, 117, 76, 185, 244, 168, 62, 129, 73, 137, 137, 90, 173, - 216, 206, 210, 140, 236, 51, 115, 81, 124, 212, 64, 246, 44, 113, 251, 122, - 212, 33, 53, 42, 92, 222, 112, 28, 69, 23, 62, 87, 211, 157, 249, 83, 121, - 168, 178, 199, 53, 205, 162, 18, 193, 8, 6, 6, 0, 5, 2, 64, 13, 3, 0, 1, 84, - 177, 216, 124, 205, 44, 255, 179, 192, 92, 144, 74, 30, 201, 136, 65, 246, - 153, 117, 74, 63, 158, 165, 244, 18, 9, 203, 219, 255, 234, 210, 53, 1, 2, 6, - 0, 1, 3, 4, 5, 6 -])