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(lc-dapp): transfers #1923

Merged
merged 12 commits into from
Mar 4, 2024
131 changes: 63 additions & 68 deletions examples/light-client-dapp/src/components/Transfer.tsx
Original file line number Diff line number Diff line change
@@ -1,70 +1,28 @@
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 { 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"
import { useSystemAccount, useTransfer } from "../hooks"
import { lastValueFrom, tap } 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<UnstableWallet.Account[]>([])
const [destination, setDestination] = useState<string>("")
const [destination, setDestination] = useState<string>(
"5CofVLAGjwvdGXvBiP6ddtZYMVbhT5Xke8ZrshUpj2ZXAnND",
)
const [amount, setAmount] = useState<bigint>(0n)
const [selectedAccount, setSelectedAccount] = useState<{
value: string
Expand All @@ -78,6 +36,10 @@ export const Transfer = ({ provider }: Props) => {
connect,
selectedAccount ? selectedAccount.value : null,
)
const { transfer } = useTransfer({ ...provider, connect }, chainId)
const [transactionStatus, setTransactionStatus] = useState("")
const [finalizedTransaction, setFinalizedTransaction] =
useState<FinalizedTransaction | null>()

const balance = accountStorage?.data.free ?? 0n

Expand All @@ -87,42 +49,55 @@ export const Transfer = ({ provider }: Props) => {
})
}, [provider])

const [isCreatingTransaction, setIsCreatingTransaction] = useState(false)
const [isSubmittingTransaction, setIsSubmittingTransaction] = useState(false)
const handleOnSubmit = useCallback(
async (e: FormEvent) => {
e.preventDefault()
if (!selectedAccount) {
return
}

setIsCreatingTransaction(true)
setIsSubmittingTransaction(true)
setTransactionStatus("")
setFinalizedTransaction(null)

try {
const tx = await provider.createTx(
chainId,
toHex(ss58Decode(selectedAccount.value)[0]),
await createTransfer(connect, destination, amount),
const sender = selectedAccount.value
const { txEvents } = await transfer(sender, destination, amount)

await lastValueFrom(
txEvents.pipe(
tap({
next: (e): void => {
setTransactionStatus(e.type)
if (e.type === "finalized") {
ryanleecode marked this conversation as resolved.
Show resolved Hide resolved
e.block.index
setFinalizedTransaction({
blockHash: e.block.hash,
index: e.block.index,
})
}
},
error: (e) => {
ryanleecode marked this conversation as resolved.
Show resolved Hide resolved
setTransactionStatus(e.type)
},
}),
),
)
console.log({ tx })
} catch (error) {
console.error(error)
}
setIsCreatingTransaction(false)
setIsSubmittingTransaction(false)
},
[provider, selectedAccount, connect, destination, amount],
[selectedAccount, transfer, destination, amount],
)

const accountOptions = accounts.map((account) => ({
value: account.address,
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 (
<article>
<header>Transfer funds</header>
Expand All @@ -147,10 +122,30 @@ export const Transfer = ({ provider }: Props) => {
<footer>
<button
type="submit"
disabled={!selectedAccount || isCreatingTransaction}
disabled={!selectedAccount || isSubmittingTransaction}
>
Transfer
</button>
{transactionStatus ? (
<p>
Transaction Status: <b>{`${transactionStatus}`}</b>
</p>
) : null}
{finalizedTransaction ? (
<div>
<p>
Finalized Block Hash:{" "}
<b>
<a
href={`https://westend.subscan.io/block/${finalizedTransaction.blockHash}`}
>{`${finalizedTransaction.blockHash}`}</a>
</b>
</p>
<p>
Transaction Index: <b>{finalizedTransaction.index}</b>
</p>
</div>
) : null}
</footer>
</form>
</article>
Expand Down
1 change: 1 addition & 0 deletions examples/light-client-dapp/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./useProvider"
export * from "./useSystemAccount"
export * from "./useTransfer"
71 changes: 71 additions & 0 deletions examples/light-client-dapp/src/hooks/useTransfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { getObservableClient } from "@polkadot-api/client"
import { Enum } from "@polkadot-api/substrate-bindings"
import type { SS58String } from "@polkadot-api/substrate-bindings"
import {
ConnectProvider,
TxEvent,
createClient,
} from "@polkadot-api/substrate-client"
import { getDynamicBuilder } from "@polkadot-api/metadata-builders"
import { firstValueFrom, filter, map, tap, ReplaySubject } from "rxjs"
import { mergeUint8, toHex } from "@polkadot-api/utils"
import { UnstableWallet } from "@substrate/unstable-wallet-provider"
import { ss58Decode } from "@polkadot-labs/hdkd-helpers"

const AccountId = (value: SS58String) =>
Enum<
{
type: "Id"
value: SS58String
},
"Id"
>("Id", value)

type Provider = UnstableWallet.Provider & { connect: ConnectProvider }

export const useTransfer = (provider: Provider, chainId: string) => {
const transfer = async (
ryanleecode marked this conversation as resolved.
Show resolved Hide resolved
sender: SS58String,
destination: SS58String,
amount: bigint,
) => {
const client = getObservableClient(createClient(provider.connect))
const { metadata$ } = client.chainHead$()

const callData = await 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,
}),
),
)
}),
),
)

const tx = await provider.createTx(
chainId,
toHex(ss58Decode(sender)[0]),
callData,
)
const txEvents = new ReplaySubject<TxEvent>()

client.tx$(tx).pipe(tap(txEvents)).subscribe()
ryanleecode marked this conversation as resolved.
Show resolved Hide resolved

return { tx, txEvents }
}

return { transfer }
}