diff --git a/packages/functions/src/canisters/cmc/cmc.canister.ts b/packages/functions/src/canisters/cmc/cmc.canister.ts index 0a03c655e..e4d411bca 100644 --- a/packages/functions/src/canisters/cmc/cmc.canister.ts +++ b/packages/functions/src/canisters/cmc/cmc.canister.ts @@ -1,8 +1,15 @@ +import {schemaFromIdl, schemaToIdl} from '@junobuild/schema/utils'; import {call} from '../../ic-cdk/call.ic-cdk'; import {Canister} from '../_canister'; import {CMC_ID} from '../_constants'; -import {type CmcDid, CmcIdl} from '../declarations'; -import {type CanisterOptions, CanisterOptionsSchema} from '../schemas'; +import {CmcIdl} from '../declarations'; +import {type CanisterOptions, CanisterOptionsSchema} from '../schema'; +import { + type NotifyTopUpArgs, + type NotifyTopUpResult, + NotifyTopUpArgsSchema, + NotifyTopUpResultSchema +} from './schema'; /** * Provides a simple interface to interact with the Cycle Minting Canister, @@ -27,14 +34,21 @@ export class CMCCanister extends Canister { * The CMC will then convert the ICP from the given ledger block into cycles and add * them to the specified canister. * - * @param {CmcDid.NotifyTopUpArg} args - Arguments containing the ledger block index and the canister ID that should receive the cycles. - * @returns {Promise} The result of the CMC conversion and deposit. + * @param {NotifyTopUpArgs} args - Arguments containing the ledger block index and the canister ID that should receive the cycles. + * @returns {Promise} The result of the CMC conversion and deposit. */ - notifyTopUp = async ({args}: {args: CmcDid.NotifyTopUpArg}): Promise => - await call({ + notifyTopUp = async ({args}: {args: NotifyTopUpArgs}): Promise => { + const parsed = NotifyTopUpArgsSchema.parse(args); + + const idlArgs = schemaToIdl({schema: NotifyTopUpArgsSchema, value: parsed}); + + const idlResult = await call({ canisterId: this.canisterId, method: 'notify_top_up', - args: [[CmcIdl.NotifyTopUpArg, args]], + args: [[CmcIdl.NotifyTopUpArg, idlArgs]], result: CmcIdl.NotifyTopUpResult }); + + return schemaFromIdl({schema: NotifyTopUpResultSchema, value: idlResult}) as NotifyTopUpResult; + }; } diff --git a/packages/functions/src/canisters/cmc/index.ts b/packages/functions/src/canisters/cmc/index.ts index f55fd737f..0ee8dc3fd 100644 --- a/packages/functions/src/canisters/cmc/index.ts +++ b/packages/functions/src/canisters/cmc/index.ts @@ -1,2 +1,3 @@ export {CmcIdl, type CmcDid} from '../declarations'; export {CMCCanister} from './cmc.canister'; +export * from './schema'; diff --git a/packages/functions/src/canisters/cmc/schema.ts b/packages/functions/src/canisters/cmc/schema.ts new file mode 100644 index 000000000..3cd15f67a --- /dev/null +++ b/packages/functions/src/canisters/cmc/schema.ts @@ -0,0 +1,57 @@ +import type {Principal} from '@icp-sdk/core/principal'; +import {j} from '@junobuild/schema'; +import * as z from 'zod'; + +export const NotifyTopUpArgsSchema = j.strictObject({ + block_index: j.bigint(), + canister_id: j.principal() +}); + +export const NotifyErrorSchema = z.union([ + z.strictObject({ + Refunded: z.strictObject({ + block_index: z.bigint().optional(), + reason: z.string() + }) + }), + z.strictObject({InvalidTransaction: z.string()}), + z.strictObject({Other: z.strictObject({error_message: z.string(), error_code: z.bigint()})}), + z.strictObject({Processing: z.null()}), + z.strictObject({TransactionTooOld: z.bigint()}) +]); + +export const NotifyTopUpResultSchema = z.union([ + z.strictObject({Ok: z.bigint()}), + z.strictObject({Err: NotifyErrorSchema}) +]); + +/** + * Arguments for the CMC `notify_top_up` call. + */ +export interface NotifyTopUpArgs { + /** Index of the block on the ICP ledger that contains the payment. */ + block_index: bigint; + /** The canister to top up. */ + canister_id: Principal; +} + +/** + * Errors that can occur during a CMC notify call. + */ +export type NotifyError = + /** The payment was returned to the caller. */ + | {Refunded: {block_index?: bigint; reason: string}} + /** The transaction does not satisfy the CMC payment protocol. */ + | {InvalidTransaction: string} + /** Other error. */ + | {Other: {error_message: string; error_code: bigint}} + /** The same payment is already being processed by a concurrent request. */ + | {Processing: null} + /** The payment was too old to be processed. */ + | {TransactionTooOld: bigint}; + +/** + * The result of a CMC `notify_top_up` call. + * Returns the amount of cycles sent to the specified canister on success. + */ +export type NotifyTopUpResult = {Ok: bigint} | {Err: NotifyError}; diff --git a/packages/functions/src/canisters/index.ts b/packages/functions/src/canisters/index.ts index e84d06c37..e27a6e2f5 100644 --- a/packages/functions/src/canisters/index.ts +++ b/packages/functions/src/canisters/index.ts @@ -1 +1 @@ -export * from './schemas'; +export * from './schema'; diff --git a/packages/functions/src/canisters/ledger/icp/index.ts b/packages/functions/src/canisters/ledger/icp/index.ts index 623496d3d..a2db4af99 100644 --- a/packages/functions/src/canisters/ledger/icp/index.ts +++ b/packages/functions/src/canisters/ledger/icp/index.ts @@ -1,2 +1,3 @@ export {IcpIndexIdl, IcpLedgerIdl, type IcpIndexDid, type IcpLedgerDid} from '../../declarations'; export {IcpLedgerCanister} from './ledger.canister'; +export * from './schema'; diff --git a/packages/functions/src/canisters/ledger/icp/ledger.canister.ts b/packages/functions/src/canisters/ledger/icp/ledger.canister.ts index 5ffc10566..fe17ce293 100644 --- a/packages/functions/src/canisters/ledger/icp/ledger.canister.ts +++ b/packages/functions/src/canisters/ledger/icp/ledger.canister.ts @@ -1,8 +1,15 @@ +import {schemaFromIdl, schemaToIdl} from '@junobuild/schema/utils'; import {call} from '../../../ic-cdk/call.ic-cdk'; import {Canister} from '../../_canister'; import {ICP_LEDGER_ID} from '../../_constants'; -import {type IcpLedgerDid, IcpLedgerIdl} from '../../declarations'; -import {type CanisterOptions, CanisterOptionsSchema} from '../../schemas'; +import {IcpLedgerIdl} from '../../declarations'; +import {type CanisterOptions, CanisterOptionsSchema} from '../../schema'; +import { + type TransferArgs, + type TransferResult, + TransferArgsSchema, + TransferResultSchema +} from './schema'; /** * Provides a simple interface to interact with the ICP Ledger, @@ -23,18 +30,24 @@ export class IcpLedgerCanister extends Canister { * Use this to transfer ICP from one account to another when writing * Juno Serverless Functions in TypeScript. * - * @param {IcpLedgerDid.TransferArgs} args - The ledger transfer arguments (amount, destination account, memo, fee, etc.). - * @returns {Promise} The result of the ICP transfer. + * @param {TransferArgs} args - The ledger transfer arguments (amount, destination account, memo, fee, etc.). + * @returns {Promise} The result of the ICP transfer. */ - transfer = async ({ - args - }: { - args: IcpLedgerDid.TransferArgs; - }): Promise => - await call({ + transfer = async ({args}: {args: TransferArgs}): Promise => { + const parsed = TransferArgsSchema.parse(args); + + const idlArgs = schemaToIdl({ + schema: TransferArgsSchema, + value: parsed + }); + + const idlResult = await call({ canisterId: this.canisterId, method: 'transfer', - args: [[IcpLedgerIdl.TransferArgs, args]], + args: [[IcpLedgerIdl.TransferArgs, idlArgs]], result: IcpLedgerIdl.TransferResult }); + + return schemaFromIdl({schema: TransferResultSchema, value: idlResult}) as TransferResult; + }; } diff --git a/packages/functions/src/canisters/ledger/icp/schema.ts b/packages/functions/src/canisters/ledger/icp/schema.ts new file mode 100644 index 000000000..65d34a76c --- /dev/null +++ b/packages/functions/src/canisters/ledger/icp/schema.ts @@ -0,0 +1,93 @@ +import {j} from '@junobuild/schema'; +import * as z from 'zod'; + +export const AccountIdentifierSchema = j.instanceof(Uint8Array); +export const TokensSchema = j.strictObject({e8s: j.bigint()}); +export const TimeStampSchema = j.strictObject({timestamp_nanos: j.bigint()}); +export const SubAccountSchema = j.instanceof(Uint8Array); + +export const TransferArgsSchema = j.strictObject({ + to: AccountIdentifierSchema, + fee: TokensSchema, + memo: j.bigint(), + from_subaccount: SubAccountSchema.optional(), + created_at_time: TimeStampSchema.optional(), + amount: TokensSchema +}); + +export const TransferErrorSchema = z.union([ + z.strictObject({TxTooOld: z.strictObject({allowed_window_nanos: z.bigint()})}), + z.strictObject({BadFee: z.strictObject({expected_fee: z.strictObject({e8s: z.bigint()})})}), + z.strictObject({TxDuplicate: z.strictObject({duplicate_of: z.bigint()})}), + z.strictObject({TxCreatedInFuture: z.null()}), + z.strictObject({InsufficientFunds: z.strictObject({balance: z.strictObject({e8s: z.bigint()})})}) +]); + +export const TransferResultSchema = z.union([ + z.strictObject({Ok: z.bigint()}), + z.strictObject({Err: TransferErrorSchema}) +]); + +/** + * The destination account identifier. + * A 32-byte array where the first 4 bytes are a CRC32 checksum of the last 28 bytes. + */ +export type AccountIdentifier = Uint8Array; + +/** + * Amount of tokens, measured in 10^-8 of a token. + */ +export interface Tokens { + e8s: bigint; +} + +/** + * A point in time, represented as nanoseconds since the Unix epoch. + */ +export interface TimeStamp { + timestamp_nanos: bigint; +} + +/** + * Subaccount is an arbitrary 32-byte array used to compute the source address. + */ +export type SubAccount = Uint8Array; + +/** + * Arguments for the ICP Ledger `transfer` call. + */ +export interface TransferArgs { + /** The destination account identifier. */ + to: AccountIdentifier; + /** The transaction fee. Must be 10000 e8s. */ + fee: Tokens; + /** An arbitrary number associated with the transaction for correlation. */ + memo: bigint; + /** The subaccount to transfer from. Uses the default subaccount if not provided. */ + from_subaccount?: SubAccount; + /** The time at which the caller created this request. Uses current IC time if not provided. */ + created_at_time?: TimeStamp; + /** The amount to transfer to the destination address. */ + amount: Tokens; +} + +/** + * Errors that can occur during an ICP Ledger transfer. + */ +export type TransferError = + /** The request is too old. The ledger only accepts requests created within a 24-hour window. */ + | {TxTooOld: {allowed_window_nanos: bigint}} + /** The fee specified in the transfer request does not match the expected fee. */ + | {BadFee: {expected_fee: Tokens}} + /** The ledger has already executed this request. */ + | {TxDuplicate: {duplicate_of: bigint}} + /** The specified `created_at_time` is too far in the future. */ + | {TxCreatedInFuture: null} + /** The caller's account does not have enough funds. */ + | {InsufficientFunds: {balance: Tokens}}; + +/** + * The result of an ICP Ledger `transfer` call. + * Returns the block index of the transaction on success. + */ +export type TransferResult = {Ok: bigint} | {Err: TransferError}; diff --git a/packages/functions/src/canisters/ledger/icrc/index.ts b/packages/functions/src/canisters/ledger/icrc/index.ts index b863b0161..5449fa35e 100644 --- a/packages/functions/src/canisters/ledger/icrc/index.ts +++ b/packages/functions/src/canisters/ledger/icrc/index.ts @@ -5,4 +5,4 @@ export { type IcrcLedgerDid } from '../../declarations'; export {IcrcLedgerCanister} from './ledger.canister'; -export * from './schemas'; +export * from './schema'; diff --git a/packages/functions/src/canisters/ledger/icrc/ledger.canister.ts b/packages/functions/src/canisters/ledger/icrc/ledger.canister.ts index f0166847c..231ac012a 100644 --- a/packages/functions/src/canisters/ledger/icrc/ledger.canister.ts +++ b/packages/functions/src/canisters/ledger/icrc/ledger.canister.ts @@ -1,7 +1,21 @@ +import {schemaFromIdl, schemaToIdl} from '@junobuild/schema/utils'; import {call} from '../../../ic-cdk/call.ic-cdk'; import {Canister} from '../../_canister'; -import {type IcrcLedgerDid, IcrcLedgerIdl} from '../../declarations'; -import {type IcrcCanisterOptions, IcrcCanisterOptionsSchema} from './schemas'; +import {IcrcLedgerIdl} from '../../declarations'; +import { + AccountSchema, + IcrcCanisterOptionsSchema, + TransferArgsSchema, + TransferFromArgsSchema, + TransferFromResultSchema, + TransferResultSchema, + type Account, + type IcrcCanisterOptions, + type TransferArgs, + type TransferFromArgs, + type TransferFromResult, + type TransferResult +} from './schema'; /** * Provides a simple interface to interact with an ICRC Ledger, @@ -19,20 +33,20 @@ export class IcrcLedgerCanister extends Canister { /** * Returns the balance of an ICRC account. * - * @param {IcrcLedgerDid.Account} account - The account to query. - * @returns {Promise} The token balance for the account. + * @param {Account} account - The account to query. + * @returns {Promise} The token balance for the account. */ - icrc1BalanceOf = async ({ - account - }: { - account: IcrcLedgerDid.Account; - }): Promise => - await call({ + icrc1BalanceOf = async ({account}: {account: Account}): Promise => { + const parsed = AccountSchema.parse(account); + const idlArgs = schemaToIdl({schema: AccountSchema, value: parsed}); + + return await call({ canisterId: this.canisterId, method: 'icrc1_balance_of', - args: [[IcrcLedgerIdl.Account, account]], + args: [[IcrcLedgerIdl.Account, idlArgs]], result: IcrcLedgerIdl.Tokens }); + }; /** * Transfers tokens using the ICRC-1 `icrc1_transfer` method. @@ -40,39 +54,46 @@ export class IcrcLedgerCanister extends Canister { * Use this to send tokens from the caller's account to another account * when writing Juno Serverless Functions in TypeScript. * - * @param {IcrcLedgerDid.TransferArg} args - Transfer arguments (amount, fee, to, memo, created_at_time, etc.). - * @returns {Promise} The result of the transfer. + * @param {TransferArgs} args - Transfer arguments (amount, fee, to, memo, created_at_time, etc.). + * @returns {Promise} The result of the transfer. */ - icrc1Transfer = async ({ - args - }: { - args: IcrcLedgerDid.TransferArg; - }): Promise => - await call({ + icrc1Transfer = async ({args}: {args: TransferArgs}): Promise => { + const parsed = TransferArgsSchema.parse(args); + const idlArgs = schemaToIdl({schema: TransferArgsSchema, value: parsed}); + + const idlResult = await call({ canisterId: this.canisterId, method: 'icrc1_transfer', - args: [[IcrcLedgerIdl.TransferArg, args]], + args: [[IcrcLedgerIdl.TransferArg, idlArgs]], result: IcrcLedgerIdl.TransferResult }); + return schemaFromIdl({schema: TransferResultSchema, value: idlResult}) as TransferResult; + }; + /** * Transfers tokens using the ICRC-2 `icrc2_transfer_from` method. * * Allows transferring tokens from another user's account when an approval * has previously been granted via `icrc2_approve`. * - * @param {IcrcLedgerDid.TransferFromArgs} args - Transfer-from arguments (amount, from_subaccount, spender, etc.). - * @returns {Promise} The result of the transfer-from operation. + * @param {TransferFromArgs} args - Transfer-from arguments (amount, from_subaccount, spender, etc.). + * @returns {Promise} The result of the transfer-from operation. */ - icrc2TransferFrom = async ({ - args - }: { - args: IcrcLedgerDid.TransferFromArgs; - }): Promise => - await call({ + icrc2TransferFrom = async ({args}: {args: TransferFromArgs}): Promise => { + const parsed = TransferFromArgsSchema.parse(args); + const idlArgs = schemaToIdl({schema: TransferFromArgsSchema, value: parsed}); + + const idlResult = await call({ canisterId: this.canisterId, method: 'icrc2_transfer_from', - args: [[IcrcLedgerIdl.TransferFromArgs, args]], + args: [[IcrcLedgerIdl.TransferFromArgs, idlArgs]], result: IcrcLedgerIdl.TransferFromResult }); + + return schemaFromIdl({ + schema: TransferFromResultSchema, + value: idlResult + }) as TransferFromResult; + }; } diff --git a/packages/functions/src/canisters/ledger/icrc/schema.ts b/packages/functions/src/canisters/ledger/icrc/schema.ts new file mode 100644 index 000000000..2c11fe7dc --- /dev/null +++ b/packages/functions/src/canisters/ledger/icrc/schema.ts @@ -0,0 +1,191 @@ +import type {Principal} from '@icp-sdk/core/principal'; +import {j} from '@junobuild/schema'; +import * as z from 'zod'; +import {CanisterOptionsSchema} from '../../schema'; + +/** + * @see CanisterOptions + */ +export const IcrcCanisterOptionsSchema = CanisterOptionsSchema.required(); + +/** + * The options to initialize an Icrc canister. + */ +export type IcrcCanisterOptions = z.infer; + +export const SubaccountSchema = j.uint8Array(); + +export const AccountSchema = j.strictObject({ + owner: j.principal(), + subaccount: SubaccountSchema.optional() +}); + +export const TokensSchema = j.nat(); + +// icrc1_transfer +export const TransferArgsSchema = j.strictObject({ + to: AccountSchema, + fee: TokensSchema.optional(), + memo: j.uint8Array().optional(), + from_subaccount: SubaccountSchema.optional(), + created_at_time: j.bigint().optional(), + amount: TokensSchema +}); + +export const TransferErrorSchema = z.union([ + z.strictObject({GenericError: z.strictObject({message: z.string(), error_code: z.bigint()})}), + z.strictObject({TemporarilyUnavailable: z.null()}), + z.strictObject({BadBurn: z.strictObject({min_burn_amount: z.bigint()})}), + z.strictObject({Duplicate: z.strictObject({duplicate_of: z.bigint()})}), + z.strictObject({BadFee: z.strictObject({expected_fee: z.bigint()})}), + z.strictObject({CreatedInFuture: z.strictObject({ledger_time: z.bigint()})}), + z.strictObject({TooOld: z.null()}), + z.strictObject({InsufficientFunds: z.strictObject({balance: z.bigint()})}) +]); + +export const TransferResultSchema = z.union([ + z.strictObject({Ok: z.bigint()}), + z.strictObject({Err: TransferErrorSchema}) +]); + +// icrc2_transfer_from +export const TransferFromArgsSchema = j.strictObject({ + to: AccountSchema, + fee: TokensSchema.optional(), + spender_subaccount: SubaccountSchema.optional(), + from: AccountSchema, + memo: j.uint8Array().optional(), + created_at_time: j.bigint().optional(), + amount: TokensSchema +}); + +export const TransferFromErrorSchema = z.union([ + z.strictObject({GenericError: z.strictObject({message: z.string(), error_code: z.bigint()})}), + z.strictObject({TemporarilyUnavailable: z.null()}), + z.strictObject({InsufficientAllowance: z.strictObject({allowance: z.bigint()})}), + z.strictObject({BadBurn: z.strictObject({min_burn_amount: z.bigint()})}), + z.strictObject({Duplicate: z.strictObject({duplicate_of: z.bigint()})}), + z.strictObject({BadFee: z.strictObject({expected_fee: z.bigint()})}), + z.strictObject({CreatedInFuture: z.strictObject({ledger_time: z.bigint()})}), + z.strictObject({TooOld: z.null()}), + z.strictObject({InsufficientFunds: z.strictObject({balance: z.bigint()})}) +]); + +export const TransferFromResultSchema = z.union([ + z.strictObject({Ok: z.bigint()}), + z.strictObject({Err: TransferFromErrorSchema}) +]); + +/** + * Subaccount is an arbitrary 32-byte array used to compute the source address. + */ +export type Subaccount = Uint8Array; + +/** + * An ICRC account, consisting of an owner principal and an optional subaccount. + */ +export interface Account { + /** The account owner. */ + owner: Principal; + /** An optional subaccount to distinguish multiple accounts for the same owner. */ + subaccount?: Subaccount; +} + +/** + * Errors that can occur during an ICRC-1 transfer. + */ +export type TransferError = + /** An error not covered by the other variants. */ + | {GenericError: {message: string; error_code: bigint}} + /** The ledger is temporarily unavailable. */ + | {TemporarilyUnavailable: null} + /** The burn amount is below the minimum. */ + | {BadBurn: {min_burn_amount: bigint}} + /** The transaction is a duplicate. */ + | {Duplicate: {duplicate_of: bigint}} + /** The fee does not match the expected fee. */ + | {BadFee: {expected_fee: bigint}} + /** The `created_at_time` is too far in the future. */ + | {CreatedInFuture: {ledger_time: bigint}} + /** The transaction is too old. */ + | {TooOld: null} + /** The account does not have enough funds. */ + | {InsufficientFunds: {balance: bigint}}; + +/** + * The result of an ICRC-1 `icrc1_transfer` call. + * Returns the block index of the transaction on success. + */ +export type TransferResult = {Ok: bigint} | {Err: TransferError}; + +/** + * Arguments for the ICRC-1 `icrc1_transfer` call. + */ +export interface TransferArgs { + /** The destination account. */ + to: Account; + /** An optional fee. Uses the default ledger fee if not provided. */ + fee?: bigint; + /** An optional memo for the transaction. */ + memo?: Uint8Array; + /** An optional subaccount to transfer from. Uses the default subaccount if not provided. */ + from_subaccount?: Subaccount; + /** An optional timestamp. Uses current IC time if not provided. */ + created_at_time?: bigint; + /** The amount to transfer. */ + amount: bigint; +} + +/** + * Errors that can occur during an ICRC-2 transfer-from. + */ +export type TransferFromError = + /** An error not covered by the other variants. */ + | {GenericError: {message: string; error_code: bigint}} + /** The ledger is temporarily unavailable. */ + | {TemporarilyUnavailable: null} + /** The spender's allowance is insufficient. */ + | {InsufficientAllowance: {allowance: bigint}} + /** The burn amount is below the minimum. */ + | {BadBurn: {min_burn_amount: bigint}} + /** The transaction is a duplicate. */ + | {Duplicate: {duplicate_of: bigint}} + /** The fee does not match the expected fee. */ + | {BadFee: {expected_fee: bigint}} + /** The `created_at_time` is too far in the future. */ + | {CreatedInFuture: {ledger_time: bigint}} + /** The transaction is too old. */ + | {TooOld: null} + /** The account does not have enough funds. */ + | {InsufficientFunds: {balance: bigint}}; + +/** + * The result of an ICRC-2 `icrc2_transfer_from` call. + * Returns the block index of the transaction on success. + */ +export type TransferFromResult = {Ok: bigint} | {Err: TransferFromError}; + +/** + * Arguments for the ICRC-2 `icrc2_transfer_from` call. + */ +export interface TransferFromArgs { + /** The destination account. */ + to: Account; + /** An optional fee. Uses the default ledger fee if not provided. */ + fee?: bigint; + /** An optional subaccount of the spender. */ + spender_subaccount?: Subaccount; + /** The account to transfer from. */ + from: Account; + /** An optional memo for the transaction. */ + memo?: Uint8Array; + /** An optional timestamp. Uses current IC time if not provided. */ + created_at_time?: bigint; + /** The amount to transfer. */ + amount: bigint; +} + +/** + * Amount of ICRC tokens, represented as a natural number. + */ +export type Tokens = bigint; diff --git a/packages/functions/src/canisters/ledger/icrc/schemas.ts b/packages/functions/src/canisters/ledger/icrc/schemas.ts deleted file mode 100644 index 5a192093f..000000000 --- a/packages/functions/src/canisters/ledger/icrc/schemas.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type * as z from 'zod'; -import {CanisterOptionsSchema} from '../../schemas'; - -/** - * @see CanisterOptions - */ -export const IcrcCanisterOptionsSchema = CanisterOptionsSchema.required(); - -/** - * The options to initialize an Icrc canister. - */ -export type IcrcCanisterOptions = z.infer; diff --git a/packages/functions/src/canisters/schemas.ts b/packages/functions/src/canisters/schema.ts similarity index 100% rename from packages/functions/src/canisters/schemas.ts rename to packages/functions/src/canisters/schema.ts diff --git a/packages/functions/src/tests/canisters/cmc/cmc.canister.spec.ts b/packages/functions/src/tests/canisters/cmc/cmc.canister.spec.ts index 314eeb1f4..f4e698587 100644 --- a/packages/functions/src/tests/canisters/cmc/cmc.canister.spec.ts +++ b/packages/functions/src/tests/canisters/cmc/cmc.canister.spec.ts @@ -2,6 +2,7 @@ import {IDL} from '@icp-sdk/core/candid'; import {Principal} from '@icp-sdk/core/principal'; import {CMC_ID} from '../../../canisters/_constants'; import {CMCCanister} from '../../../canisters/cmc'; +import {type NotifyTopUpArgs} from '../../../canisters/cmc/schema'; import {type CmcDid, CmcIdl} from '../../../canisters/declarations'; import {mockCanisterId} from '../../mocks/ic-cdk.mock'; @@ -13,6 +14,14 @@ describe('CMCCanister', () => { vi.clearAllMocks(); }); + const mockIdlResult = (result: CmcDid.NotifyTopUpResult) => + new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [result])); + + const mockArgs: NotifyTopUpArgs = { + block_index: mockBlockIndex, + canister_id: mockTargetCanisterId + }; + describe('constructor', () => { it('should create instance with default CMC canister ID', () => { const cmc = new CMCCanister(); @@ -37,27 +46,16 @@ describe('CMCCanister', () => { describe('notifyTopUp', () => { it('should successfully notify top up and return cycles', async () => { const expectedCycles = 1000000000000n; - const mockResponse: CmcDid.NotifyTopUpResult = {Ok: expectedCycles}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult({Ok: expectedCycles})) ); const cmc = new CMCCanister(); - const result = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); - - expect(result).toEqual(mockResponse); + const result = await cmc.notifyTopUp({args: mockArgs}); expect(result).toHaveProperty('Ok'); - if ('Ok' in result) { expect(result.Ok).toBe(expectedCycles); return; @@ -67,34 +65,19 @@ describe('CMCCanister', () => { }); it('should handle NotifyError.Refunded response', async () => { - const mockError: CmcDid.NotifyTopUpResult = { - Err: { - Refunded: { - block_index: [mockBlockIndex], - reason: 'Insufficient cycles' - } - } - }; - vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockError])); - }) + vi.fn(async () => + mockIdlResult({ + Err: {Refunded: {block_index: [mockBlockIndex], reason: 'Insufficient cycles'}} + }) + ) ); const cmc = new CMCCanister(); - const result = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); - - expect(result).toEqual(mockError); + const result = await cmc.notifyTopUp({args: mockArgs}); expect(result).toHaveProperty('Err'); - if ('Err' in result && 'Refunded' in result.Err) { expect(result.Err.Refunded.reason).toBe('Insufficient cycles'); return; @@ -104,75 +87,42 @@ describe('CMCCanister', () => { }); it('should handle NotifyError.InvalidTransaction response', async () => { - const mockError: CmcDid.NotifyTopUpResult = { - Err: {InvalidTransaction: 'Transaction format is invalid'} - }; - vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockError])); - }) + vi.fn(async () => + mockIdlResult({Err: {InvalidTransaction: 'Transaction format is invalid'}}) + ) ); const cmc = new CMCCanister(); - const result = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); + const result = await cmc.notifyTopUp({args: mockArgs}); - expect(result).toEqual(mockError); expect(result).toHaveProperty('Err'); }); it('should handle NotifyError.Processing response', async () => { - const mockError: CmcDid.NotifyTopUpResult = { - Err: {Processing: null} - }; - vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockError])); - }) + vi.fn(async () => mockIdlResult({Err: {Processing: null}})) ); const cmc = new CMCCanister(); - const result = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); + const result = await cmc.notifyTopUp({args: mockArgs}); - expect(result).toEqual(mockError); expect(result).toHaveProperty('Err'); }); it('should handle NotifyError.TransactionTooOld response', async () => { const oldestBlockIndex = 10000n; - const mockError: CmcDid.NotifyTopUpResult = { - Err: {TransactionTooOld: oldestBlockIndex} - }; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockError])); - }) + vi.fn(async () => mockIdlResult({Err: {TransactionTooOld: oldestBlockIndex}})) ); const cmc = new CMCCanister(); - const result = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); + const result = await cmc.notifyTopUp({args: mockArgs}); - expect(result).toEqual(mockError); expect(result).toHaveProperty('Err'); if ('Err' in result && 'TransactionTooOld' in result.Err) { expect(result.Err.TransactionTooOld).toBe(oldestBlockIndex); @@ -180,31 +130,16 @@ describe('CMCCanister', () => { }); it('should handle NotifyError.Other response', async () => { - const mockError: CmcDid.NotifyTopUpResult = { - Err: { - Other: { - error_message: 'Unknown error occurred', - error_code: 500n - } - } - }; - vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockError])); - }) + vi.fn(async () => + mockIdlResult({Err: {Other: {error_message: 'Unknown error occurred', error_code: 500n}}}) + ) ); const cmc = new CMCCanister(); - const result = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); + const result = await cmc.notifyTopUp({args: mockArgs}); - expect(result).toEqual(mockError); expect(result).toHaveProperty('Err'); if ('Err' in result && 'Other' in result.Err) { expect(result.Err.Other.error_code).toBe(500n); @@ -224,30 +159,16 @@ describe('CMCCanister', () => { const cmc = new CMCCanister(); - await expect( - cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }) - ).rejects.toThrow('Network error'); + await expect(cmc.notifyTopUp({args: mockArgs})).rejects.toThrow('Network error'); }); it('should pass correct arguments to call function', async () => { - const mockCallRaw = vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [{Ok: 1000000n}])); - }); + const mockCallRaw = vi.fn(async () => mockIdlResult({Ok: 1000000n})); vi.stubGlobal('__ic_cdk_call_raw', mockCallRaw); const cmc = new CMCCanister({canisterId: mockCanisterId}); - await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: mockTargetCanisterId - } - }); + await cmc.notifyTopUp({args: mockArgs}); expect(mockCallRaw).toHaveBeenCalled(); }); @@ -255,33 +176,23 @@ describe('CMCCanister', () => { it('should work with different canister IDs', async () => { const canisterId1 = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); const canisterId2 = Principal.fromText('qoctq-giaaa-aaaaa-aaaea-cai'); - const mockResponse: CmcDid.NotifyTopUpResult = {Ok: 2000000000n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([CmcIdl.NotifyTopUpResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult({Ok: 2000000000n})) ); const cmc = new CMCCanister(); const result1 = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex, - canister_id: canisterId1 - } + args: {block_index: mockBlockIndex, canister_id: canisterId1} }); - const result2 = await cmc.notifyTopUp({ - args: { - block_index: mockBlockIndex + 1n, - canister_id: canisterId2 - } + args: {block_index: mockBlockIndex + 1n, canister_id: canisterId2} }); - expect(result1).toEqual(mockResponse); - expect(result2).toEqual(mockResponse); + expect(result1).toHaveProperty('Ok'); + expect(result2).toHaveProperty('Ok'); }); }); }); diff --git a/packages/functions/src/tests/canisters/ledger/icp/ledger.canister.spec.ts b/packages/functions/src/tests/canisters/ledger/icp/ledger.canister.spec.ts index 3b8981b91..a11e54950 100644 --- a/packages/functions/src/tests/canisters/ledger/icp/ledger.canister.spec.ts +++ b/packages/functions/src/tests/canisters/ledger/icp/ledger.canister.spec.ts @@ -1,8 +1,9 @@ import {IDL} from '@icp-sdk/core/candid'; import {Principal} from '@icp-sdk/core/principal'; import {ICP_LEDGER_ID} from '../../../../canisters/_constants'; -import {type IcpLedgerDid, IcpLedgerIdl} from '../../../../canisters/declarations'; +import {IcpLedgerIdl} from '../../../../canisters/declarations'; import {IcpLedgerCanister} from '../../../../canisters/ledger/icp'; +import {type TransferArgs, type TransferResult} from '../../../../canisters/ledger/icp/schema'; import {mockCanisterId} from '../../../mocks/ic-cdk.mock'; describe('IcpLedgerCanister', () => { @@ -13,6 +14,9 @@ describe('IcpLedgerCanister', () => { vi.clearAllMocks(); }); + const mockIdlResult = (result: TransferResult) => + new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [result])); + describe('constructor', () => { it('should create instance with default ICP Ledger canister ID', () => { const ledger = new IcpLedgerCanister(); @@ -35,24 +39,22 @@ describe('IcpLedgerCanister', () => { }); describe('transfer', () => { - const mockTransferArgs: IcpLedgerDid.TransferArgs = { + const mockTransferArgs: TransferArgs = { to: mockToAccountIdentifier, fee: {e8s: 10000n}, memo: 0n, - from_subaccount: [], - created_at_time: [], + from_subaccount: undefined, + created_at_time: undefined, amount: {e8s: 100000000n} }; it('should successfully transfer and return block index', async () => { const expectedBlockIndex = 12345n; - const mockResponse: IcpLedgerDid.TransferResult = {Ok: expectedBlockIndex}; + const mockResponse: TransferResult = {Ok: expectedBlockIndex}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -69,19 +71,13 @@ describe('IcpLedgerCanister', () => { }); it('should handle TransferError.TxTooOld response', async () => { - const mockError: IcpLedgerDid.TransferResult = { - Err: { - TxTooOld: { - allowed_window_nanos: 86400000000000n - } - } + const mockError: TransferResult = { + Err: {TxTooOld: {allowed_window_nanos: 86400000000000n}} }; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlResult(mockError)) ); const ledger = new IcpLedgerCanister(); @@ -98,19 +94,11 @@ describe('IcpLedgerCanister', () => { }); it('should handle TransferError.BadFee response', async () => { - const mockError: IcpLedgerDid.TransferResult = { - Err: { - BadFee: { - expected_fee: {e8s: 10000n} - } - } - }; + const mockError: TransferResult = {Err: {BadFee: {expected_fee: {e8s: 10000n}}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlResult(mockError)) ); const ledger = new IcpLedgerCanister(); @@ -128,19 +116,11 @@ describe('IcpLedgerCanister', () => { it('should handle TransferError.TxDuplicate response', async () => { const duplicateOf = 5000n; - const mockError: IcpLedgerDid.TransferResult = { - Err: { - TxDuplicate: { - duplicate_of: duplicateOf - } - } - }; + const mockError: TransferResult = {Err: {TxDuplicate: {duplicate_of: duplicateOf}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlResult(mockError)) ); const ledger = new IcpLedgerCanister(); @@ -157,15 +137,11 @@ describe('IcpLedgerCanister', () => { }); it('should handle TransferError.TxCreatedInFuture response', async () => { - const mockError: IcpLedgerDid.TransferResult = { - Err: {TxCreatedInFuture: null} - }; + const mockError: TransferResult = {Err: {TxCreatedInFuture: null}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlResult(mockError)) ); const ledger = new IcpLedgerCanister(); @@ -176,19 +152,11 @@ describe('IcpLedgerCanister', () => { }); it('should handle TransferError.InsufficientFunds response', async () => { - const mockError: IcpLedgerDid.TransferResult = { - Err: { - InsufficientFunds: { - balance: {e8s: 50000000n} - } - } - }; + const mockError: TransferResult = {Err: {InsufficientFunds: {balance: {e8s: 50000000n}}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlResult(mockError)) ); const ledger = new IcpLedgerCanister(); @@ -218,18 +186,15 @@ describe('IcpLedgerCanister', () => { }); it('should handle transfer with from_subaccount', async () => { - const argsWithSubaccount: IcpLedgerDid.TransferArgs = { + const argsWithSubaccount: TransferArgs = { ...mockTransferArgs, - from_subaccount: [mockFromSubaccount] + from_subaccount: mockFromSubaccount }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 99999n}; + const mockResponse: TransferResult = {Ok: 99999n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -239,18 +204,15 @@ describe('IcpLedgerCanister', () => { }); it('should handle transfer with created_at_time', async () => { - const argsWithTime: IcpLedgerDid.TransferArgs = { + const argsWithTime: TransferArgs = { ...mockTransferArgs, - created_at_time: [{timestamp_nanos: 1234567890000000000n}] + created_at_time: {timestamp_nanos: 1234567890000000000n} }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 88888n}; + const mockResponse: TransferResult = {Ok: 88888n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -260,18 +222,12 @@ describe('IcpLedgerCanister', () => { }); it('should handle transfer with custom memo', async () => { - const argsWithMemo: IcpLedgerDid.TransferArgs = { - ...mockTransferArgs, - memo: 123456789n - }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 77777n}; + const argsWithMemo: TransferArgs = {...mockTransferArgs, memo: 123456789n}; + const mockResponse: TransferResult = {Ok: 77777n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -281,18 +237,15 @@ describe('IcpLedgerCanister', () => { }); it('should handle very large amount transfer', async () => { - const largeAmountArgs: IcpLedgerDid.TransferArgs = { + const largeAmountArgs: TransferArgs = { ...mockTransferArgs, amount: {e8s: 999999999999999999n} }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 66666n}; + const mockResponse: TransferResult = {Ok: 66666n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -302,9 +255,7 @@ describe('IcpLedgerCanister', () => { }); it('should pass correct arguments to call function', async () => { - const mockCallRaw = vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [{Ok: 1000n}])); - }); + const mockCallRaw = vi.fn(async () => mockIdlResult({Ok: 1000n})); vi.stubGlobal('__ic_cdk_call_raw', mockCallRaw); @@ -315,22 +266,19 @@ describe('IcpLedgerCanister', () => { }); it('should handle minimal transfer args', async () => { - const minimalArgs: IcpLedgerDid.TransferArgs = { + const minimalArgs: TransferArgs = { to: mockToAccountIdentifier, fee: {e8s: 10000n}, memo: 0n, - from_subaccount: [], - created_at_time: [], + from_subaccount: undefined, + created_at_time: undefined, amount: {e8s: 10000n} }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 1n}; + const mockResponse: TransferResult = {Ok: 1n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -342,13 +290,11 @@ describe('IcpLedgerCanister', () => { it('should work with different canister IDs', async () => { const canisterId1 = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); const canisterId2 = Principal.fromText('qoctq-giaaa-aaaaa-aaaea-cai'); - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 100n}; + const mockResponse: TransferResult = {Ok: 100n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger1 = new IcpLedgerCanister({canisterId: canisterId1}); @@ -364,18 +310,12 @@ describe('IcpLedgerCanister', () => { }); it('should handle zero amount transfer', async () => { - const zeroAmountArgs: IcpLedgerDid.TransferArgs = { - ...mockTransferArgs, - amount: {e8s: 0n} - }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 0n}; + const zeroAmountArgs: TransferArgs = {...mockTransferArgs, amount: {e8s: 0n}}; + const mockResponse: TransferResult = {Ok: 0n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); @@ -385,22 +325,19 @@ describe('IcpLedgerCanister', () => { }); it('should handle all optional fields populated', async () => { - const fullArgs: IcpLedgerDid.TransferArgs = { + const fullArgs: TransferArgs = { to: mockToAccountIdentifier, fee: {e8s: 10000n}, memo: 999999n, - from_subaccount: [mockFromSubaccount], - created_at_time: [{timestamp_nanos: 1700000000000000000n}], + from_subaccount: mockFromSubaccount, + created_at_time: {timestamp_nanos: 1700000000000000000n}, amount: {e8s: 500000000n} }; - - const mockResponse: IcpLedgerDid.TransferResult = {Ok: 55555n}; + const mockResponse: TransferResult = {Ok: 55555n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcpLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlResult(mockResponse)) ); const ledger = new IcpLedgerCanister(); diff --git a/packages/functions/src/tests/canisters/ledger/icrc/ledger.canister.spec.ts b/packages/functions/src/tests/canisters/ledger/icrc/ledger.canister.spec.ts index 59dbdc7fa..1c293daee 100644 --- a/packages/functions/src/tests/canisters/ledger/icrc/ledger.canister.spec.ts +++ b/packages/functions/src/tests/canisters/ledger/icrc/ledger.canister.spec.ts @@ -1,7 +1,14 @@ import {IDL} from '@icp-sdk/core/candid'; import {Principal} from '@icp-sdk/core/principal'; -import {type IcrcLedgerDid, IcrcLedgerIdl} from '../../../../canisters/declarations'; +import {IcrcLedgerIdl} from '../../../../canisters/declarations'; import {IcrcLedgerCanister} from '../../../../canisters/ledger/icrc'; +import { + type Account, + type TransferArgs, + type TransferFromArgs, + type TransferFromResult, + type TransferResult +} from '../../../../canisters/ledger/icrc/schema'; import {mockCanisterId} from '../../../mocks/ic-cdk.mock'; describe('IcrcLedgerCanister', () => { @@ -13,6 +20,15 @@ describe('IcrcLedgerCanister', () => { vi.clearAllMocks(); }); + const mockIdlBalanceResult = (balance: bigint) => + new Uint8Array(IDL.encode([IcrcLedgerIdl.Tokens], [balance])); + + const mockIdlTransferResult = (result: TransferResult) => + new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [result])); + + const mockIdlTransferFromResult = (result: TransferFromResult) => + new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [result])); + describe('constructor', () => { it('should create instance with provided canister ID', () => { const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); @@ -33,9 +49,9 @@ describe('IcrcLedgerCanister', () => { }); describe('icrc1BalanceOf', () => { - const mockAccount: IcrcLedgerDid.Account = { + const mockAccount: Account = { owner: mockOwner, - subaccount: [] + subaccount: undefined }; it('should successfully return account balance', async () => { @@ -43,9 +59,7 @@ describe('IcrcLedgerCanister', () => { vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.Tokens], [expectedBalance])); - }) + vi.fn(async () => mockIdlBalanceResult(expectedBalance)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); @@ -55,39 +69,32 @@ describe('IcrcLedgerCanister', () => { }); it('should handle zero balance', async () => { - const zeroBalance = 0n; - vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.Tokens], [zeroBalance])); - }) + vi.fn(async () => mockIdlBalanceResult(0n)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1BalanceOf({account: mockAccount}); - expect(result).toBe(zeroBalance); + expect(result).toBe(0n); }); it('should handle account with subaccount', async () => { - const accountWithSubaccount: IcrcLedgerDid.Account = { + const accountWithSubaccount: Account = { owner: mockOwner, - subaccount: [mockSubaccount] + subaccount: mockSubaccount }; - const expectedBalance = 500000000n; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.Tokens], [expectedBalance])); - }) + vi.fn(async () => mockIdlBalanceResult(500000000n)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1BalanceOf({account: accountWithSubaccount}); - expect(result).toBe(expectedBalance); + expect(result).toBe(500000000n); }); it('should throw error if canister call fails', async () => { @@ -105,31 +112,28 @@ describe('IcrcLedgerCanister', () => { }); describe('icrc1Transfer', () => { - const mockTransferArgs: IcrcLedgerDid.TransferArg = { - to: {owner: mockSpender, subaccount: []}, - fee: [], - memo: [], - from_subaccount: [], - created_at_time: [], + const mockTransferArgs: TransferArgs = { + to: {owner: mockSpender, subaccount: undefined}, + fee: undefined, + memo: undefined, + from_subaccount: undefined, + created_at_time: undefined, amount: 100000000n }; it('should successfully transfer and return block index', async () => { const expectedBlockIndex = 12345n; - const mockResponse: IcrcLedgerDid.TransferResult = {Ok: expectedBlockIndex}; + const mockResponse: TransferResult = {Ok: expectedBlockIndex}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); expect(result).toEqual(mockResponse); - expect(result).toHaveProperty('Ok'); if ('Ok' in result) { expect(result.Ok).toBe(expectedBlockIndex); return; @@ -139,192 +143,137 @@ describe('IcrcLedgerCanister', () => { }); it('should handle TransferError.GenericError', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: { - GenericError: { - message: 'Something went wrong', - error_code: 500n - } - } + const mockResponse: TransferResult = { + Err: {GenericError: {message: 'Something went wrong', error_code: 500n}} }; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); expect(result).toHaveProperty('Err'); }); it('should handle TransferError.TemporarilyUnavailable', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: {TemporarilyUnavailable: null} - }; + const mockResponse: TransferResult = {Err: {TemporarilyUnavailable: null}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferError.BadBurn', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: { - BadBurn: { - min_burn_amount: 10000n - } - } - }; + const mockResponse: TransferResult = {Err: {BadBurn: {min_burn_amount: 10000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferError.Duplicate', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: { - Duplicate: { - duplicate_of: 5000n - } - } - }; + const mockResponse: TransferResult = {Err: {Duplicate: {duplicate_of: 5000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferError.BadFee', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: { - BadFee: { - expected_fee: 10000n - } - } - }; + const mockResponse: TransferResult = {Err: {BadFee: {expected_fee: 10000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferError.CreatedInFuture', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: { - CreatedInFuture: { - ledger_time: 1700000000000000000n - } - } + const mockResponse: TransferResult = { + Err: {CreatedInFuture: {ledger_time: 1700000000000000000n}} }; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferError.TooOld', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: {TooOld: null} - }; + const mockResponse: TransferResult = {Err: {TooOld: null}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferError.InsufficientFunds', async () => { - const mockError: IcrcLedgerDid.TransferResult = { - Err: { - InsufficientFunds: { - balance: 50000000n - } - } - }; + const mockResponse: TransferResult = {Err: {InsufficientFunds: {balance: 50000000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc1Transfer({args: mockTransferArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle transfer with all optional fields', async () => { - const fullArgs: IcrcLedgerDid.TransferArg = { - to: {owner: mockSpender, subaccount: [mockSubaccount]}, - fee: [10000n], - memo: [new Uint8Array([1, 2, 3, 4])], - from_subaccount: [mockSubaccount], - created_at_time: [1700000000000000000n], + const fullArgs: TransferArgs = { + to: {owner: mockSpender, subaccount: mockSubaccount}, + fee: 10000n, + memo: new Uint8Array([1, 2, 3, 4]), + from_subaccount: mockSubaccount, + created_at_time: 1700000000000000000n, amount: 500000000n }; - const mockResponse: IcrcLedgerDid.TransferResult = {Ok: 99999n}; + const mockResponse: TransferResult = {Ok: 99999n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferResult], [mockResponse])); - }) + vi.fn(async () => mockIdlTransferResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); @@ -348,32 +297,29 @@ describe('IcrcLedgerCanister', () => { }); describe('icrc2TransferFrom', () => { - const mockTransferFromArgs: IcrcLedgerDid.TransferFromArgs = { - from: {owner: mockOwner, subaccount: []}, - to: {owner: mockSpender, subaccount: []}, - fee: [], - spender_subaccount: [], - memo: [], - created_at_time: [], + const mockTransferFromArgs: TransferFromArgs = { + from: {owner: mockOwner, subaccount: undefined}, + to: {owner: mockSpender, subaccount: undefined}, + fee: undefined, + spender_subaccount: undefined, + memo: undefined, + created_at_time: undefined, amount: 100000000n }; it('should successfully transfer from and return block index', async () => { const expectedBlockIndex = 54321n; - const mockResponse: IcrcLedgerDid.TransferFromResult = {Ok: expectedBlockIndex}; + const mockResponse: TransferFromResult = {Ok: expectedBlockIndex}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockResponse])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); expect(result).toEqual(mockResponse); - expect(result).toHaveProperty('Ok'); if ('Ok' in result) { expect(result.Ok).toBe(expectedBlockIndex); return; @@ -383,214 +329,151 @@ describe('IcrcLedgerCanister', () => { }); it('should handle TransferFromError.GenericError', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - GenericError: { - message: 'Generic error occurred', - error_code: 1000n - } - } + const mockResponse: TransferFromResult = { + Err: {GenericError: {message: 'Generic error occurred', error_code: 1000n}} }; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.TemporarilyUnavailable', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: {TemporarilyUnavailable: null} - }; + const mockResponse: TransferFromResult = {Err: {TemporarilyUnavailable: null}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.InsufficientAllowance', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - InsufficientAllowance: { - allowance: 50000n - } - } - }; + const mockResponse: TransferFromResult = {Err: {InsufficientAllowance: {allowance: 50000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.BadBurn', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - BadBurn: { - min_burn_amount: 10000n - } - } - }; + const mockResponse: TransferFromResult = {Err: {BadBurn: {min_burn_amount: 10000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.Duplicate', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - Duplicate: { - duplicate_of: 8888n - } - } - }; + const mockResponse: TransferFromResult = {Err: {Duplicate: {duplicate_of: 8888n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.BadFee', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - BadFee: { - expected_fee: 20000n - } - } - }; + const mockResponse: TransferFromResult = {Err: {BadFee: {expected_fee: 20000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.CreatedInFuture', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - CreatedInFuture: { - ledger_time: 1800000000000000000n - } - } + const mockResponse: TransferFromResult = { + Err: {CreatedInFuture: {ledger_time: 1800000000000000000n}} }; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.TooOld', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: {TooOld: null} - }; + const mockResponse: TransferFromResult = {Err: {TooOld: null}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle TransferFromError.InsufficientFunds', async () => { - const mockError: IcrcLedgerDid.TransferFromResult = { - Err: { - InsufficientFunds: { - balance: 30000000n - } - } - }; + const mockResponse: TransferFromResult = {Err: {InsufficientFunds: {balance: 30000000n}}}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockError])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); const result = await ledger.icrc2TransferFrom({args: mockTransferFromArgs}); - expect(result).toEqual(mockError); + expect(result).toEqual(mockResponse); }); it('should handle transfer from with all optional fields', async () => { - const fullArgs: IcrcLedgerDid.TransferFromArgs = { - from: {owner: mockOwner, subaccount: [mockSubaccount]}, - to: {owner: mockSpender, subaccount: [mockSubaccount]}, - fee: [15000n], - spender_subaccount: [mockSubaccount], - memo: [new Uint8Array([5, 6, 7, 8])], - created_at_time: [1750000000000000000n], + const fullArgs: TransferFromArgs = { + from: {owner: mockOwner, subaccount: mockSubaccount}, + to: {owner: mockSpender, subaccount: mockSubaccount}, + fee: 15000n, + spender_subaccount: mockSubaccount, + memo: new Uint8Array([5, 6, 7, 8]), + created_at_time: 1750000000000000000n, amount: 750000000n }; - const mockResponse: IcrcLedgerDid.TransferFromResult = {Ok: 77777n}; + const mockResponse: TransferFromResult = {Ok: 77777n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockResponse])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger = new IcrcLedgerCanister({canisterId: mockCanisterId}); @@ -617,13 +500,11 @@ describe('IcrcLedgerCanister', () => { it('should work with different ledger instances', async () => { const canisterId1 = Principal.fromText('ryjl3-tyaaa-aaaaa-aaaba-cai'); const canisterId2 = Principal.fromText('qoctq-giaaa-aaaaa-aaaea-cai'); - const mockResponse: IcrcLedgerDid.TransferFromResult = {Ok: 11111n}; + const mockResponse: TransferFromResult = {Ok: 11111n}; vi.stubGlobal( '__ic_cdk_call_raw', - vi.fn(async () => { - return new Uint8Array(IDL.encode([IcrcLedgerIdl.TransferFromResult], [mockResponse])); - }) + vi.fn(async () => mockIdlTransferFromResult(mockResponse)) ); const ledger1 = new IcrcLedgerCanister({canisterId: canisterId1}); diff --git a/packages/functions/src/tests/canisters/ledger/icrc/schemas.spec.ts b/packages/functions/src/tests/canisters/ledger/icrc/schemas.spec.ts deleted file mode 100644 index 46a5ecfc5..000000000 --- a/packages/functions/src/tests/canisters/ledger/icrc/schemas.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {Principal} from '@icp-sdk/core/principal'; -import {IcrcCanisterOptionsSchema} from '../../../../canisters/ledger/icrc'; - -describe('schemas', () => { - describe('IcrcCanisterOptionsSchema', () => { - const mockPrincipalText = 'xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe'; - - it('should NOT accept an empty object (canisterId required)', () => { - expect(() => IcrcCanisterOptionsSchema.parse({})).toThrow(); - }); - - it('should accept a valid Principal as canisterId', () => { - const canisterId = Principal.fromText(mockPrincipalText); - - expect(() => IcrcCanisterOptionsSchema.parse({canisterId})).not.toThrow(); - - const parsed = IcrcCanisterOptionsSchema.parse({canisterId}); - expect(parsed.canisterId.toText()).toBe(mockPrincipalText); - }); - - it('should reject a missing canisterId', () => { - // Slightly redundant with the empty object test, but makes intent explicit - expect(() => IcrcCanisterOptionsSchema.parse({})).toThrow(); - }); - - it('should reject an invalid canisterId', () => { - expect(() => IcrcCanisterOptionsSchema.parse({canisterId: 'invalid'})).toThrow(); - expect(() => IcrcCanisterOptionsSchema.parse({canisterId: 123})).toThrow(); - expect(() => - IcrcCanisterOptionsSchema.parse({canisterId: Uint8Array.from([1, 2, 3])}) - ).toThrow(); - expect(() => IcrcCanisterOptionsSchema.parse({canisterId: null})).toThrow(); - }); - - it('should reject unknown properties because of strictObject', () => { - expect(() => - IcrcCanisterOptionsSchema.parse({ - canisterId: Principal.fromText(mockPrincipalText), - foo: 'bar' - }) - ).toThrow(); - }); - }); -});