Skip to content

Commit

Permalink
cobuild: handle fee payment and set change output
Browse files Browse the repository at this point in the history
I call payFee before filling the witness in the previous version. The fee estimation is smaller because the tx size is smaller than the final one after adding all the witnesses. I set feeRate to 3000 to work around this.

In this commit, I refactor code to make it possible to create a dummy tx for fee estimation. It's guaranteed that the dummy tx is not less than the final tx.
  • Loading branch information
doitian committed Dec 29, 2023
1 parent 70be2d4 commit df47fb7
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 46 deletions.
11 changes: 10 additions & 1 deletion src/actions/claim.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@

import { claimDao } from "@/lib/cobuild/publishers";
import { useConfig } from "@/lib/config";
import { prepareLockActions } from "@/lib/cobuild/lock-actions";
import { payFee } from "@/lib/cobuild/fee-manager";

export default async function withdraw(from, cell, config) {
config = config ?? useConfig();

try {
const buildingPacket = await claimDao(config)({ from, cell });
let buildingPacket = await claimDao(config)({ from, cell });
buildingPacket = await payFee(
buildingPacket,
[{ address: from, feeRate: 1200 }],
config,
);
buildingPacket = prepareLockActions(buildingPacket, config.ckbChainConfig);

return {
buildingPacket,
};
Expand Down
11 changes: 10 additions & 1 deletion src/actions/deposit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { parseUnit } from "@ckb-lumos/bi";
import { depositDao } from "@/lib/cobuild/publishers";
import { useConfig } from "@/lib/config";
import { prepareLockActions } from "@/lib/cobuild/lock-actions";
import { payFee } from "@/lib/cobuild/fee-manager";

export default async function deposit(_prevState, formData, config) {
config = config ?? useConfig();
Expand All @@ -11,7 +13,14 @@ export default async function deposit(_prevState, formData, config) {
const amount = parseUnit(formData.get("amount"), "ckb");

try {
const buildingPacket = await depositDao(config)({ from, amount });
let buildingPacket = await depositDao(config)({ from, amount });
buildingPacket = await payFee(
buildingPacket,
[{ address: from, feeRate: 1200 }],
config,
);
buildingPacket = prepareLockActions(buildingPacket, config.ckbChainConfig);

return {
buildingPacket,
};
Expand Down
16 changes: 15 additions & 1 deletion src/actions/transfer.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import { parseUnit } from "@ckb-lumos/bi";
import { transferCkb } from "@/lib/cobuild/publishers";
import { prepareLockActions } from "@/lib/cobuild/lock-actions";
import { payFee } from "@/lib/cobuild/fee-manager";
import { useConfig } from "@/lib/config";

export default async function transfer(_prevState, formData, config) {
Expand All @@ -12,11 +14,23 @@ export default async function transfer(_prevState, formData, config) {
const amount = parseUnit(formData.get("amount"), "ckb");

try {
const buildingPacket = await transferCkb(config)({ from, to, amount });
let buildingPacket = await transferCkb(config)({
from,
to,
amount,
});
buildingPacket = await payFee(
buildingPacket,
[{ address: from, feeRate: 1200 }],
config,
);
buildingPacket = prepareLockActions(buildingPacket, config.ckbChainConfig);

return {
buildingPacket,
};
} catch (err) {
console.error(err.stack);
return {
error: err.toString(),
};
Expand Down
11 changes: 10 additions & 1 deletion src/actions/withdraw.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,21 @@

import { withdrawDao } from "@/lib/cobuild/publishers";
import { useConfig } from "@/lib/config";
import { prepareLockActions } from "@/lib/cobuild/lock-actions";
import { payFee } from "@/lib/cobuild/fee-manager";

export default async function withdraw(from, cell, config) {
config = config ?? useConfig();

try {
const buildingPacket = await withdrawDao(config)({ from, cell });
let buildingPacket = await withdrawDao(config)({ from, cell });
buildingPacket = await payFee(
buildingPacket,
[{ address: from, feeRate: 1200 }],
config,
);
buildingPacket = prepareLockActions(buildingPacket, config.ckbChainConfig);

return {
buildingPacket,
};
Expand Down
12 changes: 2 additions & 10 deletions src/app/accounts/[address]/sign-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import * as joyid from "@/lib/wallet/joyid";
import SubmitButton from "@/components/submit-button";
import BuildingPacketReview from "@/lib/cobuild/react/building-packet-review";
import {
prepareLockActions,
findLockActionByLockScript,
finalizeWitnesses,
} from "@/lib/cobuild/lock-actions";
Expand All @@ -25,15 +24,8 @@ export default function SignForm({
onCancel,
}) {
const [error, setError] = useState();
const preparedBuildingPacket = prepareLockActions(
buildingPacket,
ckbChainConfig,
);
const lockScript = addressToScript(address, { config: ckbChainConfig });
const lockAction = findLockActionByLockScript(
preparedBuildingPacket,
lockScript,
);
const lockAction = findLockActionByLockScript(buildingPacket, lockScript);
const lockActionData = GeneralLockAction.unpack(lockAction.data);
const sign = async () => {
try {
Expand Down Expand Up @@ -64,7 +56,7 @@ export default function SignForm({
</Button>
</form>
<BuildingPacketReview
buildingPacket={preparedBuildingPacket}
buildingPacket={buildingPacket}
lockActionData={lockActionData}
/>
</>
Expand Down
67 changes: 67 additions & 0 deletions src/lib/cobuild/fee-manager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { bytes } from "@ckb-lumos/codec";

import payFeeWithBuildingPacket from "../lumos-adapter/pay-fee-with-building-packet";
import * as generalLockActions from "./general-lock-actions";
import { finalizeWitnesses } from "./lock-actions";
import { groupByLock } from "./script-group";

// feePayments: [{address, fee?, feeRate?}]
export async function payFee(buildingPacket, feePayments, config) {
const groups = groupByLock(buildingPacket.value.resolvedInputs.outputs);

// Rember fields that should be restored
const witnesses = buildingPacket.value.payload.witnesses;

const buildingPacketWillPayFee = finalizeWitnesses(
Object.entries(groups).reduce(
(acc, [scriptHash, inputs]) =>
storeWitnessForFeeEstimation(
acc,
scriptHash,
inputs.map((e) => e[0]),
config.ckbChainConfig,
),
buildingPacket,
),
);

const buildingPacketHavePaidFee = await payFeeWithBuildingPacket(
buildingPacketWillPayFee,
feePayments,
config,
);

return {
type: buildingPacketHavePaidFee.type,
value: {
...buildingPacketHavePaidFee.value,
payload: {
...buildingPacketHavePaidFee.value.payload,
witnesses,
},
},
};
}

function storeWitnessForFeeEstimation(
buildingPacket,
scriptHash,
inputIndices,
ckbChainConfig,
) {
const script =
buildingPacket.value.resolvedInputs.outputs[inputIndices[0]].lock;
if (script.codeHash === ckbChainConfig.SCRIPTS.JOYID_COBUILD_POC.CODE_HASH) {
return generalLockActions.storeWitnessForFeeEstimation(
buildingPacket,
scriptHash,
inputIndices,
// Variable length, but 500 is usually enough.
() => bytes.hexify(new Uint8Array(500)),
);
}

throw new Error(
`NotSupportedLock: codeHash=${script.codeHash} hashType=${script.hashType}`,
);
}
18 changes: 18 additions & 0 deletions src/lib/cobuild/general-lock-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ export function prepareLockAction(
};
}

export function storeWitnessForFeeEstimation(
buildingPacket,
scriptHash,
inputIndices,
createSealPlaceHolder,
) {
buildingPacket = prepareLockAction(
buildingPacket,
scriptHash,
inputIndices,
createSealPlaceHolder,
);
const lockAction = buildingPacket.value.lockActions.find(
(action) => action.scriptHash === scriptHash,
);
return applyLockAction(buildingPacket, lockAction, createSealPlaceHolder());
}

export function applyLockAction(buildingPacket, lockAction, seal) {
const witnesses = [...buildingPacket.value.payload.witnesses];
const { witnessStore } = GeneralLockAction.unpack(lockAction.data);
Expand Down
7 changes: 1 addition & 6 deletions src/lib/cobuild/lock-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { utils as lumosBaseUtils } from "@ckb-lumos/base";

import * as generalLockActions from "./general-lock-actions";
import { parseWitnessType } from "./types";
import { groupByLock } from "./script-group";

const { computeScriptHash } = lumosBaseUtils;

Expand Down Expand Up @@ -66,12 +67,6 @@ export function findLockActionByLockScript(buildingPacket, lockScript) {
);
}

function groupByLock(cellOutputs) {
return Object.groupBy(cellOutputs.entries(), ([_i, v]) =>
computeScriptHash(v.lock),
);
}

function dispatchLockActions(
buildingPacket,
scriptHash,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/cobuild/script-group.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { utils as lumosBaseUtils } from "@ckb-lumos/base";
const { computeScriptHash } = lumosBaseUtils;

export function groupByLock(cellOutputs) {
return Object.groupBy(cellOutputs.entries(), ([_i, v]) =>
computeScriptHash(v.lock),
);
}
13 changes: 8 additions & 5 deletions src/lib/lumos-adapter/create-building-packet-from-skeleton.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { createTransactionFromSkeleton } from "@ckb-lumos/helpers";

export default function createBuildingPacketFromSkeleton(txSkeleton) {
export default function createBuildingPacketFromSkeleton(
txSkeleton,
changeOutput,
) {
const resolvedInputs = {
outputs: txSkeleton.inputs.map((cell) => cell.cellOutput).toJSON(),
outputData: txSkeleton.inputs.map((cell) => cell.data).toJSON(),
outputsData: txSkeleton.inputs.map((cell) => cell.data).toJSON(),
};
const payload = createTransactionFromSkeleton(txSkeleton);
return {
Expand All @@ -12,11 +15,11 @@ export default function createBuildingPacketFromSkeleton(txSkeleton) {
message: {
actions: [],
},
payload,
resolvedInputs,
changeOutput: null,
scriptInfos: [],
lockActions: [],
payload,
resolvedInputs,
changeOutput,
},
};
}
41 changes: 20 additions & 21 deletions src/lib/lumos-adapter/create-lumos-ckb-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,6 @@ import {
packDaoWitnessArgs,
} from "../dao";

// **Attention:** There's no witnesses set yet, so I set fee rate to 3000 to hope that the final tx fee rate will be larger than 1000.
async function payFee(txSkeleton, from, ckbChainConfig) {
return await commonScripts.payFeeByFeeRate(
txSkeleton,
[from],
3000,
undefined,
{
config: ckbChainConfig,
},
);
}

function buildCellDep(scriptInfo) {
return {
outPoint: {
Expand All @@ -33,6 +20,11 @@ function buildCellDep(scriptInfo) {
};
}

function useLastOutputAsChangeOutput(txSkeleton) {
const size = txSkeleton.get("outputs").size;
return size > 1 ? size - 1 : null;
}

export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) {
initLumosCommonScripts(ckbChainConfig);
const rpc = new RPC(ckbRpcUrl);
Expand All @@ -56,8 +48,11 @@ export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) {
},
);

txSkeleton = await payFee(txSkeleton, from, ckbChainConfig);
return createBuildingPacketFromSkeleton(txSkeleton);
// lumos always add the target output first
return createBuildingPacketFromSkeleton(
txSkeleton,
useLastOutputAsChangeOutput(txSkeleton),
);
},

depositDao: async function ({ from, amount }) {
Expand All @@ -78,8 +73,10 @@ export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) {
{ config: ckbChainConfig },
);

txSkeleton = await payFee(txSkeleton, from, ckbChainConfig);
return createBuildingPacketFromSkeleton(txSkeleton);
return createBuildingPacketFromSkeleton(
txSkeleton,
useLastOutputAsChangeOutput(txSkeleton),
);
},

withdrawDao: async function ({ from, cell }) {
Expand All @@ -96,8 +93,10 @@ export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) {
config: ckbChainConfig,
});

txSkeleton = await payFee(txSkeleton, from, ckbChainConfig);
return createBuildingPacketFromSkeleton(txSkeleton);
return createBuildingPacketFromSkeleton(
txSkeleton,
useLastOutputAsChangeOutput(txSkeleton),
);
},

claimDao: async function ({ from, cell }) {
Expand Down Expand Up @@ -157,8 +156,8 @@ export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) {

let txSkeleton = txSkeletonMutable.asImmutable();

txSkeleton = await payFee(txSkeleton, from, ckbChainConfig);
return createBuildingPacketFromSkeleton(txSkeleton);
// Allow pay fee from the unlocked cell directly.
return createBuildingPacketFromSkeleton(txSkeleton, 0);
},
};
}
1 change: 1 addition & 0 deletions src/lib/lumos-adapter/init-lumos-common-scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export function buildLockInfo(ckbChainConfig, scriptInfo) {
//===========================
// 1.Add inputCell to txSkeleton
txMutable.update("inputs", (inputs) => inputs.push(inputCell));
txMutable.update("witnesses", (witnesses) => witnesses.push("0x"));

// 2. Add output. The function `lumos.commons.common.transfer` will scan outputs for available balance for each account.
const outputCell = {
Expand Down
Loading

0 comments on commit df47fb7

Please sign in to comment.