Skip to content

Commit

Permalink
refactor(examples): secp256k1 transfer example reduce example code size
Browse files Browse the repository at this point in the history
  • Loading branch information
IronLu233 committed Sep 16, 2022
1 parent 382f73b commit 35a29d1
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 162 deletions.
2 changes: 1 addition & 1 deletion examples/secp256k1-transfer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"@ckb-lumos/lumos": "0.19.0-alpha.1",
"@ckb-lumos/lumos": "0.19.0-alpha.3",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.11",
"bulma": "^0.9.4",
Expand Down
45 changes: 16 additions & 29 deletions examples/secp256k1-transfer/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import "bulma/css/bulma.css";
import React, { useEffect, useMemo, FC, ReactNode } from "react";
import { useList, useSetState, useAsync } from "react-use";
import React, { useEffect, FC, ReactNode } from "react";
import { useList, useSetState } from "react-use";
import ReactDOM from "react-dom";
import { nanoid } from "nanoid";
import { BI } from "@ckb-lumos/lumos";
import {
fetchAddressBalance,
createUnsignedTxSkeleton,
generateAccountFromPrivateKey,
transfer,
Account,
getPaidTransactionFee,
MIN_CELL_CAPACITY,
signTransaction,
} from "./lib";
import { fetchAddressBalance, generateAccountFromPrivateKey, transfer, Account, MIN_CELL_CAPACITY } from "./lib";

type TransferTarget = {
capacity: BI;
Expand All @@ -29,18 +20,19 @@ export function App() {
accountInfo: null as Account | null,
balance: BI.from(0),
txHash: "",
fee: BI.from(0),
});
const [transferTargets, transferTargetsActions] = useList([createTransferTarget()]);
const [transferTargets, transferTargetsActions] = useList<TransferTarget>([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, address: state.accountInfo.address });
return skeleton;
}, [state.accountInfo, state.privateKey, transferTargets]);
// const { value: unsignedTxSkeleton } = useAsync(async () => {
// if (!state.accountInfo) {
// return null;
// }
// const skeleton = await createUnsignedTxSkeleton({ targets: transferTargets, address: state.accountInfo.address });
// return skeleton;
// }, [state.accountInfo, state.privateKey, transferTargets]);

// Step 2: sign the transaction and send it to CKB test network
// this method will be called when you click "Transfer" button
Expand All @@ -49,17 +41,12 @@ export function App() {
return;
}

