From 2e75f66328301f7fbd9aee01ef903dfd5ff042c4 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 23 Dec 2024 06:51:32 +0100 Subject: [PATCH 1/4] feat: icrc map token metadata to record Signed-off-by: David Dal Busco --- CHANGELOG.md | 1 + .../ledger-icrc/src/types/ledger.responses.ts | 8 ++ .../src/utils/ledger.utils.spec.ts | 129 +++++++++++++++++- .../ledger-icrc/src/utils/ledger.utils.ts | 61 ++++++++- 4 files changed, 197 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e06383342..74b4d9e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Add support for `get_subnet_types_to_subnets` to `@dfinity/cmc`. - Support `VotingPowerEconomics`, `potential_voting_power` and `deciding_voting_power` in `@dfinity/nns`. - Add utility `isEmptyString` (the opposite of existing `notEmptyString`). +- Add utility `mapTokenMetadata` in `@dfinity/ledger-icrc` to map the token metadata information from a ledger response into a structured record. # 2024.11.27-1230Z diff --git a/packages/ledger-icrc/src/types/ledger.responses.ts b/packages/ledger-icrc/src/types/ledger.responses.ts index 8198b43db..ed8cc6e81 100644 --- a/packages/ledger-icrc/src/types/ledger.responses.ts +++ b/packages/ledger-icrc/src/types/ledger.responses.ts @@ -19,3 +19,11 @@ export interface IcrcAccount { owner: Principal; subaccount?: Subaccount; } + +export interface IcrcTokenMetadata { + name: string; + symbol: string; + fee: bigint; + decimals: number; + icon?: string; +} diff --git a/packages/ledger-icrc/src/utils/ledger.utils.spec.ts b/packages/ledger-icrc/src/utils/ledger.utils.spec.ts index 85dfd6b1c..dbe6788d4 100644 --- a/packages/ledger-icrc/src/utils/ledger.utils.spec.ts +++ b/packages/ledger-icrc/src/utils/ledger.utils.spec.ts @@ -1,6 +1,14 @@ import { Principal } from "@dfinity/principal"; import { mockPrincipal } from "../mocks/ledger.mock"; -import { decodeIcrcAccount, encodeIcrcAccount } from "./ledger.utils"; +import { + IcrcMetadataResponseEntries, + type IcrcTokenMetadataResponse, +} from "../types/ledger.responses"; +import { + decodeIcrcAccount, + encodeIcrcAccount, + mapTokenMetadata, +} from "./ledger.utils"; describe("ledger-utils", () => { const ownerText = @@ -114,4 +122,123 @@ describe("ledger-utils", () => { expect(decodeIcrcAccount(encodeIcrcAccount(account4))).toEqual(account4); }); }); + + describe("mapTokenMetadata", () => { + const validResponse: IcrcTokenMetadataResponse = [ + [IcrcMetadataResponseEntries.SYMBOL, { Text: "TKN" }], + [IcrcMetadataResponseEntries.NAME, { Text: "Token" }], + [IcrcMetadataResponseEntries.FEE, { Nat: 10_000n }], + [IcrcMetadataResponseEntries.DECIMALS, { Nat: 8n }], + [IcrcMetadataResponseEntries.LOGO, { Text: "a-logo" }], + ]; + + it("should map token metadata", () => { + const result = mapTokenMetadata(validResponse); + + expect(result).toEqual({ + name: "Token", + symbol: "TKN", + fee: 10_000n, + decimals: 8, + icon: "a-logo", + }); + }); + + const missingFieldCases: [string, IcrcTokenMetadataResponse][] = [ + [ + "missing field symbol", + validResponse.filter( + ([key]) => key !== IcrcMetadataResponseEntries.SYMBOL, + ), + ], + [ + "missing field name", + validResponse.filter( + ([key]) => key !== IcrcMetadataResponseEntries.NAME, + ), + ], + [ + "missing field fee", + validResponse.filter( + ([key]) => key !== IcrcMetadataResponseEntries.FEE, + ), + ], + [ + "missing field decimals", + validResponse.filter( + ([key]) => key !== IcrcMetadataResponseEntries.DECIMALS, + ), + ], + ]; + + it.each(missingFieldCases)( + "should return undefined for %s", + (_, response) => { + const result = mapTokenMetadata(response); + expect(result).toBeUndefined(); + }, + ); + + const invalidFieldCases: [string, IcrcTokenMetadataResponse][] = [ + [ + "invalid symbol value", + validResponse.map(([key, value]) => + key === IcrcMetadataResponseEntries.SYMBOL + ? [key, { Nat: BigInt(1) }] + : [key, value], + ), + ], + [ + "invalid name value", + validResponse.map(([key, value]) => + key === IcrcMetadataResponseEntries.NAME + ? [key, { Nat: BigInt(1) }] + : [key, value], + ), + ], + [ + "invalid fee value", + validResponse.map(([key, value]) => + key === IcrcMetadataResponseEntries.FEE + ? [key, { Text: "100" }] + : [key, value], + ), + ], + [ + "invalid decimals value", + validResponse.map(([key, value]) => + key === IcrcMetadataResponseEntries.DECIMALS + ? [key, { Text: "8" }] + : [key, value], + ), + ], + ]; + + it.each(invalidFieldCases)( + "should return undefined for %s", + (_, response) => { + const result = mapTokenMetadata(response); + expect(result).toBeUndefined(); + }, + ); + + test("should return empty if response metadata is empty", () => { + const result = mapTokenMetadata([]); + expect(result).toBeUndefined(); + }); + + test("should map a metadata without logo", () => { + const responseWithoutLogo = validResponse.filter( + ([key]) => key !== IcrcMetadataResponseEntries.LOGO, + ); + + const result = mapTokenMetadata(responseWithoutLogo); + expect(result).toEqual({ + name: "Token", + symbol: "TKN", + fee: 10_000n, + decimals: 8, + }); + }); + }); }); diff --git a/packages/ledger-icrc/src/utils/ledger.utils.ts b/packages/ledger-icrc/src/utils/ledger.utils.ts index b7968d93a..dd59436c2 100644 --- a/packages/ledger-icrc/src/utils/ledger.utils.ts +++ b/packages/ledger-icrc/src/utils/ledger.utils.ts @@ -7,7 +7,12 @@ import { notEmptyString, uint8ArrayToHexString, } from "@dfinity/utils"; -import type { IcrcAccount } from "../types/ledger.responses"; +import type { + IcrcAccount, + IcrcTokenMetadata, + IcrcTokenMetadataResponse, +} from "../types/ledger.responses"; +import { IcrcMetadataResponseEntries } from "../types/ledger.responses"; const MAX_SUBACCOUNT_HEX_LENGTH = 64; @@ -89,3 +94,57 @@ export const decodeIcrcAccount = (accountString: string): IcrcAccount => { return account; }; + +/** + * Maps the token metadata information from a ledger response into a structured record. + * + * This utility processes an array of metadata key-value pairs provided by the ledger + * and extracts specific fields, such as symbol, name, fee, decimals, and logo. It then + * constructs a `IcrcTokenMetadata` record. If any required fields are missing, + * the function returns `undefined`. + * + * @param {IcrcTokenMetadataResponse} response - An array of key-value pairs representing token metadata. + * + * @returns {IcrcTokenMetadata | undefined} - A structured metadata record or `undefined` if required fields are missing. + */ +export const mapTokenMetadata = ( + response: IcrcTokenMetadataResponse, +): IcrcTokenMetadata | undefined => { + const nullishToken: Partial = response.reduce( + (acc, [key, value]) => { + switch (key) { + case IcrcMetadataResponseEntries.SYMBOL: + acc = { ...acc, ...("Text" in value && { symbol: value.Text }) }; + break; + case IcrcMetadataResponseEntries.NAME: + acc = { ...acc, ...("Text" in value && { name: value.Text }) }; + break; + case IcrcMetadataResponseEntries.FEE: + acc = { ...acc, ...("Nat" in value && { fee: value.Nat }) }; + break; + case IcrcMetadataResponseEntries.DECIMALS: + acc = { + ...acc, + ...("Nat" in value && { decimals: Number(value.Nat) }), + }; + break; + case IcrcMetadataResponseEntries.LOGO: + acc = { ...acc, ...("Text" in value && { icon: value.Text }) }; + } + + return acc; + }, + {}, + ); + + if ( + isNullish(nullishToken.symbol) || + isNullish(nullishToken.name) || + isNullish(nullishToken.fee) || + isNullish(nullishToken.decimals) + ) { + return undefined; + } + + return nullishToken as IcrcTokenMetadata; +}; From 1f7390dd7e6e68631230d3fbca95e0a6c1e36500 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 05:52:55 +0000 Subject: [PATCH 2/4] =?UTF-8?q?=F0=9F=A4=96=20Documentation=20auto-update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ledger-icrc/README.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/ledger-icrc/README.md b/packages/ledger-icrc/README.md index 3a26fbc77..008a3891a 100644 --- a/packages/ledger-icrc/README.md +++ b/packages/ledger-icrc/README.md @@ -58,6 +58,7 @@ const data = await metadata({}); - [encodeIcrcAccount](#gear-encodeicrcaccount) - [decodeIcrcAccount](#gear-decodeicrcaccount) +- [mapTokenMetadata](#gear-maptokenmetadata) - [decodePayment](#gear-decodepayment) #### :gear: encodeIcrcAccount @@ -73,7 +74,7 @@ Parameters: - `account`: : Principal, subaccount?: Uint8Array } -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L21) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L26) #### :gear: decodeIcrcAccount @@ -88,7 +89,26 @@ Parameters: - `accountString`: string -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L61) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L66) + +#### :gear: mapTokenMetadata + +Maps the token metadata information from a ledger response into a structured record. + +This utility processes an array of metadata key-value pairs provided by the ledger +and extracts specific fields, such as symbol, name, fee, decimals, and logo. It then +constructs a `IcrcTokenMetadata` record. If any required fields are missing, +the function returns `undefined`. + +| Function | Type | +| ------------------ | ------------------------------------------------------------------------- | +| `mapTokenMetadata` | `(response: IcrcTokenMetadataResponse) => IcrcTokenMetadata or undefined` | + +Parameters: + +- `response`: - An array of key-value pairs representing token metadata. + +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L110) #### :gear: decodePayment From 95e623e1358e3f6be34ae199980d01bbae7d2712 Mon Sep 17 00:00:00 2001 From: David Dal Busco Date: Mon, 23 Dec 2024 07:00:47 +0100 Subject: [PATCH 3/4] feat: better typing Signed-off-by: David Dal Busco --- .../ledger-icrc/src/utils/ledger.utils.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/ledger-icrc/src/utils/ledger.utils.ts b/packages/ledger-icrc/src/utils/ledger.utils.ts index dd59436c2..131442648 100644 --- a/packages/ledger-icrc/src/utils/ledger.utils.ts +++ b/packages/ledger-icrc/src/utils/ledger.utils.ts @@ -4,6 +4,7 @@ import { encodeBase32, hexStringToUint8Array, isNullish, + nonNullish, notEmptyString, uint8ArrayToHexString, } from "@dfinity/utils"; @@ -110,7 +111,7 @@ export const decodeIcrcAccount = (accountString: string): IcrcAccount => { export const mapTokenMetadata = ( response: IcrcTokenMetadataResponse, ): IcrcTokenMetadata | undefined => { - const nullishToken: Partial = response.reduce( + const nullishToken = response.reduce>( (acc, [key, value]) => { switch (key) { case IcrcMetadataResponseEntries.SYMBOL: @@ -137,14 +138,17 @@ export const mapTokenMetadata = ( {}, ); - if ( - isNullish(nullishToken.symbol) || - isNullish(nullishToken.name) || - isNullish(nullishToken.fee) || - isNullish(nullishToken.decimals) - ) { + const isIcrcTokenMetadata = ( + arg: Partial, + ): arg is IcrcTokenMetadata => + nonNullish(arg.symbol) && + nonNullish(arg.name) && + nonNullish(arg.fee) && + nonNullish(arg.decimals); + + if (!isIcrcTokenMetadata(nullishToken)) { return undefined; } - return nullishToken as IcrcTokenMetadata; + return nullishToken; }; From 15e7c77a962a0d7feff1438e16e06c7196dbaa68 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2024 06:02:13 +0000 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=A4=96=20Documentation=20auto-update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ledger-icrc/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ledger-icrc/README.md b/packages/ledger-icrc/README.md index 008a3891a..5f2fb4f1e 100644 --- a/packages/ledger-icrc/README.md +++ b/packages/ledger-icrc/README.md @@ -74,7 +74,7 @@ Parameters: - `account`: : Principal, subaccount?: Uint8Array } -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L26) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L27) #### :gear: decodeIcrcAccount @@ -89,7 +89,7 @@ Parameters: - `accountString`: string -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L66) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L67) #### :gear: mapTokenMetadata @@ -108,7 +108,7 @@ Parameters: - `response`: - An array of key-value pairs representing token metadata. -[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L110) +[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ledger-icrc/src/utils/ledger.utils.ts#L111) #### :gear: decodePayment