Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hook for getting account's spendable balance #86

Merged
merged 1 commit into from
Aug 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
85 changes: 85 additions & 0 deletions packages/react/src/hooks/use-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
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[],
{ includesExistentialDeposit = false, ...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 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({});