Skip to content

Commit

Permalink
feat: ckBTC update balance (#289)
Browse files Browse the repository at this point in the history
# Motivation

Provide a function to cal `update_balance` of the ckBTC minter.

# Changes

- implement `updateBalance`
- add references in the README to link easily with the IC source code
  • Loading branch information
peterpeterparker authored Feb 13, 2023
1 parent f48f964 commit d775fc0
Show file tree
Hide file tree
Showing 9 changed files with 166 additions and 25 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
## Features

- new utils moved from NNS-dapp: `isNullish`, `nonNullish`, `notEmptyString` and `debounce`
- added ckBTC `update_balance` function

## Build

Expand Down
27 changes: 24 additions & 3 deletions packages/ckbtc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const btcAddress = await getBtcAddress({});

- [create](#gear-create)
- [getBtcAddress](#gear-getbtcaddress)
- [updateBalance](#gear-updatebalance)

##### :gear: create

Expand All @@ -76,14 +77,34 @@ Returns a BTC address for a given account.

Note: an update call is required by the Minter canister.

| Method | Type |
| --------------- | -------------------------------------------------- |
| `getBtcAddress` | `(params: GetBTCAddressParams) => Promise<string>` |
| Method | Type |
| --------------- | ----------------------------------------------------------- |
| `getBtcAddress` | `({ owner, subaccount, }: MinterParams) => Promise<string>` |

Parameters:

- `params`: The parameters for which a BTC address should be resolved.
- `params.owner`: The owner for which the BTC address should be generated. If not provided, the `caller` will be use instead.
- `params.subaccount`: An optional subaccount to compute the address.

##### :gear: updateBalance

Notify the minter about the bitcoin transfer.

Upon successful notification, new ckBTC should be available on the targeted address.

| Method | Type |
| --------------- | -------------------------------------------------------------------------- |
| `updateBalance` | `({ owner, subaccount, }: MinterParams) => Promise<UpdateBalanceResponse>` |

Parameters:

- `params`: The parameters are the address to which bitcoin where transferred.
- `params.owner`: The owner of the address. If not provided, the `caller` will be use instead.
- `params.subaccount`: An optional subaccount of the address.

<!-- TSDOC_END -->

## Resources

- [ckBTC Minter](https://github.com/dfinity/ic/tree/master/rs/bitcoin/ckbtc/minter/)
2 changes: 2 additions & 0 deletions packages/ckbtc/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export type { UpdateBalanceError, UpdateBalanceResult } from "../candid/minter";
export { CkBTCMinterCanister } from "./minter.canister";
export * from "./types/minter.params";
export * from "./types/minter.responses";
98 changes: 84 additions & 14 deletions packages/ckbtc/src/minter.canister.spec.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
import { ActorSubclass } from "@dfinity/agent";
import { ledgerCanisterIdMock } from "@dfinity/ledger/src/mocks/ledger.mock";
import { Principal } from "@dfinity/principal";
import { arrayOfNumberToUint8Array } from "@dfinity/utils";
import { mock } from "jest-mock-extended";
import type { _SERVICE as CkBTCMinterService } from "../candid/minter";
import type {
UpdateBalanceResult,
_SERVICE as CkBTCMinterService,
} from "../candid/minter";
import { CkBTCMinterCanister } from "./minter.canister";
import { minterCanisterIdMock } from "./mocks/minter.mock";

describe("ckBTC minter canister", () => {
const minter = (
service: ActorSubclass<CkBTCMinterService>
): CkBTCMinterCanister =>
CkBTCMinterCanister.create({
canisterId: minterCanisterIdMock,
certifiedServiceOverride: service,
});

describe("BTC address", () => {
it("should return the BTC address of main account", async () => {
const service = mock<ActorSubclass<CkBTCMinterService>>();
const address = "bcrt1qu2aqme90t6hpac50x0xw8ljwqs250vn6tzlmsv";
service.get_btc_address.mockResolvedValue(address);

const canister = CkBTCMinterCanister.create({
canisterId: ledgerCanisterIdMock,
certifiedServiceOverride: service,
});
const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
const res = await canister.getBtcAddress({
Expand All @@ -31,10 +39,7 @@ describe("ckBTC minter canister", () => {
const address = "a_btc_address_with_subaccount";
service.get_btc_address.mockResolvedValue(address);

const canister = CkBTCMinterCanister.create({
canisterId: ledgerCanisterIdMock,
certifiedServiceOverride: service,
});
const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
const subaccount = arrayOfNumberToUint8Array([0, 0, 1]);
Expand All @@ -51,10 +56,7 @@ describe("ckBTC minter canister", () => {
throw new Error();
});

const canister = CkBTCMinterCanister.create({
canisterId: ledgerCanisterIdMock,
certifiedServiceOverride: service,
});
const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
expect(() =>
Expand All @@ -64,4 +66,72 @@ describe("ckBTC minter canister", () => {
).toThrowError();
});
});

describe("Update balance", () => {
const success: UpdateBalanceResult = {
block_index: 1n,
amount: 100_000n,
};
const ok = { Ok: success };

it("should return Ok", async () => {
const service = mock<ActorSubclass<CkBTCMinterService>>();
service.update_balance.mockResolvedValue(ok);

const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
const res = await canister.updateBalance({
owner,
});
expect(service.update_balance).toBeCalled();
expect(res).toEqual(ok);
});

it("should return Ok if a subaccount is provided", async () => {
const service = mock<ActorSubclass<CkBTCMinterService>>();
service.update_balance.mockResolvedValue(ok);

const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
const subaccount = arrayOfNumberToUint8Array([0, 0, 1]);
const res = await canister.updateBalance({
owner,
subaccount,
});
expect(res).toEqual(ok);
});

it("should return Err if an error was returned by the canister", async () => {
const service = mock<ActorSubclass<CkBTCMinterService>>();

const error = { Err: { AlreadyProcessing: null } };
service.update_balance.mockResolvedValue(error);

const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
const res = await canister.updateBalance({
owner,
});
expect(res).toEqual(error);
});

it("should bubble errors", () => {
const service = mock<ActorSubclass<CkBTCMinterService>>();
service.update_balance.mockImplementation(() => {
throw new Error();
});

const canister = minter(service);

const owner = Principal.fromText("aaaaa-aa");
expect(() =>
canister.updateBalance({
owner,
})
).toThrowError();
});
});
});
34 changes: 30 additions & 4 deletions packages/ckbtc/src/minter.canister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import type { _SERVICE as CkBTCMinterService } from "../candid/minter";
import { idlFactory as certifiedIdlFactory } from "../candid/minter.certified.idl";
import { idlFactory } from "../candid/minter.idl";
import type { CkBTCMinterCanisterOptions } from "./types/canister.options";
import type { GetBTCAddressParams } from "./types/minter.params";
import type {
GetBTCAddressParams,
UpdateBalanceParams,
} from "./types/minter.params";
import type { UpdateBalanceResponse } from "./types/minter.responses";

export class CkBTCMinterCanister extends Canister<CkBTCMinterService> {
static create(options: CkBTCMinterCanisterOptions<CkBTCMinterService>) {
Expand All @@ -27,9 +31,31 @@ export class CkBTCMinterCanister extends Canister<CkBTCMinterService> {
* @param {Principal} params.subaccount An optional subaccount to compute the address.
* @returns {Promise<string>} The BTC address of the given account.
*/
getBtcAddress = (params: GetBTCAddressParams): Promise<string> =>
getBtcAddress = ({
owner,
subaccount,
}: GetBTCAddressParams): Promise<string> =>
this.caller({ certified: true }).get_btc_address({
owner: toNullable(params.owner),
subaccount: toNullable(params.subaccount),
owner: toNullable(owner),
subaccount: toNullable(subaccount),
});

/**
* Notify the minter about the bitcoin transfer.
*
* Upon successful notification, new ckBTC should be available on the targeted address.
*
* @param {UpdateBalanceParams} params The parameters are the address to which bitcoin where transferred.
* @param {Principal} params.owner The owner of the address. If not provided, the `caller` will be use instead.
* @param {Principal} params.subaccount An optional subaccount of the address.
* @returns {Promise<UpdateBalanceParams>} The result (Ok or Error) of the balance update.
*/
updateBalance = ({
owner,
subaccount,
}: UpdateBalanceParams): Promise<UpdateBalanceResponse> =>
this.caller({ certified: true }).update_balance({
owner: toNullable(owner),
subaccount: toNullable(subaccount),
});
}
5 changes: 5 additions & 0 deletions packages/ckbtc/src/mocks/minter.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Principal } from "@dfinity/principal";

export const minterCanisterIdMock: Principal = Principal.fromText(
"q3fc5-haaaa-aaaaa-aaahq-cai"
);
15 changes: 11 additions & 4 deletions packages/ckbtc/src/types/minter.params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@ import type { Subaccount } from "@dfinity/ledger/candid/icrc1_ledger";
import type { Principal } from "@dfinity/principal";
import type { QueryParams } from "@dfinity/utils";

/**
* Params to get a BTC address.
*/
export interface GetBTCAddressParams extends Omit<QueryParams, "certified"> {
export interface MinterParams extends Omit<QueryParams, "certified"> {
owner?: Principal;
subaccount?: Subaccount;
}

/**
* Params to get a BTC address.
*/
export type GetBTCAddressParams = MinterParams;

/**
* Params to update ckBTC balance after a bitcoin transfer.
*/
export type UpdateBalanceParams = MinterParams;
8 changes: 8 additions & 0 deletions packages/ckbtc/src/types/minter.responses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type {
UpdateBalanceError,
UpdateBalanceResult,
} from "../../candid/minter";

export type UpdateBalanceResponse =
| { Ok: UpdateBalanceResult }
| { Err: UpdateBalanceError };
1 change: 1 addition & 0 deletions packages/ledger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,4 @@ Index Canister only holds the transactions ids in state, not the whole transacti
## Resources

- [Ledger & Tokenization Working Group Standards](https://github.com/dfinity/ICRC-1/)
- [ICRC-1 Ledger](https://github.com/dfinity/ic/tree/master/rs/rosetta-api/icrc1)

0 comments on commit d775fc0

Please sign in to comment.