Skip to content

Commit

Permalink
feat: hook for getting account's spendable balance
Browse files Browse the repository at this point in the history
  • Loading branch information
tien committed Aug 6, 2024
1 parent 94505d6 commit 8a4f031
Show file tree
Hide file tree
Showing 10 changed files with 223 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/poor-ghosts-obey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reactive-dot/utils": minor
---

Add utilities for getting the minimum and/or maximum value from a list of big integers.
5 changes: 5 additions & 0 deletions .changeset/serious-mayflies-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reactive-dot/react": minor
---

Add hook for getting account's spendable balance.
93 changes: 69 additions & 24 deletions apps/example/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { config } from "./config";
import { IDLE, MutationError, PENDING } from "@reactive-dot/core";
import {
IDLE,
MutationError,
PENDING,
type PolkadotAccount,
} from "@reactive-dot/core";
import type { Wallet } from "@reactive-dot/core/wallets.js";
import {
ReDotChainProvider,
Expand All @@ -13,6 +18,7 @@ import {
useMutationEffect,
useNativeTokenAmountFromPlanck,
useQueryErrorResetter,
useSpendableBalance,
useWalletConnector,
useWalletDisconnector,
useWallets,
Expand All @@ -23,11 +29,16 @@ import { Suspense, useState, useTransition } from "react";
import { ErrorBoundary, type FallbackProps } from "react-error-boundary";
import toast, { Toaster } from "react-hot-toast";

function PendingRewards(props: { address: string; rewards: bigint }) {
type PendingRewardsProps = {
account: PolkadotAccount;
rewards: bigint;
};

function PendingRewards({ account, rewards }: PendingRewardsProps) {
return (
<li>
{props.address}:{" "}
{useNativeTokenAmountFromPlanck(props.rewards).toLocaleString()}
{account.name ?? account.address}:{" "}
{useNativeTokenAmountFromPlanck(rewards).toLocaleString()}
</li>
);
}
Expand Down Expand Up @@ -68,7 +79,7 @@ function PendingPoolRewards() {
<PendingRewards
// eslint-disable-next-line @eslint-react/no-array-index-key
key={index}
address={accounts.at(index)?.address ?? ""}
account={accounts.at(index)!}
rewards={rewards}
/>
))}
Expand All @@ -77,6 +88,46 @@ function PendingPoolRewards() {
);
}

type SpendableBalanceProps = {
account: PolkadotAccount;
};

function SpendableBalance({ account }: SpendableBalanceProps) {
return (
<li>
{account.name ?? account.address}:{" "}
{useSpendableBalance(account.address).toLocaleString()}
</li>
);
}

function SpendableBalances() {
const accounts = useAccounts();

if (accounts.length === 0) {
return (
<article>
<h4>Balances</h4>
<p>Please connect accounts to see balances</p>
</article>
);
}

return (
<article>
<h4>Spendable balances</h4>
<ul>
{accounts.map((account) => (
<SpendableBalance
key={account.wallet.id + account.address}
account={account}
/>
))}
</ul>
</article>
);
}

function Query() {
const block = useBlock();

Expand Down Expand Up @@ -159,6 +210,7 @@ function Query() {
<p key={index}>{x.asText()}</p>
))}
</article>
<SpendableBalances />
<PendingPoolRewards />
</section>
);
Expand All @@ -168,16 +220,16 @@ type WalletItemProps = {
wallet: Wallet;
};

