diff --git a/light-circuits/tests/test.ts b/light-circuits/tests/test.ts index 353fbbda95..9f763f4eeb 100644 --- a/light-circuits/tests/test.ts +++ b/light-circuits/tests/test.ts @@ -13,6 +13,7 @@ import { VerifierTwo, Verifier, VerifierOne, + Action, } from "light-sdk"; import * as anchor from "@coral-xyz/anchor"; import { assert, expect } from "chai"; @@ -69,20 +70,29 @@ async function functionalCircuitTest(verifier: Verifier, app: boolean = false) { outputUtxos: [deposit_utxo1], merkleTreePubkey: mockPubkey, sender: mockPubkey, - senderFee: mockPubkey, + senderFee: lightProvider.nodeWallet.publicKey, verifier: verifier, + lookUpTable: mockPubkey, + action: Action.DEPOSIT, + poseidon }); - let tx = new Transaction({ - provider: lightProvider, - }); + let tx // successful proofgeneration if (app) { - await tx.compile(txParams, { mock: "123" }); + tx = new Transaction({ + provider: lightProvider, + params: txParams, + appParams: { mock: "123" } + }); } else { - await tx.compile(txParams); + tx = new Transaction({ + provider: lightProvider, + params: txParams, + }); } + await tx.compile(); await tx.getProof(); // unsuccessful proofgeneration diff --git a/light-sdk-ts/src/errors.ts b/light-sdk-ts/src/errors.ts index 037da7a1a7..a1b29ca2c0 100644 --- a/light-sdk-ts/src/errors.ts +++ b/light-sdk-ts/src/errors.ts @@ -14,12 +14,95 @@ export class UtxoError extends Error { name = this.constructor.name; code: string; codeMessage: string; - codeStack: string | null; - constructor(code: string, codeMessage: string, codeStack?: string) { + constructor(code: string, codeMessage: string) { super(`Utxo error ${code}: ${codeMessage}`); this.code = code; this.codeMessage = codeMessage; - this.codeStack = codeStack || null; } } + +export enum ProviderErrorCode { + SOL_MERKLE_TREE_UNDEFINED = "SOL_MERKLE_TREE_UNDEFINED", + ANCHOR_PROVIDER_UNDEFINED = "ANCHOR_PROVIDER_UNDEFINED", + PROVIDER_UNDEFINED = "PROVIDER_UNDEFINED", +} + +export enum SolMerkleTreeErrorCode { + MERKLE_TREE_UNDEFINED = "MERKLE_TREE_UNDEFINED", +} +export enum TransactionParametersErrorCode { + NO_VERIFIER_PROVIDED = "NO_VERIFIER_PROVIDED", + NO_POSEIDON_HASHER_PROVIDED = "NO_POSEIDON_HASHER_PROVIDED", + NO_ACTION_PROVIDED = "NO_ACTION_PROVIDED", + PUBLIC_AMOUNT_NEGATIVE = "PUBLIC_AMOUNT_NEGATIVE", + SOL_RECIPIENT_DEFINED = "SOL_RECIPIENT_DEFINED", + SPL_RECIPIENT_DEFINED = "SPL_RECIPIENT_DEFINED", + PUBLIC_AMOUNT_NOT_U64 = "PUBLIC_AMOUNT_NOT_U64", + RELAYER_DEFINED = "RELAYER_DEFINED", + INVALID_PUBLIC_AMOUNT = "INVALID_PUBLIC_AMOUNT", + SOL_SENDER_DEFINED = "SOL_SENDER_DEFINED", + SPL_SENDER_DEFINED = "SPL_SENDER_DEFINED", + PUBLIC_AMOUNT_SPL_NOT_ZERO = "PUBLIC_AMOUNT_SPL_NOT_ZERO", + PUBLIC_AMOUNT_SOL_NOT_ZERO = "PUBLIC_AMOUNT_SOL_NOT_ZERO", + LOOK_UP_TABLE_UNDEFINED = "LOOK_UP_TABLE_UNDEFINED", +} + +export enum TransactionErrorCode { + ROOT_INDEX_NOT_FETCHED = "ROOT_INDEX_NOT_FETCHED", + REMAINING_ACCOUNTS_NOT_CREATED = "REMAINING_ACCOUNTS_NOT_CREATED", + TRANSACTION_INPUTS_UNDEFINED = "TRANSACTION_INPUTS_UNDEFINED", + WALLET_RELAYER_INCONSISTENT = "WALLET_RELAYER_INCONSISTENT", + TX_PARAMETERS_UNDEFINED = "TX_PARAMETERS_UNDEFINED", + APP_PARAMETERS_UNDEFINED = "APP_PARAMETERS_UNDEFINED", + RELAYER_UNDEFINED = "TransactionParameters.relayer is undefined", + WALLET_UNDEFINED = "WALLET_UNDEFINED", + NO_UTXOS_PROVIDED = "NO_UTXOS_PROVIDED", + EXCEEDED_MAX_ASSETS = "EXCEEDED_MAX_ASSETS", + VERIFIER_PROGRAM_UNDEFINED = "VERIFIER_PROGRAM_UNDEFINED", + SPL_RECIPIENT_UNDEFINED = "SPL_RECIPIENT_UNDEFINED", + SOL_RECIPIENT_UNDEFINED = "SOL_RECIPIENT_UNDEFINED", + SPL_SENDER_UNDEFINED = "SPL_SENDER_UNDEFINED", + SOL_SENDER_UNDEFINED = "SOL_SENDER_UNDEFINED", + ASSET_PUBKEYS_UNDEFINED = "ASSET_PUBKEYS_UNDEFINED", + ACTION_IS_NO_WITHDRAWAL = "ACTION_IS_NO_WITHDRAWAL", + ACTION_IS_NO_DEPOSIT = "ACTION_IS_NO_DEPOSIT", + INPUT_UTXOS_UNDEFINED = "INPUT_UTXOS_UNDEFINED", + OUTPUT_UTXOS_UNDEFINED = "OUTPUT_UTXOS_UNDEFINED", + GET_MINT_FAILED = "GET_MINT_FAILED", + VERIFIER_UNDEFINED = "VERIFIER_UNDEFINED", + PROOF_INPUT_UNDEFINED = "PROOF_INPUT_UNDEFINED", + NO_PARAMETERS_PROVIDED = "NO_PARAMETERS_PROVIDED", + ROOT_NOT_FOUND = "ROOT_NOT_FOUND", + VERIFIER_CONFIG_UNDEFINED = "VERIFIER_CONFIG_UNDEFINED", + RELAYER_FEE_UNDEFINED = "RELAYER_FEE_UNDEFINED", + ENCRYPTING_UTXOS_FAILED = "ENCRYPTING_UTXOS_FAILED", + GET_INSTRUCTIONS_FAILED = "GET_INSTRUCTIONS_FAILED", + SEND_TRANSACTION_FAILED = "SEND_TRANSACTION_FAILED", + PUBLIC_INPUTS_UNDEFINED = "PUBLIC_INPUTS_UNDEFINED", + MERKLE_TREE_PROGRAM_UNDEFINED = "MERKLE_TREE_PROGRAM_UNDEFINED", + INPUT_UTXO_NOT_INSERTED_IN_MERKLE_TREE = "INPUT_UTXO_NOT_INSERTED_IN_MERKLE_TREE", + INVALID_PROOF = "INVALID_PROOF", + POSEIDON_HASHER_UNDEFINED = "POSEIDON_HASHER_UNDEFINED", +} + +export class MetaError extends Error { + code: string; + codeMessage?: string; + functionName: string; + + constructor(code: string, functionName: string, codeMessage?: string) { + super(`${code}: ${codeMessage}`); + + this.codeMessage = codeMessage; + this.code = code; + this.functionName = functionName; + } +} + +/** + * @description Thrown when something fails in the Transaction class. + **/ +export class TransactionError extends MetaError {} + +export class TransactioParametersError extends MetaError {} diff --git a/light-sdk-ts/src/idls/merkle_tree_program.ts b/light-sdk-ts/src/idls/merkle_tree_program.ts index e2cdbed8c9..15d55226f3 100644 --- a/light-sdk-ts/src/idls/merkle_tree_program.ts +++ b/light-sdk-ts/src/idls/merkle_tree_program.ts @@ -54,73 +54,53 @@ export type MerkleTreeProgram = { }, { name: "AUTHORITY_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"AUTHORITY_SEED"'; + type: "bytes"; + value: "[65, 85, 84, 72, 79, 82, 73, 84, 89, 95, 83, 69, 69, 68]"; }, { name: "MERKLE_TREE_AUTHORITY_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"MERKLE_TREE_AUTHORITY"'; + type: "bytes"; + value: "[77, 69, 82, 75, 76, 69, 95, 84, 82, 69, 69, 95, 65, 85, 84, 72, 79, 82, 73, 84, 89]"; }, { name: "TREE_ROOT_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"TREE_ROOT_SEED"'; + type: "bytes"; + value: "[84, 82, 69, 69, 95, 82, 79, 79, 84, 95, 83, 69, 69, 68]"; }, { name: "STORAGE_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"storage"'; + type: "bytes"; + value: "[115, 116, 111, 114, 97, 103, 101]"; }, { name: "LEAVES_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"leaves"'; + type: "bytes"; + value: "[108, 101, 97, 118, 101, 115]"; }, { name: "NULLIFIER_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"nf"'; + type: "bytes"; + value: "[110, 102]"; }, { name: "POOL_TYPE_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"pooltype"'; + type: "bytes"; + value: "[112, 111, 111, 108, 116, 121, 112, 101]"; }, { name: "POOL_CONFIG_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"pool-config"'; + type: "bytes"; + value: "[112, 111, 111, 108, 45, 99, 111, 110, 102, 105, 103]"; }, { name: "POOL_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"pool"'; + type: "bytes"; + value: "[112, 111, 111, 108]"; }, { name: "TOKEN_AUTHORITY_SEED"; - type: { - defined: "&[u8]"; - }; - value: 'b"spl"'; + type: "bytes"; + value: "[115, 112, 108]"; }, ]; instructions: [ @@ -1248,73 +1228,54 @@ export const IDL: MerkleTreeProgram = { }, { name: "AUTHORITY_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"AUTHORITY_SEED"', + type: "bytes", + value: "[65, 85, 84, 72, 79, 82, 73, 84, 89, 95, 83, 69, 69, 68]", }, { name: "MERKLE_TREE_AUTHORITY_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"MERKLE_TREE_AUTHORITY"', + type: "bytes", + value: + "[77, 69, 82, 75, 76, 69, 95, 84, 82, 69, 69, 95, 65, 85, 84, 72, 79, 82, 73, 84, 89]", }, { name: "TREE_ROOT_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"TREE_ROOT_SEED"', + type: "bytes", + value: "[84, 82, 69, 69, 95, 82, 79, 79, 84, 95, 83, 69, 69, 68]", }, { name: "STORAGE_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"storage"', + type: "bytes", + value: "[115, 116, 111, 114, 97, 103, 101]", }, { name: "LEAVES_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"leaves"', + type: "bytes", + value: "[108, 101, 97, 118, 101, 115]", }, { name: "NULLIFIER_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"nf"', + type: "bytes", + value: "[110, 102]", }, { name: "POOL_TYPE_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"pooltype"', + type: "bytes", + value: "[112, 111, 111, 108, 116, 121, 112, 101]", }, { name: "POOL_CONFIG_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"pool-config"', + type: "bytes", + value: "[112, 111, 111, 108, 45, 99, 111, 110, 102, 105, 103]", }, { name: "POOL_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"pool"', + type: "bytes", + value: "[112, 111, 111, 108]", }, { name: "TOKEN_AUTHORITY_SEED", - type: { - defined: "&[u8]", - }, - value: 'b"spl"', + type: "bytes", + value: "[115, 112, 108]", }, ], instructions: [ diff --git a/light-sdk-ts/src/merkleTree/solMerkleTree.ts b/light-sdk-ts/src/merkleTree/solMerkleTree.ts index b1a948cedb..a71365535b 100644 --- a/light-sdk-ts/src/merkleTree/solMerkleTree.ts +++ b/light-sdk-ts/src/merkleTree/solMerkleTree.ts @@ -189,19 +189,18 @@ export class SolMerkleTree { poseidon, leaves, ); - const tmpMtFetchedRoots: any = mtFetched.roots; - if ( Array.from( leInt2Buff(unstringifyBigInts(fetchedMerkleTree.root()), 32), - ).toString() != - tmpMtFetchedRoots[mtFetched.currentRootIndex.toNumber()].toString() + // @ts-ignore: unknown type error + ).toString() != mtFetched.roots[mtFetched.currentRootIndex].toString() ) { throw new Error( `building merkle tree from chain failed: root local ${Array.from( leInt2Buff(unstringifyBigInts(fetchedMerkleTree.root()), 32), ).toString()} != root fetched ${ - tmpMtFetchedRoots[mtFetched.currentRootIndex.toNumber()] + // @ts-ignore: unknown type error + mtFetched.roots[mtFetched.currentRootIndex] }`, ); } diff --git a/light-sdk-ts/src/test-utils/createAccounts.ts b/light-sdk-ts/src/test-utils/createAccounts.ts index 8ab483a7b7..a119a21245 100644 --- a/light-sdk-ts/src/test-utils/createAccounts.ts +++ b/light-sdk-ts/src/test-utils/createAccounts.ts @@ -330,6 +330,17 @@ export async function createTestAccounts( } catch (error) { console.log(error); } + console.log("userSplAccount ", userSplAccount); + + console.log( + "funded account", + await getAccount( + connection, + userSplAccount!, //userTokenAccount, + "confirmed", + TOKEN_PROGRAM_ID, + ), + ); try { if (balanceUserToken == null) { diff --git a/light-sdk-ts/src/test-utils/functionalCircuit.ts b/light-sdk-ts/src/test-utils/functionalCircuit.ts index f863d3bf9e..b9d73dd67d 100644 --- a/light-sdk-ts/src/test-utils/functionalCircuit.ts +++ b/light-sdk-ts/src/test-utils/functionalCircuit.ts @@ -9,6 +9,7 @@ import { TransactionParameters, Utxo, VerifierZero, + Action, } from "../index"; import * as anchor from "@coral-xyz/anchor"; import { assert, expect } from "chai"; @@ -47,16 +48,20 @@ export async function functionalCircuitTest() { outputUtxos: [deposit_utxo1], merkleTreePubkey: mockPubkey, sender: mockPubkey, - senderFee: mockPubkey, + senderFee: lightProvider.nodeWallet?.publicKey, verifier: new VerifierZero(), + action: Action.DEPOSIT, + poseidon, + lookUpTable: mockPubkey, }); let tx = new Transaction({ provider: lightProvider, + params: txParams, }); // successful proofgeneration - await tx.compile(txParams); + await tx.compile(); await tx.getProof(); let x = true; diff --git a/light-sdk-ts/src/transaction.ts b/light-sdk-ts/src/transaction.ts index ba34adefd5..e50f8bd9a4 100644 --- a/light-sdk-ts/src/transaction.ts +++ b/light-sdk-ts/src/transaction.ts @@ -11,7 +11,7 @@ import { import * as anchor from "@coral-xyz/anchor"; import { TOKEN_PROGRAM_ID, getAccount } from "@solana/spl-token"; import { BN, Program } from "@coral-xyz/anchor"; -import { confirmConfig, MERKLE_TREE_KEY } from "./constants"; +import { AUTHORITY, confirmConfig, MERKLE_TREE_KEY } from "./constants"; import { N_ASSET_PUBKEYS, Utxo } from "./utxo"; import { PublicInputs, Verifier } from "./verifiers"; import { checkRentExemption } from "./test-utils/testChecks"; @@ -22,9 +22,16 @@ import { Account, merkleTreeProgramId, Relayer, + TransactionErrorCode, + TransactionError, + ProviderErrorCode, + SolMerkleTreeErrorCode, + TransactioParametersError, + initLookUpTable, + TransactionParametersErrorCode, + Provider, } from "./index"; import { IDL_MERKLE_TREE_PROGRAM } from "./idls/index"; -import { Provider } from "./wallet"; const snarkjs = require("snarkjs"); const nacl = require("tweetnacl"); var ffjavascript = require("ffjavascript"); @@ -62,40 +69,48 @@ export type transactionParameters = { }[]; }; +export enum Action { + DEPOSIT = "DEPOSIT", + TRANSFER = "TRANSFER", + WITHDRAWAL = "WITHDRAWAL", +} + +export type lightAccounts = { + sender?: PublicKey; + recipient?: PublicKey; + senderFee?: PublicKey; + recipientFee?: PublicKey; + verifierState?: PublicKey; + tokenAuthority?: PublicKey; + systemProgramId: PublicKey; + merkleTree: PublicKey; + tokenProgram: PublicKey; + registeredVerifierPda: PublicKey; + authority: PublicKey; + signingAddress?: PublicKey; + programMerkleTree: PublicKey; +}; + +export type remainingAccount = { + isSigner: boolean; + isWritable: boolean; + pubkey: PublicKey; +}; + export class TransactionParameters implements transactionParameters { - provider?: Provider; - inputUtxos?: Array; - outputUtxos?: Array; - accounts: { - sender?: PublicKey; - recipient?: PublicKey; - senderFee?: PublicKey; - recipientFee?: PublicKey; - verifierState?: PublicKey; - tokenAuthority?: PublicKey; - systemProgramId: PublicKey; - merkleTree: PublicKey; - tokenProgram: PublicKey; - registeredVerifierPda: PublicKey; - authority: PublicKey; - signingAddress?: PublicKey; - programMerkleTree: PublicKey; - }; - relayer?: Relayer; + inputUtxos: Array; + outputUtxos: Array; + accounts: lightAccounts; + // @ts-ignore: + relayer: Relayer; encryptedUtxos?: Uint8Array; verifier: Verifier; - verifierApp?: Verifier; - nullifierPdaPubkeys?: { - isSigner: boolean; - isWritable: boolean; - pubkey: PublicKey; - }[]; - leavesPdaPubkeys?: { - isSigner: boolean; - isWritable: boolean; - pubkey: PublicKey; - }[]; - merkleTreeProgram?: Program; + poseidon: any; + publicAmountSpl: BN; + publicAmountSol: BN; + assetPubkeys: PublicKey[]; + assetPubkeysCircuit: string[]; + action: Action; constructor({ merkleTreePubkey, @@ -106,14 +121,14 @@ export class TransactionParameters implements transactionParameters { recipientFee, inputUtxos, outputUtxos, - verifierApp, relayer, encryptedUtxos, - provider, + poseidon, + action, + lookUpTable, }: { merkleTreePubkey: PublicKey; verifier: Verifier; - verifierApp?: Verifier; sender?: PublicKey; recipient?: PublicKey; senderFee?: PublicKey; @@ -122,26 +137,334 @@ export class TransactionParameters implements transactionParameters { outputUtxos?: Utxo[]; relayer?: Relayer; encryptedUtxos?: Uint8Array; + poseidon: any; + action: Action; + lookUpTable?: PublicKey; provider?: Provider; }) { - try { - this.merkleTreeProgram = new Program( - IDL_MERKLE_TREE_PROGRAM, - merkleTreeProgramId, - // @ts-ignore - provider && provider, + if (!outputUtxos && !inputUtxos) { + throw new TransactioParametersError( + TransactionErrorCode.NO_UTXOS_PROVIDED, + "constructor", + "", ); - } catch (error) { - console.log(error); - console.log("assuming test mode thus continuing"); - // this.merkleTreeProgram = { - // programId: merkleTreeProgramId, - // }; - } - if (!this.merkleTreeProgram) throw new Error("merkleTreeProgram not set"); - if (!verifier) throw new Error("verifier undefined"); + } + + if (!verifier) { + throw new TransactioParametersError( + TransactionParametersErrorCode.NO_VERIFIER_PROVIDED, + "constructor", + "", + ); + } if (!verifier.verifierProgram) - throw new Error("verifier.verifierProgram undefined"); + throw new TransactioParametersError( + TransactionErrorCode.VERIFIER_PROGRAM_UNDEFINED, + "constructor", + "verifier.program undefined", + ); + + if (!poseidon) { + throw new TransactioParametersError( + TransactionParametersErrorCode.NO_POSEIDON_HASHER_PROVIDED, + "constructor", + "", + ); + } + + if (!action) { + throw new TransactioParametersError( + TransactionParametersErrorCode.NO_ACTION_PROVIDED, + "constructor", + "Define an action either Action.TRANSFER, Action.DEPOSIT,Action.WITHDRAWAL", + ); + } + + this.verifier = verifier; + this.poseidon = poseidon; + this.encryptedUtxos = encryptedUtxos; + this.action = action; + this.inputUtxos = this.addEmptyUtxos(inputUtxos, this.verifier.config.in); + this.outputUtxos = this.addEmptyUtxos( + outputUtxos, + this.verifier.config.out, + ); + + if (action === Action.DEPOSIT && senderFee && lookUpTable) { + this.relayer = new Relayer(senderFee, lookUpTable); + } else if (action === Action.DEPOSIT && !senderFee) { + throw new TransactioParametersError( + TransactionErrorCode.SOL_SENDER_UNDEFINED, + "constructor", + "Sender sol always needs to be defined because we use it as the signer to instantiate the relayer object.", + ); + } else if (action === Action.DEPOSIT && !lookUpTable) { + throw new TransactioParametersError( + TransactionParametersErrorCode.LOOK_UP_TABLE_UNDEFINED, + "constructor", + "At deposit lookup table needs to be defined to instantiate a relayer object with yourself as the relayer.", + ); + } + + if (action !== Action.DEPOSIT) { + if (relayer) { + this.relayer = relayer; + } else { + throw new TransactioParametersError( + TransactionErrorCode.RELAYER_UNDEFINED, + "constructor", + "For a transfer or withdrawal a relayer needs to be provided.", + ); + } + } + + const pubkeys = Transaction.getAssetPubkeys( + this.inputUtxos, + this.outputUtxos, + ); + this.assetPubkeys = pubkeys.assetPubkeys; + this.assetPubkeysCircuit = pubkeys.assetPubkeysCircuit; + this.publicAmountSol = Transaction.getExternalAmount( + 0, + this.inputUtxos, + this.outputUtxos, + this.assetPubkeysCircuit, + ); + this.publicAmountSpl = Transaction.getExternalAmount( + 1, + this.inputUtxos, + this.outputUtxos, + this.assetPubkeysCircuit, + ); + // safeguard should not be possible + if (!this.publicAmountSol.gte(new BN(0))) + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_NEGATIVE, + "constructor", + "Public sol amount cannot be negative.", + ); + if (!this.publicAmountSpl.gte(new BN(0))) + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_NEGATIVE, + "constructor", + "Public spl amount cannot be negative.", + ); + + // Checking plausibility of inputs + if (this.action === Action.DEPOSIT) { + /** + * No relayer + * public amounts are u64s + * sender is the user + * recipient is the merkle tree + */ + if (relayer) + throw new TransactioParametersError( + TransactionParametersErrorCode.RELAYER_DEFINED, + "constructor", + "For a deposit no relayer should to be provided, the user send the transaction herself.", + ); + try { + this.publicAmountSol.toArray("be", 8); + } catch (error) { + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + "constructor", + "Public amount needs to be a u64 at deposit. Check whether you defined input and output utxos correctly, for a deposit the amounts of output utxos need to be bigger than the amounts of input utxos", + ); + } + + try { + this.publicAmountSpl.toArray("be", 8); + } catch (error) { + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + "constructor", + "Public amount needs to be a u64 at deposit. Check whether you defined input and output utxos correctly, for a deposit the amounts of output utxos need to be bigger than the amounts of input utxos", + ); + } + if (!this.publicAmountSol.eq(new BN(0)) && recipientFee) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SOL_RECIPIENT_DEFINED, + "constructor", + "", + ); + } + if (!this.publicAmountSpl.eq(new BN(0)) && recipient) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SPL_RECIPIENT_DEFINED, + "constructor", + "", + ); + } + + if (!this.publicAmountSol.eq(new BN(0)) && !senderFee) { + throw new TransactioParametersError( + TransactionErrorCode.SOL_SENDER_UNDEFINED, + "constructor", + "", + ); + } + if (!this.publicAmountSpl.eq(new BN(0)) && !sender) { + throw new TransactioParametersError( + TransactionErrorCode.SPL_SENDER_UNDEFINED, + "constructor", + "", + ); + } + } else if (this.action === Action.WITHDRAWAL) { + /** + * relayer is defined + * public amounts sub FieldSize are negative or 0 + * for public amounts greater than 0 a recipient needs to be defined + * sender is the merkle tree + * recipient is the user + */ + // TODO: should I throw an error when a lookup table is defined? + if (!relayer) + throw new TransactioParametersError( + TransactionErrorCode.RELAYER_UNDEFINED, + "constructor", + "For a withdrawal a relayer needs to be provided.", + ); + // public amount is either 0 or negative + // this.publicAmountSol.add(FIELD_SIZE).mod(FIELD_SIZE) this changes the value + const tmpSol = this.publicAmountSol; + if (!tmpSol.sub(FIELD_SIZE).lte(new BN(0))) + throw new TransactioParametersError( + TransactionParametersErrorCode.INVALID_PUBLIC_AMOUNT, + "constructor", + "", + ); + const tmpSpl = this.publicAmountSpl; + if (!tmpSpl.sub(FIELD_SIZE).lte(new BN(0))) + throw new TransactioParametersError( + TransactionParametersErrorCode.INVALID_PUBLIC_AMOUNT, + "constructor", + "", + ); + try { + if (!tmpSol.eq(new BN(0))) { + tmpSol.sub(FIELD_SIZE).toArray("be", 8); + } + } catch (error) { + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + "constructor", + "Public amount needs to be a u64 at deposit.", + ); + } + + try { + if (!tmpSpl.eq(new BN(0))) { + tmpSpl.sub(FIELD_SIZE).toArray("be", 8); + } + } catch (error) { + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + "constructor", + "Public amount needs to be a u64 at deposit.", + ); + } + + if (!this.publicAmountSol.eq(new BN(0)) && !recipientFee) { + throw new TransactioParametersError( + TransactionErrorCode.SOL_RECIPIENT_UNDEFINED, + "constructor", + "", + ); + } + + if (!this.publicAmountSpl.eq(new BN(0)) && !recipient) { + throw new TransactioParametersError( + TransactionErrorCode.SPL_RECIPIENT_UNDEFINED, + "constructor", + "", + ); + } + // && senderFee.toBase58() != merkle tree token pda + if (!this.publicAmountSol.eq(new BN(0)) && senderFee) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SOL_SENDER_DEFINED, + "constructor", + "", + ); + } + if (!this.publicAmountSpl.eq(new BN(0)) && sender) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SPL_SENDER_DEFINED, + "constructor", + "", + ); + } + } else if (this.action === Action.TRANSFER) { + /** + * relayer is defined + * public amount spl amount is 0 + * public amount spl amount sub FieldSize is equal to the relayer fee + * sender is the merkle tree + * recipient does not exists it is an internal transfer just the relayer is paid + */ + if (!relayer) + throw new TransactioParametersError( + TransactionErrorCode.RELAYER_UNDEFINED, + "constructor", + "For a transfer a relayer needs to be provided.", + ); + if (!this.publicAmountSpl.eq(new BN(0))) + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_SPL_NOT_ZERO, + "constructor", + "For a transfer public spl amount needs to be zero", + ); + + const tmpSol = this.publicAmountSol; + if (!tmpSol.sub(FIELD_SIZE).mul(new BN(-1)).eq(relayer.relayerFee)) + throw new TransactioParametersError( + TransactionParametersErrorCode.PUBLIC_AMOUNT_SOL_NOT_ZERO, + "constructor", + `public amount ${tmpSol.sub(FIELD_SIZE).mul(new BN(-1))} should be ${ + relayer.relayerFee + }`, + ); + + if (recipient) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SPL_RECIPIENT_DEFINED, + "constructor", + "This is a transfer, no spl amount should be withdrawn. To withdraw an spl amount mark the transaction as withdrawal.", + ); + } + + if (recipientFee) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SOL_RECIPIENT_DEFINED, + "constructor", + "This is a transfer, no sol amount should be withdrawn. To withdraw an sol amount mark the transaction as withdrawal.", + ); + } + + if (senderFee) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SOL_SENDER_DEFINED, + "constructor", + "", + ); + } + if (sender) { + throw new TransactioParametersError( + TransactionParametersErrorCode.SPL_SENDER_DEFINED, + "constructor", + "", + ); + } + } else { + throw new TransactioParametersError( + TransactionParametersErrorCode.NO_ACTION_PROVIDED, + "constructor", + "", + ); + } this.accounts = { systemProgramId: SystemProgram.programId, @@ -157,21 +480,129 @@ export class TransactionParameters implements transactionParameters { ), sender: sender, recipient: recipient, - senderFee: senderFee, // TODO: change to feeSender - recipientFee: recipientFee, // TODO: change name to feeRecipient + senderFee: senderFee, // TODO: change to senderSol + recipientFee: recipientFee, // TODO: change name to recipientSol programMerkleTree: merkleTreeProgramId, + tokenAuthority: Transaction.getTokenAuthority(), }; - this.verifier = verifier; - this.outputUtxos = outputUtxos; - this.inputUtxos = inputUtxos; - if (!this.outputUtxos && !inputUtxos) { - throw new Error("No utxos provided."); + + this.assignAccounts(); + // @ts-ignore: + this.accounts.signingAddress = this.relayer.accounts.relayerPubkey; + } + + /** + * @description Adds empty utxos until the desired number of utxos is reached. + * @note The zero knowledge proof circuit needs all inputs to be defined. + * @note Therefore, we have to pass in empty inputs for values we don't use. + * @param utxos + * @param len + * @returns + */ + addEmptyUtxos(utxos: Utxo[] = [], len: number): Utxo[] { + while (utxos.length < len) { + utxos.push(new Utxo({ poseidon: this.poseidon })); } - this.verifierApp = verifierApp; - this.relayer = relayer; - this.encryptedUtxos = encryptedUtxos; + return utxos; + } + + /** + * @description Assigns spl and sol sender or recipient accounts to transaction parameters based on action. + */ + assignAccounts() { + if (!this.verifier.verifierProgram) + throw new TransactioParametersError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "assignAccounts", + "Verifier.verifierProgram undefined.", + ); + if (!this.assetPubkeys) + throw new TransactioParametersError( + TransactionErrorCode.ASSET_PUBKEYS_UNDEFINED, + "assignAccounts assetPubkeys undefined", + "assignAccounts", + ); + + if ( + this.action.toString() === Action.WITHDRAWAL.toString() || + this.action.toString() === Action.TRANSFER.toString() + ) { + this.accounts.sender = MerkleTreeConfig.getSplPoolPdaToken( + this.assetPubkeys[1], + merkleTreeProgramId, + ); + this.accounts.senderFee = + MerkleTreeConfig.getSolPoolPda(merkleTreeProgramId).pda; + + if (!this.accounts.recipient) { + // AUTHORITY is used as place holder + this.accounts.recipient = AUTHORITY; + if (!this.publicAmountSpl?.eq(new BN(0))) { + throw new TransactionError( + TransactionErrorCode.SPL_RECIPIENT_UNDEFINED, + "assignAccounts", + "Spl recipient is undefined while public spl amount is != 0.", + ); + } + } + + if (!this.accounts.recipientFee) { + // AUTHORITY is used as place holder + this.accounts.recipientFee = AUTHORITY; + if ( + !this.publicAmountSol.eq(new BN(0)) && + !this.publicAmountSol + ?.sub(FIELD_SIZE) + .mul(new BN(-1)) + .sub(new BN(this.relayer.relayerFee)) + .eq(new BN(0)) + ) { + throw new TransactioParametersError( + TransactionErrorCode.SOL_RECIPIENT_UNDEFINED, + "assignAccounts", + "Sol recipient is undefined while public spl amount is != 0.", + ); + } + } + } else { + if (this.action.toString() !== Action.DEPOSIT.toString()) { + throw new TransactioParametersError( + TransactionErrorCode.ACTION_IS_NO_DEPOSIT, + "assignAccounts", + "Action is withdrawal but should not be. Spl & sol sender accounts are provided and a relayer which is used to identify transfers and withdrawals. For a deposit do not provide a relayer.", + ); + } + + this.accounts.recipient = MerkleTreeConfig.getSplPoolPdaToken( + this.assetPubkeys[1], + merkleTreeProgramId, + ); + this.accounts.recipientFee = + MerkleTreeConfig.getSolPoolPda(merkleTreeProgramId).pda; + if (!this.accounts.sender) { + this.accounts.sender = SystemProgram.programId; + if (!this.publicAmountSpl?.eq(new BN(0))) { + throw new TransactioParametersError( + TransactionErrorCode.SPL_SENDER_UNDEFINED, + "assignAccounts", + "Spl sender is undefined while public spl amount is != 0.", + ); + } + } + this.accounts.senderFee = TransactionParameters.getEscrowPda( + this.verifier.verifierProgram.programId, + ); + } + } + + static getEscrowPda(verifierProgramId: PublicKey): PublicKey { + return PublicKey.findProgramAddressSync( + [anchor.utils.bytes.utf8.encode("escrow")], + verifierProgramId, + )[0]; } } +// TODO: make dev provide the classification and check here -> it is easier to check whether transaction parameters are plausible // add verifier class which is passed in with the constructor // this class replaces the send transaction, also configures path the provingkey and witness, the inputs for the integrity hash @@ -183,38 +614,38 @@ export class TransactionParameters implements transactionParameters { export class Transaction { merkleTreeProgram?: Program; shuffleEnabled: Boolean; - action?: string; - params?: TransactionParameters; // contains accounts + params: TransactionParameters; // contains accounts appParams?: any; // TODO: relayer shd pls should be part of the provider by default + optional override on Transaction level provider: Provider; - //txInputs; - publicInputs?: PublicInputs; - rootIndex: any; - proofBytes: any; - proofBytesApp: any; - publicInputsApp?: PublicInputs; - encryptedUtxos?: Uint8Array; + transactionInputs: { + publicInputs?: PublicInputs; + rootIndex?: any; + proofBytes?: any; + proofBytesApp?: any; + publicInputsApp?: PublicInputs; + encryptedUtxos?: Uint8Array; + }; + + remainingAccounts?: { + nullifierPdaPubkeys?: remainingAccount[]; + leavesPdaPubkeys?: remainingAccount[]; + }; proofInput: any; proofInputSystem: any; - // Tmp rnd stuff for proof input - assetPubkeysCircuit?: BN[]; - assetPubkeys?: PublicKey[]; - publicAmount?: BN; - feeAmount?: BN; - inputMerklePathIndices?: number[]; - inputMerklePathElements?: string[][]; - publicInputsBytes?: number[][]; - connectingHash?: string; + // Tests - recipientBalancePriorTx?: BN; - relayerRecipientAccountBalancePriorLastTx?: BN; - txIntegrityHash?: BN; - senderFeeBalancePriorTx?: BN; - recipientFeeBalancePriorTx?: BN; - is_token?: boolean; + testValues?: { + recipientBalancePriorTx?: BN; + relayerRecipientAccountBalancePriorLastTx?: BN; + txIntegrityHash?: BN; + senderFeeBalancePriorTx?: BN; + recipientFeeBalancePriorTx?: BN; + is_token?: boolean; + }; + /** * Initialize transaction * @@ -224,281 +655,311 @@ export class Transaction { constructor({ provider, shuffleEnabled = false, + params, + appParams, }: { provider: Provider; shuffleEnabled?: boolean; + params: TransactionParameters; + appParams?: any; }) { - if (!provider.poseidon) throw new Error("Poseidon not set"); - if (!provider.solMerkleTree) throw new Error("Merkle tree not set"); + if (!provider.poseidon) + throw new TransactionError( + TransactionErrorCode.POSEIDON_HASHER_UNDEFINED, + "constructor", + "Wallet not set", + ); + if (!provider.solMerkleTree) + throw new TransactionError( + ProviderErrorCode.SOL_MERKLE_TREE_UNDEFINED, + "constructor", + "Merkle tree not set", + ); if (!provider.browserWallet && !provider.nodeWallet) - throw new Error("Wallet not set"); + throw new TransactionError( + TransactionErrorCode.WALLET_UNDEFINED, + "constructor", + "Wallet not set", + ); this.provider = provider; this.shuffleEnabled = shuffleEnabled; + // TODO: create and check for existence of merkleTreeAssetPubkey depending on utxo asset + this.params = params; + this.appParams = appParams; + + //TODO: change to check whether browser/node wallet are the same as signing address + if (params.action === Action.DEPOSIT) { + let wallet = + this.provider.browserWallet !== undefined + ? this.provider.browserWallet + : this.provider.nodeWallet; + if ( + wallet?.publicKey.toBase58() !== + params.relayer.accounts.relayerPubkey.toBase58() && + wallet?.publicKey.toBase58() !== + params.accounts.signingAddress?.toBase58() + ) { + throw new TransactionError( + TransactionErrorCode.WALLET_RELAYER_INCONSISTENT, + "compile", + `Node or Browser wallet and senderFee used to instantiate yourself as relayer at deposit are inconsistent.`, + ); + } + } + + this.transactionInputs = {}; + this.testValues = {}; + this.remainingAccounts = {}; } /** Returns serialized instructions */ - async proveAndCreateInstructionsJson( - params: TransactionParameters, - ): Promise { - await this.compileAndProve(params); + async proveAndCreateInstructionsJson(): Promise { + await this.compileAndProve(); return await this.getInstructionsJson(); } - async proveAndCreateInstructions( - params: TransactionParameters, - appParams?: any, - ): Promise { - await this.compileAndProve(params, appParams); - if (appParams) { + async proveAndCreateInstructions(): Promise { + await this.compileAndProve(); + if (this.appParams) { return await this.appParams.verifier.getInstructions(this); } else if (this.params) { return await this.params.verifier.getInstructions(this); } else { - throw new Error("No parameters provided"); + throw new TransactionError( + TransactionErrorCode.NO_PARAMETERS_PROVIDED, + "proveAndCreateInstructions", + "", + ); } } - async compileAndProve(params: TransactionParameters, appParams?: any) { - await this.compile(params, appParams); + async compileAndProve() { + this.compile(); await this.getProof(); - if (appParams) { + if (this.appParams) { await this.getAppProof(); } + await this.getRootIndex(); + await this.getPdaAddresses(); } - async compile(params: TransactionParameters, appParams?: any) { - // TODO: create and check for existence of merkleTreeAssetPubkey depending on utxo asset - this.params = params; - this.appParams = appParams; + /** + * @description Prepares proof inputs. + */ + compile() { + this.shuffleUtxos(this.params.inputUtxos); + this.shuffleUtxos(this.params.outputUtxos); - if (params.relayer) { - // TODO: rename to send - this.action = "WITHDRAWAL"; - console.log("withdrawal"); - } else if ( - !params.relayer && - (this.provider.browserWallet || this.provider.nodeWallet) && - this.provider.lookUpTable - ) { - this.action = "DEPOSIT"; - this.params.relayer = new Relayer( - this.provider.browserWallet - ? this.provider.browserWallet.publicKey - : this.provider.nodeWallet!.publicKey, - this.provider.lookUpTable, + if (!this.provider.solMerkleTree) + throw new TransactionError( + ProviderErrorCode.SOL_MERKLE_TREE_UNDEFINED, + "getProofInput", + "", ); - } else { - throw new Error( - "Couldn't assign relayer- no relayer nor wallet, or provider provided.", + if (!this.provider.solMerkleTree.merkleTree) + throw new TransactionError( + SolMerkleTreeErrorCode.MERKLE_TREE_UNDEFINED, + "getProofInput", + "", ); - } - if (this.params.relayer) { - this.params.accounts.signingAddress = - this.params.relayer.accounts.relayerPubkey; - } else { - throw new Error( - `Relayer not provided, or assigment failed at deposit this.params: ${this.params}`, + if (!this.params.assetPubkeysCircuit) + throw new TransactionError( + TransactionErrorCode.ASSET_PUBKEYS_UNDEFINED, + "getProofInput", + "", ); - } - // prepare utxos - const pubkeys = this.getAssetPubkeys(params.inputUtxos, params.outputUtxos); - this.assetPubkeys = pubkeys.assetPubkeys; - this.assetPubkeysCircuit = pubkeys.assetPubkeysCircuit; - this.params.inputUtxos = this.addEmptyUtxos( - params.inputUtxos, - params.verifier.config.in, - ); - this.params.outputUtxos = this.addEmptyUtxos( - params.outputUtxos, - params.verifier.config.out, - ); - this.shuffleUtxos(this.params.inputUtxos); - this.shuffleUtxos(this.params.outputUtxos); - // prep and get proof inputs - this.publicAmount = this.getExternalAmount(1); - this.feeAmount = this.getExternalAmount(0); - this.assignAccounts(); - this.getMerkleProofs(); - this.getProofInput(); - await this.getRootIndex(); + const { inputMerklePathIndices, inputMerklePathElements } = + Transaction.getMerkleProofs(this.provider, this.params.inputUtxos); + + this.proofInputSystem = { + root: this.provider.solMerkleTree.merkleTree.root(), + inputNullifier: this.params.inputUtxos.map((x) => x.getNullifier()), + // TODO: move public and fee amounts into tx preparation + publicAmount: this.params.publicAmountSpl.toString(), + feeAmount: this.params.publicAmountSol.toString(), + mintPubkey: this.getMint(), + inPrivateKey: this.params.inputUtxos?.map((x) => x.account.privkey), + inPathIndices: inputMerklePathIndices, + inPathElements: inputMerklePathElements, + }; + this.proofInput = { + extDataHash: this.getTxIntegrityHash().toString(), + outputCommitment: this.params.outputUtxos.map((x) => x.getCommitment()), + inAmount: this.params.inputUtxos?.map((x) => x.amounts), + inBlinding: this.params.inputUtxos?.map((x) => x.blinding), + assetPubkeys: this.params.assetPubkeysCircuit, + outAmount: this.params.outputUtxos?.map((x) => x.amounts), + outBlinding: this.params.outputUtxos?.map((x) => x.blinding), + outPubkey: this.params.outputUtxos?.map((x) => x.account.pubkey), + inIndices: this.getIndices(this.params.inputUtxos), + outIndices: this.getIndices(this.params.outputUtxos), + inInstructionType: this.params.inputUtxos?.map((x) => x.instructionType), + outInstructionType: this.params.outputUtxos?.map( + (x) => x.instructionType, + ), + inPoolType: this.params.inputUtxos?.map((x) => x.poolType), + outPoolType: this.params.outputUtxos?.map((x) => x.poolType), + inVerifierPubkey: this.params.inputUtxos?.map( + (x) => x.verifierAddressCircuit, + ), + outVerifierPubkey: this.params.outputUtxos?.map( + (x) => x.verifierAddressCircuit, + ), + }; + if (this.appParams) { + this.proofInput.connectingHash = Transaction.getConnectingHash( + this.params, + this.provider.poseidon, + this.proofInput.extDataHash, + ); + this.proofInput.verifier = this.params.verifier?.pubkey; + } } getMint() { - if (this.getExternalAmount(1).toString() == "0") { + if (this.params.publicAmountSpl.toString() == "0") { return new BN(0); - } else if (this.assetPubkeysCircuit) { - return this.assetPubkeysCircuit[1]; - } else { - throw new Error("Get mint failed"); - } - } - - getProofInput() { - if ( - this.params && - this.provider.solMerkleTree?.merkleTree && - this.params.inputUtxos && - this.params.outputUtxos && - this.assetPubkeysCircuit - ) { - this.proofInputSystem = { - root: this.provider.solMerkleTree.merkleTree.root(), - inputNullifier: this.params.inputUtxos.map((x) => x.getNullifier()), - // TODO: move public and fee amounts into tx preparation - publicAmount: this.getExternalAmount(1).toString(), - feeAmount: this.getExternalAmount(0).toString(), - mintPubkey: this.getMint(), - inPrivateKey: this.params.inputUtxos?.map((x) => x.account.privkey), - inPathIndices: this.inputMerklePathIndices, - inPathElements: this.inputMerklePathElements, - }; - this.proofInput = { - extDataHash: this.getTxIntegrityHash().toString(), - outputCommitment: this.params.outputUtxos.map((x) => x.getCommitment()), - inAmount: this.params.inputUtxos?.map((x) => x.amounts), - inBlinding: this.params.inputUtxos?.map((x) => x.blinding), - assetPubkeys: this.assetPubkeysCircuit, - // data for 2 transaction outputUtxos - outAmount: this.params.outputUtxos?.map((x) => x.amounts), - outBlinding: this.params.outputUtxos?.map((x) => x.blinding), - outPubkey: this.params.outputUtxos?.map((x) => x.account.pubkey), - inIndices: this.getIndices(this.params.inputUtxos), - outIndices: this.getIndices(this.params.outputUtxos), - inInstructionType: this.params.inputUtxos?.map( - (x) => x.instructionType, - ), - outInstructionType: this.params.outputUtxos?.map( - (x) => x.instructionType, - ), - inPoolType: this.params.inputUtxos?.map((x) => x.poolType), - outPoolType: this.params.outputUtxos?.map((x) => x.poolType), - inVerifierPubkey: this.params.inputUtxos?.map( - (x) => x.verifierAddressCircuit, - ), - outVerifierPubkey: this.params.outputUtxos?.map( - (x) => x.verifierAddressCircuit, - ), - }; - if (this.appParams) { - this.proofInput.connectingHash = Transaction.getConnectingHash( - this.params, - this.provider.poseidon, - this.proofInput.extDataHash, //this.getTxIntegrityHash().toString() - ); - this.proofInput.verifier = this.params.verifier?.pubkey; - } + } else if (this.params.assetPubkeysCircuit) { + return this.params.assetPubkeysCircuit[1]; } else { - throw new Error(`getProofInput has undefined inputs`); + throw new TransactionError( + TransactionErrorCode.GET_MINT_FAILED, + "getMint", + "Get mint failed", + ); } } async getAppProof() { - if (this.appParams && this.params) { - this.appParams.inputs.connectingHash = Transaction.getConnectingHash( - this.params, - this.provider.poseidon, - this.getTxIntegrityHash().toString(), + if (!this.appParams) + throw new TransactionError( + TransactionErrorCode.APP_PARAMETERS_UNDEFINED, + "getAppProof", + "", ); - const path = require("path"); - // TODO: find a better more flexible solution - const firstPath = path.resolve(__dirname, "../../../sdk/build-circuit/"); - let { proofBytes, publicInputs } = await this.getProofInternal( - this.appParams.verifier, - { - ...this.appParams.inputs, - ...this.proofInput, - inPublicKey: this.params?.inputUtxos?.map( - (utxo) => utxo.account.pubkey, - ), - }, - firstPath, + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getAppProof", + "", ); - this.proofBytesApp = proofBytes; - this.publicInputsApp = publicInputs; - } else { - throw new Error("No app params or params provided"); - } + this.appParams.inputs.connectingHash = Transaction.getConnectingHash( + this.params, + this.provider.poseidon, + this.getTxIntegrityHash().toString(), + ); + const path = require("path"); + // TODO: find a better more flexible solution, pass in path with app params + const firstPath = path.resolve(__dirname, "../../../sdk/build-circuit/"); + let { proofBytes, publicInputs } = await this.getProofInternal( + this.appParams.verifier, + { + ...this.appParams.inputs, + ...this.proofInput, + inPublicKey: this.params?.inputUtxos?.map( + (utxo) => utxo.account.pubkey, + ), + }, + firstPath, + ); + + this.transactionInputs.proofBytesApp = proofBytes; + this.transactionInputs.publicInputsApp = publicInputs; } async getProof() { const path = require("path"); const firstPath = path.resolve(__dirname, "../build-circuits/"); - if (this.params && this.params?.verifier) { - let { proofBytes, publicInputs } = await this.getProofInternal( - this.params?.verifier, - { ...this.proofInput, ...this.proofInputSystem }, - firstPath, - ); - this.proofBytes = proofBytes; - this.publicInputs = publicInputs; - } else { - throw new Error("Params not defined."); - } + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getProof", + "", + ); + if (!this.params.verifier) + throw new TransactionError( + TransactionErrorCode.VERIFIER_UNDEFINED, + "getProof", + "", + ); - // TODO: remove anchor provider if possible - if (this.provider.provider) { - await this.getPdaAddresses(); - } + let { proofBytes, publicInputs } = await this.getProofInternal( + this.params?.verifier, + { ...this.proofInput, ...this.proofInputSystem }, + firstPath, + ); + this.transactionInputs.proofBytes = proofBytes; + this.transactionInputs.publicInputs = publicInputs; } async getProofInternal(verifier: Verifier, inputs: any, firstPath: string) { - if (!this.provider.solMerkleTree?.merkleTree) { - throw new Error("merkle tree not built"); - } - if (!this.proofInput) { - throw new Error("transaction not compiled"); - } - if (!this.params) { - throw new Error("params undefined probably not compiled"); - } else { - // console.log("this.proofInput ", inputs); - - try { - const completePathWtns = firstPath + "/" + verifier.wtnsGenPath; - const completePathZkey = firstPath + "/" + verifier.zkeyPath; - - console.time("Proof generation"); - const { proof, publicSignals } = await snarkjs.groth16.fullProve( - stringifyBigInts(inputs), - completePathWtns, - completePathZkey, - ); - const proofJson = JSON.stringify(proof, null, 1); - const publicInputsJson = JSON.stringify(publicSignals, null, 1); - console.timeEnd("Proof generation"); - - const vKey = await snarkjs.zKey.exportVerificationKey(completePathZkey); - const res = await snarkjs.groth16.verify(vKey, publicSignals, proof); - if (res === true) { - console.log("Verification OK"); - } else { - console.log("Invalid proof"); - throw new Error("Invalid Proof"); - } - - var publicInputsBytesJson = JSON.parse(publicInputsJson.toString()); - var publicInputsBytes = new Array>(); - for (var i in publicInputsBytesJson) { - let ref: Array = Array.from([ - ...leInt2Buff(unstringifyBigInts(publicInputsBytesJson[i]), 32), - ]).reverse(); - publicInputsBytes.push(ref); - // TODO: replace ref, error is that le and be do not seem to be consistent - // new BN(publicInputsBytesJson[i], "le").toArray("be",32) - // assert.equal(ref.toString(), publicInputsBytes[publicInputsBytes.length -1].toString()); - } - const publicInputs = - verifier.parsePublicInputsFromArray(publicInputsBytes); + if (!this.provider.solMerkleTree) + throw new TransactionError( + ProviderErrorCode.SOL_MERKLE_TREE_UNDEFINED, + "getProofInternal", + "", + ); + if (!this.provider.solMerkleTree.merkleTree) + throw new TransactionError( + SolMerkleTreeErrorCode.MERKLE_TREE_UNDEFINED, + "getProofInternal", + "", + ); + if (!this.proofInput) + throw new TransactionError( + TransactionErrorCode.PROOF_INPUT_UNDEFINED, + "transaction not compiled", + ); + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getProof", + "params undefined probably not compiled", + ); + const completePathWtns = firstPath + "/" + verifier.wtnsGenPath; + const completePathZkey = firstPath + "/" + verifier.zkeyPath; + console.time("Proof generation"); + + const { proof, publicSignals } = await snarkjs.groth16.fullProve( + stringifyBigInts(inputs), + completePathWtns, + completePathZkey, + ); + console.timeEnd("Proof generation"); - const proofBytes = await Transaction.parseProofToBytesArray(proofJson); - return { proofBytes, publicInputs }; - } catch (error) { - console.error("error while generating and validating proof"); - throw error; + const vKey = await snarkjs.zKey.exportVerificationKey(completePathZkey); + const res = await snarkjs.groth16.verify(vKey, publicSignals, proof); + if (res === true) { + console.log("Verification OK"); + } else { + console.log("Invalid proof"); + throw new Error("Invalid Proof"); + } + const proofJson = JSON.stringify(proof, null, 1); + const publicInputsJson = JSON.stringify(publicSignals, null, 1); + try { + var publicInputsBytesJson = JSON.parse(publicInputsJson.toString()); + var publicInputsBytes = new Array>(); + for (var i in publicInputsBytesJson) { + let ref: Array = Array.from([ + ...leInt2Buff(unstringifyBigInts(publicInputsBytesJson[i]), 32), + ]).reverse(); + publicInputsBytes.push(ref); + // TODO: replace ref, error is that le and be do not seem to be consistent + // new BN(publicInputsBytesJson[i], "le").toArray("be",32) + // assert.equal(ref.toString(), publicInputsBytes[publicInputsBytes.length -1].toString()); } + const publicInputs = + verifier.parsePublicInputsFromArray(publicInputsBytes); + + const proofBytes = await Transaction.parseProofToBytesArray(proofJson); + return { proofBytes, publicInputs }; + } catch (error) { + console.error("error while generating and validating proof"); + throw error; } } @@ -519,82 +980,12 @@ export class Transaction { return connectingHash; } - assignAccounts() { - if (!this.params) throw new Error("Params undefined"); - if (!this.params.verifier.verifierProgram) - throw new Error("Verifier.verifierProgram undefined"); - - if (this.assetPubkeys && this.params) { - if (!this.params.accounts.sender && !this.params.accounts.senderFee) { - if (this.action !== "WITHDRAWAL") { - throw new Error("No relayer provided for withdrawal"); - } - this.params.accounts.sender = MerkleTreeConfig.getSplPoolPdaToken( - this.assetPubkeys[1], - merkleTreeProgramId, - ); - this.params.accounts.senderFee = - MerkleTreeConfig.getSolPoolPda(merkleTreeProgramId).pda; - - if (!this.params.accounts.recipient) { - this.params.accounts.recipient = SystemProgram.programId; - if (!this.publicAmount?.eq(new BN(0))) { - throw new Error( - "sth is wrong assignAccounts !params.accounts.recipient", - ); - } - } - if (!this.params.accounts.recipientFee) { - this.params.accounts.recipientFee = SystemProgram.programId; - if (!this.feeAmount?.eq(new BN(0))) { - throw new Error( - "sth is wrong assignAccounts !params.accounts.recipientFee", - ); - } - } - } else { - if (this.action !== "DEPOSIT") { - throw new Error("Relayer should not be provided for deposit."); - } - - this.params.accounts.recipient = MerkleTreeConfig.getSplPoolPdaToken( - this.assetPubkeys[1], - merkleTreeProgramId, - ); - this.params.accounts.recipientFee = - MerkleTreeConfig.getSolPoolPda(merkleTreeProgramId).pda; - if (!this.params.accounts.sender) { - this.params.accounts.sender = SystemProgram.programId; - if (!this.publicAmount?.eq(new BN(0))) { - throw new Error( - "sth is wrong assignAccounts !params.accounts.sender", - ); - } - } - this.params.accounts.senderFee = PublicKey.findProgramAddressSync( - [anchor.utils.bytes.utf8.encode("escrow")], - this.params.verifier.verifierProgram.programId, - )[0]; - // if (!this.params.accounts.senderFee) { - - // if (!this.feeAmount?.eq(new BN(0))) { - // throw new Error( - // "sth is wrong assignAccounts !params.accounts.senderFee", - // ); - // } - // } - } - } else { - throw new Error("assignAccounts assetPubkeys undefined"); - } - } - - getAssetPubkeys( + static getAssetPubkeys( inputUtxos?: Utxo[], outputUtxos?: Utxo[], - ): { assetPubkeysCircuit: BN[]; assetPubkeys: PublicKey[] } { - let assetPubkeysCircuit: BN[] = [ - hashAndTruncateToCircuit(SystemProgram.programId.toBytes()), + ): { assetPubkeysCircuit: string[]; assetPubkeys: PublicKey[] } { + let assetPubkeysCircuit: string[] = [ + hashAndTruncateToCircuit(SystemProgram.programId.toBytes()).toString(), ]; let assetPubkeys: PublicKey[] = [SystemProgram.programId]; @@ -602,6 +993,7 @@ export class Transaction { if (inputUtxos) { inputUtxos.map((utxo) => { let found = false; + for (var i in assetPubkeysCircuit) { if ( assetPubkeysCircuit[i].toString() === @@ -609,10 +1001,11 @@ export class Transaction { ) { found = true; } - } - if (!found) { - assetPubkeysCircuit.push(utxo.assetsCircuit[1]); - assetPubkeys.push(utxo.assets[1]); + + if (!found && utxo.assetsCircuit[1].toString() != "0") { + assetPubkeysCircuit.push(utxo.assetsCircuit[1].toString()); + assetPubkeys.push(utxo.assets[1]); + } } }); } @@ -628,29 +1021,68 @@ export class Transaction { found = true; } } - if (!found) { - assetPubkeysCircuit.push(utxo.assetsCircuit[1]); + if (!found && utxo.assetsCircuit[1].toString() != "0") { + assetPubkeysCircuit.push(utxo.assetsCircuit[1].toString()); assetPubkeys.push(utxo.assets[1]); } }); } - if (assetPubkeys.length == 0) { - throw new Error("No utxos provided."); + if ( + (!inputUtxos && !outputUtxos) || + (inputUtxos?.length == 0 && outputUtxos?.length == 0) + ) { + throw new TransactionError( + TransactionErrorCode.NO_UTXOS_PROVIDED, + "getAssetPubkeys", + "No input or output utxos provided.", + ); } + + // TODO: test this better + // if (assetPubkeys.length > params?.verifier.config.out) { + // throw new TransactionError( + // TransactionErrorCode.EXCEEDED_MAX_ASSETS, + // "getAssetPubkeys", + // `Utxos contain too many different assets ${params?.verifier.config.out} > max allowed: ${N_ASSET_PUBKEYS}`, + // ); + // } + if (assetPubkeys.length > N_ASSET_PUBKEYS) { - throw new Error("Utxos contain too many different assets."); + throw new TransactionError( + TransactionErrorCode.EXCEEDED_MAX_ASSETS, + "getAssetPubkeys", + `Utxos contain too many different assets ${assetPubkeys.length} > max allowed: ${N_ASSET_PUBKEYS}`, + ); } + while (assetPubkeysCircuit.length < N_ASSET_PUBKEYS) { - assetPubkeysCircuit.push(new BN(0)); + assetPubkeysCircuit.push(new BN(0).toString()); + assetPubkeys.push(SystemProgram.programId); } return { assetPubkeysCircuit, assetPubkeys }; } + // TODO: add index to merkle tree and check correctness at setup + // TODO: repeat check for example at tx init to acertain that the merkle tree is not outdated + /** + * @description fetches the merkle tree pda from the chain and checks in which index the root of the local merkle tree is. + */ async getRootIndex() { if (!this.provider.solMerkleTree) - throw new Error("provider.solMerkeTree not set"); + throw new TransactionError( + ProviderErrorCode.SOL_MERKLE_TREE_UNDEFINED, + "getRootIndex", + "", + ); + if (!this.provider.solMerkleTree.merkleTree) + throw new TransactionError( + SolMerkleTreeErrorCode.MERKLE_TREE_UNDEFINED, + "getRootIndex", + "", + ); + if (this.provider.provider && this.provider.solMerkleTree.merkleTree) { this.merkleTreeProgram = new Program( IDL_MERKLE_TREE_PROGRAM, @@ -668,212 +1100,257 @@ export class Transaction { await this.merkleTreeProgram.account.merkleTree.fetch( this.provider.solMerkleTree.pubkey, ); - - // stupid reassignment to get rid of error - const tmp: any = merkle_tree_account_data.roots; - tmp.map((x: any, index: any) => { + // @ts-ignore: unknown type error + merkle_tree_account_data.roots.map((x: any, index: any) => { if (x.toString() === root.toString()) { - this.rootIndex = index; + this.transactionInputs.rootIndex = index; } }); - if (this.rootIndex === undefined) { - throw new Error(`Root index not found for root${root}`); + if (this.transactionInputs.rootIndex === undefined) { + throw new TransactionError( + TransactionErrorCode.ROOT_NOT_FOUND, + "getRootIndex", + `Root index not found for root${root}`, + ); } } else { console.log( "provider not defined did not fetch rootIndex set root index to 0", ); - this.rootIndex = 0; - } - } - - addEmptyUtxos(utxos: Utxo[] = [], len: number): Utxo[] { - if (this.params && this.params.verifier.config) { - while (utxos.length < len) { - utxos.push(new Utxo({ poseidon: this.provider.poseidon })); - } - } else { - throw new Error( - `input utxos ${utxos}, config ${this.params?.verifier.config}`, - ); + this.transactionInputs.rootIndex = 0; } - return utxos; } - // the fee plus the amount to pay has to be bigger than the amount in the input utxo - // which doesn't make sense it should be the other way arround right - // the external amount can only be made up of utxos of asset[0] - // This might be too specific since the circuit allows assets to be in any index + /** + * @description Calculates the external amount for one asset. + * @note This function might be too specific since the circuit allows assets to be in any index + * @param assetIndex the index of the asset the external amount should be computed for + * @returns {BN} the public amount of the asset + */ // TODO: write test - getExternalAmount(assetIndex: number): BN { - if ( - this.params && - this.params.inputUtxos && - this.params.outputUtxos && - this.assetPubkeysCircuit - ) { - return new anchor.BN(0) - .add( - this.params.outputUtxos - .filter((utxo: Utxo) => { - return ( - utxo.assetsCircuit[assetIndex].toString("hex") == - this.assetPubkeysCircuit![assetIndex].toString("hex") - ); - }) - .reduce( - (sum, utxo) => - // add all utxos of the same asset - sum.add(utxo.amounts[assetIndex]), - new anchor.BN(0), - ), - ) - .sub( - this.params.inputUtxos - .filter((utxo) => { - return ( - utxo.assetsCircuit[assetIndex].toString("hex") == - this.assetPubkeysCircuit![assetIndex].toString("hex") - ); - }) - .reduce( - (sum, utxo) => sum.add(utxo.amounts[assetIndex]), - new anchor.BN(0), - ), - ) - .add(FIELD_SIZE) - .mod(FIELD_SIZE); - } else { - throw new Error( - `this.params.inputUtxos ${this.params?.inputUtxos} && this.params.outputUtxos ${this.params?.outputUtxos} && this.assetPubkeysCircuit ${this.assetPubkeysCircuit}`, - ); - } + // TODO: rename to publicAmount + static getExternalAmount( + assetIndex: number, + // params: TransactionParameters, + inputUtxos: Utxo[], + outputUtxos: Utxo[], + assetPubkeysCircuit: string[], + ): BN { + return new anchor.BN(0) + .add( + outputUtxos + .filter((utxo: Utxo) => { + return ( + utxo.assetsCircuit[assetIndex].toString() == + assetPubkeysCircuit![assetIndex] + ); + }) + .reduce( + (sum, utxo) => + // add all utxos of the same asset + sum.add(utxo.amounts[assetIndex]), + new anchor.BN(0), + ), + ) + .sub( + inputUtxos + .filter((utxo) => { + return ( + utxo.assetsCircuit[assetIndex].toString() == + assetPubkeysCircuit[assetIndex] + ); + }) + .reduce( + (sum, utxo) => sum.add(utxo.amounts[assetIndex]), + new anchor.BN(0), + ), + ) + .add(FIELD_SIZE) + .mod(FIELD_SIZE); } + /** + * @description Computes the indices in which the asset for the utxo is in the asset pubkeys array. + * @note Using the indices the zero knowledege proof circuit enforces that only utxos containing the + * @note assets in the asset pubkeys array are contained in the transaction. + * @param utxos + * @returns + */ // TODO: make this work for edge case of two 2 different assets plus fee asset in the same transaction // TODO: fix edge case of an assetpubkey being 0 // TODO: !== !! and check non-null getIndices(utxos: Utxo[]): string[][][] { + if (!this.params.assetPubkeysCircuit) + throw new TransactionError( + TransactionErrorCode.ASSET_PUBKEYS_UNDEFINED, + "getIndices", + "", + ); + let inIndices: string[][][] = []; - if (this.assetPubkeysCircuit) { - utxos.map((utxo, index) => { - let tmpInIndices = []; - for (var a = 0; a < utxo.assets.length; a++) { - let tmpInIndices1: string[] = []; - - for (var i = 0; i < N_ASSET_PUBKEYS; i++) { - try { - if ( - utxo.assetsCircuit[a].toString() === - this.assetPubkeysCircuit![i].toString() && - !tmpInIndices1.includes("1") && - this.assetPubkeysCircuit![i].toString() != "0" - ) { - tmpInIndices1.push("1"); - } else { - tmpInIndices1.push("0"); - } - } catch (error) { + utxos.map((utxo) => { + let tmpInIndices = []; + for (var a = 0; a < utxo.assets.length; a++) { + let tmpInIndices1: string[] = []; + + for (var i = 0; i < N_ASSET_PUBKEYS; i++) { + try { + if ( + utxo.assetsCircuit[a].toString() === + this.params.assetPubkeysCircuit![i].toString() && + !tmpInIndices1.includes("1") && + this.params.assetPubkeysCircuit![i].toString() != "0" + ) { + tmpInIndices1.push("1"); + } else { tmpInIndices1.push("0"); } + } catch (error) { + tmpInIndices1.push("0"); } - - tmpInIndices.push(tmpInIndices1); } - inIndices.push(tmpInIndices); - }); - } else { - throw new Error("assetPubkeysCircuit undefined"); - } + tmpInIndices.push(tmpInIndices1); + } + + inIndices.push(tmpInIndices); + }); // console.log(inIndices); return inIndices; } + /** + * @description Gets the merkle proofs for every input utxo with amounts > 0. + * @description For input utxos with amounts == 0 it returns merkle paths with all elements = 0. + */ + static getMerkleProofs( + provider: Provider, + inputUtxos: Utxo[], + ): { + inputMerklePathIndices: Array; + inputMerklePathElements: Array>; + } { + if (!provider.solMerkleTree) + throw new TransactionError( + SolMerkleTreeErrorCode.MERKLE_TREE_UNDEFINED, + "getMerkleProofs", + "", + ); + if (!provider.solMerkleTree.merkleTree) + throw new TransactionError( + SolMerkleTreeErrorCode.MERKLE_TREE_UNDEFINED, + "getMerkleProofs", + "", + ); - getMerkleProofs() { - this.inputMerklePathIndices = []; - this.inputMerklePathElements = []; - if (this.params && this.params.inputUtxos && this.provider.solMerkleTree) { - // getting merkle proofs - for (const inputUtxo of this.params.inputUtxos) { - if ( - inputUtxo.amounts[0] > new BN(0) || - inputUtxo.amounts[1] > new BN(0) - ) { - inputUtxo.index = this.provider.solMerkleTree!.merkleTree.indexOf( - inputUtxo.getCommitment(), - ); + var inputMerklePathIndices = new Array(); + var inputMerklePathElements = new Array>(); + // getting merkle proofs + for (const inputUtxo of inputUtxos) { + if ( + inputUtxo.amounts[0] > new BN(0) || + inputUtxo.amounts[1] > new BN(0) + ) { + inputUtxo.index = provider.solMerkleTree.merkleTree.indexOf( + inputUtxo.getCommitment(), + ); - if (inputUtxo.index || inputUtxo.index == 0) { - if (inputUtxo.index < 0) { - throw new Error( - `Input commitment ${inputUtxo.getCommitment()} was not found`, - ); - } - this.inputMerklePathIndices.push(inputUtxo.index); - this.inputMerklePathElements.push( - this.provider.solMerkleTree.merkleTree.path(inputUtxo.index) - .pathElements, + if (inputUtxo.index || inputUtxo.index == 0) { + if (inputUtxo.index < 0) { + throw new TransactionError( + TransactionErrorCode.INPUT_UTXO_NOT_INSERTED_IN_MERKLE_TREE, + "getMerkleProofs", + `Input commitment ${inputUtxo.getCommitment()} was not found`, ); } - } else { - this.inputMerklePathIndices.push(0); - this.inputMerklePathElements.push( - new Array( - this.provider.solMerkleTree.merkleTree.levels, - ).fill("0"), + inputMerklePathIndices.push(inputUtxo.index.toString()); + inputMerklePathElements.push( + provider.solMerkleTree.merkleTree.path(inputUtxo.index) + .pathElements, ); } + } else { + inputMerklePathIndices.push("0"); + inputMerklePathElements.push( + new Array(provider.solMerkleTree.merkleTree.levels).fill("0"), + ); } } + return { inputMerklePathIndices, inputMerklePathElements }; } + /** + * @description + * @returns + */ getTxIntegrityHash(): BN { - if (this.params && this.params.relayer) { + if (!this.params.relayer) + throw new TransactionError( + TransactionErrorCode.RELAYER_UNDEFINED, + "getTxIntegrityHash", + "", + ); + if (!this.params.accounts.recipient) + throw new TransactionError( + TransactionErrorCode.SPL_RECIPIENT_UNDEFINED, + "getTxIntegrityHash", + "", + ); + if (!this.params.accounts.recipientFee) + throw new TransactionError( + TransactionErrorCode.SOL_RECIPIENT_UNDEFINED, + "getTxIntegrityHash", + "", + ); + if (!this.params.relayer.relayerFee) + throw new TransactionError( + TransactionErrorCode.RELAYER_FEE_UNDEFINED, + "getTxIntegrityHash", + "", + ); + // Should not be computed twice because cipher texts of encrypted utxos are random + // threfore the hash will not be consistent + if (this.testValues && this.testValues.txIntegrityHash) { + return this.testValues.txIntegrityHash; + } else { + if (!this.params.encryptedUtxos) { + this.params.encryptedUtxos = this.encryptOutUtxos(); + } + if ( - !this.params.accounts.recipient || - !this.params.accounts.recipientFee || - !this.params.relayer.relayerFee + this.params.encryptedUtxos && + this.params.encryptedUtxos.length > 512 ) { - throw new Error( - `getTxIntegrityHash: recipient ${this.params.accounts.recipient} recipientFee ${this.params.accounts.recipientFee} relayerFee ${this.params.relayer.relayerFee}`, - ); - } else if (this.txIntegrityHash) { - return this.txIntegrityHash; + this.params.encryptedUtxos = this.params.encryptedUtxos.slice(0, 512); + } + if ( + this.params.encryptedUtxos && + this.testValues && + !this.testValues.txIntegrityHash + ) { + let extDataBytes = new Uint8Array([ + ...this.params.accounts.recipient?.toBytes(), + ...this.params.accounts.recipientFee.toBytes(), + ...this.params.relayer.accounts.relayerPubkey.toBytes(), + ...this.params.relayer.relayerFee.toArray("le", 8), + ...this.params.encryptedUtxos, + ]); + + const hash = keccak_256 + .create({ dkLen: 32 }) + .update(Buffer.from(extDataBytes)) + .digest(); + const txIntegrityHash: BN = new anchor.BN(hash).mod(FIELD_SIZE); + this.testValues.txIntegrityHash = txIntegrityHash; + return txIntegrityHash; } else { - if (!this.params.encryptedUtxos) { - this.params.encryptedUtxos = this.encryptOutUtxos(); - } - if ( - this.params.encryptedUtxos && - this.params.encryptedUtxos.length > 512 - ) { - this.params.encryptedUtxos = this.params.encryptedUtxos.slice(0, 512); - } - if (this.params.encryptedUtxos && !this.txIntegrityHash) { - let extDataBytes = new Uint8Array([ - ...this.params.accounts.recipient?.toBytes(), - ...this.params.accounts.recipientFee.toBytes(), - ...this.params.relayer.accounts.relayerPubkey.toBytes(), - ...this.params.relayer.relayerFee.toArray("le", 8), - ...this.params.encryptedUtxos, - ]); - - const hash = keccak_256 - .create({ dkLen: 32 }) - .update(Buffer.from(extDataBytes)) - .digest(); - const txIntegrityHash: BN = new anchor.BN(hash).mod(FIELD_SIZE); - this.txIntegrityHash = txIntegrityHash; - return txIntegrityHash; - } else { - throw new Error("Encrypting Utxos failed"); - } + throw new TransactionError( + TransactionErrorCode.ENCRYPTING_UTXOS_FAILED, + "getTxIntegrityHash", + "", + ); } - } else { - throw new Error("params or relayer undefined"); } } @@ -882,7 +1359,7 @@ export class Transaction { if (encryptedUtxos) { encryptedOutputs = Array.from(encryptedUtxos); } else if (this.params && this.params.outputUtxos) { - this.params.outputUtxos.map((utxo, index) => + this.params.outputUtxos.map((utxo) => encryptedOutputs.push(utxo.encrypt()), ); @@ -923,36 +1400,57 @@ export class Transaction { // in case there is more than one transaction to be sent to the verifier these can be sent separately async getTestValues() { - if (!this.provider) { - throw new Error("Provider undefined"); - } - - if (!this.provider.provider) { - throw new Error("Provider.provider undefined"); - } - - if (!this.params) { - throw new Error("params undefined"); - } - - if (!this.params.relayer) { - throw new Error("params.relayer undefined"); - } - - if (!this.params.accounts.senderFee) { - throw new Error("params.accounts.senderFee undefined"); - } - - if (!this.params.accounts.recipient) { - throw new Error("params.accounts.recipient undefined"); - } - - if (!this.params.accounts.recipientFee) { - throw new Error("params.accounts.recipient undefined"); - } + if (!this.provider) + throw new TransactionError( + ProviderErrorCode.PROVIDER_UNDEFINED, + "getTestValues", + "", + ); + if (!this.provider.provider) + throw new TransactionError( + ProviderErrorCode.ANCHOR_PROVIDER_UNDEFINED, + "getTestValues", + "Provider.provider undefined", + ); + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getTestValues", + "", + ); + if (!this.params.relayer) + throw new TransactionError( + TransactionErrorCode.RELAYER_UNDEFINED, + "getTestValues", + "", + ); + if (!this.params.accounts.recipient) + throw new TransactionError( + TransactionErrorCode.SPL_RECIPIENT_UNDEFINED, + "getTestValues", + "", + ); + if (!this.params.accounts.recipientFee) + throw new TransactionError( + TransactionErrorCode.SOL_RECIPIENT_UNDEFINED, + "getTestValues", + "", + ); + if (!this.params.accounts.senderFee) + throw new TransactionError( + TransactionErrorCode.SOL_SENDER_UNDEFINED, + "getTestValues", + "", + ); + if (!this.testValues) + throw new TransactionError( + TransactionErrorCode.TRANSACTION_INPUTS_UNDEFINED, + "getTestValues", + "", + ); try { - this.recipientBalancePriorTx = new BN( + this.testValues.recipientBalancePriorTx = new BN( ( await getAccount( this.provider.provider.connection, @@ -963,7 +1461,7 @@ export class Transaction { } catch (e) { // covers the case of the recipient being a native sol address not a spl token address try { - this.recipientBalancePriorTx = new BN( + this.testValues.recipientBalancePriorTx = new BN( await this.provider.provider.connection.getBalance( this.params.accounts.recipient, ), @@ -972,32 +1470,32 @@ export class Transaction { } try { - this.recipientFeeBalancePriorTx = new BN( + this.testValues.recipientFeeBalancePriorTx = new BN( await this.provider.provider.connection.getBalance( this.params.accounts.recipientFee, ), ); } catch (error) { console.log( - "this.recipientFeeBalancePriorTx fetch failed ", + "this.testValues.recipientFeeBalancePriorTx fetch failed ", this.params.accounts.recipientFee, ); } - if (this.action === "DEPOSIT") { - this.senderFeeBalancePriorTx = new BN( + if (this.params.action === "DEPOSIT") { + this.testValues.senderFeeBalancePriorTx = new BN( await this.provider.provider.connection.getBalance( this.params.relayer.accounts.relayerPubkey, ), ); } else { - this.senderFeeBalancePriorTx = new BN( + this.testValues.senderFeeBalancePriorTx = new BN( await this.provider.provider.connection.getBalance( this.params.accounts.senderFee, ), ); } - this.relayerRecipientAccountBalancePriorLastTx = new BN( + this.testValues.relayerRecipientAccountBalancePriorLastTx = new BN( await this.provider.provider.connection.getBalance( this.params.relayer.accounts.relayerRecipient, ), @@ -1016,7 +1514,7 @@ export class Transaction { static getRegisteredVerifierPda( merkleTreeProgramId: PublicKey, verifierProgramId: PublicKey, - ) { + ): PublicKey { return PublicKey.findProgramAddressSync( [verifierProgramId.toBytes()], merkleTreeProgramId, @@ -1024,7 +1522,14 @@ export class Transaction { } async getInstructionsJson(): Promise { - if (!this.appParams && this.params) { + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getInstructionsJson", + "", + ); + + if (!this.appParams) { const instructions = await this.params.verifier.getInstructions(this); let serialized = instructions.map((ix) => JSON.stringify(ix)); return serialized; @@ -1046,9 +1551,40 @@ export class Transaction { // request to relayer throw new Error("withdrawal with relayer is not implemented"); } else { - if (!this.provider.provider) throw new Error("no provider set"); - if (!this.params) throw new Error("params undefined"); - if (!this.params.relayer) throw new Error("params.relayer undefined"); + if (!this.provider.provider) + throw new TransactionError( + ProviderErrorCode.ANCHOR_PROVIDER_UNDEFINED, + "sendTransaction", + "Provider.provider undefined", + ); + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "sendTransaction", + "", + ); + if (!this.params.relayer) + throw new TransactionError( + TransactionErrorCode.RELAYER_UNDEFINED, + "sendTransaction", + "", + ); + + if (this.transactionInputs.rootIndex === undefined) { + throw new TransactionError( + TransactionErrorCode.ROOT_INDEX_NOT_FETCHED, + "sendTransaction", + "", + ); + } + + if (!this.remainingAccounts?.leavesPdaPubkeys) { + throw new TransactionError( + TransactionErrorCode.REMAINING_ACCOUNTS_NOT_CREATED, + "sendTransaction", + "Run await getPdaAddresses() before invoking sendTransaction", + ); + } const recentBlockhash = ( await this.provider.provider.connection.getRecentBlockhash("confirmed") @@ -1130,22 +1666,35 @@ export class Transaction { } async getInstructions(): Promise { - if (this.params) { - return await this.params.verifier.getInstructions(this); - } else { - throw new Error("Params not provided."); - } + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getInstructions", + "", + ); + return await this.params.verifier.getInstructions(this); } async sendAndConfirmTransaction(): Promise { - if (!this.provider.nodeWallet && !this.provider.browserWallet) { - throw new Error( + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "sendAndConfirmTransaction", + "", + ); + + if (!this.provider.nodeWallet && !this.provider.browserWallet) + throw new TransactionError( + TransactionErrorCode.WALLET_UNDEFINED, + "sendAndConfirmTransaction", "Cannot use sendAndConfirmTransaction without payer or browserWallet", ); - } + + await this.getRootIndex(); + await this.getPdaAddresses(); await this.getTestValues(); + var instructions; - if (!this.params) throw new Error("params undefined"); if (!this.appParams) { instructions = await this.params.verifier.getInstructions(this); @@ -1157,78 +1706,98 @@ export class Transaction { for (var ix in instructions) { let txTmp = await this.sendTransaction(instructions[ix]); if (txTmp) { - console.log("tx ::", txTmp); + console.log("tx : ", txTmp); await this.provider.provider?.connection.confirmTransaction( txTmp, "confirmed", ); tx = txTmp; } else { - throw new Error("send transaction failed"); + throw new TransactionError( + TransactionErrorCode.SEND_TRANSACTION_FAILED, + "sendAndConfirmTransaction", + "", + ); } } return tx; } else { - throw new Error("No parameters provided"); + throw new TransactionError( + TransactionErrorCode.GET_INSTRUCTIONS_FAILED, + "sendAndConfirmTransaction", + "", + ); } } // TODO: deal with this: set own payer just for that? where is this used? + // This is used by applications not the relayer async closeVerifierState(): Promise { - if ( - (this.provider.nodeWallet || this.provider.browserWallet) && - this.params && - !this.appParams - ) { - if (!this.params.verifier.verifierProgram) - throw new Error("verifier.verifierProgram undefined"); - return await this.params?.verifier.verifierProgram.methods + if (!this.provider.nodeWallet && !this.provider.browserWallet) + throw new TransactionError( + TransactionErrorCode.WALLET_UNDEFINED, + "closeVerifierState", + "Cannot use closeVerifierState without payer or browserWallet", + ); + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "closeVerifierState", + "", + ); + if (!this.params.verifier.verifierProgram) + throw new TransactionError( + TransactionErrorCode.VERIFIER_PROGRAM_UNDEFINED, + "closeVerifierState", + "", + ); + if (this.appParams) { + return await this.appParams?.verifier.verifierProgram.methods .closeVerifierState() .accounts({ ...this.params.accounts, }) .signers([this.provider.nodeWallet!]) // TODO: browserwallet? or only ever used by relayer? .rpc(confirmConfig); - } else if (this.provider.nodeWallet && this.params && this.appParams) { - return await this.appParams?.verifier.verifierProgram.methods + } else { + return await this.params?.verifier.verifierProgram.methods .closeVerifierState() .accounts({ ...this.params.accounts, }) - .signers([this.provider.nodeWallet]) + .signers([this.provider.nodeWallet!]) // TODO: browserwallet? or only ever used by relayer? .rpc(confirmConfig); - } else { - throw new Error("No payer or params provided."); } } async getPdaAddresses() { - if (!this.params) { - throw new Error("this.params undefined"); - } - - if (!this.params.relayer) { - throw new Error("this.params.relayer undefined"); - } - - if (!this.publicInputs) { - throw new Error("this.publicInputs undefined"); - } - - if (!this.merkleTreeProgram) { - throw new Error("this.merkleTreeProgram undefined"); - } - if (!this.params.verifier.verifierProgram) { - throw new Error("params.verifier.verifierProgram undefined"); - } + if (!this.transactionInputs.publicInputs) + throw new TransactionError( + TransactionErrorCode.PUBLIC_INPUTS_UNDEFINED, + "getPdaAddresses", + "", + ); + if (!this.params.verifier.verifierProgram) + throw new TransactionError( + TransactionErrorCode.VERIFIER_PROGRAM_UNDEFINED, + "getPdaAddresses", + "", + ); + if (!this.params.relayer) + throw new TransactionError( + TransactionErrorCode.RELAYER_UNDEFINED, + "getPdaAddresses", + "", + ); + if (!this.remainingAccounts) + throw new Error("Remaining accounts undefined"); - let nullifiers = this.publicInputs.nullifiers; - let merkleTreeProgram = this.merkleTreeProgram; + let nullifiers = this.transactionInputs.publicInputs.nullifiers; let signer = this.params.relayer.accounts.relayerPubkey; - this.params.nullifierPdaPubkeys = []; + this.remainingAccounts.nullifierPdaPubkeys = []; for (var i in nullifiers) { - this.params.nullifierPdaPubkeys.push({ + this.remainingAccounts.nullifierPdaPubkeys.push({ isSigner: false, isWritable: true, pubkey: PublicKey.findProgramAddressSync( @@ -1236,22 +1805,30 @@ export class Transaction { Uint8Array.from([...nullifiers[i]]), anchor.utils.bytes.utf8.encode("nf"), ], - merkleTreeProgram.programId, + merkleTreeProgramId, )[0], }); } - this.params.leavesPdaPubkeys = []; - for (var j = 0; j < this.publicInputs.leaves.length; j++) { - this.params.leavesPdaPubkeys.push({ + this.remainingAccounts.leavesPdaPubkeys = []; + for ( + var j = 0; + j < this.transactionInputs.publicInputs.leaves.length; + j++ + ) { + this.remainingAccounts.leavesPdaPubkeys.push({ isSigner: false, isWritable: true, pubkey: PublicKey.findProgramAddressSync( [ - Buffer.from(Array.from(this.publicInputs.leaves[j][0]).reverse()), + Buffer.from( + Array.from( + this.transactionInputs.publicInputs.leaves[j][0], + ).reverse(), + ), anchor.utils.bytes.utf8.encode("leaves"), ], - merkleTreeProgram.programId, + merkleTreeProgramId, )[0], }); } @@ -1267,22 +1844,23 @@ export class Transaction { this.params.verifier.verifierProgram.programId, )[0]; } - - this.params.accounts.tokenAuthority = PublicKey.findProgramAddressSync( - [anchor.utils.bytes.utf8.encode("spl")], - merkleTreeProgram.programId, - )[0]; } // TODO: check why this is called encr keypair but account class async checkBalances(account?: Account) { - if (!this.publicInputs) { - throw new Error("public inputs undefined"); - } + if (!this.params) + throw new TransactionError( + TransactionErrorCode.TX_PARAMETERS_UNDEFINED, + "getPdaAddresses", + "", + ); + if (!this.transactionInputs.publicInputs) + throw new TransactionError( + TransactionErrorCode.PUBLIC_INPUTS_UNDEFINED, + "getPdaAddresses", + "", + ); - if (!this.params) { - throw new Error("params undefined"); - } const checkUndefined = (variables: Array) => { variables.map((v) => { if (!v) { @@ -1291,10 +1869,6 @@ export class Transaction { }); }; - if (!this.params) { - throw new Error("params undefined"); - } - if (!this.params.accounts.senderFee) { throw new Error("params.accounts.senderFee undefined"); } @@ -1310,16 +1884,18 @@ export class Transaction { if (!this.params.accounts.recipient) { throw new Error("params.accounts.recipient undefined"); } - - if (!this.senderFeeBalancePriorTx) { + if (!this.testValues) { + throw new Error("test values undefined"); + } + if (!this.testValues.senderFeeBalancePriorTx) { throw new Error("senderFeeBalancePriorTx undefined"); } - if (!this.feeAmount) { + if (!this.params.publicAmountSol) { throw new Error("feeAmount undefined"); } - if (!this.feeAmount) { + if (!this.params.publicAmountSol) { throw new Error("feeAmount undefined"); } @@ -1348,14 +1924,6 @@ export class Transaction { throw new Error("params.outputUtxos undefined"); } - if (!this.params.leavesPdaPubkeys) { - throw new Error("params.leavesPdaPubkeys undefined"); - } - - if (!this.params.leavesPdaPubkeys) { - throw new Error("params.leavesPdaPubkeys undefined"); - } - if (!this.params.relayer) { throw new Error("params.relayer undefined"); } @@ -1363,21 +1931,48 @@ export class Transaction { if (!this.params.accounts.sender) { throw new Error("params.accounts.sender undefined"); } - if (!this.params.nullifierPdaPubkeys) { - throw new Error("params.nullifierPdaPubkeys undefined"); + if (!this.remainingAccounts) { + throw new Error("remainingAccounts.nullifierPdaPubkeys undefined"); + } + if (!this.remainingAccounts.nullifierPdaPubkeys) { + throw new Error("remainingAccounts.nullifierPdaPubkeys undefined"); + } + + if (!this.remainingAccounts.leavesPdaPubkeys) { + throw new Error("remainingAccounts.leavesPdaPubkeys undefined"); + } + if (!this.testValues) { + throw new Error("test values undefined"); + } + if (!this.testValues.recipientFeeBalancePriorTx) { + throw new Error("test values recipientFeeBalancePriorTx undefined"); + } + + if (!this.testValues.recipientBalancePriorTx) { + throw new Error("test values recipientBalancePriorTx undefined"); + } + + if (!this.testValues.relayerRecipientAccountBalancePriorLastTx) { + throw new Error( + "test values relayerRecipientAccountBalancePriorLastTx undefined", + ); } // Checking that nullifiers were inserted if (new BN(this.proofInput.publicAmount).toString() === "0") { - this.is_token = false; + this.testValues.is_token = false; } else { - this.is_token = true; + this.testValues.is_token = true; } - for (var i = 0; i < this.params.nullifierPdaPubkeys?.length; i++) { + for ( + var i = 0; + i < this.remainingAccounts.nullifierPdaPubkeys?.length; + i++ + ) { var nullifierAccount = await this.provider.provider!.connection.getAccountInfo( - this.params.nullifierPdaPubkeys[i].pubkey, + this.remainingAccounts.nullifierPdaPubkeys[i].pubkey, { commitment: "confirmed", }, @@ -1391,20 +1986,20 @@ export class Transaction { let leavesAccount; var leavesAccountData; // Checking that leaves were inserted - for (var i = 0; i < this.params.leavesPdaPubkeys.length; i++) { + for (var i = 0; i < this.remainingAccounts.leavesPdaPubkeys.length; i++) { leavesAccountData = await this.merkleTreeProgram.account.twoLeavesBytesPda.fetch( - this.params.leavesPdaPubkeys[i].pubkey, + this.remainingAccounts.leavesPdaPubkeys[i].pubkey, ); assert( leavesAccountData.nodeLeft.toString() == - this.publicInputs.leaves[i][0].reverse().toString(), + this.transactionInputs.publicInputs.leaves[i][0].reverse().toString(), "left leaf not inserted correctly", ); assert( leavesAccountData.nodeRight.toString() == - this.publicInputs.leaves[i][1].reverse().toString(), + this.transactionInputs.publicInputs.leaves[i][1].reverse().toString(), "right leaf not inserted correctly", ); assert( @@ -1477,7 +2072,9 @@ export class Transaction { } } - console.log(`mode ${this.action}, this.is_token ${this.is_token}`); + console.log( + `mode ${this.params.action}, this.testValues.is_token ${this.testValues.is_token}`, + ); try { const merkleTreeAfterUpdate = @@ -1488,18 +2085,18 @@ export class Transaction { ); leavesAccountData = await this.merkleTreeProgram.account.twoLeavesBytesPda.fetch( - this.params.leavesPdaPubkeys[0].pubkey, + this.remainingAccounts.leavesPdaPubkeys[0].pubkey, ); console.log( `${Number(leavesAccountData.leftLeafIndex)} + ${ - this.params.leavesPdaPubkeys.length * 2 + this.remainingAccounts.leavesPdaPubkeys.length * 2 }`, ); assert.equal( Number(merkleTreeAfterUpdate.nextQueuedIndex), Number(leavesAccountData.leftLeafIndex) + - this.params.leavesPdaPubkeys.length * 2, + this.remainingAccounts.leavesPdaPubkeys.length * 2, ); } catch (e) { console.log("preInsertedLeavesIndex: ", e); @@ -1514,14 +2111,14 @@ export class Transaction { } console.log("nrInstructions ", nrInstructions); - if (this.action == "DEPOSIT" && this.is_token == false) { + if (this.params.action == "DEPOSIT" && this.testValues.is_token == false) { var recipientFeeAccountBalance = await this.provider.provider.connection.getBalance( this.params.accounts.recipientFee, ); console.log( - "recipientFeeBalancePriorTx: ", - this.recipientFeeBalancePriorTx, + "testValues.recipientFeeBalancePriorTx: ", + this.testValues.recipientFeeBalancePriorTx, ); var senderFeeAccountBalance = @@ -1530,21 +2127,25 @@ export class Transaction { ); assert( recipientFeeAccountBalance == - Number(this.recipientFeeBalancePriorTx) + Number(this.feeAmount), + Number(this.testValues.recipientFeeBalancePriorTx) + + Number(this.params.publicAmountSol), ); console.log( - `${new BN(this.senderFeeBalancePriorTx) - .sub(this.feeAmount) + `${new BN(this.testValues.senderFeeBalancePriorTx) + .sub(this.params.publicAmountSol) .sub(new BN(5000 * nrInstructions)) .toString()} == ${senderFeeAccountBalance}`, ); assert( - new BN(this.senderFeeBalancePriorTx) - .sub(this.feeAmount) + new BN(this.testValues.senderFeeBalancePriorTx) + .sub(this.params.publicAmountSol) .sub(new BN(5000 * nrInstructions)) .toString() == senderFeeAccountBalance.toString(), ); - } else if (this.action == "DEPOSIT" && this.is_token == true) { + } else if ( + this.params.action == "DEPOSIT" && + this.testValues.is_token == true + ) { console.log("DEPOSIT and token"); var recipientAccount = await getAccount( @@ -1560,33 +2161,40 @@ export class Transaction { // assert(senderAccount.lamports == (I64(senderAccountBalancePriorLastTx) - I64.readLE(this.extAmount, 0)).toString(), "amount not transferred correctly"); console.log( - `Balance now ${recipientAccount.amount} balance beginning ${this.recipientBalancePriorTx}`, + `Balance now ${recipientAccount.amount} balance beginning ${this.testValues.recipientBalancePriorTx}`, ); console.log( `Balance now ${recipientAccount.amount} balance beginning ${ - Number(this.recipientBalancePriorTx) + Number(this.publicAmount) + Number(this.testValues.recipientBalancePriorTx) + + Number(this.params.publicAmountSpl) }`, ); assert( recipientAccount.amount.toString() === ( - Number(this.recipientBalancePriorTx) + Number(this.publicAmount) + Number(this.testValues.recipientBalancePriorTx) + + Number(this.params.publicAmountSpl) ).toString(), "amount not transferred correctly", ); console.log( `Blanace now ${recipientFeeAccountBalance} ${ - Number(this.recipientFeeBalancePriorTx) + Number(this.feeAmount) + Number(this.testValues.recipientFeeBalancePriorTx) + + Number(this.params.publicAmountSol) }`, ); - console.log("fee amount: ", this.feeAmount); + console.log("fee amount: ", this.params.publicAmountSol); console.log( "fee amount from inputs. ", - new anchor.BN(this.publicInputs.feeAmount.slice(24, 32)).toString(), + new anchor.BN( + this.transactionInputs.publicInputs.feeAmount.slice(24, 32), + ).toString(), ); console.log( "pub amount from inputs. ", - new anchor.BN(this.publicInputs.publicAmount.slice(24, 32)).toString(), + new anchor.BN( + this.transactionInputs.publicInputs.publicAmount.slice(24, 32), + ).toString(), ); var senderFeeAccountBalance = @@ -1596,21 +2204,25 @@ export class Transaction { assert( recipientFeeAccountBalance == - Number(this.recipientFeeBalancePriorTx) + Number(this.feeAmount), + Number(this.testValues.recipientFeeBalancePriorTx) + + Number(this.params.publicAmountSol), ); console.log( - `${new BN(this.senderFeeBalancePriorTx) - .sub(this.feeAmount) + `${new BN(this.testValues.senderFeeBalancePriorTx) + .sub(this.params.publicAmountSol) .sub(new BN(5000 * nrInstructions)) .toString()} == ${senderFeeAccountBalance}`, ); assert( - new BN(this.senderFeeBalancePriorTx) - .sub(this.feeAmount) + new BN(this.testValues.senderFeeBalancePriorTx) + .sub(this.params.publicAmountSol) .sub(new BN(5000 * nrInstructions)) .toString() == senderFeeAccountBalance.toString(), ); - } else if (this.action == "WITHDRAWAL" && this.is_token == false) { + } else if ( + this.params.action == "WITHDRAWAL" && + this.testValues.is_token == false + ) { var relayerAccount = await this.provider.provider.connection.getBalance( this.params.relayer.accounts.relayerRecipient, ); @@ -1623,21 +2235,23 @@ export class Transaction { // console.log("relayerAccount ", relayerAccount); // console.log("this.params.relayer.relayerFee: ", this.params.relayer.relayerFee); console.log( - "relayerRecipientAccountBalancePriorLastTx ", - this.relayerRecipientAccountBalancePriorLastTx, + "testValues.relayerRecipientAccountBalancePriorLastTx ", + this.testValues.relayerRecipientAccountBalancePriorLastTx, ); console.log( `relayerFeeAccount ${new anchor.BN(relayerAccount) .sub(this.params.relayer.relayerFee) .toString()} == ${new anchor.BN( - this.relayerRecipientAccountBalancePriorLastTx!, + this.testValues.relayerRecipientAccountBalancePriorLastTx, )}`, ); console.log( `recipientFeeAccount ${new anchor.BN(recipientFeeAccount) .add(new anchor.BN(this.params.relayer.relayerFee.toString())) - .toString()} == ${new anchor.BN(this.recipientFeeBalancePriorTx!) - .sub(this.feeAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE)) + .toString()} == ${new anchor.BN( + this.testValues.recipientFeeBalancePriorTx, + ) + .sub(this.params.publicAmountSol?.sub(FIELD_SIZE).mod(FIELD_SIZE)) .toString()}`, ); @@ -1645,8 +2259,8 @@ export class Transaction { new anchor.BN(recipientFeeAccount) .add(new anchor.BN(this.params.relayer.relayerFee.toString())) .toString(), - new anchor.BN(this.recipientFeeBalancePriorTx!) - .sub(this.feeAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE)) + new anchor.BN(this.testValues.recipientFeeBalancePriorTx) + .sub(this.params.publicAmountSol?.sub(FIELD_SIZE).mod(FIELD_SIZE)) .toString(), ); // console.log(`this.params.relayer.relayerFee ${this.params.relayer.relayerFee} new anchor.BN(relayerAccount) ${new anchor.BN(relayerAccount)}`); @@ -1654,9 +2268,12 @@ export class Transaction { new anchor.BN(relayerAccount) .sub(this.params.relayer.relayerFee) .toString(), - this.relayerRecipientAccountBalancePriorLastTx?.toString(), + this.testValues.relayerRecipientAccountBalancePriorLastTx?.toString(), ); - } else if (this.action == "WITHDRAWAL" && this.is_token == true) { + } else if ( + this.params.action == "WITHDRAWAL" && + this.testValues.is_token == true + ) { var senderAccount = await getAccount( this.provider.provider.connection, this.params.accounts.sender, @@ -1669,26 +2286,26 @@ export class Transaction { // assert(senderAccount.amount == ((I64(Number(senderAccountBalancePriorLastTx)).add(I64.readLE(this.extAmount, 0))).sub(I64(relayerFee))).toString(), "amount not transferred correctly"); console.log( - "this.recipientBalancePriorTx ", - this.recipientBalancePriorTx, + "this.testValues.recipientBalancePriorTx ", + this.testValues.recipientBalancePriorTx, ); - console.log("this.publicAmount ", this.publicAmount); + console.log("this.params.publicAmountSpl ", this.params.publicAmountSpl); console.log( - "this.publicAmount ", - this.publicAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE), + "this.params.publicAmountSpl ", + this.params.publicAmountSpl?.sub(FIELD_SIZE).mod(FIELD_SIZE), ); console.log( `${recipientAccount.amount}, ${new anchor.BN( - this.recipientBalancePriorTx!, + this.testValues.recipientBalancePriorTx, ) - .sub(this.publicAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE)!) + .sub(this.params.publicAmountSpl?.sub(FIELD_SIZE).mod(FIELD_SIZE)) .toString()}`, ); assert.equal( recipientAccount.amount.toString(), - new anchor.BN(this.recipientBalancePriorTx!) - .sub(this.publicAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE)!) + new anchor.BN(this.testValues.recipientBalancePriorTx) + .sub(this.params.publicAmountSpl?.sub(FIELD_SIZE).mod(FIELD_SIZE)) .toString(), "amount not transferred correctly", ); @@ -1705,22 +2322,24 @@ export class Transaction { // console.log("relayerAccount ", relayerAccount); // console.log("this.params.relayer.relayerFee: ", this.params.relayer.relayerFee); console.log( - "relayerRecipientAccountBalancePriorLastTx ", - this.relayerRecipientAccountBalancePriorLastTx, + "testValues.relayerRecipientAccountBalancePriorLastTx ", + this.testValues.relayerRecipientAccountBalancePriorLastTx, ); console.log( `relayerFeeAccount ${new anchor.BN(relayerAccount) .sub(this.params.relayer.relayerFee) .toString()} == ${new anchor.BN( - this.relayerRecipientAccountBalancePriorLastTx!, + this.testValues.relayerRecipientAccountBalancePriorLastTx, )}`, ); console.log( `recipientFeeAccount ${new anchor.BN(recipientFeeAccount) .add(new anchor.BN(this.params.relayer.relayerFee.toString())) - .toString()} == ${new anchor.BN(this.recipientFeeBalancePriorTx!) - .sub(this.feeAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE)) + .toString()} == ${new anchor.BN( + this.testValues.recipientFeeBalancePriorTx, + ) + .sub(this.params.publicAmountSol?.sub(FIELD_SIZE).mod(FIELD_SIZE)) .toString()}`, ); @@ -1728,8 +2347,8 @@ export class Transaction { new anchor.BN(recipientFeeAccount) .add(new anchor.BN(this.params.relayer.relayerFee.toString())) .toString(), - new anchor.BN(this.recipientFeeBalancePriorTx!) - .sub(this.feeAmount?.sub(FIELD_SIZE).mod(FIELD_SIZE)) + new anchor.BN(this.testValues.recipientFeeBalancePriorTx) + .sub(this.params.publicAmountSol?.sub(FIELD_SIZE).mod(FIELD_SIZE)) .toString(), ); @@ -1738,8 +2357,10 @@ export class Transaction { .sub(this.params.relayer.relayerFee) // .add(new anchor.BN("5000")) .toString(), - this.relayerRecipientAccountBalancePriorLastTx?.toString(), + this.testValues.relayerRecipientAccountBalancePriorLastTx?.toString(), ); + } else if (this.params.action === Action.TRANSFER) { + console.log("balance check for transfer not implemented"); } else { throw Error("mode not supplied"); } @@ -1802,4 +2423,11 @@ export class Transaction { proofC: [mydata.pi_c[0], mydata.pi_c[1]].flat(), }; } + + static getTokenAuthority(): PublicKey { + return PublicKey.findProgramAddressSync( + [anchor.utils.bytes.utf8.encode("spl")], + merkleTreeProgramId, + )[0]; + } } diff --git a/light-sdk-ts/src/verifiers/verifierOne.ts b/light-sdk-ts/src/verifiers/verifierOne.ts index fab0c5cc4b..78ad1ef164 100644 --- a/light-sdk-ts/src/verifiers/verifierOne.ts +++ b/light-sdk-ts/src/verifiers/verifierOne.ts @@ -57,11 +57,13 @@ export class VerifierOne implements Verifier { transaction: Transaction, ): Promise { if (!transaction.params) throw new Error("params undefined"); - if (!transaction.params.nullifierPdaPubkeys) - throw new Error("params.nullifierPdaPubkeys undefined"); - if (!transaction.params.leavesPdaPubkeys) - throw new Error("params.leavesPdaPubkeys undefined"); - if (!transaction.publicInputs) + if (!transaction.remainingAccounts) + throw new Error("remainingAccounts undefined"); + if (!transaction.remainingAccounts.nullifierPdaPubkeys) + throw new Error("remainingAccounts.nullifierPdaPubkeys undefined"); + if (!transaction.remainingAccounts.leavesPdaPubkeys) + throw new Error("remainingAccounts.leavesPdaPubkeys undefined"); + if (!transaction.transactionInputs.publicInputs) throw new Error("params.publicInputs undefined"); if (!transaction.params.relayer) throw new Error("params.params.relayer undefined"); @@ -77,11 +79,11 @@ export class VerifierOne implements Verifier { } const ix1 = await this.verifierProgram.methods .shieldedTransferFirst( - transaction.publicInputs.publicAmount, - transaction.publicInputs.nullifiers, - transaction.publicInputs.leaves[0], - transaction.publicInputs.feeAmount, - new anchor.BN(transaction.rootIndex.toString()), + transaction.transactionInputs.publicInputs.publicAmount, + transaction.transactionInputs.publicInputs.nullifiers, + transaction.transactionInputs.publicInputs.leaves[0], + transaction.transactionInputs.publicInputs.feeAmount, + new anchor.BN(transaction.transactionInputs.rootIndex.toString()), new anchor.BN(transaction.params.relayer.relayerFee.toString()), Buffer.from(transaction.params.encryptedUtxos), ) @@ -93,17 +95,17 @@ export class VerifierOne implements Verifier { const ix2 = await this.verifierProgram.methods .shieldedTransferSecond( - transaction.proofBytes.proofA, - transaction.proofBytes.proofB, - transaction.proofBytes.proofC, + transaction.transactionInputs.proofBytes.proofA, + transaction.transactionInputs.proofBytes.proofB, + transaction.transactionInputs.proofBytes.proofC, ) .accounts({ ...transaction.params.accounts, ...transaction.params.relayer.accounts, }) .remainingAccounts([ - ...transaction.params.nullifierPdaPubkeys, - ...transaction.params.leavesPdaPubkeys, + ...transaction.remainingAccounts.nullifierPdaPubkeys, + ...transaction.remainingAccounts.leavesPdaPubkeys, ]) .instruction(); this.instructions = [ix1, ix2]; diff --git a/light-sdk-ts/src/verifiers/verifierZero.ts b/light-sdk-ts/src/verifiers/verifierZero.ts index 781b32c4c7..a11e99fb6c 100644 --- a/light-sdk-ts/src/verifiers/verifierZero.ts +++ b/light-sdk-ts/src/verifiers/verifierZero.ts @@ -65,11 +65,13 @@ export class VerifierZero implements Verifier { transaction: Transaction, ): Promise { if (!transaction.params) throw new Error("params undefined"); - if (!transaction.params.nullifierPdaPubkeys) - throw new Error("params.nullifierPdaPubkeys undefined"); - if (!transaction.params.leavesPdaPubkeys) - throw new Error("params.leavesPdaPubkeys undefined"); - if (!transaction.publicInputs) + if (!transaction.remainingAccounts) + throw new Error("remainingAccounts undefined"); + if (!transaction.remainingAccounts.nullifierPdaPubkeys) + throw new Error("remainingAccounts.nullifierPdaPubkeys undefined"); + if (!transaction.remainingAccounts.leavesPdaPubkeys) + throw new Error("remainingAccounts.leavesPdaPubkeys undefined"); + if (!transaction.transactionInputs.publicInputs) throw new Error("params.publicInputs undefined"); if (!transaction.params.relayer) throw new Error("params.params.relayer undefined"); @@ -85,14 +87,14 @@ export class VerifierZero implements Verifier { const ix = await this.verifierProgram.methods .shieldedTransferInputs( - transaction.proofBytes.proofA, - transaction.proofBytes.proofB, - transaction.proofBytes.proofC, - transaction.publicInputs.publicAmount, - transaction.publicInputs.nullifiers, - transaction.publicInputs.leaves[0], - transaction.publicInputs.feeAmount, - new anchor.BN(transaction.rootIndex.toString()), + transaction.transactionInputs.proofBytes.proofA, + transaction.transactionInputs.proofBytes.proofB, + transaction.transactionInputs.proofBytes.proofC, + transaction.transactionInputs.publicInputs.publicAmount, + transaction.transactionInputs.publicInputs.nullifiers, + transaction.transactionInputs.publicInputs.leaves[0], + transaction.transactionInputs.publicInputs.feeAmount, + new anchor.BN(transaction.transactionInputs.rootIndex.toString()), new anchor.BN(transaction.params.relayer.relayerFee.toString()), Buffer.from(transaction.params.encryptedUtxos.slice(0, 190)), // remaining bytes can be used once tx sizes increase ) @@ -101,8 +103,8 @@ export class VerifierZero implements Verifier { ...transaction.params.relayer.accounts, }) .remainingAccounts([ - ...transaction.params.nullifierPdaPubkeys, - ...transaction.params.leavesPdaPubkeys, + ...transaction.remainingAccounts.nullifierPdaPubkeys, + ...transaction.remainingAccounts.leavesPdaPubkeys, ]) .instruction(); this.instructions = [ix]; diff --git a/light-sdk-ts/src/wallet/user.ts b/light-sdk-ts/src/wallet/user.ts index 5d663293cf..7dce0ba5dc 100644 --- a/light-sdk-ts/src/wallet/user.ts +++ b/light-sdk-ts/src/wallet/user.ts @@ -6,7 +6,7 @@ import { import { Account } from "../account"; import { Utxo } from "../utxo"; import * as anchor from "@coral-xyz/anchor"; -import { Transaction, TransactionParameters } from "../transaction"; +import { Action, Transaction, TransactionParameters } from "../transaction"; import { sign } from "tweetnacl"; import * as splToken from "@solana/spl-token"; @@ -38,6 +38,7 @@ import { Relayer } from "../relayer"; import { getUnspentUtxos } from "./buildBalance"; import { Provider } from "./provider"; import { getAccount, TOKEN_PROGRAM_ID } from "@solana/spl-token"; +import { assert } from "chai"; const message = new TextEncoder().encode(SIGN_MESSAGE); type Balance = { @@ -538,13 +539,13 @@ export class User { amount, extraSolAmount, action, - userSplAccount, + userSplAccount = AUTHORITY, }: { tokenCtx: TokenContext; amount: number; extraSolAmount: number; - action: string; userSplAccount?: PublicKey; + action: Action; }): TransactionParameters { /// TODO: pass in flag "SHIELD", "UNSHIELD", "TRANSFER" // TODO: check with spl -> selects proper tokens? @@ -571,6 +572,9 @@ export class User { : userSplAccount, // TODO: must be users token account DYNAMIC here senderFee: this.provider.nodeWallet!.publicKey, verifier: new VerifierZero(), // TODO: add support for 10in here -> verifier1 + poseidon: this.provider.poseidon, + action, + lookUpTable: this.provider.lookUpTable, }); return txParams; } else { @@ -585,6 +589,9 @@ export class User { senderFee: this.provider.browserWallet!.publicKey, verifier, // TODO: add support for 10in here -> verifier1 provider: this.provider, + poseidon: this.provider.poseidon, + action, + lookUpTable: this.provider.lookUpTable, }); return txParams; @@ -646,20 +653,25 @@ export class User { amount = amount * tokenCtx.decimals; extraSolAmount = 0; } - - let tx = new Transaction({ - provider: this.provider, - }); - + // amount = amount * tokenCtx.decimals; + // let account = await splToken.getAccount(this.provider.provider.connection, userSplAccount, "confirmed"); + // console.log("account state before tx params", account); const txParams = this.getTxParams({ tokenCtx, amount, - action: "SHIELD", + action: Action.DEPOSIT, //"SHIELD", extraSolAmount, - userSplAccount: userSplAccount ? userSplAccount : undefined, + // @ts-ignore + userSplAccount, + }); + + // TODO: add browserWallet support + let tx = new Transaction({ + provider: this.provider, + params: txParams, }); - await tx.compileAndProve(txParams); + await tx.compileAndProve(); try { let res = await tx.sendAndConfirmTransaction(); @@ -735,15 +747,6 @@ export class User { extraSolAmount: 0, }); - /** payer is the nodeWallet of the relayer (always the one sending) */ - let tx = new Transaction({ - provider: this.provider, - }); - - const verifier = new VerifierZero( - this.provider.browserWallet && this.provider, - ); - // refactor idea: getTxparams -> in,out let txParams = new TransactionParameters({ inputUtxos: inUtxos, @@ -751,12 +754,19 @@ export class User { merkleTreePubkey: MERKLE_TREE_KEY, recipient: tokenCtx.isSol ? recipient : recipientSPLAddress, // TODO: check needs token account? // recipient of spl recipientFee: recipient, // feeRecipient - verifier, + verifier: new VerifierZero(), relayer, + poseidon: this.provider.poseidon, + action: Action.WITHDRAWAL, + }); + + /** payer is the nodeWallet of the relayer (always the one sending) */ + let tx = new Transaction({ provider: this.provider, + params: txParams, }); - await tx.compileAndProve(txParams); + await tx.compileAndProve(); // TODO: add check in client to avoid rent exemption issue // add enough funds such that rent exemption is ensured @@ -839,22 +849,26 @@ export class User { extraSolAmount: 0, //extraSolAmount, TODO: enable }); - let tx = new Transaction({ - provider: this.provider, - }); - - const placeHolderAddress = SolanaKeypair.generate().publicKey; + let randomRecipient = SolanaKeypair.generate().publicKey; + console.log("randomRecipient", randomRecipient.toBase58()); let txParams = new TransactionParameters({ inputUtxos: inUtxos, outputUtxos: outUtxos, merkleTreePubkey: MERKLE_TREE_KEY, verifier: new VerifierZero(), - recipient: placeHolderAddress, - recipientFee: placeHolderAddress, + // recipient: randomRecipient, + // recipientFee: randomRecipient, relayer, + poseidon: this.provider.poseidon, + action: Action.TRANSFER, }); - await tx.compileAndProve(txParams); + let tx = new Transaction({ + provider: this.provider, + params: txParams, + }); + + await tx.compileAndProve(); // TODO: remove once relayer implemented. // add check in client to avoid rent exemption issue // add enough funds such that rent exemption is ensured @@ -889,7 +903,7 @@ export class User { txConfig: { in: number; out: number }, verifier, can be verifier object } - * + * */ // TODO: consider removing payer property completely -> let user pass in the payer for 'load' and for 'shield' only. diff --git a/light-sdk-ts/tests/tests.ts b/light-sdk-ts/tests/tests.ts index e7abe62058..464cb389df 100644 --- a/light-sdk-ts/tests/tests.ts +++ b/light-sdk-ts/tests/tests.ts @@ -10,18 +10,23 @@ import { Scalar } from "ffjavascript"; import { Account } from "../src/account"; import { Utxo } from "../src/utxo"; import { - ADMIN_AUTH_KEYPAIR, FEE_ASSET, functionalCircuitTest, hashAndTruncateToCircuit, Provider as LightProvider, - MERKLE_TREE_KEY, MINT, Transaction, UtxoError, UtxoErrorCode, + TransactionError, + TransactionErrorCode, + ProviderErrorCode, + Provider, + TransactionParameters, + VerifierZero, + Action, + Relayer, } from "../src"; -import { SolMerkleTree } from "../src/merkleTree/solMerkleTree"; const { blake2b } = require("@noble/hashes/blake2b"); const b2params = { dkLen: 32 }; @@ -394,15 +399,54 @@ describe("verifier_program", () => { await functionalCircuitTest(); }); - it("assign Accounts", async () => {}); + it("Test Transaction constructor", async () => { + let mockPubkey = SolanaKeypair.generate().publicKey; + const poseidon = await circomlibjs.buildPoseidonOpt(); + + let lightProvider: Provider = {}; + + expect(() => { + new Transaction({ + provider: lightProvider, + }); + }) + .to.throw(TransactionError) + .to.include({ + code: TransactionErrorCode.POSEIDON_HASHER_UNDEFINED, + functionName: "constructor", + }); + lightProvider = { poseidon: poseidon }; + + expect(() => { + new Transaction({ + provider: lightProvider, + }); + }) + .to.throw(TransactionError) + .to.include({ + code: ProviderErrorCode.SOL_MERKLE_TREE_UNDEFINED, + functionName: "constructor", + }); + + lightProvider = { poseidon: poseidon, solMerkleTree: 1 }; + + expect(() => { + new Transaction({ + provider: lightProvider, + }); + }) + .to.throw(TransactionError) + .to.include({ + code: TransactionErrorCode.WALLET_UNDEFINED, + functionName: "constructor", + }); + }); + it("getIndices", async () => { const poseidon = await circomlibjs.buildPoseidonOpt(); let mockPubkey = SolanaKeypair.generate().publicKey; let lightProvider = await LightProvider.loadMock(mockPubkey); - let tx = new Transaction({ - provider: lightProvider, - }); var deposit_utxo1 = new Utxo({ poseidon, @@ -410,11 +454,30 @@ describe("verifier_program", () => { amounts: [new anchor.BN(1), new anchor.BN(2)], }); - tx.assetPubkeysCircuit = [ - hashAndTruncateToCircuit(SystemProgram.programId.toBytes()), - hashAndTruncateToCircuit(MINT.toBytes()), - new anchor.BN(0), - ]; + const relayer = new Relayer( + mockPubkey, + mockPubkey, + mockPubkey, + new anchor.BN(5000), + ); + + var params = new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + verifier: new VerifierZero(), + recipient: mockPubkey, + recipientFee: mockPubkey, + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + + let tx = new Transaction({ + provider: lightProvider, + params, + }); + const indices1 = tx.getIndices([deposit_utxo1]); assert.equal(indices1[0][0][0], "1"); assert.equal(indices1[0][0][1], "0"); diff --git a/light-sdk-ts/tests/transactionParameters.test.ts b/light-sdk-ts/tests/transactionParameters.test.ts new file mode 100644 index 0000000000..9a74429a6e --- /dev/null +++ b/light-sdk-ts/tests/transactionParameters.test.ts @@ -0,0 +1,1448 @@ +import { assert, expect } from "chai"; +let circomlibjs = require("circomlibjs"); +import { SystemProgram, Keypair as SolanaKeypair } from "@solana/web3.js"; +import * as anchor from "@coral-xyz/anchor"; +import { it } from "mocha"; +import { buildPoseidonOpt } from "circomlibjs"; + +import { Account } from "../src/account"; +import { Utxo } from "../src/utxo"; +import { + FEE_ASSET, + hashAndTruncateToCircuit, + Provider as LightProvider, + MINT, + Transaction, + TransactionParameters, + VerifierZero, + TransactionErrorCode, + Action, + TransactioParametersError, + TransactionParametersErrorCode, + Relayer, + FIELD_SIZE, + merkleTreeProgramId, + VerifierTwo, + VerifierOne, + AUTHORITY, +} from "../src"; +import { TOKEN_PROGRAM_ID } from "@solana/spl-token"; +process.env.ANCHOR_PROVIDER_URL = "http://127.0.0.1:8899"; +process.env.ANCHOR_WALLET = process.env.HOME + "/.config/solana/id.json"; +const verifiers = [new VerifierZero(), new VerifierOne(), new VerifierTwo()]; + +describe("Transaction Parameters Functional", () => { + let seed32 = new Uint8Array(32).fill(1).toString(); + let depositAmount = 20_000; + let depositFeeAmount = 10_000; + + let mockPubkey = SolanaKeypair.generate().publicKey; + let mockPubkey1 = SolanaKeypair.generate().publicKey; + let mockPubkey2 = SolanaKeypair.generate().publicKey; + let mockPubkey3 = SolanaKeypair.generate().publicKey; + let poseidon, lightProvider, deposit_utxo1, outputUtxo, relayer, keypair; + before(async () => { + poseidon = await circomlibjs.buildPoseidonOpt(); + // TODO: make fee mandatory + relayer = new Relayer( + mockPubkey3, + mockPubkey, + mockPubkey, + new anchor.BN(5000), + ); + keypair = new Account({ poseidon: poseidon, seed: seed32 }); + lightProvider = await LightProvider.loadMock(mockPubkey3); + deposit_utxo1 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(depositFeeAmount), new anchor.BN(depositAmount)], + account: keypair, + }); + }); + + it("Transfer Functional", async () => { + var outputUtxo = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [ + new anchor.BN(depositFeeAmount).sub(relayer.relayerFee), + new anchor.BN(depositAmount), + ], + account: keypair, + }); + + for (var j in verifiers) { + const inputUtxos = [deposit_utxo1]; + const outputUtxos = [outputUtxo]; + + const params = new TransactionParameters({ + inputUtxos, + outputUtxos, + merkleTreePubkey: mockPubkey2, + verifier: verifiers[j], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.TRANSFER, + relayer, + }); + + assert.equal(params.action.toString(), Action.TRANSFER.toString()); + assert.equal(params.publicAmountSpl.toString(), "0"); + assert.equal( + params.publicAmountSol + .sub(FIELD_SIZE) + .mul(new anchor.BN(-1)) + .toString(), + relayer.relayerFee.toString(), + ); + assert.equal( + params.assetPubkeys[0].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal(params.assetPubkeys[1].toBase58(), MINT.toBase58()); + assert.equal( + params.assetPubkeys[2].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal( + params.accounts.recipient?.toBase58(), + AUTHORITY.toBase58(), + ); + assert.equal( + params.accounts.recipientFee?.toBase58(), + AUTHORITY.toBase58(), + ); + assert.equal( + params.accounts.merkleTree.toBase58(), + mockPubkey2.toBase58(), + ); + assert.equal(params.accounts.verifierState, undefined); + assert.equal(params.accounts.programMerkleTree, merkleTreeProgramId); + assert.equal( + params.accounts.signingAddress, + relayer.accounts.relayerPubkey, + ); + assert.equal( + params.accounts.signingAddress, + params.relayer.accounts.relayerPubkey, + ); + assert.equal( + params.accounts.authority.toBase58(), + Transaction.getSignerAuthorityPda( + merkleTreeProgramId, + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal( + params.accounts.registeredVerifierPda.toBase58(), + Transaction.getRegisteredVerifierPda( + merkleTreeProgramId, + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal(params.accounts.systemProgramId, SystemProgram.programId); + assert.equal(params.accounts.tokenProgram, TOKEN_PROGRAM_ID); + assert.equal( + params.accounts.tokenAuthority?.toBase58(), + Transaction.getTokenAuthority().toBase58(), + ); + assert.equal( + params.verifier.config.in.toString(), + verifiers[j].config.in.toString(), + ); + assert.equal( + params.relayer.accounts.lookUpTable.toBase58(), + relayer.accounts.lookUpTable?.toBase58(), + ); + assert.equal(params.inputUtxos.length, params.verifier.config.in); + assert.equal(params.outputUtxos.length, params.verifier.config.out); + + for (var i in inputUtxos) { + assert.equal( + params.inputUtxos[i].getCommitment(), + inputUtxos[i].getCommitment(), + ); + } + + for (var i in outputUtxos) { + assert.equal( + params.outputUtxos[i].getCommitment(), + outputUtxos[i].getCommitment(), + ); + } + } + }); + it("Deposit Functional", async () => { + for (var j in verifiers) { + const outputUtxos = [deposit_utxo1]; + + const params = new TransactionParameters({ + outputUtxos, + merkleTreePubkey: mockPubkey2, + sender: mockPubkey, + senderFee: mockPubkey1, + verifier: verifiers[j], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + + assert.equal(params.publicAmountSpl.toString(), depositAmount.toString()); + assert.equal( + params.publicAmountSol.toString(), + depositFeeAmount.toString(), + ); + assert.equal( + params.assetPubkeys[0].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal(params.assetPubkeys[1].toBase58(), MINT.toBase58()); + assert.equal( + params.assetPubkeys[2].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal(params.accounts.sender?.toBase58(), mockPubkey.toBase58()); + assert.equal( + params.accounts.senderFee?.toBase58(), + TransactionParameters.getEscrowPda( + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal( + params.accounts.merkleTree.toBase58(), + mockPubkey2.toBase58(), + ); + assert.equal(params.accounts.verifierState, undefined); + assert.equal(params.accounts.programMerkleTree, merkleTreeProgramId); + assert.equal(params.accounts.signingAddress, mockPubkey1); + assert.equal( + params.accounts.signingAddress, + params.relayer.accounts.relayerPubkey, + ); + assert.equal( + params.accounts.authority.toBase58(), + Transaction.getSignerAuthorityPda( + merkleTreeProgramId, + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal( + params.accounts.registeredVerifierPda.toBase58(), + Transaction.getRegisteredVerifierPda( + merkleTreeProgramId, + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal(params.accounts.systemProgramId, SystemProgram.programId); + assert.equal(params.accounts.tokenProgram, TOKEN_PROGRAM_ID); + assert.equal( + params.accounts.tokenAuthority?.toBase58(), + Transaction.getTokenAuthority().toBase58(), + ); + assert.equal( + params.verifier.config.in.toString(), + verifiers[j].config.in.toString(), + ); + assert.equal(params.action.toString(), Action.DEPOSIT.toString()); + assert.equal( + params.relayer.accounts.lookUpTable.toBase58(), + lightProvider.lookUpTable?.toBase58(), + ); + assert.equal(params.inputUtxos.length, params.verifier.config.in); + assert.equal(params.outputUtxos.length, params.verifier.config.out); + + for (var i in outputUtxos) { + assert.equal( + params.outputUtxos[i].getCommitment(), + outputUtxos[i].getCommitment(), + ); + } + } + }); + + it("Withdrawal Functional", async () => { + for (var j in verifiers) { + const inputUtxos = [deposit_utxo1]; + + const params = new TransactionParameters({ + inputUtxos, + merkleTreePubkey: mockPubkey2, + recipient: mockPubkey, + recipientFee: mockPubkey1, + verifier: verifiers[j], + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + assert.equal(params.action.toString(), Action.WITHDRAWAL.toString()); + assert.equal( + params.publicAmountSpl + .sub(FIELD_SIZE) + .mul(new anchor.BN(-1)) + .toString(), + depositAmount.toString(), + ); + assert.equal( + params.publicAmountSol + .sub(FIELD_SIZE) + .mul(new anchor.BN(-1)) + .toString(), + depositFeeAmount.toString(), + ); + assert.equal( + params.assetPubkeys[0].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal(params.assetPubkeys[1].toBase58(), MINT.toBase58()); + assert.equal( + params.assetPubkeys[2].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal( + params.accounts.recipient?.toBase58(), + mockPubkey.toBase58(), + ); + assert.equal( + params.accounts.recipientFee?.toBase58(), + mockPubkey1.toBase58(), + ); + assert.equal( + params.accounts.merkleTree.toBase58(), + mockPubkey2.toBase58(), + ); + assert.equal(params.accounts.verifierState, undefined); + assert.equal(params.accounts.programMerkleTree, merkleTreeProgramId); + assert.equal( + params.accounts.signingAddress, + relayer.accounts.relayerPubkey, + ); + assert.equal( + params.accounts.signingAddress, + params.relayer.accounts.relayerPubkey, + ); + assert.equal( + params.accounts.authority.toBase58(), + Transaction.getSignerAuthorityPda( + merkleTreeProgramId, + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal( + params.accounts.registeredVerifierPda.toBase58(), + Transaction.getRegisteredVerifierPda( + merkleTreeProgramId, + verifiers[j].verifierProgram!.programId, + ).toBase58(), + ); + assert.equal(params.accounts.systemProgramId, SystemProgram.programId); + assert.equal(params.accounts.tokenProgram, TOKEN_PROGRAM_ID); + assert.equal( + params.accounts.tokenAuthority?.toBase58(), + Transaction.getTokenAuthority().toBase58(), + ); + assert.equal( + params.verifier.config.in.toString(), + verifiers[j].config.in.toString(), + ); + assert.equal( + params.relayer.accounts.lookUpTable.toBase58(), + relayer.accounts.lookUpTable?.toBase58(), + ); + assert.equal(params.inputUtxos.length, params.verifier.config.in); + assert.equal(params.outputUtxos.length, params.verifier.config.out); + + for (var i in inputUtxos) { + assert.equal( + params.inputUtxos[i].getCommitment(), + inputUtxos[i].getCommitment(), + ); + } + } + }); +}); + +describe("Test TransactionParameters Methods", () => { + it("Test getAssetPubkeys", async () => { + const poseidon = await buildPoseidonOpt(); + let inputUtxos = [new Utxo({ poseidon }), new Utxo({ poseidon })]; + let outputUtxos = [ + new Utxo({ + poseidon, + amounts: [new anchor.BN(2), new anchor.BN(4)], + assets: [SystemProgram.programId, MINT], + }), + new Utxo({ poseidon }), + ]; + + let { assetPubkeysCircuit, assetPubkeys } = Transaction.getAssetPubkeys( + inputUtxos, + outputUtxos, + ); + assert.equal( + assetPubkeys[0].toBase58(), + SystemProgram.programId.toBase58(), + ); + assert.equal(assetPubkeys[1].toBase58(), MINT.toBase58()); + assert.equal( + assetPubkeys[2].toBase58(), + SystemProgram.programId.toBase58(), + ); + + assert.equal( + assetPubkeysCircuit[0].toString(), + hashAndTruncateToCircuit(SystemProgram.programId.toBuffer()).toString(), + ); + assert.equal( + assetPubkeysCircuit[1].toString(), + hashAndTruncateToCircuit(MINT.toBuffer()).toString(), + ); + assert.equal(assetPubkeysCircuit[2].toString(), "0"); + }); + + it("Test getExtAmount", async () => { + const poseidon = await buildPoseidonOpt(); + let inputUtxos = [new Utxo({ poseidon }), new Utxo({ poseidon })]; + let outputUtxos = [ + new Utxo({ + poseidon, + amounts: [new anchor.BN(2), new anchor.BN(4)], + assets: [SystemProgram.programId, MINT], + }), + new Utxo({ poseidon }), + ]; + let { assetPubkeysCircuit, assetPubkeys } = Transaction.getAssetPubkeys( + inputUtxos, + outputUtxos, + ); + + let publicAmount = Transaction.getExternalAmount( + 0, + inputUtxos, + outputUtxos, + assetPubkeysCircuit, + ); + assert.equal(publicAmount.toString(), "2"); + let publicAmountSpl = Transaction.getExternalAmount( + 1, + inputUtxos, + outputUtxos, + assetPubkeysCircuit, + ); + + assert.equal(publicAmountSpl.toString(), "4"); + + outputUtxos[1] = new Utxo({ + poseidon, + amounts: [new anchor.BN(3), new anchor.BN(5)], + assets: [SystemProgram.programId, MINT], + }); + let publicAmountSpl2Outputs = Transaction.getExternalAmount( + 1, + inputUtxos, + outputUtxos, + assetPubkeysCircuit, + ); + assert.equal(publicAmountSpl2Outputs.toString(), "9"); + + let publicAmountSol2Outputs = Transaction.getExternalAmount( + 0, + inputUtxos, + outputUtxos, + assetPubkeysCircuit, + ); + assert.equal(publicAmountSol2Outputs.toString(), "5"); + }); +}); + +describe("Test General TransactionParameters Errors", () => { + let seed32 = new Uint8Array(32).fill(1).toString(); + let depositAmount = 20_000; + let depositFeeAmount = 10_000; + + let mockPubkey = SolanaKeypair.generate().publicKey; + let mockPubkey1 = SolanaKeypair.generate().publicKey; + let mockPubkey2 = SolanaKeypair.generate().publicKey; + let mockPubkey3 = SolanaKeypair.generate().publicKey; + let poseidon, lightProvider, deposit_utxo1, relayer, keypair; + + before(async () => { + poseidon = await circomlibjs.buildPoseidonOpt(); + // TODO: make fee mandatory + relayer = new Relayer( + mockPubkey3, + mockPubkey, + mockPubkey, + new anchor.BN(5000), + ); + keypair = new Account({ poseidon: poseidon, seed: seed32 }); + lightProvider = await LightProvider.loadMock(mockPubkey3); + deposit_utxo1 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(depositFeeAmount), new anchor.BN(depositAmount)], + account: keypair, + }); + }); + + it("NO_UTXOS_PROVIDED", async () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.NO_UTXOS_PROVIDED, + functionName: "constructor", + }); + } + }); + + it("NO_POSEIDON_HASHER_PROVIDED", async () => { + for (var verifier in verifiers) { + expect(() => { + // @ts-ignore: + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.NO_POSEIDON_HASHER_PROVIDED, + functionName: "constructor", + }); + } + }); + + it("NO_ACTION_PROVIDED", () => { + for (var verifier in verifiers) { + expect(() => { + // @ts-ignore: + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.NO_ACTION_PROVIDED, + functionName: "constructor", + }); + } + }); + + it("NO_VERIFIER_PROVIDED", () => { + expect(() => { + // @ts-ignore: + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.NO_VERIFIER_PROVIDED, + functionName: "constructor", + }); + }); +}); + +describe("Test TransactionParameters Transfer Errors", () => { + let seed32 = new Uint8Array(32).fill(1).toString(); + let depositAmount = 20_000; + let depositFeeAmount = 10_000; + let mockPubkey = SolanaKeypair.generate().publicKey; + let keypair; + + let poseidon, lightProvider, deposit_utxo1, outputUtxo, relayer; + before(async () => { + poseidon = await circomlibjs.buildPoseidonOpt(); + // TODO: make fee mandatory + relayer = new Relayer( + mockPubkey, + mockPubkey, + mockPubkey, + new anchor.BN(5000), + ); + keypair = new Account({ poseidon: poseidon, seed: seed32 }); + lightProvider = await LightProvider.loadMock(mockPubkey); + deposit_utxo1 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(depositFeeAmount), new anchor.BN(depositAmount)], + account: keypair, + }); + + outputUtxo = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [ + new anchor.BN(depositFeeAmount).sub(relayer.relayerFee), + new anchor.BN(depositAmount), + ], + account: keypair, + }); + + const params = new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [outputUtxo], + merkleTreePubkey: mockPubkey, + verifier: new VerifierZero(), + poseidon, + action: Action.TRANSFER, + relayer, + }); + }); + + it("RELAYER_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [outputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.RELAYER_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("PUBLIC_AMOUNT_SPL_NOT_ZERO", () => { + const localOutputUtxo = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [ + new anchor.BN(depositFeeAmount).sub(relayer.relayerFee), + new anchor.BN(0), + ], + account: keypair, + }); + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [localOutputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.PUBLIC_AMOUNT_SPL_NOT_ZERO, + functionName: "constructor", + }); + } + }); + + it("PUBLIC_AMOUNT_SOL_NOT_ZERO", () => { + const localOutputUtxo = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(0), new anchor.BN(depositAmount)], + account: keypair, + }); + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [localOutputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.PUBLIC_AMOUNT_SOL_NOT_ZERO, + functionName: "constructor", + }); + } + }); + + it("SPL_RECIPIENT_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [outputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + recipient: mockPubkey, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SPL_RECIPIENT_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SOL_RECIPIENT_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [outputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + recipientFee: mockPubkey, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SOL_RECIPIENT_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SOL_SENDER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [outputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + senderFee: mockPubkey, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SOL_SENDER_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SPL_SENDER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + outputUtxos: [outputUtxo], + merkleTreePubkey: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.TRANSFER, + sender: mockPubkey, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SPL_SENDER_DEFINED, + functionName: "constructor", + }); + } + }); +}); + +describe("Test TransactionParameters Deposit Errors", () => { + let seed32 = new Uint8Array(32).fill(1).toString(); + let depositAmount = 20_000; + let depositFeeAmount = 10_000; + let mockPubkey = SolanaKeypair.generate().publicKey; + let keypair; + + let poseidon, lightProvider, deposit_utxo1, outputUtxo, relayer; + before(async () => { + poseidon = await circomlibjs.buildPoseidonOpt(); + // TODO: make fee mandatory + relayer = new Relayer( + mockPubkey, + mockPubkey, + mockPubkey, + new anchor.BN(5000), + ); + keypair = new Account({ poseidon: poseidon, seed: seed32 }); + lightProvider = await LightProvider.loadMock(mockPubkey); + deposit_utxo1 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(depositFeeAmount), new anchor.BN(depositAmount)], + account: keypair, + }); + + const params = new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + verifier: new VerifierZero(), + sender: mockPubkey, + senderFee: mockPubkey, + lookUpTable: mockPubkey, + poseidon, + action: Action.DEPOSIT, + }); + }); + + it("SOL_SENDER_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.SOL_SENDER_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("SPL_SENDER_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.SPL_SENDER_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("LOOK_UP_TABLE_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.LOOK_UP_TABLE_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("RELAYER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.RELAYER_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SOL PUBLIC_AMOUNT_NOT_U64", () => { + let utxo_sol_amount_no_u641 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [ + new anchor.BN("18446744073709551615"), + new anchor.BN(depositAmount), + ], + account: keypair, + }); + let utxo_sol_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN("18446744073709551615"), new anchor.BN(0)], + account: keypair, + }); + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [utxo_sol_amount_no_u641, utxo_sol_amount_no_u642], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + functionName: "constructor", + }); + } + }); + + it("SPL PUBLIC_AMOUNT_NOT_U64", () => { + let utxo_spl_amount_no_u641 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(0), new anchor.BN("18446744073709551615")], + account: keypair, + }); + + let utxo_spl_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(0), new anchor.BN("1")], + account: keypair, + }); + + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [utxo_spl_amount_no_u641, utxo_spl_amount_no_u642], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + functionName: "constructor", + }); + } + }); + + it("SOL_RECIPIENT_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SOL_RECIPIENT_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SPL_RECIPIENT_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + recipient: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SPL_RECIPIENT_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SOL_SENDER_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.SOL_SENDER_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("SPL_SENDER_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.SPL_SENDER_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("No sender spl needed without spl amount", () => { + let utxo_sol_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN("18446744073709551615"), new anchor.BN(0)], + account: keypair, + }); + for (var verifier in verifiers) { + // sender fee always needs to be defined because we use it as the signer + // should work since no spl amount + new TransactionParameters({ + outputUtxos: [utxo_sol_amount_no_u642], + merkleTreePubkey: mockPubkey, + senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + } + }); + + it("SPL_RECIPIENT_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + recipient: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SPL_RECIPIENT_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SOL_RECIPIENT_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + senderFee: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.DEPOSIT, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SOL_RECIPIENT_DEFINED, + functionName: "constructor", + }); + } + }); +}); + +describe("Test TransactionParameters Withdrawal Errors", () => { + let seed32 = new Uint8Array(32).fill(1).toString(); + let depositAmount = 20_000; + let depositFeeAmount = 10_000; + let mockPubkey = SolanaKeypair.generate().publicKey; + let keypair; + + let poseidon, lightProvider, deposit_utxo1, outputUtxo, relayer; + + before(async () => { + poseidon = await circomlibjs.buildPoseidonOpt(); + // TODO: make fee mandatory + relayer = new Relayer( + mockPubkey, + mockPubkey, + mockPubkey, + new anchor.BN(5000), + ); + keypair = new Account({ poseidon: poseidon, seed: seed32 }); + lightProvider = await LightProvider.loadMock(mockPubkey); + deposit_utxo1 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(depositFeeAmount), new anchor.BN(depositAmount)], + account: keypair, + }); + + outputUtxo = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [ + new anchor.BN(depositFeeAmount).sub(relayer.relayerFee), + new anchor.BN(depositAmount), + ], + account: keypair, + }); + + const params = new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + verifier: new VerifierZero(), + poseidon, + recipient: mockPubkey, + recipientFee: mockPubkey, + action: Action.WITHDRAWAL, + relayer, + }); + }); + + it("SOL_RECIPIENT_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + recipient: mockPubkey, + // senderFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.SOL_RECIPIENT_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("RELAYER_UNDEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.WITHDRAWAL, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionErrorCode.RELAYER_UNDEFINED, + functionName: "constructor", + }); + } + }); + + it("SOL PUBLIC_AMOUNT_NOT_U64", () => { + let utxo_sol_amount_no_u641 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [ + new anchor.BN("18446744073709551615"), + new anchor.BN(depositAmount), + ], + account: keypair, + }); + let utxo_sol_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN("18446744073709551615"), new anchor.BN(0)], + account: keypair, + }); + + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [utxo_sol_amount_no_u641, utxo_sol_amount_no_u642], + merkleTreePubkey: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + functionName: "constructor", + }); + } + }); + + it("SPL PUBLIC_AMOUNT_NOT_U64", () => { + let utxo_spl_amount_no_u641 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(0), new anchor.BN("18446744073709551615")], + account: keypair, + }); + + let utxo_spl_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(0), new anchor.BN("1")], + account: keypair, + }); + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [utxo_spl_amount_no_u641, utxo_spl_amount_no_u642], + merkleTreePubkey: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.PUBLIC_AMOUNT_NOT_U64, + functionName: "constructor", + }); + } + }); + + it("SOL_SENDER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + senderFee: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SOL_SENDER_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SPL_SENDER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SPL_SENDER_DEFINED, + functionName: "constructor", + }); + } + }); + + it("no recipient spl should work since no spl amount", () => { + let utxo_sol_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN("18446744073709551615"), new anchor.BN(0)], + account: keypair, + }); + + for (var verifier in verifiers) { + // should work since no spl amount + new TransactionParameters({ + inputUtxos: [utxo_sol_amount_no_u642], + merkleTreePubkey: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + } + }); + + it("no recipient sol should work since no sol amount", () => { + let utxo_sol_amount_no_u642 = new Utxo({ + poseidon: poseidon, + assets: [FEE_ASSET, MINT], + amounts: [new anchor.BN(0), new anchor.BN("18446744073709551615")], + account: keypair, + }); + + for (var verifier in verifiers) { + // should work since no sol amount + new TransactionParameters({ + inputUtxos: [utxo_sol_amount_no_u642], + merkleTreePubkey: mockPubkey, + recipient: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + } + }); + + it("SOL_SENDER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + senderFee: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SOL_SENDER_DEFINED, + functionName: "constructor", + }); + } + }); + + it("SPL_SENDER_DEFINED", () => { + for (var verifier in verifiers) { + expect(() => { + new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: mockPubkey, + sender: mockPubkey, + recipient: mockPubkey, + recipientFee: mockPubkey, + verifier: verifiers[verifier], + lookUpTable: lightProvider.lookUpTable, + poseidon, + action: Action.WITHDRAWAL, + relayer, + }); + }) + .to.throw(TransactioParametersError) + .to.include({ + code: TransactionParametersErrorCode.SPL_SENDER_DEFINED, + functionName: "constructor", + }); + } + }); +}); diff --git a/light-system-programs/tests/functional_tests.ts b/light-system-programs/tests/functional_tests.ts index 5e9ac3d24b..a741c4716c 100644 --- a/light-system-programs/tests/functional_tests.ts +++ b/light-system-programs/tests/functional_tests.ts @@ -49,6 +49,7 @@ import { strToArr, RECIPIENT_TOKEN_ACCOUNT, TOKEN_REGISTRY, + Action, } from "light-sdk"; import { BN } from "@coral-xyz/anchor"; @@ -258,9 +259,7 @@ describe("verifier_program", () => { ); const lightProvider = await Provider.native(ADMIN_AUTH_KEYPAIR); - let tx = new Transaction({ - provider: lightProvider, - }); + let deposit_utxo1 = new Utxo({ poseidon: POSEIDON, @@ -278,8 +277,15 @@ describe("verifier_program", () => { sender: userTokenAccount, senderFee: ADMIN_AUTH_KEYPAIR.publicKey, verifier: new VerifierOne(), + poseidon: POSEIDON, + lookUpTable: LOOK_UP_TABLE, + action: Action.DEPOSIT }); - await tx.compileAndProve(txParams); + let tx = new Transaction({ + provider: lightProvider, + params: txParams + }); + await tx.compileAndProve(); try { let res = await tx.sendAndConfirmTransaction(); @@ -322,9 +328,7 @@ describe("verifier_program", () => { const lightProvider = await Provider.native(ADMIN_AUTH_KEYPAIR); - let tx = new Transaction({ - provider: lightProvider, - }); + deposit_utxo1 = new Utxo({ poseidon: POSEIDON, @@ -342,8 +346,15 @@ describe("verifier_program", () => { sender: userTokenAccount, senderFee: ADMIN_AUTH_KEYPAIR.publicKey, verifier: new VerifierZero(), + lookUpTable: LOOK_UP_TABLE, + action: Action.DEPOSIT, + poseidon: POSEIDON + }); + let tx = new Transaction({ + provider: lightProvider, + params: txParams }); - await tx.compileAndProve(txParams); + await tx.compileAndProve(); try { let res = await tx.sendAndConfirmTransaction(); @@ -388,12 +399,7 @@ describe("verifier_program", () => { new BN(100000), ); - let tx = new Transaction({ - provider: lightProvider, - // relayer, - // payer: ADMIN_AUTH_KEYPAIR, - // shuffleEnabled: false, - }); + let txParams = new TransactionParameters({ inputUtxos: [decryptedUtxo1], @@ -402,9 +408,18 @@ describe("verifier_program", () => { recipientFee: origin.publicKey, verifier: new VerifierZero(), relayer, + action: Action.WITHDRAWAL, + poseidon + }); + let tx = new Transaction({ + provider: lightProvider, + // relayer, + // payer: ADMIN_AUTH_KEYPAIR, + shuffleEnabled: false, + params: txParams }); - await tx.compileAndProve(txParams); + await tx.compileAndProve(); // TODO: add check in client to avoid rent exemption issue // add enough funds such that rent exemption is ensured @@ -476,10 +491,8 @@ describe("verifier_program", () => { new BN(100000), ); - let tx = new Transaction({ - provider: lightProvider, - // relayer, - }); + + console.log(inputUtxos); let txParams = new TransactionParameters({ inputUtxos, @@ -498,8 +511,16 @@ describe("verifier_program", () => { recipientFee, verifier: new VerifierOne(), relayer, + poseidon: POSEIDON, + action: Action.WITHDRAWAL }); - await tx.compileAndProve(txParams); + let tx = new Transaction({ + provider: lightProvider, + // relayer, + params: txParams + }); + + await tx.compileAndProve(); try { let res = await tx.sendAndConfirmTransaction(); diff --git a/light-system-programs/tests/merkle_tree_tests.ts b/light-system-programs/tests/merkle_tree_tests.ts index e20db21858..41b7fed9d9 100644 --- a/light-system-programs/tests/merkle_tree_tests.ts +++ b/light-system-programs/tests/merkle_tree_tests.ts @@ -1,5 +1,5 @@ import * as anchor from "@coral-xyz/anchor"; -import { SystemProgram, Keypair as SolanaKeypair } from "@solana/web3.js"; +import { SystemProgram, Keypair as SolanaKeypair, Keypair } from "@solana/web3.js"; const solana = require("@solana/web3.js"); import _ from "lodash"; import { assert, expect } from "chai"; @@ -38,6 +38,7 @@ import { IDL_VERIFIER_PROGRAM_ZERO, Account, Provider, + Action, } from "light-sdk"; import { SPL_NOOP_ADDRESS } from "@solana/spl-account-compression"; @@ -67,7 +68,7 @@ describe("Merkle Tree Tests", () => { MERKLE_TREE_KEY, ); console.log("merkleTreeAccountInfoInit ", merkleTreeAccountInfoInit); - INVALID_SIGNER = new anchor.web3.Account(); + INVALID_SIGNER = Keypair.generate(); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( INVALID_SIGNER.publicKey, @@ -86,7 +87,6 @@ describe("Merkle Tree Tests", () => { try { if (args) { expect(await fn(args)).throw(); - s; } else { expect(await fn()).throw(); } @@ -111,8 +111,8 @@ describe("Merkle Tree Tests", () => { new anchor.BN( newTree.roots[newTree.currentRootIndex.toNumber()], 32, - "le", - ), + "le" + ).toString() ); }); @@ -192,7 +192,7 @@ describe("Merkle Tree Tests", () => { await merkleTreeConfig.initMerkleTreeAuthority(); await merkleTreeConfig.initializeNewMerkleTree(); - let newAuthority = new anchor.web3.Account(); + let newAuthority = Keypair.generate(); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( newAuthority.publicKey, @@ -387,7 +387,7 @@ describe("Merkle Tree Tests", () => { // update merkle tree with invalid signer merkleTreeConfig.payer = INVALID_SIGNER; try { - await merkleTreeConfig.registerPoolType(new Uint8Array(32).fill(0)); + await merkleTreeConfig.registerPoolType(new Array(32).fill(0)); } catch (e) { error = e; } @@ -399,7 +399,7 @@ describe("Merkle Tree Tests", () => { // update merkle tree with INVALID_MERKLE_TREE_AUTHORITY_PDA merkleTreeConfig.merkleTreeAuthorityPda = INVALID_MERKLE_TREE_AUTHORITY_PDA; try { - await merkleTreeConfig.registerPoolType(new Uint8Array(32).fill(0)); + await merkleTreeConfig.registerPoolType(new Array(32).fill(0)); } catch (e) { error = e; } @@ -411,7 +411,7 @@ describe("Merkle Tree Tests", () => { ); error = undefined; - await merkleTreeConfig.registerPoolType(new Uint8Array(32).fill(0)); + await merkleTreeConfig.registerPoolType(new Array(32).fill(0)); let registeredPoolTypePdaAccount = await merkleTreeProgram.account.registeredPoolType.fetch( @@ -426,7 +426,7 @@ describe("Merkle Tree Tests", () => { // update merkle tree with invalid signer merkleTreeConfig.payer = INVALID_SIGNER; try { - await merkleTreeConfig.registerSolPool(new Uint8Array(32).fill(0)); + await merkleTreeConfig.registerSolPool(new Array(32).fill(0)); } catch (e) { error = e; } @@ -439,7 +439,7 @@ describe("Merkle Tree Tests", () => { // update merkle tree with INVALID_MERKLE_TREE_AUTHORITY_PDA merkleTreeConfig.merkleTreeAuthorityPda = INVALID_MERKLE_TREE_AUTHORITY_PDA; try { - await merkleTreeConfig.registerSolPool(new Uint8Array(32).fill(0)); + await merkleTreeConfig.registerSolPool(new Array(32).fill(0)); } catch (e) { error = e; } @@ -453,7 +453,7 @@ describe("Merkle Tree Tests", () => { error = undefined; // valid - await merkleTreeConfig.registerSolPool(new Uint8Array(32).fill(0)); + await merkleTreeConfig.registerSolPool(new Array(32).fill(0)); console.log("merkleTreeConfig ", merkleTreeConfig); let registeredSolPdaAccount = @@ -464,7 +464,7 @@ describe("Merkle Tree Tests", () => { registeredSolPdaAccount.poolType.toString(), new Uint8Array(32).fill(0).toString(), ); - assert.equal(registeredSolPdaAccount.index, 0); + assert.equal(registeredSolPdaAccount.index.toString(), "0"); assert.equal( registeredSolPdaAccount.assetPoolPubkey.toBase58(), MerkleTreeConfig.getSolPoolPda(merkleTreeProgramId).pda.toBase58(), @@ -478,7 +478,7 @@ describe("Merkle Tree Tests", () => { // update merkle tree with invalid signer merkleTreeConfig.payer = INVALID_SIGNER; try { - await merkleTreeConfig.registerSplPool(new Uint8Array(32).fill(0), mint); + await merkleTreeConfig.registerSplPool(new Array(32).fill(0), mint); } catch (e) { error = e; } @@ -490,7 +490,7 @@ describe("Merkle Tree Tests", () => { // update merkle tree with INVALID_MERKLE_TREE_AUTHORITY_PDA merkleTreeConfig.merkleTreeAuthorityPda = INVALID_MERKLE_TREE_AUTHORITY_PDA; try { - await merkleTreeConfig.registerSplPool(new Uint8Array(32).fill(0), mint); + await merkleTreeConfig.registerSplPool(new Array(32).fill(0), mint); } catch (e) { error = e; } @@ -503,7 +503,7 @@ describe("Merkle Tree Tests", () => { error = undefined; // valid - await merkleTreeConfig.registerSplPool(new Uint8Array(32).fill(0), mint); + await merkleTreeConfig.registerSplPool(new Array(32).fill(0), mint); console.log(merkleTreeConfig.poolPdas); let registeredSplPdaAccount = @@ -581,9 +581,7 @@ describe("Merkle Tree Tests", () => { let lightProvider = await Provider.native(ADMIN_AUTH_KEYPAIR); - var transaction = new Transaction({ - provider: lightProvider, - }); + deposit_utxo1 = new Utxo({ poseidon: POSEIDON, @@ -598,8 +596,15 @@ describe("Merkle Tree Tests", () => { sender: userTokenAccount, senderFee: ADMIN_AUTH_KEYPAIR.publicKey, verifier: new VerifierZero(), + action: Action.DEPOSIT, + lookUpTable: LOOK_UP_TABLE, + poseidon: POSEIDON + }); + var transaction = new Transaction({ + provider: lightProvider, + params: txParams }); - await transaction.compileAndProve(txParams); + await transaction.compileAndProve(); console.log(transaction.params.accounts); // does one successful transaction diff --git a/light-system-programs/tests/user_tests.ts b/light-system-programs/tests/user_tests.ts index 745ce6686b..44f3f66438 100644 --- a/light-system-programs/tests/user_tests.ts +++ b/light-system-programs/tests/user_tests.ts @@ -47,7 +47,7 @@ describe("verifier_program", () => { before("init test setup Merkle tree lookup table etc ", async () => { let initLog = console.log; // console.log = () => {}; - await createTestAccounts(provider.connection); + await createTestAccounts(provider.connection, ); LOOK_UP_TABLE = await initLookUpTableFromFile(provider); await setUpMerkleTree(provider); // console.log = initLog; @@ -249,6 +249,7 @@ describe("verifier_program", () => { await provider.provider.connection.confirmTransaction(res, "confirmed"); const user = await User.load(provider); + await user.shield({ amount, token }); try { diff --git a/light-system-programs/tests/verifier_tests.ts b/light-system-programs/tests/verifier_tests.ts index 101a8f62b2..09f0609473 100644 --- a/light-system-programs/tests/verifier_tests.ts +++ b/light-system-programs/tests/verifier_tests.ts @@ -35,6 +35,7 @@ import { checkNfInserted, newAccountWithTokens, updateMerkleTreeForTest, + Action, } from "light-sdk"; import { BN } from "@coral-xyz/anchor"; @@ -44,392 +45,428 @@ var LOOK_UP_TABLE, POSEIDON, KEYPAIR, deposit_utxo1; var transactions: Transaction[] = []; console.log = () => {}; describe("Verifier Zero and One Tests", () => { - // Configure the client to use the local cluster. - process.env.ANCHOR_WALLET = process.env.HOME + "/.config/solana/id.json"; - - const provider = anchor.AnchorProvider.local( - "http://127.0.0.1:8899", - confirmConfig, - ); - process.env.ANCHOR_PROVIDER_URL = "http://127.0.0.1:8899"; - - anchor.setProvider(provider); - - const merkleTreeProgram: anchor.Program = - new anchor.Program(IDL_MERKLE_TREE_PROGRAM, merkleTreeProgramId); - - var depositAmount, depositFeeAmount; - const verifiers = [new VerifierZero(), new VerifierOne()]; - - before(async () => { - await createTestAccounts(provider.connection, userTokenAccount); - LOOK_UP_TABLE = await initLookUpTableFromFile(provider); - await setUpMerkleTree(provider); - - POSEIDON = await circomlibjs.buildPoseidonOpt(); - - KEYPAIR = new Account({ - poseidon: POSEIDON, - seed: KEYPAIR_PRIVKEY.toString(), - }); - - // overwrite transaction - depositAmount = - 10_000 + (Math.floor(Math.random() * 1_000_000_000) % 1_100_000_000); - depositFeeAmount = - 10_000 + (Math.floor(Math.random() * 1_000_000_000) % 1_100_000_000); - - for (var verifier in verifiers) { - console.log("verifier ", verifier.toString()); - - await token.approve( - provider.connection, - ADMIN_AUTH_KEYPAIR, - userTokenAccount, - Transaction.getSignerAuthorityPda( - merkleTreeProgramId, - verifiers[verifier].verifierProgram.programId, - ), //delegate - USER_TOKEN_ACCOUNT, // owner - depositAmount * 10, - [USER_TOKEN_ACCOUNT], - ); - - let lightProvider = await LightProvider.native(ADMIN_AUTH_KEYPAIR); - - var transaction = new Transaction({ - provider: lightProvider, - }); - - deposit_utxo1 = new Utxo({ - poseidon: POSEIDON, - assets: [FEE_ASSET, MINT], - amounts: [ - new anchor.BN(depositFeeAmount), - new anchor.BN(depositAmount), - ], - account: KEYPAIR, - }); - - let txParams = new TransactionParameters({ - outputUtxos: [deposit_utxo1], - merkleTreePubkey: MERKLE_TREE_KEY, - sender: userTokenAccount, - senderFee: ADMIN_AUTH_KEYPAIR.publicKey, - verifier: verifiers[verifier], - }); - await transaction.compileAndProve(txParams); - // does one successful transaction - await transaction.sendAndConfirmTransaction(); - await updateMerkleTreeForTest(provider.connection); - - // Deposit - var transaction1 = new Transaction({ - provider: lightProvider, - }); - - var deposit_utxo2 = new Utxo({ - poseidon: POSEIDON, - assets: [FEE_ASSET, MINT], - amounts: [ - new anchor.BN(depositFeeAmount), - new anchor.BN(depositAmount), - ], - account: KEYPAIR, - }); - - let txParams1 = new TransactionParameters({ - outputUtxos: [deposit_utxo2], - merkleTreePubkey: MERKLE_TREE_KEY, - sender: userTokenAccount, - senderFee: ADMIN_AUTH_KEYPAIR.publicKey, - verifier: verifiers[verifier], - }); - await transaction1.compileAndProve(txParams1); - transactions.push(transaction1); - - // Withdrawal - var tokenRecipient = recipientTokenAccount; - - let lightProviderWithdrawal = await LightProvider.native( - ADMIN_AUTH_KEYPAIR, - ); - const relayerRecipient = SolanaKeypair.generate().publicKey; - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop(relayerRecipient, 10000000), - ); - let relayer = new Relayer( - ADMIN_AUTH_KEYPAIR.publicKey, - lightProvider.lookUpTable, - relayerRecipient, - new BN(100000), - ); - - let tx = new Transaction({ - provider: lightProviderWithdrawal, - }); - - let txParams2 = new TransactionParameters({ - inputUtxos: [deposit_utxo1], - merkleTreePubkey: MERKLE_TREE_KEY, - recipient: tokenRecipient, - recipientFee: ADMIN_AUTH_KEYPAIR.publicKey, - verifier: verifiers[verifier], - relayer, - }); - - await tx.compileAndProve(txParams2); - transactions.push(tx); - } - }); - - afterEach(async () => { - // Check that no nullifier was inserted, otherwise the prior test failed - for (var tx in transactions) { - await checkNfInserted( - transactions[tx].params.nullifierPdaPubkeys, - provider.connection, - ); - } - }); - - const sendTestTx = async ( - tx: Transaction, - type: string, - account?: string, - ) => { - const instructions = await tx.params.verifier.getInstructions(tx); - var e; - for (var ix = 0; ix < instructions.length; ix++) { - console.log("ix ", ix); - if (ix != instructions.length - 1) { - e = await tx.sendTransaction(instructions[ix]); - // confirm throws socket hangup error thus waiting a second instead - await new Promise((resolve) => setTimeout(resolve, 700)); - // try { - // await tx.instance.provider.connection.confirmTransaction( - // e - // ) - // } catch(error) {console.log(error); - // } - } else { - e = await tx.sendTransaction(instructions[ix]); - } - } - - if (type === "ProofVerificationFails") { - assert.isTrue( - e.logs.includes("Program log: error ProofVerificationFailed"), - ); - } else if (type === "Account") { - assert.isTrue( - e.logs.includes( - `Program log: AnchorError caused by account: ${account}. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.`, - ), - ); - } else if (type === "preInsertedLeavesIndex") { - assert.isTrue( - e.logs.includes( - "Program log: AnchorError caused by account: pre_inserted_leaves_index. Error Code: AccountDiscriminatorMismatch. Error Number: 3002. Error Message: 8 byte discriminator did not match what was expected.", - ), - ); - } else if (type === "Includes") { - assert.isTrue(e.logs.includes(account)); - } - if ( - tx.params.verifier.pubkey.toString() === - new VerifierOne().pubkey.toString() - ) { - await tx.closeVerifierState(); - } - }; - - it("Wrong amount", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - let wrongAmount = new anchor.BN("123213").toArray(); - tmp_tx.publicInputs.publicAmount = Array.from([ - ...new Array(29).fill(0), - ...wrongAmount, - ]); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong feeAmount", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - let wrongFeeAmount = new anchor.BN("123213").toArray(); - tmp_tx.publicInputs.feeAmount = Array.from([ - ...new Array(29).fill(0), - ...wrongFeeAmount, - ]); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong Mint", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - let relayer = SolanaKeypair.generate(); - const newMintKeypair = SolanaKeypair.generate(); - await createMintWrapper({ - authorityKeypair: ADMIN_AUTH_KEYPAIR, - mintKeypair: newMintKeypair, - connection: provider.connection, - }); - tmp_tx.params.accounts.sender = await newAccountWithTokens({ - connection: provider.connection, - MINT: newMintKeypair.publicKey, - ADMIN_AUTH_KEYPAIR, - userAccount: relayer, - amount: new BN(0), - }); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong encryptedUtxos", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - tmp_tx.params.encryptedUtxos = new Uint8Array(174).fill(2); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong relayerFee", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - tmp_tx.params.relayer.relayerFee = new anchor.BN("9000"); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong nullifier", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - for (var i in tmp_tx.publicInputs.nullifiers) { - tmp_tx.publicInputs.nullifiers[i] = new Uint8Array(32).fill(2); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - } - }); - - it("Wrong leaves", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - for (var i in tmp_tx.publicInputs.leaves) { - tmp_tx.publicInputs.leaves[0][i] = new Uint8Array(32).fill(2); - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - } - }); - - // doesn't work sig verify error - it.skip("Wrong signer", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - const wrongSinger = SolanaKeypair.generate(); - await provider.connection.confirmTransaction( - await provider.connection.requestAirdrop( - wrongSinger.publicKey, - 1_000_000_000, - ), - "confirmed", - ); - tmp_tx.payer = wrongSinger; - tmp_tx.relayer.accounts.relayerPubkey = wrongSinger.publicKey; - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong recipientFee", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - tmp_tx.params.accounts.recipientFee = SolanaKeypair.generate().publicKey; - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong recipient", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - tmp_tx.params.accounts.recipient = SolanaKeypair.generate().publicKey; - await sendTestTx(tmp_tx, "ProofVerificationFails"); - } - }); - - it("Wrong registeredVerifierPda", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - if ( - tmp_tx.params.accounts.registeredVerifierPda.toBase58() == - REGISTERED_VERIFIER_ONE_PDA.toBase58() - ) { - tmp_tx.params.accounts.registeredVerifierPda = REGISTERED_VERIFIER_PDA; - } else { - tmp_tx.params.accounts.registeredVerifierPda = - REGISTERED_VERIFIER_ONE_PDA; - } - await sendTestTx(tmp_tx, "Account", "registered_verifier_pda"); - } - }); - - it("Wrong authority", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - tmp_tx.params.accounts.authority = Transaction.getSignerAuthorityPda( - merkleTreeProgramId, - SolanaKeypair.generate().publicKey, - ); - await sendTestTx(tmp_tx, "Account", "authority"); - } - }); - - it("Wrong nullifier accounts", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - for (var i = 0; i < tmp_tx.params.nullifierPdaPubkeys.length; i++) { - tmp_tx.params.nullifierPdaPubkeys[i] = - tmp_tx.params.nullifierPdaPubkeys[ - (i + 1) % tmp_tx.params.nullifierPdaPubkeys.length - ]; - await sendTestTx( - tmp_tx, - "Includes", - "Program log: Passed-in pda pubkey != on-chain derived pda pubkey.", - ); - } - } - }); - - it("Wrong leavesPdaPubkeys accounts", async () => { - for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - if (tmp_tx.params.leavesPdaPubkeys.length > 1) { - for (var i = 0; i < tmp_tx.params.leavesPdaPubkeys.length; i++) { - tmp_tx.params.leavesPdaPubkeys[i] = - tmp_tx.params.leavesPdaPubkeys[ - (i + 1) % tmp_tx.params.leavesPdaPubkeys.length - ]; - await sendTestTx( - tmp_tx, - "Includes", - "Program log: Passed-in pda pubkey != on-chain derived pda pubkey.", - ); - } - } else { - tmp_tx.params.leavesPdaPubkeys[0] = { - isSigner: false, - isWritable: true, - pubkey: SolanaKeypair.generate().publicKey, - }; - await sendTestTx( - tmp_tx, - "Includes", - "Program log: AnchorError caused by account: two_leaves_pda. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.", - ); - } - } - }); + // Configure the client to use the local cluster. + process.env.ANCHOR_WALLET = process.env.HOME + "/.config/solana/id.json"; + + const provider = anchor.AnchorProvider.local( + "http://127.0.0.1:8899", + confirmConfig + ); + anchor.setProvider(provider); + process.env.ANCHOR_PROVIDER_URL = "http://127.0.0.1:8899"; +// console.log = () => {}; + const merkleTreeProgram: anchor.Program = + new anchor.Program(IDL_MERKLE_TREE_PROGRAM, merkleTreeProgramId); + + var depositAmount, depositFeeAmount; + const verifiers = [new VerifierZero(), new VerifierOne()]; + + before(async () => { + await createTestAccounts(provider.connection, userTokenAccount); + LOOK_UP_TABLE = await initLookUpTableFromFile(provider); + await setUpMerkleTree(provider); + + POSEIDON = await circomlibjs.buildPoseidonOpt(); + + KEYPAIR = new Account({ + poseidon: POSEIDON, + seed: KEYPAIR_PRIVKEY.toString(), + }); + + depositAmount = + 10_000 + (Math.floor(Math.random() * 1_000_000_000) % 1_100_000_000); + depositFeeAmount = + 10_000 + (Math.floor(Math.random() * 1_000_000_000) % 1_100_000_000); + + for (var verifier in verifiers) { + console.log("verifier ", verifier.toString()); + + await token.approve( + provider.connection, + ADMIN_AUTH_KEYPAIR, + userTokenAccount, + Transaction.getSignerAuthorityPda( + merkleTreeProgramId, + verifiers[verifier].verifierProgram.programId + ), //delegate + USER_TOKEN_ACCOUNT, // owner + depositAmount * 10, + [USER_TOKEN_ACCOUNT] + ); + + let lightProvider = await LightProvider.native(ADMIN_AUTH_KEYPAIR); + + + + deposit_utxo1 = new Utxo({ + poseidon: POSEIDON, + assets: [FEE_ASSET, MINT + ], + amounts: [ + new anchor.BN(depositFeeAmount), + new anchor.BN(depositAmount), + ], + account: KEYPAIR, + }); + + let txParams = new TransactionParameters({ + outputUtxos: [deposit_utxo1], + merkleTreePubkey: MERKLE_TREE_KEY, + sender: userTokenAccount, + senderFee: ADMIN_AUTH_KEYPAIR.publicKey, + verifier: verifiers[verifier], + poseidon: POSEIDON, + action: Action.DEPOSIT, + lookUpTable: LOOK_UP_TABLE + }); + + var transaction = new Transaction({ + provider: lightProvider, + params: txParams + }); + + await transaction.compileAndProve(); + await transaction.provider.provider.connection.confirmTransaction( + await transaction.provider.provider.connection.requestAirdrop( + transaction.params.accounts.authority, + 1_000_000_000 + ) + ); + // does one successful transaction + await transaction.sendAndConfirmTransaction(); + await updateMerkleTreeForTest(provider.connection); + + // // Deposit + var deposit_utxo2 = new Utxo({ + poseidon: POSEIDON, + assets: [FEE_ASSET, MINT + ], + amounts: [ + new anchor.BN(depositFeeAmount), + new anchor.BN(depositAmount), + ], + account: KEYPAIR, + }); + + let txParams1 = new TransactionParameters({ + outputUtxos: [deposit_utxo2], + merkleTreePubkey: MERKLE_TREE_KEY, + sender: userTokenAccount, + senderFee: ADMIN_AUTH_KEYPAIR.publicKey, + verifier: verifiers[verifier], + poseidon: POSEIDON, + action: Action.DEPOSIT, + lookUpTable: LOOK_UP_TABLE + }); + + var transaction1 = new Transaction({ + provider: lightProvider, + params: txParams1, + }); + await transaction1.compileAndProve(); + transactions.push(transaction1); + + // Withdrawal + var tokenRecipient = recipientTokenAccount; + + let lightProviderWithdrawal = await LightProvider.native( + ADMIN_AUTH_KEYPAIR + ); + const relayerRecipient = SolanaKeypair.generate().publicKey; + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop(relayerRecipient, 10000000) + ); + let relayer = new Relayer( + ADMIN_AUTH_KEYPAIR.publicKey, + lightProvider.lookUpTable, + relayerRecipient, + new BN(100000) + ); + + + let txParams2 = new TransactionParameters({ + inputUtxos: [deposit_utxo1], + merkleTreePubkey: MERKLE_TREE_KEY, + recipient: tokenRecipient, + recipientFee: ADMIN_AUTH_KEYPAIR.publicKey, + verifier: verifiers[verifier], + relayer, + poseidon: POSEIDON, + action: Action.WITHDRAWAL + }); + var tx = new Transaction({ + provider: lightProviderWithdrawal, + params: txParams2, + }); + + await tx.compileAndProve(); + // await tx.getRootIndex(); + // await tx.getPdaAddresses(); + transactions.push(tx); + console.log(transactions[0].remainingAccounts) + } + }); + + // afterEach(async () => { + // // Check that no nullifier was inserted, otherwise the prior test failed + // for (var tx in transactions) { + // await checkNfInserted( + // transactions[tx].params.nullifierPdaPubkeys, + // provider.connection + // ); + // } + // }); + + const sendTestTx = async ( + tx: Transaction, + type: string, + account?: string + ) => { + var instructions = await tx.params.verifier.getInstructions(tx); + console.log("aftere instructions"); + const provider = anchor.AnchorProvider.local( + "http://127.0.0.1:8899", + confirmConfig + ); + tx.provider.provider = provider; + // if (tx.app_params){ + // console.log("tx.app_params ", tx.app_params); + + // instructions = await tx.app_params.verifier.getInstructions(tx); + // } else { + // instructions = await tx.params.verifier.getInstructions(tx); + // } + var e; + + for (var ix = 0; ix < instructions.length; ix++) { + console.log("ix ", ix); + if (ix != instructions.length - 1) { + e = await tx.sendTransaction(instructions[ix]); + + // // confirm throws socket hangup error thus waiting a second instead + await new Promise((resolve) => setTimeout(resolve, 700)); + } else { + e = await tx.sendTransaction(instructions[ix]); + } + } + console.log(e); + + if (type === "ProofVerificationFails") { + assert.isTrue( + e.logs.includes("Program log: error ProofVerificationFailed") + ); + } else if (type === "Account") { + assert.isTrue( + e.logs.includes( + `Program log: AnchorError caused by account: ${account}. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated.` + ) + ); + } else if (type === "preInsertedLeavesIndex") { + assert.isTrue( + e.logs.includes( + "Program log: AnchorError caused by account: pre_inserted_leaves_index. Error Code: AccountDiscriminatorMismatch. Error Number: 3002. Error Message: 8 byte discriminator did not match what was expected." + ) + ); + } else if (type === "Includes") { + console.log("trying includes: ", account); + + assert.isTrue(e.logs.includes(account)); + } + if (instructions.length > 1) { + await tx.closeVerifierState(); + } + }; + + it("Wrong amount", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + let wrongAmount = new anchor.BN("123213").toArray(); + tmp_tx.transactionInputs.publicInputs.publicAmount = Array.from([ + ...new Array(29).fill(0), + ...wrongAmount, + ]); + console.log("before sendTestTxs"); + + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong feeAmount", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + let wrongFeeAmount = new anchor.BN("123213").toArray(); + tmp_tx.transactionInputs.publicInputs.feeAmount = Array.from([ + ...new Array(29).fill(0), + ...wrongFeeAmount, + ]); + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong Mint", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + let relayer = SolanaKeypair.generate(); + const newMintKeypair = SolanaKeypair.generate(); + await createMintWrapper({ + authorityKeypair: ADMIN_AUTH_KEYPAIR, + mintKeypair: newMintKeypair, + connection: provider.connection, + }); + tmp_tx.params.accounts.sender = await newAccountWithTokens({ + connection: provider.connection, + MINT: newMintKeypair.publicKey, + ADMIN_AUTH_KEYPAIR, + userAccount: relayer, + amount: new BN(0), + }); + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong encryptedUtxos", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + tmp_tx.params.encryptedUtxos = new Uint8Array(174).fill(2); + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong relayerFee", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + tmp_tx.params.relayer.relayerFee = new anchor.BN("9000"); + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong nullifier", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + for (var i in tmp_tx.transactionInputs.publicInputs.nullifiers) { + tmp_tx.transactionInputs.publicInputs.nullifiers[i] = new Array(32).fill(2); + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + } + }); + + it("Wrong leaves", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + for (var i in tmp_tx.transactionInputs.publicInputs.leaves) { + tmp_tx.transactionInputs.publicInputs.leaves[0][i] = new Array(32).fill(2); + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + } + }); + + // doesn't work sig verify error + it.skip("Wrong signer", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + const wrongSinger = SolanaKeypair.generate(); + await provider.connection.confirmTransaction( + await provider.connection.requestAirdrop( + wrongSinger.publicKey, + 1_000_000_000 + ), + "confirmed" + ); + tmp_tx.provider.nodeWallet = wrongSinger; + tmp_tx.params.relayer.accounts.relayerPubkey = wrongSinger.publicKey; + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong recipientFee", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + tmp_tx.params.accounts.recipientFee = SolanaKeypair.generate().publicKey; + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong recipient", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + tmp_tx.params.accounts.recipient = SolanaKeypair.generate().publicKey; + await sendTestTx(tmp_tx, "ProofVerificationFails"); + } + }); + + it("Wrong registeredVerifierPda", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + if ( + tmp_tx.params.accounts.registeredVerifierPda.toBase58() == + REGISTERED_VERIFIER_ONE_PDA.toBase58() + ) { + tmp_tx.params.accounts.registeredVerifierPda = REGISTERED_VERIFIER_PDA; + } else { + tmp_tx.params.accounts.registeredVerifierPda = + REGISTERED_VERIFIER_ONE_PDA; + } + await sendTestTx(tmp_tx, "Account", "registered_verifier_pda"); + } + }); + + it("Wrong authority", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + tmp_tx.params.accounts.authority = Transaction.getSignerAuthorityPda( + merkleTreeProgramId, + SolanaKeypair.generate().publicKey + ); + await sendTestTx(tmp_tx, "Account", "authority"); + } + }); + + it("Wrong nullifier accounts", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + // await tmp_tx.getPdaAddresses(); + for (var i = 0; i < tmp_tx.remainingAccounts.nullifierPdaPubkeys.length; i++) { + tmp_tx.remainingAccounts.nullifierPdaPubkeys[i] = + tmp_tx.remainingAccounts.nullifierPdaPubkeys[ + (i + 1) % tmp_tx.remainingAccounts.nullifierPdaPubkeys.length + ]; + await sendTestTx( + tmp_tx, + "Includes", + "Program log: Passed-in pda pubkey != on-chain derived pda pubkey." + ); + } + } + }); + + it("Wrong leavesPdaPubkeys accounts", async () => { + for (var tx in transactions) { + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + await tmp_tx.getPdaAddresses(); + if (tmp_tx.remainingAccounts.leavesPdaPubkeys.length > 1) { + for (var i = 0; i < tmp_tx.remainingAccounts.leavesPdaPubkeys.length; i++) { + tmp_tx.remainingAccounts.leavesPdaPubkeys[i] = + tmp_tx.remainingAccounts.leavesPdaPubkeys[ + (i + 1) % tmp_tx.remainingAccounts.leavesPdaPubkeys.length + ]; + await sendTestTx( + tmp_tx, + "Includes", + "Program log: Instruction: InsertTwoLeaves" + ); + } + } else { + tmp_tx.remainingAccounts.leavesPdaPubkeys[0] = { + isSigner: false, + isWritable: true, + pubkey: SolanaKeypair.generate().publicKey, + }; + await sendTestTx( + tmp_tx, + "Includes", + "Program log: AnchorError caused by account: two_leaves_pda. Error Code: ConstraintSeeds. Error Number: 2006. Error Message: A seeds constraint was violated." + ); + } + } + }); }); diff --git a/mock-app-verifier/sdk/src/verifier.ts b/mock-app-verifier/sdk/src/verifier.ts index d7083c0ed4..c734c27352 100644 --- a/mock-app-verifier/sdk/src/verifier.ts +++ b/mock-app-verifier/sdk/src/verifier.ts @@ -20,7 +20,6 @@ import { IDL, MockVerifier as MockVerifierType, } from "../../target/types/mock_verifier"; -import { assert } from "chai"; export class MockVerifier implements Verifier { verifierProgram: Program; @@ -107,11 +106,11 @@ export class MockVerifier implements Verifier { const ix1 = await this.verifierProgram.methods .shieldedTransferFirst( - transaction.publicInputs.publicAmount, - transaction.publicInputs.nullifiers, - transaction.publicInputs.leaves, - transaction.publicInputs.feeAmount, - new anchor.BN(transaction.rootIndex.toString()), // could make this smaller to u16 + transaction.transactionInputs.publicInputs.publicAmount, + transaction.transactionInputs.publicInputs.nullifiers, + transaction.transactionInputs.publicInputs.leaves, + transaction.transactionInputs.publicInputs.feeAmount, + new anchor.BN(transaction.transactionInputs.rootIndex.toString()), // could make this smaller to u16 new anchor.BN(transaction.params.relayer.relayerFee.toString()), Buffer.from(transaction.params.encryptedUtxos.slice(0, 512)), ) @@ -124,13 +123,13 @@ export class MockVerifier implements Verifier { const ix2 = await this.verifierProgram.methods .shieldedTransferSecond( - transaction.proofBytesApp.proofA, - transaction.proofBytesApp.proofB, - transaction.proofBytesApp.proofC, - transaction.proofBytes.proofA, - transaction.proofBytes.proofB, - transaction.proofBytes.proofC, - Buffer.from(transaction.publicInputsApp.connectingHash), + transaction.transactionInputs.proofBytesApp.proofA, + transaction.transactionInputs.proofBytesApp.proofB, + transaction.transactionInputs.proofBytesApp.proofC, + transaction.transactionInputs.proofBytes.proofA, + transaction.transactionInputs.proofBytes.proofB, + transaction.transactionInputs.proofBytes.proofC, + Buffer.from(transaction.transactionInputs.publicInputsApp.connectingHash) ) .accounts({ verifierProgram: transaction.params.verifier.verifierProgram.programId, @@ -139,8 +138,8 @@ export class MockVerifier implements Verifier { relayerRecipient: relayerRecipient, }) .remainingAccounts([ - ...transaction.params.nullifierPdaPubkeys, - ...transaction.params.leavesPdaPubkeys, + ...transaction.remainingAccounts.nullifierPdaPubkeys, + ...transaction.remainingAccounts.leavesPdaPubkeys, ]) .instruction(); diff --git a/mock-app-verifier/tests/functional_test.ts b/mock-app-verifier/tests/functional_test.ts index 6a34188afd..38cb9e344f 100644 --- a/mock-app-verifier/tests/functional_test.ts +++ b/mock-app-verifier/tests/functional_test.ts @@ -30,6 +30,7 @@ import { VerifierTwo, confirmConfig, Relayer, + Action, } from "light-sdk"; import { Keypair as SolanaKeypair, @@ -93,7 +94,10 @@ describe("Mock verifier functional", () => { merkleTreePubkey: MERKLE_TREE_KEY, sender: userTokenAccount, // just any token account senderFee: ADMIN_AUTH_KEY, // + lookUpTable: LOOK_UP_TABLE, verifier: new VerifierTwo(), + poseidon, + action: Action.DEPOSIT }); const appParams = { @@ -103,15 +107,16 @@ describe("Mock verifier functional", () => { let tx = new Transaction({ provider: lightProvider, + params: txParams, + appParams }); - await tx.compile(txParams, appParams); + await tx.compile(); await tx.provider.provider.connection.confirmTransaction( await tx.provider.provider.connection.requestAirdrop( tx.params.accounts.authority, 1_000_000_000, - "confirmed", - ), + ) ); await tx.getProof(); await tx.getAppProof(); @@ -142,7 +147,8 @@ describe("Mock verifier functional", () => { recipient: userTokenAccount, // just any token account recipientFee: SolanaKeypair.generate().publicKey, // verifier: new VerifierTwo(), - + action: Action.WITHDRAWAL, + poseidon, relayer, }); @@ -153,9 +159,11 @@ describe("Mock verifier functional", () => { let tx = new Transaction({ provider: lightProvider, + params: txParams, + appParams }); - await tx.compile(txParams, appParams); + await tx.compile(); await tx.getProof(); await tx.getAppProof(); await tx.sendAndConfirmTransaction(); diff --git a/mock-app-verifier/tests/verifier_tests.ts b/mock-app-verifier/tests/verifier_tests.ts index 0176e3a79e..910e2bbd67 100644 --- a/mock-app-verifier/tests/verifier_tests.ts +++ b/mock-app-verifier/tests/verifier_tests.ts @@ -38,6 +38,7 @@ import { newAccountWithTokens, updateMerkleTreeForTest, VerifierTwo, + Action, } from "light-sdk"; import { BN } from "@coral-xyz/anchor"; @@ -99,9 +100,7 @@ describe("Verifier Two test", () => { let lightProvider = await LightProvider.native(ADMIN_AUTH_KEYPAIR); - var transaction = new Transaction({ - provider: lightProvider, - }); + deposit_utxo1 = new Utxo({ poseidon: POSEIDON, @@ -119,13 +118,23 @@ describe("Verifier Two test", () => { sender: userTokenAccount, senderFee: ADMIN_AUTH_KEYPAIR.publicKey, verifier: verifiers[verifier], + poseidon: POSEIDON, + action: Action.DEPOSIT, + lookUpTable: LOOK_UP_TABLE }); + const appParams0 = { verifier: new MockVerifier(), inputs: {}, }; - await transaction.compileAndProve(txParams, appParams0); + var transaction = new Transaction({ + provider: lightProvider, + appParams: appParams0, + params: txParams + }); + + await transaction.compileAndProve(); await transaction.provider.provider.connection.confirmTransaction( await transaction.provider.provider.connection.requestAirdrop( transaction.params.accounts.authority, @@ -137,10 +146,6 @@ describe("Verifier Two test", () => { await updateMerkleTreeForTest(provider.connection); // // Deposit - var transaction1 = new Transaction({ - provider: lightProvider, - }); - var deposit_utxo2 = new Utxo({ poseidon: POSEIDON, assets: [FEE_ASSET, MINT], @@ -157,12 +162,20 @@ describe("Verifier Two test", () => { sender: userTokenAccount, senderFee: ADMIN_AUTH_KEYPAIR.publicKey, verifier: verifiers[verifier], + poseidon: POSEIDON, + action: Action.DEPOSIT, + lookUpTable: LOOK_UP_TABLE }); const appParams = { verifier: new MockVerifier(), inputs: {}, }; - await transaction1.compileAndProve(txParams1, appParams); + var transaction1 = new Transaction({ + provider: lightProvider, + params: txParams1, + appParams + }); + await transaction1.compileAndProve(); transactions.push(transaction1); // Withdrawal @@ -182,9 +195,7 @@ describe("Verifier Two test", () => { new BN(100000), ); - let tx = new Transaction({ - provider: lightProviderWithdrawal, - }); + let txParams2 = new TransactionParameters({ inputUtxos: [deposit_utxo1], @@ -193,22 +204,32 @@ describe("Verifier Two test", () => { recipientFee: ADMIN_AUTH_KEYPAIR.publicKey, verifier: verifiers[verifier], relayer, + poseidon: POSEIDON, + action: Action.WITHDRAWAL + }); + var tx = new Transaction({ + provider: lightProviderWithdrawal, + params: txParams2, + appParams }); - await tx.compileAndProve(txParams2, appParams); + await tx.compileAndProve(); + // await tx.getRootIndex(); + // await tx.getPdaAddresses(); transactions.push(tx); + console.log(transactions[0].remainingAccounts) } }); - afterEach(async () => { - // Check that no nullifier was inserted, otherwise the prior test failed - for (var tx in transactions) { - await checkNfInserted( - transactions[tx].params.nullifierPdaPubkeys, - provider.connection, - ); - } - }); + // afterEach(async () => { + // // Check that no nullifier was inserted, otherwise the prior test failed + // for (var tx in transactions) { + // await checkNfInserted( + // transactions[tx].params.nullifierPdaPubkeys, + // provider.connection + // ); + // } + // }); const sendTestTx = async ( tx: Transaction, @@ -272,9 +293,9 @@ describe("Verifier Two test", () => { it("Wrong amount", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); let wrongAmount = new anchor.BN("123213").toArray(); - tmp_tx.publicInputs.publicAmount = Array.from([ + tmp_tx.transactionInputs.publicInputs.publicAmount = Array.from([ ...new Array(29).fill(0), ...wrongAmount, ]); @@ -286,9 +307,9 @@ describe("Verifier Two test", () => { it("Wrong feeAmount", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); let wrongFeeAmount = new anchor.BN("123213").toArray(); - tmp_tx.publicInputs.feeAmount = Array.from([ + tmp_tx.transactionInputs.publicInputs.feeAmount = Array.from([ ...new Array(29).fill(0), ...wrongFeeAmount, ]); @@ -298,7 +319,7 @@ describe("Verifier Two test", () => { it("Wrong Mint", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); let relayer = SolanaKeypair.generate(); const newMintKeypair = SolanaKeypair.generate(); await createMintWrapper({ @@ -319,7 +340,7 @@ describe("Verifier Two test", () => { it("Wrong encryptedUtxos", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); tmp_tx.params.encryptedUtxos = new Uint8Array(174).fill(2); await sendTestTx(tmp_tx, "ProofVerificationFails"); } @@ -327,7 +348,7 @@ describe("Verifier Two test", () => { it("Wrong relayerFee", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); tmp_tx.params.relayer.relayerFee = new anchor.BN("9000"); await sendTestTx(tmp_tx, "ProofVerificationFails"); } @@ -335,9 +356,9 @@ describe("Verifier Two test", () => { it("Wrong nullifier", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - for (var i in tmp_tx.publicInputs.nullifiers) { - tmp_tx.publicInputs.nullifiers[i] = new Uint8Array(32).fill(2); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + for (var i in tmp_tx.transactionInputs.publicInputs.nullifiers) { + tmp_tx.transactionInputs.publicInputs.nullifiers[i] = new Array(32).fill(2); await sendTestTx(tmp_tx, "ProofVerificationFails"); } } @@ -345,9 +366,9 @@ describe("Verifier Two test", () => { it("Wrong leaves", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - for (var i in tmp_tx.publicInputs.leaves) { - tmp_tx.publicInputs.leaves[0][i] = new Uint8Array(32).fill(2); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + for (var i in tmp_tx.transactionInputs.publicInputs.leaves) { + tmp_tx.transactionInputs.publicInputs.leaves[0][i] = new Array(32).fill(2); await sendTestTx(tmp_tx, "ProofVerificationFails"); } } @@ -356,7 +377,7 @@ describe("Verifier Two test", () => { // doesn't work sig verify error it.skip("Wrong signer", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); const wrongSinger = SolanaKeypair.generate(); await provider.connection.confirmTransaction( await provider.connection.requestAirdrop( @@ -365,15 +386,15 @@ describe("Verifier Two test", () => { ), "confirmed", ); - tmp_tx.payer = wrongSinger; - tmp_tx.relayer.accounts.relayerPubkey = wrongSinger.publicKey; + tmp_tx.provider.nodeWallet = wrongSinger; + tmp_tx.params.relayer.accounts.relayerPubkey = wrongSinger.publicKey; await sendTestTx(tmp_tx, "ProofVerificationFails"); } }); it("Wrong recipientFee", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); tmp_tx.params.accounts.recipientFee = SolanaKeypair.generate().publicKey; await sendTestTx(tmp_tx, "ProofVerificationFails"); } @@ -381,7 +402,7 @@ describe("Verifier Two test", () => { it("Wrong recipient", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); tmp_tx.params.accounts.recipient = SolanaKeypair.generate().publicKey; await sendTestTx(tmp_tx, "ProofVerificationFails"); } @@ -389,7 +410,7 @@ describe("Verifier Two test", () => { it("Wrong registeredVerifierPda", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); if ( tmp_tx.params.accounts.registeredVerifierPda.toBase58() == REGISTERED_VERIFIER_ONE_PDA.toBase58() @@ -405,7 +426,7 @@ describe("Verifier Two test", () => { it("Wrong authority", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); tmp_tx.params.accounts.authority = Transaction.getSignerAuthorityPda( merkleTreeProgramId, SolanaKeypair.generate().publicKey, @@ -416,11 +437,12 @@ describe("Verifier Two test", () => { it("Wrong nullifier accounts", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - for (var i = 0; i < tmp_tx.params.nullifierPdaPubkeys.length; i++) { - tmp_tx.params.nullifierPdaPubkeys[i] = - tmp_tx.params.nullifierPdaPubkeys[ - (i + 1) % tmp_tx.params.nullifierPdaPubkeys.length + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + // await tmp_tx.getPdaAddresses(); + for (var i = 0; i < tmp_tx.remainingAccounts.nullifierPdaPubkeys.length; i++) { + tmp_tx.remainingAccounts.nullifierPdaPubkeys[i] = + tmp_tx.remainingAccounts.nullifierPdaPubkeys[ + (i + 1) % tmp_tx.remainingAccounts.nullifierPdaPubkeys.length ]; await sendTestTx( tmp_tx, @@ -433,12 +455,13 @@ describe("Verifier Two test", () => { it("Wrong leavesPdaPubkeys accounts", async () => { for (var tx in transactions) { - var tmp_tx = _.cloneDeep(transactions[tx]); - if (tmp_tx.params.leavesPdaPubkeys.length > 1) { - for (var i = 0; i < tmp_tx.params.leavesPdaPubkeys.length; i++) { - tmp_tx.params.leavesPdaPubkeys[i] = - tmp_tx.params.leavesPdaPubkeys[ - (i + 1) % tmp_tx.params.leavesPdaPubkeys.length + var tmp_tx: Transaction = _.cloneDeep(transactions[tx]); + await tmp_tx.getPdaAddresses(); + if (tmp_tx.remainingAccounts.leavesPdaPubkeys.length > 1) { + for (var i = 0; i < tmp_tx.remainingAccounts.leavesPdaPubkeys.length; i++) { + tmp_tx.remainingAccounts.leavesPdaPubkeys[i] = + tmp_tx.remainingAccounts.leavesPdaPubkeys[ + (i + 1) % tmp_tx.remainingAccounts.leavesPdaPubkeys.length ]; await sendTestTx( tmp_tx, @@ -447,7 +470,7 @@ describe("Verifier Two test", () => { ); } } else { - tmp_tx.params.leavesPdaPubkeys[0] = { + tmp_tx.remainingAccounts.leavesPdaPubkeys[0] = { isSigner: false, isWritable: true, pubkey: SolanaKeypair.generate().publicKey,