Skip to content

Commit

Permalink
feat(vue): useNativeToken & useSpendableBalance composables (#334)
Browse files Browse the repository at this point in the history
  • Loading branch information
tien authored Nov 13, 2024
1 parent 5f36185 commit ee5d6a3
Show file tree
Hide file tree
Showing 16 changed files with 351 additions and 87 deletions.
7 changes: 7 additions & 0 deletions .changeset/ninety-jeans-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@reactive-dot/core": minor
"@reactive-dot/vue": minor
"@reactive-dot/react": patch
---

Added `useNativeToken` and `useSpendableBalance` composables.
33 changes: 22 additions & 11 deletions examples/vue/src/query-section.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
<script setup lang="ts">
import { useAccounts, useQuery } from "@reactive-dot/vue";
import {
useAccounts,
useNativeToken,
useQuery,
useSpendableBalance,
} from "@reactive-dot/vue";
import { computed } from "vue";
const { data: nativeToken } = await useNativeToken();
const { data: accounts } = await useAccounts();
const { data } = await useQuery((builder) =>
builder
.readStorage("System", "Number", [])
.readStorage("Balances", "TotalIssuance", [])
.readStorages(
"System",
"Account",
accounts.value?.map((account) => [account.address] as const) ?? [],
),
.readStorage("Balances", "TotalIssuance", []),
);
const totalIssuance = computed(() =>
nativeToken.value.amountFromPlanck(data.value[1]),
);
const { data: balances } = await useSpendableBalance(
computed(() => accounts.value.map((account) => account.address)),
);
</script>

Expand All @@ -24,11 +35,11 @@ const { data } = await useQuery((builder) =>
<dd>{{ data[0].toLocaleString() }}</dd>

<dt>Total issuance</dt>
<dd>{{ data[1].toLocaleString() }}</dd>
<dd>{{ totalIssuance.toLocaleString() }}</dd>

<div v-for="(balance, index) in data[2]" :key="index">
<dt>Balance of: {{ accounts?.at(index)?.address }}</dt>
<dd>{{ balance.data.free.toLocaleString() }}</dd>
<div v-for="(balance, index) in balances" :key="index">
<dt>Balance of: {{ accounts.at(index)?.address }}</dt>
<dd>{{ balance.toLocaleString() }}</dd>
</div>
</dl>
</section>
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"exports": {
".": "./build/index.js",
"./wallets.js": "./build/wallets/index.js",
"./internal/maths.js": "./build/maths/index.js",
"./internal.js": "./build/internal.js"
},
"scripts": {
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/maths/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { spendableBalance } from "./spendable-balance.js";
24 changes: 24 additions & 0 deletions packages/core/src/maths/spendable-balance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { spendableBalance } from "./spendable-balance";
import { expect, test } from "vitest";

// https://wiki.polkadot.network/docs/learn-account-balances
test.each([
{ free: 100n, frozen: 0n, reserved: 0n, spendable: 99n },
{ free: 100n, frozen: 80n, reserved: 0n, spendable: 20n },
{ free: 80n, frozen: 80n, reserved: 20n, spendable: 20n },
])(
"$spendable = $free - max($frozen - $reserved, existentialDeposit)",
({ free, frozen, reserved, spendable }) => {
const existentialDeposit = 1n;

expect(
spendableBalance({
free,
frozen,
reserved,
existentialDeposit,
includesExistentialDeposit: false,
}),
).toBe(spendable);
},
);
26 changes: 26 additions & 0 deletions packages/core/src/maths/spendable-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BigIntMath } from "@reactive-dot/utils";

type SpendableBalanceParam = {
free: bigint;
reserved: bigint;
frozen: bigint;
existentialDeposit: bigint;
includesExistentialDeposit?: boolean;
};

export function spendableBalance({
free,
reserved,
frozen,
existentialDeposit,
includesExistentialDeposit = false,
}: SpendableBalanceParam) {
return BigIntMath.max(
0n,
free -
BigIntMath.max(
frozen - reserved,
includesExistentialDeposit ? 0n : existentialDeposit,
),
);
}
42 changes: 24 additions & 18 deletions packages/react/src/hooks/use-balance.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { ChainHookOptions } from "./types.js";
import { useNativeTokenAmountFromPlanck } from "./use-native-token-amount.js";
import { useLazyLoadQuery } from "./use-query.js";
import { type DenominatedNumber, BigIntMath } from "@reactive-dot/utils";
import { flatHead } from "@reactive-dot/core/internal.js";
import { spendableBalance } from "@reactive-dot/core/internal/maths.js";
import { type DenominatedNumber } from "@reactive-dot/utils";
import type { SS58String } from "polkadot-api";
import { useMemo } from "react";

type SystemAccount = {
nonce: number;
Expand Down Expand Up @@ -63,23 +66,26 @@ export function useSpendableBalance(

const nativeTokenFromPlanck = useNativeTokenAmountFromPlanck(options);

const spendableNativeTokenFromAccount = ({
data: { free, reserved, frozen },
}: SystemAccount) =>
nativeTokenFromPlanck(
BigIntMath.max(
0n,
free -
BigIntMath.max(
frozen - reserved,
includesExistentialDeposit ? 0n : existentialDeposit,
return useMemo(
() =>
flatHead(
accounts.map(({ data: { free, reserved, frozen } }) =>
nativeTokenFromPlanck(
spendableBalance({
free,
reserved,
frozen,
existentialDeposit,
includesExistentialDeposit,
}),
),
),
),
);

const spendableBalances = accounts.map(spendableNativeTokenFromAccount);

return Array.isArray(addressOrAddresses)
? spendableBalances
: spendableBalances.at(0)!;
[
accounts,
existentialDeposit,
includesExistentialDeposit,
nativeTokenFromPlanck,
],
);
}
4 changes: 2 additions & 2 deletions packages/vue/src/composables/use-accounts.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ChainComposableOptions } from "../types.js";
import { useAsyncData } from "./use-async-data.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useChainSpecPromise } from "./use-chain-spec-data.js";
import { useChainSpecDataPromise } from "./use-chain-spec-data.js";
import { useLazyValue } from "./use-lazy-value.js";
import { useConnectedWalletsObservable } from "./use-wallets.js";
import { getAccounts } from "@reactive-dot/core";
Expand All @@ -20,7 +20,7 @@ export function useAccounts(options?: ChainComposableOptions) {
function useAccountsPromise(options?: ChainComposableOptions) {
const chainId = internal_useChainId({ ...options, optionalChainId: true });
const connectedWalletsObservable = useConnectedWalletsObservable();
const chainSpecPromise = useChainSpecPromise(options);
const chainSpecPromise = useChainSpecDataPromise(options);

return useLazyValue(
computed(() =>
Expand Down
4 changes: 2 additions & 2 deletions packages/vue/src/composables/use-async-data.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AsyncState } from "../types.js";
import type { AsyncState, PromiseLikeAsyncState } from "../types.js";
import { refresh } from "../utils/refreshable.js";
import { useAsyncState } from "./use-async-state.js";
import type { lazyValue } from "./use-lazy-value.js";
Expand Down Expand Up @@ -101,5 +101,5 @@ export function useAsyncData<
onfulfilled: () => unknown,
onrejected: (reason: unknown) => unknown,
) => promiseLike.then(onfulfilled, onrejected),
} as AsyncState<Value> & PromiseLike<AsyncState<Value, unknown, Value>>;
} as PromiseLikeAsyncState<Value>;
}
118 changes: 118 additions & 0 deletions packages/vue/src/composables/use-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type {
ChainComposableOptions,
PromiseLikeAsyncState,
} from "../types.js";
import { useAsyncData } from "./use-async-data.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useLazyValue } from "./use-lazy-value.js";
import { useNativeTokenPromise } from "./use-native-token.js";
import { useQueryObservable } from "./use-query.js";
import { spendableBalance } from "@reactive-dot/core/internal/maths.js";
import { type DenominatedNumber } from "@reactive-dot/utils";
import type { SS58String } from "polkadot-api";
import { combineLatest, from } from "rxjs";
import { map } from "rxjs/operators";
import { computed, type MaybeRefOrGetter, toValue } from "vue";

type SystemAccount = {
nonce: number;
consumers: number;
providers: number;
sufficients: number;
data: {
free: bigint;
reserved: bigint;
frozen: bigint;
flags: bigint;
};
};

type Options = ChainComposableOptions & {
includesExistentialDeposit?: MaybeRefOrGetter<boolean>;
};

/**
* Composable for getting an account's spendable balance.
*
* @param address - The account's address
* @param options - Additional options
* @returns The account's spendable balance
*/
export function useSpendableBalance(
address: MaybeRefOrGetter<SS58String>,
options?: Options,
): PromiseLikeAsyncState<DenominatedNumber>;
/**
* Composable for getting accounts’ spendable balances.
*
* @param addresses - The account-addresses
* @param options - Additional options
* @returns The accounts’ spendable balances
*/
export function useSpendableBalance(
addresses: MaybeRefOrGetter<SS58String[]>,
options?: Options,
): PromiseLikeAsyncState<DenominatedNumber[]>;
export function useSpendableBalance(
addressOrAddresses: MaybeRefOrGetter<SS58String | SS58String[]>,
options?: Options,
): PromiseLikeAsyncState<DenominatedNumber | DenominatedNumber[]> {
const chainId = internal_useChainId(options);

const addresses = computed(() => {
const addressOrAddressesValue = toValue(addressOrAddresses);
return Array.isArray(addressOrAddressesValue)
? toValue(addressOrAddressesValue)
: [toValue(addressOrAddressesValue)];
});

const includesExistentialDeposit = computed(
() => toValue(options?.includesExistentialDeposit) ?? false,
);

const dataObservable = useQueryObservable(
(builder) =>
builder.getConstant("Balances", "ExistentialDeposit").readStorages(
"System",
"Account",
addresses.value.map((address) => [address] as const),
),
options,
);

const nativeTokenPromise = useNativeTokenPromise(options);

return useAsyncData(
useLazyValue(
computed(() => [
"spendable-balances",
chainId.value,
...addresses.value.toSorted(),
]),
() => {
const includesExistentialDepositValue =
includesExistentialDeposit.value;

return combineLatest([
dataObservable.value,
from(nativeTokenPromise.value),
]).pipe(
map(([[existentialDeposit, accounts], { amountFromPlanck }]) =>
(accounts as SystemAccount[]).map(
({ data: { free, reserved, frozen } }) =>
amountFromPlanck(
spendableBalance({
free,
reserved,
frozen,
existentialDeposit: existentialDeposit as bigint,
includesExistentialDeposit: includesExistentialDepositValue,
}),
),
),
),
);
},
),
);
}
4 changes: 2 additions & 2 deletions packages/vue/src/composables/use-chain-spec-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import { computed } from "vue";
* @returns The [JSON-RPC spec](https://paritytech.github.io/json-rpc-interface-spec/api/chainSpec.html)
*/
export function useChainSpecData(options?: ChainComposableOptions) {
return useAsyncData(useChainSpecPromise(options));
return useAsyncData(useChainSpecDataPromise(options));
}

/**
* @internal
*/
export function useChainSpecPromise(options?: ChainComposableOptions) {
export function useChainSpecDataPromise(options?: ChainComposableOptions) {
const chainId = internal_useChainId(options);
const clientPromise = useClientPromise(options);

Expand Down
45 changes: 45 additions & 0 deletions packages/vue/src/composables/use-native-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ChainComposableOptions } from "../types.js";
import { useAsyncData } from "./use-async-data.js";
import { internal_useChainId } from "./use-chain-id.js";
import { useChainSpecDataPromise } from "./use-chain-spec-data.js";
import { useLazyValue } from "./use-lazy-value.js";
import { DenominatedNumber } from "@reactive-dot/utils";
import { computed } from "vue";

/**
* Composable for getting the chain's native token
* @param options - Additional options
* @returns The chain's native token
*/
export function useNativeToken(options?: ChainComposableOptions) {
return useAsyncData(useNativeTokenPromise(options));
}

/**
* @internal
*/
export function useNativeTokenPromise(options?: ChainComposableOptions) {
const chainId = internal_useChainId(options);
const chainSpecDataPromise = useChainSpecDataPromise(options);

return useLazyValue(
computed(() => ["native-token", chainId.value]),
() =>
chainSpecDataPromise.value.then((chainSpecData) => ({
symbol: chainSpecData.properties.tokenSymbol as string,
decimals: chainSpecData.properties.tokenDecimals as number,
amountFromPlanck: (planck: bigint | number | string) =>
new DenominatedNumber(
planck,
chainSpecData.properties.tokenDecimals,
chainSpecData.properties.tokenSymbol,
),
amountFromNumber: (number: number | string) =>
DenominatedNumber.fromNumber(
number,
chainSpecData.properties.tokenDecimals,
chainSpecData.properties.tokenSymbol,
),
})),
);
}
Loading

0 comments on commit ee5d6a3

Please sign in to comment.