const tx = signTransaction(unsignedTxSkeleton, state.privateKey);
transfer(tx).then((txHash) => {
setState({ txHash });
const { tx } = signTransaction({}, state.privateKey);
transfer({ targets: transferTargets, address: state.accountInfo.address }, tx).then(({ txHash, fee }) => {
setState({ txHash: tx, fee });
});
};

// recalculate when transaction changes
const transactionFee = useMemo(() => (unsignedTxSkeleton ? getPaidTransactionFee(unsignedTxSkeleton) : BI.from(0)), [
unsignedTxSkeleton,
]);

// fetch and update account info and balance when private key changes
useEffect(() => {
if (state.privateKey) {
Expand Down Expand Up @@ -140,7 +127,7 @@ export function App() {
Add New Transfer Target
</div>
</th>
<th>Transaction fee {(transactionFee.toNumber() / 1e8).toString()}</th>
<th>Transaction fee {(state.fee.toNumber() / 1e8).toString()}</th>
<th>
<button className="button is-primary" onClick={doTransfer}>
Transfer!
Expand Down
96 changes: 50 additions & 46 deletions examples/secp256k1-transfer/src/lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Indexer, helpers, Address, Script, RPC, hd, config, commons, BI, Transaction } from "@ckb-lumos/lumos";
import { Indexer, helpers, Address, Script, RPC, hd, config, commons, BI } from "@ckb-lumos/lumos";
import { BIish } from "@ckb-lumos/bi";
import { payFeeByFeeRate } from "@ckb-lumos/common-scripts/lib/common";

Expand All @@ -15,6 +15,50 @@ export type Account = {
pubKey: string;
};

/**
* send a transaction to CKB testnet
* @returns Promise with transaction hash
*/
export async function transfer(options: TransactionIO, privateKey: string): Promise<{ txHash: string; fee: BI }> {
// step 1, create an raw transaction
// an raw transaction have it's inputs and outputs, but no signature
let txSkeleton = helpers.TransactionSkeleton({ cellProvider: indexer });
for (const target of options.targets) {
// add each outputs to the transaction skeleton
txSkeleton = await commons.common.transfer(
txSkeleton,
[options.address],
target.address,
target.capacity,
options.address,
undefined,
{ config: AGGRON4 }
);
}

// these methods add transaction fee to transaction.
// see the calculate algorithm in https://docs.nervos.org/docs/essays/faq/#how-do-you-calculate-transaction-fee
txSkeleton = await payFeeByFeeRate(txSkeleton, [options.address], 1000, undefined, { config: AGGRON4 });

// step2: sign an transaction

txSkeleton = commons.common.prepareSigningEntries(txSkeleton);

// message is the hash of raw transaction
const message = txSkeleton.get("signingEntries").get(0)?.message;
const signature = hd.key.signRecoverable(message!, privateKey);
const tx = helpers.sealTransaction(txSkeleton, [signature]);

// step3: send the transaction to block chain
const txHash = await rpc.sendTransaction(tx, "passthrough");

// how about transaction fee? it's just sum(transaction.inputs) - sum(transaction.outputs).
// the transaction fee will be sent to miner.

const transactionFee = getPaidTransactionFee(txSkeleton);
return { txHash, fee: transactionFee };
}

export const generateAccountFromPrivateKey = (privKey: string): Account => {
const pubKey = hd.key.privateToPublic(privKey);
const args = hd.key.publicKeyToBlake160(pubKey);
Expand Down Expand Up @@ -42,6 +86,7 @@ export function getPaidTransactionFee(skeleton: helpers.TransactionSkeletonType)
const outputs = skeleton.outputs.reduce((acc, cur) => acc.add(cur.cellOutput.capacity), BI.from(0));
return inputs.sub(outputs);
}

/**
* fetch all cells and calculate the sum of their capacities
*/
Expand All @@ -55,54 +100,13 @@ export async function fetchAddressBalance(address: string): Promise<BI> {
return balance;
}

interface Options {
/**
* Transaction input and output
*/
interface TransactionIO {
targets: {
address: string;
capacity: BIish;
}[];
address: string;
}

/**
* create an unsigned transaction skeleton which includes several inputs and outputs(for multiple transaction receivers)
*/
export async function createUnsignedTxSkeleton(options: Options) {
let txSkeleton = helpers.TransactionSkeleton({ cellProvider: indexer });
for (const target of options.targets) {
txSkeleton = await commons.common.transfer(
txSkeleton,
[options.address],
target.address,
target.capacity,
options.address,
undefined,
{ config: AGGRON4 }
);
}

txSkeleton = await payFeeByFeeRate(txSkeleton, [options.address], 1000, undefined, { config: AGGRON4 });
return txSkeleton;
}

/**
* sign a transaction skeleton
* @param txSkeleton unsigned transaction skeleton
* @param privateKey the private key which can unlock input cells
* @returns
*/
export function signTransaction(txSkeleton: helpers.TransactionSkeletonType, privateKey: string) {
txSkeleton = commons.common.prepareSigningEntries(txSkeleton);
const message = txSkeleton.get("signingEntries").get(0)?.message;
const signature = hd.key.signRecoverable(message!, privateKey);
const tx = helpers.sealTransaction(txSkeleton, [signature]);
return tx;
}

/**
* send a transaction to CKB testnet
* @returns Promise with transaction hash
*/
export async function transfer(tx: Transaction): Promise<string> {
const hash = await rpc.sendTransaction(tx, "passthrough");
return hash;
}
Loading

0 comments on commit 35a29d1

Please sign in to comment.