Skip to content

Commit

Permalink
feat(examples): simplify secp256k1-transfer example
Browse files Browse the repository at this point in the history
  • Loading branch information
IronLu233 committed Aug 26, 2022
1 parent 857b60d commit 6e14630
Show file tree
Hide file tree
Showing 4 changed files with 352 additions and 254 deletions.
3 changes: 2 additions & 1 deletion examples/secp256k1-transfer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"bulma": "^0.9.4",
"nanoid": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2"
"react-dom": "^17.0.2",
"react-use": "^17.4.0"
},
"devDependencies": {
"@babel/cli": "^7.16.0",
Expand Down
243 changes: 112 additions & 131 deletions examples/secp256k1-transfer/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,133 +1,90 @@
import "bulma/css/bulma.css";
import React, { ChangeEvent, useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, FC, ReactNode } from "react";
import { useList, useSetState, useAsync } from "react-use";
import ReactDOM from "react-dom";
import { nanoid } from "nanoid";
import { BI, Script, helpers } from "@ckb-lumos/lumos";
import { capacityOf, createTxSkeleton, generateAccountFromPrivateKey, transfer, Options } from "./lib";
import { BIish } from "@ckb-lumos/bi";

type TxTarget = {
amount: BIish;
import { BI } from "@ckb-lumos/lumos";
import {
fetchAddressBalance,
createUnsignedTxSkeleton,
generateAccountFromPrivateKey,
transfer,
Account,
calculateTransactionFee,
MIN_CELL_CAPACITY,
} from "./lib";

type TransferTarget = {
amount: BI;
address: string;
key: string;
};

const createTxTo = (): TxTarget => ({ key: nanoid(), amount: 0, address: "" });
const createTransferTarget = (): TransferTarget => ({ key: nanoid(), amount: MIN_CELL_CAPACITY, address: "" });

export function App() {
const [privKey, setPrivKey] = useState("");
const [fromAddr, setFromAddr] = useState("");
const [fromLock, setFromLock] = useState<Script>();
const [balance, setBalance] = useState("0");
const [txHash, setTxHash] = useState("");
const [errorMessage, setErrorMessage] = useState("");

const [txTo, setTxTo] = useState<TxTarget[]>([createTxTo()]);
const [txSkeleton, setTxSkeleton] = useState<ReturnType<typeof helpers.TransactionSkeleton> | undefined>();
const setTargetByIndex = (index: number, field: "amount" | "address") => (e: ChangeEvent<HTMLInputElement>) => {
setErrorMessage("");
const newTo = [...txTo];
if (field === "amount") {
newTo[index].amount = e.target.value;
} else {
newTo[index]["address"] = e.target.value;
const [state, setState] = useSetState({
privKey: "",
accountInfo: null as Account | null,
balance: BI.from(0),
txHash: "",
});
const [transferTargets, transferTargetsActions] = useList([createTransferTarget()]);

// Step 1: get the unsigned transaction skeleton
// `useAsync` method can keep the transaction is newest from state
const { value: unsignedTxSkeleton } = useAsync(async () => {
if (!state.accountInfo) {
return null;
}
const skeleton = await createUnsignedTxSkeleton({ targets: transferTargets, privKey: state.privKey });
return skeleton;
}, [state.accountInfo, state.privKey, transferTargets]);

// Step 2: sign the transaction and send it to CKB test network
// this method will be called when you click "Transfer" button
const doTransfer = () => {
if (!state.accountInfo) {
return;
}
setTxTo(newTo);
};

const insertTxTarget = () => {
setTxTo((origin) => [...origin, createTxTo()]);
};

const removeTxTarget = (index: number) => () => {
setTxTo((origin) => origin.filter((_, i) => i !== index));
transfer(unsignedTxSkeleton, state.privKey).then((txHash) => {
setState({ txHash });
});
};

const txOptions = useMemo<Options>(
() => ({
from: fromAddr,
to: txTo.map((tx) => ({ address: tx.address, amount: BI.from(tx.amount) })),
privKey,
}),
[fromAddr, txTo, privKey]
// recalculate when transaction changes
const transactionFee = useMemo(
() => (unsignedTxSkeleton ? calculateTransactionFee(unsignedTxSkeleton) : BI.from(0)),
[unsignedTxSkeleton]
);

// fetch and update account info and balance when private key changes
useEffect(() => {
const updateFromInfo = async () => {
const { lockScript, address } = generateAccountFromPrivateKey(privKey);
const capacity = await capacityOf(address);
setFromAddr(address);
setFromLock(lockScript);
setBalance(capacity.toString());
};

setErrorMessage("");
if (privKey) {
updateFromInfo().catch((e: Error) => {
setErrorMessage(e.toString());
if (state.privKey) {
const accountInfo = generateAccountFromPrivateKey(state.privKey);
setState({
accountInfo,
});
}
}, [privKey]);

useEffect(() => {
(async () => {
if (!txOptions.privKey || !txOptions.from) {
return;
}
try {
const skeleton = await createTxSkeleton({ ...txOptions, to: txOptions.to.filter((it) => it.address) });
setTxSkeleton(skeleton);
} catch (e) {
setErrorMessage(e.toString());
}
})();
}, [txOptions, privKey]);

const txFee = useMemo(() => {
if (!txSkeleton) return BI.from(0);
const outputs = txSkeleton.outputs.reduce((prev, cur) => prev.add(cur.cell_output.capacity), BI.from(0));
const inputs = txSkeleton.inputs.reduce((prev, cur) => prev.add(cur.cell_output.capacity), BI.from(0));
return inputs.sub(outputs);
}, [txSkeleton]);

const doTransfer = async () => {
try {
const txHash = await transfer({
from: fromAddr,
to: txTo.map((tx) => ({ address: tx.address, amount: BI.from(tx.amount) })),
privKey,
fetchAddressBalance(accountInfo.address).then((balance) => {
setState({ balance });
});
setTxHash(txHash);
} catch (e) {
setErrorMessage(e.toString());
}
};
}, [state.privKey]);

const txExplorer = useMemo(() => `https://pudge.explorer.nervos.org/transaction/${txHash}`, [txHash]);
return (
<div className="m-5">
<div className="field">
<label htmlFor="privateKey" className="label">
Private Key
</label>
<input
type="text"
onChange={(e) => setPrivKey(e.target.value)}
className="input is-primary"
placeholder="Your CKB Testnet Private Key"
/>
</div>
<div className="box">
<div>
<strong>CKB Address: </strong> {fromAddr}
</div>
<div className="mt-2">
<strong>Current Lockscript: </strong> {JSON.stringify(fromLock)}
</div>
<div className="mt-2">
<strong>Balance: </strong> {balance} <div className="tag is-info is-light">Shannon</div>
</div>
</div>
<Field
value={state.privKey}
onChange={(e) => {
setState({ privKey: e.target.value });
}}
label="Private Key"
/>
<ul>
<li>CKB Address: {state.accountInfo?.address}</li>
<li>CKB Balance: {state.balance.div(1e8).toString()}</li>
</ul>
<table className="table table is-fullwidth">
<thead>
<tr>
Expand All @@ -137,27 +94,29 @@ export function App() {
</tr>
</thead>
<tbody>
{txTo.map((txTarget, index) => (
{transferTargets.map((txTarget, index) => (
<tr key={txTarget.key}>
<td>
<input
type="text"
value={txTarget.address}
onChange={setTargetByIndex(index, "address")}
onChange={(e) => transferTargetsActions.updateAt(index, { ...txTarget, address: e.target.value })}
className="input"
/>
</td>
<td>
<input
type="text"
value={txTarget.amount as string}
onChange={setTargetByIndex(index, "amount")}
value={txTarget.amount.div(1e8).toString()}
onChange={(e) =>
transferTargetsActions.updateAt(index, { ...txTarget, amount: BI.from(e.target.value).mul(1e8) })
}
className="input"
/>
</td>
<td>
{txTo.length > 1 && (
<button onClick={removeTxTarget(index)} className="button is-danger">
{transferTargets.length > 1 && (
<button onClick={() => transferTargetsActions.removeAt(index)} className="button is-danger">
Remove
</button>
)}
Expand All @@ -168,11 +127,16 @@ export function App() {
<tfoot>
<tr>
<th>
<div className="button" onClick={insertTxTarget}>
<div
className="button"
onClick={() => {
transferTargetsActions.push(createTransferTarget());
}}
>
Add New Transfer Target
</div>
</th>
<th>Transaction fee {txFee.toBigInt().toString()}</th>
<th>Transaction fee {(transactionFee.toNumber() / 1e8).toString()}</th>
<th>
<button className="button is-primary" onClick={doTransfer}>
Transfer!
Expand All @@ -181,26 +145,43 @@ export function App() {
</tr>
</tfoot>
</table>

{txHash && (
<div className="notification is-primary">
<button className="delete" onClick={() => setTxHash("")} />
Transaction created, View it on{" "}
<a target="_blank" href={txExplorer}>
👉CKB Explorer
</a>
</div>
)}
{errorMessage && (
<div className="notification is-danger">
<button className="delete" onClick={() => setErrorMessage("")} />
{errorMessage}
</div>
{state.txHash && (
<Notification onClear={() => setState({ txHash: "" })}>
Transaction has sent, View it on{" "}
<a href={`https://pudge.explorer.nervos.org/transaction/${state.txHash}`}>CKB Explorer</a>
</Notification>
)}
</div>
);
}

const Field: FC<{ label: string; value: string; onChange: React.ChangeEventHandler<HTMLInputElement> }> = ({
label,
value,
onChange,
}) => (
<div className="field">
<label htmlFor={label} className="label">
{label}
</label>
<input
name={label}
type="text"
onChange={onChange}
value={value}
className="input is-primary"
placeholder="Your CKB Testnet Private Key"
/>
</div>
);

const Notification: FC<{ children: ReactNode; onClear: () => unknown }> = ({ children, onClear }) => (
<div className="notification is-success">
<button className="delete" onClick={onClear}></button>
{children}
</div>
);

// prevent can not find DOM element on Codesandbox
const el = document.getElementById("root") || document.createElement("div");
el.id = "root";
Expand Down
Loading

0 comments on commit 6e14630

Please sign in to comment.