diff --git a/examples/light-client-dapp/src/components/Transfer.tsx b/examples/light-client-dapp/src/components/Transfer.tsx index 4225a3611..917b5f173 100644 --- a/examples/light-client-dapp/src/components/Transfer.tsx +++ b/examples/light-client-dapp/src/components/Transfer.tsx @@ -1,70 +1,29 @@ import { FormEvent, useCallback, useEffect, useMemo, useState } from "react" -import { ss58Decode } from "@polkadot-labs/hdkd-helpers" import { UnstableWallet } from "@substrate/unstable-wallet-provider" -import { mergeUint8, toHex } from "@polkadot-api/utils" import Select from "react-select" +import { transaction, transferAllowDeathCallData } from "../transaction" +import { lastValueFrom, mergeMap, tap } from "rxjs" import { useSystemAccount } from "../hooks" -import { getObservableClient } from "@polkadot-api/client" -import { ConnectProvider, createClient } from "@polkadot-api/substrate-client" -import { Enum, SS58String } from "@polkadot-api/substrate-bindings" -import { getDynamicBuilder } from "@polkadot-api/metadata-builders" -import { firstValueFrom, filter, map } from "rxjs" type Props = { provider: UnstableWallet.Provider } +type FinalizedTransaction = { + blockHash: string + index: number +} + // FIXME: use dynamic chainId // Westend chainId const chainId = "0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e" -const AccountId = (value: SS58String) => - Enum< - { - type: "Id" - value: SS58String - }, - "Id" - >("Id", value) - -// TODO: Extract to hook that creates and submits the tx while also managing -// the tx lifecycle -const createTransfer = ( - provider: ConnectProvider, - destination: string, - amount: bigint, -) => { - const client = getObservableClient(createClient(provider)) - const { metadata$ } = client.chainHead$() - - return firstValueFrom( - metadata$.pipe( - filter(Boolean), - map((metadata) => { - const dynamicBuilder = getDynamicBuilder(metadata) - const { location, args } = dynamicBuilder.buildCall( - "Balances", - "transfer_allow_death", - ) - - return toHex( - mergeUint8( - new Uint8Array(location), - args.enc({ - dest: AccountId(destination), - value: amount, - }), - ), - ) - }), - ), - ) -} - export const Transfer = ({ provider }: Props) => { const [accounts, setAccounts] = useState([]) - const [destination, setDestination] = useState("") + const [destination, setDestination] = useState( + "5CofVLAGjwvdGXvBiP6ddtZYMVbhT5Xke8ZrshUpj2ZXAnND", + ) const [amount, setAmount] = useState(0n) const [selectedAccount, setSelectedAccount] = useState<{ value: string @@ -78,6 +37,10 @@ export const Transfer = ({ provider }: Props) => { connect, selectedAccount ? selectedAccount.value : null, ) + const [transactionStatus, setTransactionStatus] = useState("") + const [finalizedTransaction, setFinalizedTransaction] = + useState() + const [error, setError] = useState<{ type: string; error: string }>() const balance = accountStorage?.data.free ?? 0n @@ -87,7 +50,7 @@ export const Transfer = ({ provider }: Props) => { }) }, [provider]) - const [isCreatingTransaction, setIsCreatingTransaction] = useState(false) + const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false) const handleOnSubmit = useCallback( async (e: FormEvent) => { e.preventDefault() @@ -95,20 +58,43 @@ export const Transfer = ({ provider }: Props) => { return } - setIsCreatingTransaction(true) + setIsSubmittingTransaction(true) + setTransactionStatus("") + setFinalizedTransaction(null) + + const sender = selectedAccount.value + const connectProvider = { ...provider, connect } + try { - const tx = await provider.createTx( - chainId, - toHex(ss58Decode(selectedAccount.value)[0]), - await createTransfer(connect, destination, amount), + await lastValueFrom( + transferAllowDeathCallData(connectProvider, destination, amount).pipe( + mergeMap((callData) => + transaction(connectProvider, chainId, sender, callData), + ), + tap(({ txEvent }) => { + setTransactionStatus(txEvent.type) + if (txEvent.type === "finalized") { + setFinalizedTransaction({ + blockHash: txEvent.block.hash, + index: txEvent.block.index, + }) + } + if (txEvent.type === "invalid" || txEvent.type === "dropped") { + setError({ type: txEvent.type, error: txEvent.error }) + } + }), + ), ) - console.log({ tx }) - } catch (error) { - console.error(error) + } catch (err) { + if (err instanceof Error) { + setError({ type: "error", error: err.message }) + } + console.error(err) } - setIsCreatingTransaction(false) + + setIsSubmittingTransaction(false) }, - [provider, selectedAccount, connect, destination, amount], + [selectedAccount, provider, connect, destination, amount], ) const accountOptions = accounts.map((account) => ({ @@ -116,13 +102,7 @@ export const Transfer = ({ provider }: Props) => { label: account.address, })) - // TODO: handle form fields and submission with react - // TODO: fetch accounts from extension // TODO: validate destination address - // TODO: use PAPI to encode the transaction calldata - // TODO: transfer should trigger an extension popup that signs the transaction - // TODO: extract transaction submission into a hook - // TODO: follow transaction submission events until it is finalized return (
Transfer funds
@@ -147,10 +127,35 @@ export const Transfer = ({ provider }: Props) => {
diff --git a/examples/light-client-dapp/src/transaction.ts b/examples/light-client-dapp/src/transaction.ts new file mode 100644 index 000000000..d48e1d09b --- /dev/null +++ b/examples/light-client-dapp/src/transaction.ts @@ -0,0 +1,67 @@ +import { getObservableClient } from "@polkadot-api/client" +import { Enum } from "@polkadot-api/substrate-bindings" +import type { SS58String } from "@polkadot-api/substrate-bindings" +import { ConnectProvider, createClient } from "@polkadot-api/substrate-client" +import { getDynamicBuilder } from "@polkadot-api/metadata-builders" +import { filter, map, mergeMap, first } from "rxjs" +import { mergeUint8, toHex } from "@polkadot-api/utils" +import { UnstableWallet } from "@substrate/unstable-wallet-provider" +import { ss58Decode } from "@polkadot-labs/hdkd-helpers" +import { fromPromise } from "rxjs/internal/observable/innerFrom" + +const AccountId = (value: SS58String) => + Enum< + { + type: "Id" + value: SS58String + }, + "Id" + >("Id", value) + +type Provider = UnstableWallet.Provider & { connect: ConnectProvider } + +export const transaction = ( + provider: Provider, + chainId: string, + from: SS58String, + callData: string, +) => { + const client = getObservableClient(createClient(provider.connect)) + + return fromPromise( + provider.createTx(chainId, toHex(ss58Decode(from)[0]), callData), + ).pipe( + mergeMap((tx) => client.tx$(tx).pipe(map((txEvent) => ({ tx, txEvent })))), + ) +} + +export const transferAllowDeathCallData = ( + provider: Provider, + destination: SS58String, + amount: bigint, +) => { + const client = getObservableClient(createClient(provider.connect)) + const { metadata$ } = client.chainHead$() + + return metadata$.pipe( + filter(Boolean), + map((metadata) => { + const dynamicBuilder = getDynamicBuilder(metadata) + const { location, args } = dynamicBuilder.buildCall( + "Balances", + "transfer_allow_death", + ) + + return toHex( + mergeUint8( + new Uint8Array(location), + args.enc({ + dest: AccountId(destination), + value: amount, + }), + ), + ) + }), + first(), + ) +}