Skip to content

Commit

Permalink
feat: decode payment code (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
peterpeterparker authored Jun 22, 2023
1 parent 24aeafa commit 43622ad
Show file tree
Hide file tree
Showing 6 changed files with 216 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
- utils `v0.0.18`
- nns-proto `v0.0.4`

## Features

- add a new utils function `decodePayment` to the `@dfinity/ledger` library. Useful to decode payment through QR code that contains target address and amount

# 0.17.2 (2023-06-21)

## Release
Expand Down
26 changes: 26 additions & 0 deletions packages/ledger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ const data = await metadata();

- [encodeIcrcAccount](#gear-encodeicrcaccount)
- [decodeIcrcAccount](#gear-decodeicrcaccount)
- [decodePayment](#gear-decodepayment)

#### :gear: encodeIcrcAccount

Expand Down Expand Up @@ -85,6 +86,31 @@ Parameters:

- `accountString`: string

#### :gear: decodePayment

A naive implementation of a payment parser. Given a code, the function attempts to extract a token name, account identifier (textual representation), and an optional amount.

If the code doesn't match the expected pattern, `undefined` is returned for simplicity.
Similarly, if an optional amount is provided but it's not a valid number, the parser will not throw an exception and returns `undefined`.

Please note that this function doesn't perform any validity checks on the extracted information.
It does not verify if the token is known or if the identifier is a valid address.

urn = token ":" address [ "?" params]
token = [ ckbtc / icp / chat / bitcoin / ethereum ... ]
address = STRING
params = param [ "&" params ]
param = [ amountparam ]
amountparam = "amount=" *digit [ "." *digit ]

| Function | Type |
| --------------- | --------------------------------------------------------------------------- |
| `decodePayment` | `(code: string) => { token: string; identifier: string; amount?: number; }` |

Parameters:

- `code`: string

### :factory: IcrcLedgerCanister

#### Constructors
Expand Down
1 change: 1 addition & 0 deletions packages/ledger/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ export * from "./types/index.params";
export * from "./types/ledger.params";
export * from "./types/ledger.responses";
export * from "./utils/ledger.utils";
export * from "./utils/payment.utils";
142 changes: 142 additions & 0 deletions packages/ledger/src/utils/payment.utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { decodePayment } from "./payment.utils";

describe("payment.utils", () => {
it("extract payment information", () => {
expect(
decodePayment(
"bitcoin:BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001"
)
).toEqual({
token: "bitcoin",
identifier: "BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U",
amount: 0.00001,
});

expect(
decodePayment(
"icp:646f4d2d6fcb6fab5ba1547647526b666553467ecb5cb28c8d9ddf451c8f4c21?amount=1234"
)
).toEqual({
token: "icp",
identifier:
"646f4d2d6fcb6fab5ba1547647526b666553467ecb5cb28c8d9ddf451c8f4c21",
amount: 1234,
});

expect(
decodePayment(
"icp:646f4d2d6fcb6fab5ba1547647526b666553467ecb5cb28c8d9ddf451c8f4c21"
)
).toEqual({
token: "icp",
identifier:
"646f4d2d6fcb6fab5ba1547647526b666553467ecb5cb28c8d9ddf451c8f4c21",
});

expect(
decodePayment(
"icp:646f4d2d6fcb6fab5ba1547647526b666553467ecb5cb28c8d9ddf451c8f4c21?amount=1234.456"
)
).toEqual({
token: "icp",
identifier:
"646f4d2d6fcb6fab5ba1547647526b666553467ecb5cb28c8d9ddf451c8f4c21",
amount: 1234.456,
});
});

it("extract payment amount information with value keyword", () => {
expect(
decodePayment(
"ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7?value=987.321"
)
).toEqual({
token: "ethereum",
identifier: "0xdAC17F958D2ee523a2206206994597C13D831ec7",
amount: 987.321,
});
});

it("should ignore content between address and parameters", () => {
expect(
decodePayment(
"ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1"
)
).toEqual({
token: "ethereum",
identifier: "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7",
});

expect(
decodePayment(
"ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1?value=444.555"
)
).toEqual({
token: "ethereum",
identifier: "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7",
amount: 444.555,
});
});

it("cannot extract payment information if token or address not provided", () => {
expect(
decodePayment("BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=0.00001")
).toBeUndefined();

expect(decodePayment("bitcoin:?amount=0.00001")).toBeUndefined();

expect(
decodePayment("BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U")
).toBeUndefined();

expect(
decodePayment("BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U")
).toBeUndefined();

expect(decodePayment("bitcoin:")).toBeUndefined();

expect(decodePayment("bitcoin")).toBeUndefined();
});

it("cannot extract payment information if amount is not a number", () => {
expect(
decodePayment("BC1QYLH3U67J673H6Y6ALV70M0PL2YZ53TZHVXGG7U?amount=test")
).toBeUndefined();
});

it("should extract amount even if multiple parameters", () => {
expect(
decodePayment(
"ethereum:0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7/transfer?address=0x8e23ee67d1332ad560396262c48ffbb01f93d052&uint256=1&value=444.555"
)
).toEqual({
token: "ethereum",
identifier: "0x89205a3a3b2a69de6dbf7f01ed13b2108b2c43e7",
amount: 444.555,
});
});

it("extract payment information for ICRC-1 as well", () => {
expect(
decodePayment(
"chat:k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae?amount=0.00001"
)
).toEqual({
token: "chat",
identifier:
"k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae",
amount: 0.00001,
});

expect(
decodePayment(
"chat:k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20?amount=120.123"
)
).toEqual({
token: "chat",
identifier:
"k2t6j-2nvnp-4zjm3-25dtz-6xhaa-c7boj-5gayf-oj3xs-i43lp-teztq-6ae-dfxgiyy.102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
amount: 120.123,
});
});
});
42 changes: 42 additions & 0 deletions packages/ledger/src/utils/payment.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { isNullish, nonNullish } from "@dfinity/utils";

/**
* A naive implementation of a payment parser. Given a code, the function attempts to extract a token name, account identifier (textual representation), and an optional amount.
*
* If the code doesn't match the expected pattern, `undefined` is returned for simplicity.
* Similarly, if an optional amount is provided but it's not a valid number, the parser will not throw an exception and returns `undefined`.
*
* Please note that this function doesn't perform any validity checks on the extracted information.
* It does not verify if the token is known or if the identifier is a valid address.
*
* urn = token ":" address [ "?" params]
* token = [ ckbtc / icp / chat / bitcoin / ethereum ... ]
* address = STRING
* params = param [ "&" params ]
* param = [ amountparam ]
* amountparam = "amount=" *digit [ "." *digit ]
*
* @param code string
* @returns { token: string; identifier: string; amount?: number } | undefined
*/
export const decodePayment = (
code: string
): { token: string; identifier: string; amount?: number } | undefined => {
const regex =
/^([a-zA-Z]+):([A-Za-z0-9:\-.]+).*?(?:[?&](?:amount|value)=(\d+(?:\.\d+)?))?$/;

const match = code.match(regex);
if (isNullish(match)) {
return undefined;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, token, identifier, amount] = match;

return {
token,
identifier,
...(nonNullish(amount) &&
!isNaN(parseFloat(amount)) && { amount: parseFloat(amount) }),
};
};
1 change: 1 addition & 0 deletions scripts/docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const cmcInputFiles = ["./packages/cmc/src/cmc.canister.ts"];
const ledgerInputFiles = [
"./packages/ledger/src/ledger.canister.ts",
"./packages/ledger/src/utils/ledger.utils.ts",
"./packages/ledger/src/utils/payment.utils.ts",
"./packages/ledger/src/index.canister.ts",
];

Expand Down

0 comments on commit 43622ad

Please sign in to comment.