Skip to content

Commit

Permalink
feat: add getBalance to BTC package (#701)
Browse files Browse the repository at this point in the history
# Motivation

Expose function getBalance, used to retrieve the balance of a Bitcoin
address given a network and, optionally, a minimum number of
confirmations.

# Changes

- Create params types (note that the endpoint returns `bigint` number
identified by the type `satoshi`).
- Create util function to parse "string" network type to "candid"
network type.
- Expose `get_balance` from the canister into new function `getBalance`
of object `BitcoinCanister`.
- Add tests.

# Tests

Create set of test mirroring the ones used for `getUtxos` function.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
1 parent 497c8ba commit b2f76c9
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 7 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

- Provide a new utility to convert Candid `Nat` to `BigInt`. This utility is useful for interpreting the fees provided by the SNS Aggregator.
- Support conversion of `InstallCode`, `StopOrStartCanister` and `UpdateCanisterSettings` actions, `SetVisibility` neuron operation, and `Neuron::visibility` attribute.
- Add function `getBalance` to `BitcoinCanister` object of package `@dfinity/ckbtc`, that implements the `bitcoin_get_balance` method of the IC Bitcoin API.

## Build

Expand Down
24 changes: 21 additions & 3 deletions packages/ckbtc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,20 +276,21 @@ Parameters:

### :factory: BitcoinCanister

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L11)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L17)

#### Methods

- [create](#gear-create)
- [getUtxos](#gear-getutxos)
- [getBalance](#gear-getbalance)

##### :gear: create

| Method | Type |
| -------- | -------------------------------------------------------------- |
| `create` | `(options: CkBTCCanisterOptions<_SERVICE>) => BitcoinCanister` |

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L12)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L18)

##### :gear: getUtxos

Expand All @@ -306,7 +307,24 @@ Parameters:
- `params.address`: A Bitcoin address.
- `params.certified`: query or update call

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L35)
[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L41)

##### :gear: getBalance

Given a `get_balance_request`, which must specify a Bitcoin address and a Bitcoin network (`mainnet` or `testnet`), the function returns the current balance of this address in `Satoshi` (10^8 Satoshi = 1 Bitcoin) in the specified Bitcoin network.

| Method | Type |
| ------------ | ----------------------------------------------------------------- |
| `getBalance` | `({ certified, ...params }: GetBalanceParams) => Promise<bigint>` |

Parameters:

- `params.network`: Tesnet or mainnet.
- `params.min_confirmations`: The optional filter parameter can be used to limit the set of considered UTXOs for the calculation of the balance to those with at least the provided number of confirmations in the same manner as for the `bitcoin_get_utxos` call.
- `params.address`: A Bitcoin address.
- `params.certified`: query or update call

[:link: Source](https://github.com/dfinity/ic-js/tree/main/packages/ckbtc/src/bitcoin.canister.ts#L64)

<!-- TSDOC_END -->

Expand Down
103 changes: 102 additions & 1 deletion packages/ckbtc/src/bitcoin.canister.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import { mock } from "jest-mock-extended";
import type {
_SERVICE as BitcoinService,
get_utxos_response,
satoshi,
} from "../candid/bitcoin";
import { BitcoinCanister } from "./bitcoin.canister";
import { bitcoinCanisterIdMock } from "./mocks/minter.mock";
import { GetUtxosParams } from "./types/bitcoin.params";
import { GetBalanceParams, GetUtxosParams } from "./types/bitcoin.params";

describe("BitcoinCanister", () => {
const createBitcoinCanister = (
Expand Down Expand Up @@ -143,4 +144,104 @@ describe("BitcoinCanister", () => {
});
});
});