function WalletItem(props: WalletItemProps) {
function WalletItem({ wallet }: WalletItemProps) {
const connectedWallets = useConnectedWallets();

const [connectingState, connect] = useWalletConnector(props.wallet);
const [disconnectingState, disconnect] = useWalletDisconnector(props.wallet);
const [connectingState, connect] = useWalletConnector(wallet);
const [disconnectingState, disconnect] = useWalletDisconnector(wallet);

return (
<li>
{props.wallet.name}:{" "}
{connectedWallets.includes(props.wallet) ? (
{wallet.name}:{" "}
{connectedWallets.includes(wallet) ? (
<button
type="button"
onClick={() => disconnect()}
Expand Down Expand Up @@ -297,16 +349,13 @@ function Mutation() {
);
}

function ErrorFallback(props: FallbackProps) {
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<article>
<header>
<strong>Oops, something went wrong!</strong>
</header>
<button
type="button"
onClick={() => props.resetErrorBoundary(props.error)}
>
<button type="button" onClick={() => resetErrorBoundary(error)}>
Retry
</button>
</article>
Expand All @@ -315,7 +364,7 @@ function ErrorFallback(props: FallbackProps) {

type ExampleProps = { chainName: string };

function Example(props: ExampleProps) {
function Example({ chainName }: ExampleProps) {
const resetQueryError = useQueryErrorResetter();

useMutationEffect((event) => {
Expand Down Expand Up @@ -353,8 +402,8 @@ function Example(props: ExampleProps) {
}
}}
>
<Suspense fallback={<h2>Loading {props.chainName}...</h2>}>
<h2>{props.chainName}</h2>
<Suspense fallback={<h2>Loading {chainName}...</h2>}>
<h2>{chainName}</h2>
<Query />
<Mutation />
</Suspense>
Expand All @@ -373,14 +422,10 @@ export function App() {
<Example chainName="Polkadot" />
</ReDotChainProvider>
<ReDotChainProvider chainId="kusama">
<Suspense fallback={<h2>Loading Kusama...</h2>}>
<Example chainName="Kusama" />
</Suspense>
<Example chainName="Kusama" />
</ReDotChainProvider>
<ReDotChainProvider chainId="westend">
<Suspense fallback={<h2>Loading Westend...</h2>}>
<Example chainName="Westend" />
</Suspense>
<Example chainName="Westend" />
</ReDotChainProvider>
<Toaster />
</ReDotProvider>
Expand Down
88 changes: 88 additions & 0 deletions packages/react/src/hooks/use-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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 type { SS58String } from "polkadot-api";

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

type Options = ChainHookOptions & {
includesExistentialDeposit?: boolean;
};

/**
* Hook 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: SS58String,
options?: Options,
): DenominatedNumber;
/**
* Hook for getting accounts’ spendable balances.
*
* @param addresses - The account-addresses
* @param options - Additional options
* @returns The accounts’ spendable balances
*/
export function useSpendableBalance(
addresses: SS58String[],
options?: Options,
): DenominatedNumber[];
export function useSpendableBalance(
addressOrAddresses: SS58String | SS58String[],
options?: Options,
): DenominatedNumber | DenominatedNumber[] {
const addresses = Array.isArray(addressOrAddresses)
? addressOrAddresses
: [addressOrAddresses];

const [existentialDeposit, accounts] = useLazyLoadQuery(
(builder) =>
builder.getConstant("Balances", "ExistentialDeposit").readStorages(
"System",
"Account",
addresses.map((address) => [address]),
),
options,
) as [bigint, SystemAccount[]];

const nativeTokenFromPlanck = useNativeTokenAmountFromPlanck(options);

const includesExistentialDeposit =
options?.includesExistentialDeposit ?? true;

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

const spendableBalances = accounts.map(spendableNativeTokenFromAccount);

return Array.isArray(addressOrAddresses)
? spendableBalances
: spendableBalances.at(0)!;
}
4 changes: 2 additions & 2 deletions packages/react/src/hooks/use-native-token-amount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useChainSpecData } from "./use-chain-spec-data.js";
import { DenominatedNumber } from "@reactive-dot/utils";

/**
* Hook for returning the native token amount from a planck value
* Hook for returning the native token amount from a planck value.
*
* @param planck - The planck value
* @param options - Additional options
Expand All @@ -14,7 +14,7 @@ export function useNativeTokenAmountFromPlanck(
options?: ChainHookOptions,
): DenominatedNumber;
/**
* Hook for returning a function that converts planck value to native token amount
* Hook for returning a function that converts planck value to native token amount.
*
* @param options - Additional options
* @returns Function for getting the native token amount from a planck value
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { useChainSpecData } from "./hooks/use-chain-spec-data.js";
export { useClient } from "./hooks/use-client.js";
export { useMutationEffect } from "./hooks/use-mutation-effect.js";
export { useMutation } from "./hooks/use-mutation.js";
export { useSpendableBalance } from "./hooks/use-balance.js";
export {
useNativeTokenAmountFromNumber,
useNativeTokenAmountFromPlanck,
Expand Down
26 changes: 26 additions & 0 deletions packages/utils/src/big-int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { BigIntMath } from "./big-int";
import { describe, expect, it } from "vitest";

describe("BigIntMath", () => {
describe("min", () => {
it("gets the correct minimum value", () =>
expect(BigIntMath.min(-1n, 0n, 1n)).toBe(-1n));

it("returns the first value if only one value is passed", () =>
expect(BigIntMath.min(1n)).toBe(1n));

it("returns 0n if no values is passed", () =>
expect(BigIntMath.min()).toBe(0n));
});

describe("max", () => {
it("gets the correct maximum value", () =>
expect(BigIntMath.max(-1n, 0n, 1n)).toBe(1n));

it("returns the first value if only one value is passed", () =>
expect(BigIntMath.max(-1n)).toBe(-1n));

it("returns 0n if no values is passed", () =>
expect(BigIntMath.max()).toBe(0n));
});
});
23 changes: 23 additions & 0 deletions packages/utils/src/big-int.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const BigIntMath = Object.freeze({
min(...values: bigint[]) {
if (values.length === 0) {
return 0n;
}

return values.reduce((previousValue, currentValue) =>
currentValue < previousValue ? currentValue : previousValue,
);
},
max(...values: bigint[]) {
if (values.length === 0) {
return 0n;
}

return values.reduce((previousValue, currentValue) =>
currentValue > previousValue ? currentValue : previousValue,
);
},
get [Symbol.toStringTag]() {
return "BigIntMath";
},
});
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { BigIntMath } from "./big-int.js";
export { DenominatedNumber } from "./denominated-number.js";
3 changes: 3 additions & 0 deletions packages/utils/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { defineConfig } from "vitest/config";

export default defineConfig({});

0 comments on commit 8a4f031

Please sign in to comment.