Skip to content

Commit 6e14630

Browse files
committed
feat(examples): simplify secp256k1-transfer example
1 parent 857b60d commit 6e14630

File tree

4 files changed

+352
-254
lines changed

4 files changed

+352
-254
lines changed

examples/secp256k1-transfer/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"bulma": "^0.9.4",
2222
"nanoid": "^4.0.0",
2323
"react": "^17.0.2",
24-
"react-dom": "^17.0.2"
24+
"react-dom": "^17.0.2",
25+
"react-use": "^17.4.0"
2526
},
2627
"devDependencies": {
2728
"@babel/cli": "^7.16.0",

examples/secp256k1-transfer/src/index.tsx

Lines changed: 112 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,133 +1,90 @@
11
import "bulma/css/bulma.css";
2-
import React, { ChangeEvent, useEffect, useMemo, useState } from "react";
2+
import React, { useEffect, useMemo, FC, ReactNode } from "react";
3+
import { useList, useSetState, useAsync } from "react-use";
34
import ReactDOM from "react-dom";
45
import { nanoid } from "nanoid";
5-
import { BI, Script, helpers } from "@ckb-lumos/lumos";
6-
import { capacityOf, createTxSkeleton, generateAccountFromPrivateKey, transfer, Options } from "./lib";
7-
import { BIish } from "@ckb-lumos/bi";
8-
9-
type TxTarget = {
10-
amount: BIish;
6+
import { BI } from "@ckb-lumos/lumos";
7+
import {
8+
fetchAddressBalance,
9+
createUnsignedTxSkeleton,
10+
generateAccountFromPrivateKey,
11+
transfer,
12+
Account,
13+
calculateTransactionFee,
14+
MIN_CELL_CAPACITY,
15+
} from "./lib";
16+
17+
type TransferTarget = {
18+
amount: BI;
1119
address: string;
1220
key: string;
1321
};
1422

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

1725
export function App() {
18-
const [privKey, setPrivKey] = useState("");
19-
const [fromAddr, setFromAddr] = useState("");
20-
const [fromLock, setFromLock] = useState<Script>();
21-
const [balance, setBalance] = useState("0");
22-
const [txHash, setTxHash] = useState("");
23-
const [errorMessage, setErrorMessage] = useState("");
24-
25-
const [txTo, setTxTo] = useState<TxTarget[]>([createTxTo()]);
26-
const [txSkeleton, setTxSkeleton] = useState<ReturnType<typeof helpers.TransactionSkeleton> | undefined>();
27-
const setTargetByIndex = (index: number, field: "amount" | "address") => (e: ChangeEvent<HTMLInputElement>) => {
28-
setErrorMessage("");
29-
const newTo = [...txTo];
30-
if (field === "amount") {
31-
newTo[index].amount = e.target.value;
32-
} else {
33-
newTo[index]["address"] = e.target.value;
26+
const [state, setState] = useSetState({
27+
privKey: "",
28+
accountInfo: null as Account | null,
29+
balance: BI.from(0),
30+
txHash: "",
31+
});
32+
const [transferTargets, transferTargetsActions] = useList([createTransferTarget()]);
33+
34+
// Step 1: get the unsigned transaction skeleton
35+
// `useAsync` method can keep the transaction is newest from state
36+
const { value: unsignedTxSkeleton } = useAsync(async () => {
37+
if (!state.accountInfo) {
38+
return null;
39+
}
40+
const skeleton = await createUnsignedTxSkeleton({ targets: transferTargets, privKey: state.privKey });
41+
return skeleton;
42+
}, [state.accountInfo, state.privKey, transferTargets]);
43+
44+
// Step 2: sign the transaction and send it to CKB test network
45+
// this method will be called when you click "Transfer" button
46+
const doTransfer = () => {
47+
if (!state.accountInfo) {
48+
return;
3449
}
35-
setTxTo(newTo);
36-
};
37-
38-
const insertTxTarget = () => {
39-
setTxTo((origin) => [...origin, createTxTo()]);
40-
};
4150

42-
const removeTxTarget = (index: number) => () => {
43-
setTxTo((origin) => origin.filter((_, i) => i !== index));
51+
transfer(unsignedTxSkeleton, state.privKey).then((txHash) => {
52+
setState({ txHash });
53+
});
4454
};
4555

46-
const txOptions = useMemo<Options>(
47-
() => ({
48-
from: fromAddr,
49-
to: txTo.map((tx) => ({ address: tx.address, amount: BI.from(tx.amount) })),
50-
privKey,
51-
}),
52-
[fromAddr, txTo, privKey]
56+
// recalculate when transaction changes
57+
const transactionFee = useMemo(
58+
() => (unsignedTxSkeleton ? calculateTransactionFee(unsignedTxSkeleton) : BI.from(0)),
59+
[unsignedTxSkeleton]
5360
);
5461

62+
// fetch and update account info and balance when private key changes
5563
useEffect(() => {
56-
const updateFromInfo = async () => {
57-
const { lockScript, address } = generateAccountFromPrivateKey(privKey);
58-
const capacity = await capacityOf(address);
59-
setFromAddr(address);
60-
setFromLock(lockScript);
61-
setBalance(capacity.toString());
62-
};
63-
64-
setErrorMessage("");
65-
if (privKey) {
66-
updateFromInfo().catch((e: Error) => {
67-
setErrorMessage(e.toString());
64+
if (state.privKey) {
65+
const accountInfo = generateAccountFromPrivateKey(state.privKey);
66+
setState({
67+
accountInfo,
6868
});
69-
}
70-
}, [privKey]);
71-
72-
useEffect(() => {
73-
(async () => {
74-
if (!txOptions.privKey || !txOptions.from) {
75-
return;
76-
}
77-
try {
78-
const skeleton = await createTxSkeleton({ ...txOptions, to: txOptions.to.filter((it) => it.address) });
79-
setTxSkeleton(skeleton);
80-
} catch (e) {
81-
setErrorMessage(e.toString());
82-
}
83-
})();
84-
}, [txOptions, privKey]);
85-
86-
const txFee = useMemo(() => {
87-
if (!txSkeleton) return BI.from(0);
88-
const outputs = txSkeleton.outputs.reduce((prev, cur) => prev.add(cur.cell_output.capacity), BI.from(0));
89-
const inputs = txSkeleton.inputs.reduce((prev, cur) => prev.add(cur.cell_output.capacity), BI.from(0));
90-
return inputs.sub(outputs);
91-
}, [txSkeleton]);
92-
93-
const doTransfer = async () => {
94-
try {
95-
const txHash = await transfer({
96-
from: fromAddr,
97-
to: txTo.map((tx) => ({ address: tx.address, amount: BI.from(tx.amount) })),
98-
privKey,
69+
fetchAddressBalance(accountInfo.address).then((balance) => {
70+
setState({ balance });
9971
});
100-
setTxHash(txHash);
101-
} catch (e) {
102-
setErrorMessage(e.toString());
10372
}
104-
};
73+
}, [state.privKey]);
10574

106-
const txExplorer = useMemo(() => `https://pudge.explorer.nervos.org/transaction/${txHash}`, [txHash]);
10775
return (
10876
<div className="m-5">
109-
<div className="field">
110-
<label htmlFor="privateKey" className="label">
111-
Private Key
112-
</label>
113-
<input
114-
type="text"
115-
onChange={(e) => setPrivKey(e.target.value)}
116-
className="input is-primary"
117-
placeholder="Your CKB Testnet Private Key"
118-
/>
119-
</div>
120-
<div className="box">
121-
<div>
122-
<strong>CKB Address: </strong> {fromAddr}
123-
</div>
124-
<div className="mt-2">
125-
<strong>Current Lockscript: </strong> {JSON.stringify(fromLock)}
126-
</div>
127-
<div className="mt-2">
128-
<strong>Balance: </strong> {balance} <div className="tag is-info is-light">Shannon</div>
129-
</div>
130-
</div>
77+
<Field
78+
value={state.privKey}
79+
onChange={(e) => {
80+
setState({ privKey: e.target.value });
81+
}}
82+
label="Private Key"
83+
/>
84+
<ul>
85+
<li>CKB Address: {state.accountInfo?.address}</li>
86+
<li>CKB Balance: {state.balance.div(1e8).toString()}</li>
87+
</ul>
13188
<table className="table table is-fullwidth">
13289
<thead>
13390
<tr>
@@ -137,27 +94,29 @@ export function App() {
13794
</tr>
13895
</thead>
13996
<tbody>
140-
{txTo.map((txTarget, index) => (
97+
{transferTargets.map((txTarget, index) => (
14198
<tr key={txTarget.key}>
14299
<td>
143100
<input
144101
type="text"
145102
value={txTarget.address}
146-
onChange={setTargetByIndex(index, "address")}
103+
onChange={(e) => transferTargetsActions.updateAt(index, { ...txTarget, address: e.target.value })}
147104
className="input"
148105
/>
149106
</td>
150107
<td>
151108
<input
152109
type="text"
153-
value={txTarget.amount as string}
154-
onChange={setTargetByIndex(index, "amount")}
110+
value={txTarget.amount.div(1e8).toString()}
111+
onChange={(e) =>
112+
transferTargetsActions.updateAt(index, { ...txTarget, amount: BI.from(e.target.value).mul(1e8) })
113+
}
155114
className="input"
156115
/>
157116
</td>
158117
<td>
159-
{txTo.length > 1 && (
160-
<button onClick={removeTxTarget(index)} className="button is-danger">
118+
{transferTargets.length > 1 && (
119+
<button onClick={() => transferTargetsActions.removeAt(index)} className="button is-danger">
161120
Remove
162121
</button>
163122
)}
@@ -168,11 +127,16 @@ export function App() {
168127
<tfoot>
169128
<tr>
170129
<th>
171-
<div className="button" onClick={insertTxTarget}>
130+
<div
131+
className="button"
132+
onClick={() => {
133+
transferTargetsActions.push(createTransferTarget());
134+
}}
135+
>
172136
Add New Transfer Target
173137
</div>
174138
</th>
175-
<th>Transaction fee {txFee.toBigInt().toString()}</th>
139+
<th>Transaction fee {(transactionFee.toNumber() / 1e8).toString()}</th>
176140
<th>
177141
<button className="button is-primary" onClick={doTransfer}>
178142
Transfer!
@@ -181,26 +145,43 @@ export function App() {
181145
</tr>
182146
</tfoot>
183147
</table>
184-
185-
{txHash && (
186-
<div className="notification is-primary">
187-
<button className="delete" onClick={() => setTxHash("")} />
188-
Transaction created, View it on{" "}
189-
<a target="_blank" href={txExplorer}>
190-
👉CKB Explorer
191-
</a>
192-
</div>
193-
)}
194-
{errorMessage && (
195-
<div className="notification is-danger">
196-
<button className="delete" onClick={() => setErrorMessage("")} />
197-
{errorMessage}
198-
</div>
148+
{state.txHash && (
149+
<Notification onClear={() => setState({ txHash: "" })}>
150+
Transaction has sent, View it on{" "}
151+
<a href={`https://pudge.explorer.nervos.org/transaction/${state.txHash}`}>CKB Explorer</a>
152+
</Notification>
199153
)}
200154
</div>
201155
);
202156
}
203157

158+
const Field: FC<{ label: string; value: string; onChange: React.ChangeEventHandler<HTMLInputElement> }> = ({
159+
label,
160+
value,
161+
onChange,
162+
}) => (
163+
<div className="field">
164+
<label htmlFor={label} className="label">
165+
{label}
166+
</label>
167+
<input
168+
name={label}
169+
type="text"
170+
onChange={onChange}
171+
value={value}
172+
className="input is-primary"
173+
placeholder="Your CKB Testnet Private Key"
174+
/>
175+
</div>
176+
);
177+
178+
const Notification: FC<{ children: ReactNode; onClear: () => unknown }> = ({ children, onClear }) => (
179+
<div className="notification is-success">
180+
<button className="delete" onClick={onClear}></button>
181+
{children}
182+
</div>
183+
);
184+
204185
// prevent can not find DOM element on Codesandbox
205186
const el = document.getElementById("root") || document.createElement("div");
206187
el.id = "root";

0 commit comments

Comments
 (0)