describe("bitcoinGetBalance", () => {
const params: Omit<GetBalanceParams, "certified"> = {
network: "testnet",
min_confirmations: 2,
address: bitcoinAddressMock,
};

const response: satoshi = 1000n;

describe("certified", () => {
it("returns balance result when success", async () => {
const certifiedService = mock<ActorSubclass<BitcoinService>>();
certifiedService.bitcoin_get_balance.mockResolvedValue(response);

const service = mock<ActorSubclass<BitcoinService>>();

const { getBalance } = await createBitcoinCanister({
certifiedServiceOverride: certifiedService,
serviceOverride: service,
});

const res = await getBalance({
...params,
certified: true,
});

expect(res).toEqual(response);
expect(certifiedService.bitcoin_get_balance).toHaveBeenCalledWith({
network: { testnet: null },
min_confirmations: [2],
address: bitcoinAddressMock,
});
expect(service.bitcoin_get_balance_query).not.toHaveBeenCalled();
});

it("throws Error", async () => {
const error = new Error("Test");
const certifiedService = mock<ActorSubclass<BitcoinService>>();
certifiedService.bitcoin_get_balance.mockRejectedValue(error);

const { getBalance } = await createBitcoinCanister({
certifiedServiceOverride: certifiedService,
});

const call = () =>
getBalance({
...params,
certified: true,
});

expect(call).rejects.toThrowError(Error);
});
});

describe("Not certified", () => {
it("returns balance query result when success", async () => {
const service = mock<ActorSubclass<BitcoinService>>();
service.bitcoin_get_balance_query.mockResolvedValue(response);

const certifiedService = mock<ActorSubclass<BitcoinService>>();

const { getBalance } = await createBitcoinCanister({
certifiedServiceOverride: certifiedService,
serviceOverride: service,
});

const res = await getBalance({
...params,
certified: false,
});

expect(res).toEqual(response);
expect(service.bitcoin_get_balance_query).toHaveBeenCalledWith({
network: { testnet: null },
min_confirmations: [2],
address: bitcoinAddressMock,
});
expect(certifiedService.bitcoin_get_balance).not.toHaveBeenCalled();
});

it("throws Error", async () => {
const error = new Error("Test");
const service = mock<ActorSubclass<BitcoinService>>();
service.bitcoin_get_balance_query.mockRejectedValue(error);

const { getBalance } = await createBitcoinCanister({
serviceOverride: service,
});

const call = () =>
getBalance({
...params,
certified: false,
});

expect(call).rejects.toThrowError(Error);
});
});
});
});
31 changes: 30 additions & 1 deletion packages/ckbtc/src/bitcoin.canister.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import { Canister, createServices } from "@dfinity/utils";
import type {
_SERVICE as BitcoinService,
get_utxos_response,
satoshi,
} from "../candid/bitcoin";
import { idlFactory as certifiedIdlFactory } from "../candid/bitcoin.certified.idl";
import { idlFactory } from "../candid/bitcoin.idl";
import { toGetUtxosParams, type GetUtxosParams } from "./types/bitcoin.params";
import {
toGetBalanceParams,
toGetUtxosParams,
type GetBalanceParams,
type GetUtxosParams,
} from "./types/bitcoin.params";
import type { CkBTCCanisterOptions } from "./types/canister.options";

export class BitcoinCanister extends Canister<BitcoinService> {
Expand Down Expand Up @@ -42,4 +48,27 @@ export class BitcoinCanister extends Canister<BitcoinService> {
const fn = certified ? bitcoin_get_utxos : bitcoin_get_utxos_query;
return fn(toGetUtxosParams(params));
};

/**
* Given a `get_balance_request`, which must specify a Bitcoin address and a Bitcoin network (`mainnet` or `testnet`), the function returns the current balance of this address in `Satoshi` (10^8 Satoshi = 1 Bitcoin) in the specified Bitcoin network.
*
* @link https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-bitcoin_get_balance
*
* @param {Object} params
* @param {BitcoinNetwork} params.network Tesnet or mainnet.
* @param {Object} params.min_confirmations The optional filter parameter can be used to limit the set of considered UTXOs for the calculation of the balance to those with at least the provided number of confirmations in the same manner as for the `bitcoin_get_utxos` call.
* @param {string} params.address A Bitcoin address.
* @param {boolean} params.certified query or update call
* @returns {Promise<satoshi>} The balance is returned in `Satoshi` (10^8 Satoshi = 1 Bitcoin).
*/
getBalance = ({
certified = true,
...params
}: GetBalanceParams): Promise<satoshi> => {
const { bitcoin_get_balance, bitcoin_get_balance_query } = this.caller({
certified,
});
const fn = certified ? bitcoin_get_balance : bitcoin_get_balance_query;
return fn(toGetBalanceParams(params));
};
}
29 changes: 27 additions & 2 deletions packages/ckbtc/src/types/bitcoin.params.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { toNullable, type QueryParams } from "@dfinity/utils";
import type { get_utxos_request } from "../../candid/bitcoin";
import type {
get_balance_request,
get_utxos_request,
network,
} from "../../candid/bitcoin";

export type BitcoinNetwork = "testnet" | "mainnet";

const mapBitcoinNetwork = (network: BitcoinNetwork): network =>
network === "testnet" ? { testnet: null } : { mainnet: null };

export type GetUtxosParams = Omit<get_utxos_request, "network" | "filter"> & {
network: BitcoinNetwork;
filter?: { page: Uint8Array | number[] } | { min_confirmations: number };
Expand All @@ -14,6 +21,24 @@ export const toGetUtxosParams = ({
...rest
}: GetUtxosParams): get_utxos_request => ({
filter: toNullable(filter),
network: network === "testnet" ? { testnet: null } : { mainnet: null },
network: mapBitcoinNetwork(network),
...rest,
});

export type GetBalanceParams = Omit<
get_balance_request,
"network" | "min_confirmations"
> & {
network: BitcoinNetwork;
min_confirmations?: number;
} & QueryParams;

export const toGetBalanceParams = ({
network,
min_confirmations,
...rest
}: GetBalanceParams): get_balance_request => ({
min_confirmations: toNullable(min_confirmations),
network: mapBitcoinNetwork(network),
...rest,
});

0 comments on commit b2f76c9

Please sign in to comment.