diff --git a/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.spec.ts b/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.spec.ts new file mode 100644 index 00000000..74cf9211 --- /dev/null +++ b/packages/ledger-icp/src/canisters/ledger/ledger.request.converts.spec.ts @@ -0,0 +1,147 @@ +import { Principal } from "@dfinity/principal"; +import { toNullable } from "@dfinity/utils"; +import { mockAccountIdentifier } from "../../mocks/ledger.mock"; +import { mockConsentMessageRequest } from "../../mocks/ledger.request.mock"; +import type { + Icrc1TransferRequest, + Icrc2ApproveRequest, + TransferRequest, +} from "../../types/ledger_converters"; +import { + toIcrc1TransferRawRequest, + toIcrc21ConsentMessageRawRequest, + toIcrc2ApproveRawRequest, + toTransferRawRequest, +} from "./ledger.request.converts"; + +describe("ledger.request.converts", () => { + const now = BigInt(Date.now()) * BigInt(1e6); + + const mockTransferRequest: TransferRequest = { + to: mockAccountIdentifier, + amount: 123n, + memo: 456n, + fee: 10_000n, + fromSubAccount: [0, 1, 2], + createdAt: now, + }; + + const to = { + owner: Principal.fromHex("abcd"), + subaccount: [] as [], + }; + + const mockIcrc1TransferRequest: Icrc1TransferRequest = { + to, + fromSubAccount: [0, 1, 2], + amount: 1_000_000n, + fee: 11_000n, + icrc1Memo: new Uint8Array([1, 2, 3]), + createdAt: now, + }; + + const mockIcrc2ApproveRequest: Icrc2ApproveRequest = { + spender: to, + fromSubAccount: [0, 1, 2], + expected_allowance: 5_000n, + expires_at: now + 200n, + amount: 1_200_000n, + fee: 12_000n, + icrc1Memo: new Uint8Array([1, 2, 3]), + createdAt: now, + }; + + it("toTransferRawRequest should return a valid raw request", () => { + const result = toTransferRawRequest(mockTransferRequest); + + expect(result.to).toEqual(mockTransferRequest.to.toUint8Array()); + expect(result.fee).toEqual({ e8s: mockTransferRequest.fee }); + expect(result.amount).toEqual({ e8s: mockTransferRequest.amount }); + expect(result.memo).toEqual(mockTransferRequest.memo); + expect(result.created_at_time).toEqual([ + { timestamp_nanos: mockTransferRequest.createdAt }, + ]); + expect(result.from_subaccount).toEqual([ + new Uint8Array(mockTransferRequest.fromSubAccount ?? []), + ]); + }); + + it("toIcrc1TransferRawRequest should return a valid ICRC-1 raw request", () => { + const result = toIcrc1TransferRawRequest(mockIcrc1TransferRequest); + + expect(result.to).toEqual(mockIcrc1TransferRequest.to); + expect(result.fee).toEqual(toNullable(mockIcrc1TransferRequest.fee)); + expect(result.amount).toEqual(mockIcrc1TransferRequest.amount); + expect(result.memo).toEqual(toNullable(mockIcrc1TransferRequest.icrc1Memo)); + expect(result.created_at_time).toEqual( + toNullable(mockIcrc1TransferRequest.createdAt), + ); + expect(result.from_subaccount).toEqual( + toNullable(mockIcrc1TransferRequest.fromSubAccount), + ); + }); + + it("toIcrc2ApproveRawRequest should return a valid ICRC-2 raw request", () => { + const result = toIcrc2ApproveRawRequest(mockIcrc2ApproveRequest); + + expect(result.spender).toEqual(mockIcrc2ApproveRequest.spender); + expect(result.fee).toEqual(toNullable(mockIcrc2ApproveRequest.fee)); + expect(result.memo).toEqual(toNullable(mockIcrc2ApproveRequest.icrc1Memo)); + expect(result.created_at_time).toEqual( + toNullable(mockIcrc2ApproveRequest.createdAt), + ); + expect(result.amount).toEqual(mockIcrc2ApproveRequest.amount); + expect(result.expected_allowance).toEqual( + toNullable(mockIcrc2ApproveRequest.expected_allowance), + ); + expect(result.expires_at).toEqual( + toNullable(mockIcrc2ApproveRequest.expires_at), + ); + expect(result.from_subaccount).toEqual( + toNullable(mockIcrc2ApproveRequest.fromSubAccount), + ); + }); + + it("toIcrc21ConsentMessageRawRequest should return a valid ICRC-21 generic consent message request", () => { + const result = toIcrc21ConsentMessageRawRequest(mockConsentMessageRequest); + + expect(result.method).toEqual(mockConsentMessageRequest.method); + expect(result.arg).toEqual(mockConsentMessageRequest.arg); + expect(result.user_preferences.metadata.language).toEqual( + mockConsentMessageRequest.userPreferences.metadata.language, + ); + expect(result.user_preferences.metadata.utc_offset_minutes).toEqual( + toNullable( + mockConsentMessageRequest.userPreferences.metadata.utcOffsetMinutes, + ), + ); + }); + + it("toIcrc21ConsentMessageRawRequest should return a valid ICRC-21 line display consent message request", () => { + const lineDisplay = { + charactersPerLine: 2, + linesPerPage: 10, + }; + + const consentMessageRequest = { + ...mockConsentMessageRequest, + userPreferences: { + ...mockConsentMessageRequest.userPreferences, + deriveSpec: { + LineDisplay: lineDisplay, + }, + }, + }; + + const result = toIcrc21ConsentMessageRawRequest(consentMessageRequest); + + expect(result.user_preferences.device_spec).toEqual( + toNullable({ + LineDisplay: { + characters_per_line: lineDisplay.charactersPerLine, + lines_per_page: lineDisplay.linesPerPage, + }, + }), + ); + }); +}); diff --git a/packages/ledger-icp/src/index.ts b/packages/ledger-icp/src/index.ts index aa4e80ef..84d35094 100644 --- a/packages/ledger-icp/src/index.ts +++ b/packages/ledger-icp/src/index.ts @@ -8,6 +8,7 @@ export type { Value, } from "../candid/ledger"; export { AccountIdentifier, SubAccount } from "./account_identifier"; +export * from "./canisters/ledger/ledger.request.converts"; export * from "./errors/ledger.errors"; export { IndexCanister } from "./index.canister"; export { LedgerCanister } from "./ledger.canister"; diff --git a/packages/ledger-icp/src/ledger.canister.spec.ts b/packages/ledger-icp/src/ledger.canister.spec.ts index bc8ce6fe..1c9f1c95 100644 --- a/packages/ledger-icp/src/ledger.canister.spec.ts +++ b/packages/ledger-icp/src/ledger.canister.spec.ts @@ -30,6 +30,7 @@ import { } from "./errors/ledger.errors"; import { LedgerCanister } from "./ledger.canister"; import { mockAccountIdentifier, mockPrincipal } from "./mocks/ledger.mock"; +import { mockConsentMessageRequest } from "./mocks/ledger.request.mock"; import type { Icrc21ConsentMessageRequest, Icrc2ApproveRequest, @@ -1059,19 +1060,6 @@ describe("LedgerCanister", () => { }); describe("icrc21ConsentMessage", () => { - const consentMessageRequest: Icrc21ConsentMessageRequest = { - method: "icrc1_transfer", - arg: new Uint8Array([1, 2, 3]), - userPreferences: { - metadata: { - language: "en-US", - }, - deriveSpec: { - GenericDisplay: null, - }, - }, - }; - const consentMessageResponse: icrc21_consent_message_response = { Ok: { consent_message: { @@ -1111,12 +1099,14 @@ describe("LedgerCanister", () => { certifiedServiceOverride: service, }); - const response = await ledger.icrc21ConsentMessage(consentMessageRequest); + const response = await ledger.icrc21ConsentMessage( + mockConsentMessageRequest, + ); expect(response).toEqual(consentMessageResponse.Ok); expect(service.icrc21_canister_call_consent_message).toBeCalledWith({ - method: consentMessageRequest.method, - arg: consentMessageRequest.arg, + method: mockConsentMessageRequest.method, + arg: mockConsentMessageRequest.arg, user_preferences: { metadata: { language: "en-US", @@ -1142,7 +1132,7 @@ describe("LedgerCanister", () => { }); const requestWithLineDisplay: Icrc21ConsentMessageRequest = { - ...consentMessageRequest, + ...mockConsentMessageRequest, userPreferences: { metadata: { language: "en-US", @@ -1192,7 +1182,7 @@ describe("LedgerCanister", () => { }); const requestWithUtcOffset: Icrc21ConsentMessageRequest = { - ...consentMessageRequest, + ...mockConsentMessageRequest, userPreferences: { metadata: { language: "en-US", @@ -1246,7 +1236,7 @@ describe("LedgerCanister", () => { }); await expect( - ledger.icrc21ConsentMessage(consentMessageRequest), + ledger.icrc21ConsentMessage(mockConsentMessageRequest), ).rejects.toThrowError(new GenericError(errorDescription, BigInt(500))); }); @@ -1272,7 +1262,7 @@ describe("LedgerCanister", () => { }); await expect( - ledger.icrc21ConsentMessage(consentMessageRequest), + ledger.icrc21ConsentMessage(mockConsentMessageRequest), ).rejects.toThrowError( new InsufficientPaymentError(insufficientPaymentDescription), ); @@ -1301,7 +1291,7 @@ describe("LedgerCanister", () => { }); await expect( - ledger.icrc21ConsentMessage(consentMessageRequest), + ledger.icrc21ConsentMessage(mockConsentMessageRequest), ).rejects.toThrowError( new UnsupportedCanisterCallError(unsupportedCanisterCallDescription), ); @@ -1330,7 +1320,7 @@ describe("LedgerCanister", () => { }); await expect( - ledger.icrc21ConsentMessage(consentMessageRequest), + ledger.icrc21ConsentMessage(mockConsentMessageRequest), ).rejects.toThrowError( new ConsentMessageUnavailableError( consentMessageUnavailableDescription, @@ -1361,7 +1351,7 @@ describe("LedgerCanister", () => { }); await expect( - ledger.icrc21ConsentMessage(consentMessageRequest), + ledger.icrc21ConsentMessage(mockConsentMessageRequest), ).rejects.toThrowError( new ConsentMessageError(`Unknown error type ${JSON.stringify(Err)}`), ); diff --git a/packages/ledger-icp/src/mocks/ledger.request.mock.ts b/packages/ledger-icp/src/mocks/ledger.request.mock.ts new file mode 100644 index 00000000..02df00d1 --- /dev/null +++ b/packages/ledger-icp/src/mocks/ledger.request.mock.ts @@ -0,0 +1,14 @@ +import type { Icrc21ConsentMessageRequest } from "../types/ledger_converters"; + +export const mockConsentMessageRequest: Icrc21ConsentMessageRequest = { + method: "icrc1_transfer", + arg: new Uint8Array([1, 2, 3]), + userPreferences: { + metadata: { + language: "en-US", + }, + deriveSpec: { + GenericDisplay: null, + }, + }, +};