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 a6a04b3 commit b86603f
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 24 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 maximum & minimum 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 @@ -15,6 +20,7 @@ import {
useMutationEffect,
useNativeTokenAmountFromPlanck,
useResetQueryError,
useSpendableBalance,
useWallets,
} from "@reactive-dot/react";
import { formatDistance } from "date-fns";
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] = useConnectWallet(props.wallet);
const [disconnectingState, disconnect] = useDisconnectWallet(props.wallet);
const [connectingState, connect] = useConnectWallet(wallet);
const [disconnectingState, disconnect] = useDisconnectWallet(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 = useResetQueryError();

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
77 changes: 77 additions & 0 deletions packages/react/src/hooks/use-balance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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;
};
};

/**
* 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?: ChainHookOptions,
): DenominatedNumber;
/**
* Hook for getting account spendable balance
*
* @param addresses - The account-addresses
* @param options - Additional options
* @returns The spendable balances
*/
export function useSpendableBalance(
addresses: SS58String[],
options?: ChainHookOptions,
): DenominatedNumber[];
export function useSpendableBalance(
addressOrAddresses: SS58String | SS58String[],
options?: ChainHookOptions,
): 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, existentialDeposit),
),
);

const spendableBalances = accounts.map(spendableNativeTokenFromAccount);

return Array.isArray(addressOrAddresses)
? spendableBalances
: spendableBalances.at(0)!;
}
1 change: 1 addition & 0 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export { useConnectWallet } from "./hooks/use-connect-wallet.js";
export { useDisconnectWallet } from "./hooks/use-disconnect-wallet.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
32 changes: 32 additions & 0 deletions packages/utils/src/big-int.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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 { DenominatedNumber } from "./denominated-number.js";
export { BigIntMath } from "./big-int.js";

0 comments on commit b86603f

Please sign in to comment.