From 2c9dac5dbed2c8e5373f03134ca7e08038b31be7 Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 9 Feb 2024 15:03:27 +0800 Subject: [PATCH 1/5] :sparkles: Pack verifier for depositing --- bin/use-env.sh | 11 ++ src/actions/deposit.js | 6 +- .../u/[wallet]/[connection]/deposit/form.js | 15 ++- src/app/u/[wallet]/[connection]/sign-form.js | 5 +- src/components/packing-verifier-help-text.js | 11 ++ src/lib/cobuild/fee-manager.js | 15 ++- src/lib/cobuild/general-lock-actions.js | 6 +- src/lib/cobuild/lock-actions.js | 49 +++++++- .../cobuild/react/building-packet-review.js | 6 +- src/lib/config.js | 20 +++ src/lib/papps/dao/action-creators.js | 7 -- src/lib/papps/dao/verifier.js | 115 ++++++++++++++++++ 12 files changed, 243 insertions(+), 23 deletions(-) create mode 100644 src/components/packing-verifier-help-text.js create mode 100644 src/lib/papps/dao/verifier.js diff --git a/bin/use-env.sh b/bin/use-env.sh index c794482..4cb22b6 100755 --- a/bin/use-env.sh +++ b/bin/use-env.sh @@ -25,6 +25,7 @@ esac JOYID_INFO_FILE="$(ls migrations/joyid/*.json | grep -v deployment | head -n 1)" OMNILOCK_INFO_FILE="$(ls migrations/omnilock/*.json | grep -v deployment | head -n 1)" +DAO_ACTION_VERIFIER_INFO_FILE="$(ls migrations/dao-action-verifier/*.json | grep -v deployment | head -n 1)" sed -n \ -e 's/,$//' \ @@ -45,3 +46,13 @@ sed -n \ -e 's/,$//' \ -e 's/^ *"tx_hash": /NEXT_PUBLIC_OMNILOCK_TX_HASH=/p' \ "$OMNILOCK_INFO_FILE" | tail -1 + +sed -n \ + -e 's/,$//' \ + -e 's/^ *"type_id": "/NEXT_PUBLIC_DAO_ACTION_VERIFIER_CODE_HASH="/p' \ + "$DAO_ACTION_VERIFIER_INFO_FILE" | head -1 + +sed -n \ + -e 's/,$//' \ + -e 's/^ *"tx_hash": /NEXT_PUBLIC_DAO_ACTION_VERIFIER_TX_HASH=/p' \ + "$DAO_ACTION_VERIFIER_INFO_FILE" | tail -1 diff --git a/src/actions/deposit.js b/src/actions/deposit.js index 19341bc..b7f8a47 100644 --- a/src/actions/deposit.js +++ b/src/actions/deposit.js @@ -1,18 +1,22 @@ "use server"; -import { parseUnit } from "@ckb-lumos/bi"; import { depositDao } from "@/lib/cobuild/publishers"; import { getConfig } from "@/lib/config"; import { prepareLockActions } from "@/lib/cobuild/lock-actions"; import { payFee } from "@/lib/cobuild/fee-manager"; +import { prepareVerifier } from "@/lib/papps/dao/verifier"; export default async function deposit(_prevState, formData, config) { config = config ?? getConfig(); const from = formData.get("from"); + const shouldPackVerifier = formData.get("packVerifier") !== undefined; try { let buildingPacket = await depositDao(config)(formData); + if (shouldPackVerifier) { + buildingPacket = await prepareVerifier(buildingPacket, from, config); + } buildingPacket = await payFee( buildingPacket, [{ address: from, feeRate: 1200 }], diff --git a/src/app/u/[wallet]/[connection]/deposit/form.js b/src/app/u/[wallet]/[connection]/deposit/form.js index 1f7d603..0a9b8db 100644 --- a/src/app/u/[wallet]/[connection]/deposit/form.js +++ b/src/app/u/[wallet]/[connection]/deposit/form.js @@ -1,14 +1,15 @@ "use client"; +import { Alert, Checkbox, Label, Popover, TextInput } from "flowbite-react"; +import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useFormState } from "react-dom"; -import { useRouter } from "next/navigation"; -import { Label, TextInput, Alert } from "flowbite-react"; +import deposit from "@/actions/deposit"; +import { fetchAssetsWithCache } from "@/actions/fetch-assets"; import Capacity from "@/components/capacity"; +import PackingVerifierHelpText from "@/components/packing-verifier-help-text"; import SubmitButton from "@/components/submit-button"; -import { fetchAssetsWithCache } from "@/actions/fetch-assets"; -import deposit from "@/actions/deposit"; import Loading from "../loading"; import SignForm from "../sign-form"; import SubmitBuildingPacket from "../submit-building-packet"; @@ -52,6 +53,12 @@ export function TransactionForm({ formAction, formState, address }) { } /> +
+ + +
Deposit ); diff --git a/src/app/u/[wallet]/[connection]/sign-form.js b/src/app/u/[wallet]/[connection]/sign-form.js index 3e9a4a5..1823364 100644 --- a/src/app/u/[wallet]/[connection]/sign-form.js +++ b/src/app/u/[wallet]/[connection]/sign-form.js @@ -40,7 +40,10 @@ export default function SignForm({ ckbChainConfig, ); onSubmit( - finalizeWitnesses(applyLockAction(buildingPacket, lockAction, seal)), + finalizeWitnesses( + applyLockAction(buildingPacket, lockAction, seal), + ckbChainConfig, + ), ); } catch (err) { console.error(err.stack); diff --git a/src/components/packing-verifier-help-text.js b/src/components/packing-verifier-help-text.js new file mode 100644 index 0000000..6d52a71 --- /dev/null +++ b/src/components/packing-verifier-help-text.js @@ -0,0 +1,11 @@ +export default function PackingVerifierHelpText() { + return ( + + What's This? + + ); +} diff --git a/src/lib/cobuild/fee-manager.js b/src/lib/cobuild/fee-manager.js index 16d10da..231450b 100644 --- a/src/lib/cobuild/fee-manager.js +++ b/src/lib/cobuild/fee-manager.js @@ -23,6 +23,7 @@ export async function payFee(buildingPacket, feePayments, config) { ), buildingPacket, ), + config.ckbChainConfig, ); const buildingPacketHavePaidFee = await payFeeWithBuildingPacket( @@ -55,7 +56,12 @@ function storeWitnessForFeeEstimation( return generalLockActions.storeWitnessForFeeEstimation( buildingPacket, scriptHash, - inputIndices, + { + type: "WitnessArgsStore", + value: { + inputIndices, + }, + }, // Variable length, but 500 is usually enough. () => bytes.hexify(new Uint8Array(500)), ); @@ -65,7 +71,12 @@ function storeWitnessForFeeEstimation( return generalLockActions.storeWitnessForFeeEstimation( buildingPacket, scriptHash, - inputIndices, + { + type: "WitnessArgsStore", + value: { + inputIndices, + }, + }, // 85 = 65 signature in OmnilockWitnessLock () => `0x${"0".repeat(85 * 2)}`, ); diff --git a/src/lib/cobuild/general-lock-actions.js b/src/lib/cobuild/general-lock-actions.js index abce542..23bddff 100644 --- a/src/lib/cobuild/general-lock-actions.js +++ b/src/lib/cobuild/general-lock-actions.js @@ -66,13 +66,13 @@ export function prepareLockActionWithWitnessStore( export function storeWitnessForFeeEstimation( buildingPacket, scriptHash, - inputIndices, + witnessStore, createSealPlaceHolder, ) { - buildingPacket = prepareLockAction( + buildingPacket = prepareLockActionWithWitnessStore( buildingPacket, scriptHash, - inputIndices, + witnessStore, createSealPlaceHolder, ); const lockAction = buildingPacket.value.lockActions.find( diff --git a/src/lib/cobuild/lock-actions.js b/src/lib/cobuild/lock-actions.js index 556fae9..1e04c43 100644 --- a/src/lib/cobuild/lock-actions.js +++ b/src/lib/cobuild/lock-actions.js @@ -1,8 +1,9 @@ import { utils as lumosBaseUtils } from "@ckb-lumos/base"; +import { bytes } from "@ckb-lumos/codec"; import * as generalLockActions from "./general-lock-actions"; -import { parseWitnessType } from "./types"; import { groupByLock } from "./script-group"; +import { parseWitnessType, WitnessLayout } from "./types"; const { computeScriptHash } = lumosBaseUtils; @@ -10,6 +11,11 @@ const { computeScriptHash } = lumosBaseUtils; // // Do not set any witness in payload to use the Cobuild layout. export function prepareLockActions(buildingPacket, ckbChainConfig) { + // Workaround to pack Cobuild message as the extra witness. + // + // Before the next action, there's no actual witness set yet. So `finalizeWitnesses` will assume there's no SighashAll witness in the tx and will pack the message as the extra witness. This assumption is currect because this PoC uses WitnessArgs layout for lock actions and DAO type script. + buildingPacket = finalizeWitnesses(buildingPacket, ckbChainConfig); + const groups = groupByLock(buildingPacket.value.resolvedInputs.outputs); return Object.entries(groups).reduce( (acc, [scriptHash, inputs]) => @@ -23,11 +29,28 @@ export function prepareLockActions(buildingPacket, ckbChainConfig) { ); } -export function finalizeWitnesses(buildingPacket) { +function hasVerifierCell(buildingPacket, ckbChainConfig) { + const template = ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER; + for (const output of buildingPacket.value.payload.outputs) { + if ( + output.type !== undefined && + output.type !== null && + output.type.codeHash === template.CODE_HASH + ) { + return true; + } + } + + return false; +} + +export function finalizeWitnesses(buildingPacket, ckbChainConfig) { // fill holes const witnesses = Array.from(buildingPacket.value.payload.witnesses).map( (w) => w ?? "0x", ); + + let hasSighashAll = false; // If there's no SighashAll before SighashAllOnly, replace the first SighashAllOnly with SighashAll for (const i of witnesses.keys()) { if (witnesses[i] === "0x") { @@ -35,8 +58,10 @@ export function finalizeWitnesses(buildingPacket) { } const witnessType = parseWitnessType(witnesses[i]); if (witnessType === "SighashAll") { + hasSighashAll = true; break; } else if (witnessType === "SighashAllOnly") { + hasSighashAll = true; const witness = WitnessLayout.unpack(witnesses[i]); witnesses[i] = bytes.hexify( WitnessLayout.pack({ @@ -50,6 +75,26 @@ export function finalizeWitnesses(buildingPacket) { } } + // if there's a verifier cell, pack the Cobuild message into witness + if (!hasSighashAll && hasVerifierCell(buildingPacket, ckbChainConfig)) { + for ( + let i = witnesses.length; + i < buildingPacket.value.payload.inputs.length; + ++i + ) { + witnesses[i] = "0x"; + } + witnesses[buildingPacket.value.payload.inputs.length] = bytes.hexify( + WitnessLayout.pack({ + type: "SighashAll", + value: { + seal: "0x", + message: buildingPacket.value.message, + }, + }), + ); + } + return { type: buildingPacket.type, value: { diff --git a/src/lib/cobuild/react/building-packet-review.js b/src/lib/cobuild/react/building-packet-review.js index ab74cfd..116eca1 100644 --- a/src/lib/cobuild/react/building-packet-review.js +++ b/src/lib/cobuild/react/building-packet-review.js @@ -399,7 +399,7 @@ export function TxSection({ return (
- {process.env.DEBUG !== undefined ? ( + {process.env.NODE_ENV === "development" ? (
Hash
@@ -567,7 +567,7 @@ function collectAssets( } else if (isNoneDaoTypedCell(cellOutput, cellData, ckbChainConfig)) { assets.destroyedTypedCells.push({ cellOutput, - outPoint: buildingPacket.value.packet.inputs[i].previousOutput, + outPoint: buildingPacket.value.payload.inputs[i].previousOutput, data: cellData, }); } @@ -582,7 +582,7 @@ function collectAssets( assets.daoDeposited = assets.daoDeposited.add(cellCapacity); } else if (isNoneDaoTypedCell(cellOutput, cellData, ckbChainConfig)) { assets.createdTypedCells.push({ - outPoint: buildingPacket.value.packet.inputs[i].previousOutput, + outPoint: buildingPacket.value.payload.outputs[i].previousOutput, data: cellData, }); } diff --git a/src/lib/config.js b/src/lib/config.js index fb04a8a..f0744ec 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -27,6 +27,16 @@ const CKB_CHAINS_CONFIGS = { INDEX: "0x0", DEP_TYPE: "code", }, + + DAO_ACTION_VERIFIER: { + CODE_HASH: + "0xbdca5b74e5d0c913fed19d8482a99af1ef8a639541438b2e00189f5e18907ef9", + HASH_TYPE: "type", + TX_HASH: + "0x9157bcc278176ba9e823a50d72631be9e9b964e7a5ca11db2782c059c4c788ad", + INDEX: "0x0", + DEP_TYPE: "code", + }, }, }, }; @@ -66,6 +76,15 @@ function buildCkbChainConfig(ckbChain) { TX_HASH: presence(process.env.NEXT_PUBLIC_OMNILOCK_TX_HASH), }, ); + const DAO_ACTION_VERIFIER = assign( + { ...template.SCRIPTS.DAO_ACTION_VERIFIER }, + { + CODE_HASH: presence( + process.env.NEXT_PUBLIC_DAO_ACTION_VERIFIER_CODE_HASH, + ), + TX_HASH: presence(process.env.NEXT_PUBLIC_DAO_ACTION_VERIFIER_TX_HASH), + }, + ); const tx0 = presence(process.env.NEXT_PUBLIC_CKB_GENESIS_TX_0) ?? @@ -80,6 +99,7 @@ function buildCkbChainConfig(ckbChain) { SCRIPTS: { JOYID, OMNILOCK_CUSTOM, + DAO_ACTION_VERIFIER, DAO: { ...template.SCRIPTS.DAO, TX_HASH: tx0, diff --git a/src/lib/papps/dao/action-creators.js b/src/lib/papps/dao/action-creators.js index f8a8c81..e22023c 100644 --- a/src/lib/papps/dao/action-creators.js +++ b/src/lib/papps/dao/action-creators.js @@ -11,13 +11,6 @@ function addressToScriptOpt(address, lumosOptions) { return undefined; } -function buildSingleOperation(operation) { - return { - type: "SingleOperation", - value: operation, - }; -} - export function depositWithFormData(config, formData) { const lumosOptions = { config: config.ckbChainConfig }; diff --git a/src/lib/papps/dao/verifier.js b/src/lib/papps/dao/verifier.js new file mode 100644 index 0000000..fcace0a --- /dev/null +++ b/src/lib/papps/dao/verifier.js @@ -0,0 +1,115 @@ +import { Indexer } from "@ckb-lumos/ckb-indexer"; +import { + minimalCellCapacityCompatible, + addressToScript, +} from "@ckb-lumos/helpers"; +import { common as commonScripts } from "@ckb-lumos/common-scripts"; +import { mergeBuildingPacketFromSkeleton } from "@/lib/lumos-adapter/create-building-packet-from-skeleton"; +import createSkeletonFromBuildingPacket from "@/lib/lumos-adapter/create-skeleton-from-building-packet"; + +function buildScript(scriptInfo, args) { + return { + codeHash: scriptInfo.CODE_HASH, + hashType: scriptInfo.HASH_TYPE, + args, + }; +} + +function buildCellDep(scriptInfo) { + return { + outPoint: { + txHash: scriptInfo.TX_HASH, + index: scriptInfo.INDEX, + }, + depType: scriptInfo.DEP_TYPE, + }; +} + +export async function prepareVerifier(buildingPacket, fromAddress, config) { + let txSkeleton = createSkeletonFromBuildingPacket(buildingPacket, config); + + const { ckbChainConfig, ckbRpcUrl } = config; + const indexer = new Indexer(ckbRpcUrl); + const verifierTypeScript = buildScript( + ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER, + "0x", + ); + const from = addressToScript(fromAddress, { config: ckbChainConfig }); + + const verifierCollector = indexer.collector({ + lock: from, + argsLen: (from.args.length - 2) / 2, + type: verifierTypeScript, + data: "0x", + }); + const verifierCell = (await verifierCollector.collect().next()).value; + if (verifierCell === null || verifierCell === undefined) { + const newVerifierCell = { + cellOutput: { + capacity: "0x0", + lock: from, + type: verifierTypeScript, + }, + data: "0x", + outPoint: undefined, + blockHash: undefined, + }; + const minimalCapacity = minimalCellCapacityCompatible(newVerifierCell); + newVerifierCell.cellOutput.capacity = minimalCapacity.toHexString(); + + // create a new verifier cell + txSkeleton = await commonScripts.injectCapacity( + txSkeleton, + [fromAddress], + minimalCapacity, + fromAddress, + undefined, + { config: ckbChainConfig }, + ); + txSkeleton = txSkeleton.update("outputs", (outputs) => + outputs.push(newVerifierCell), + ); + } else { + txSkeleton = await commonScripts.setupInputCell( + txSkeleton, + verifierCell, + fromAddress, + { + config: ckbChainConfig, + }, + ); + } + + // exchange change output and verifier cell + const changeOutputIndex = buildingPacket.value.changeOutput; + const outputsSize = txSkeleton.get("outputs").size; + + if ( + changeOutputIndex !== undefined && + changeOutputIndex !== null && + changeOutputIndex < outputsSize - 1 + ) { + // exchange change cell and verifier cell to avoid multiple change cells + txSkeleton = txSkeleton.update("outputs", (outputs) => { + const verifierCell = outputs.get(outputsSize - 1); + const changeCell = outputs.get(changeOutputIndex); + return outputs + .set(changeOutputIndex, verifierCell) + .set(outputsSize - 1, changeCell); + }); + txSkeleton = txSkeleton.update("fixedEntries", (fixedEntries) => + fixedEntries.push({ field: "outputs", index: outputsSize - 2 }), + ); + } else { + // no change output, fixed all outputs + txSkeleton = txSkeleton.update("fixedEntries", (fixedEntries) => + fixedEntries.push({ field: "outputs", index: outputsSize - 1 }), + ); + } + + txSkeleton = txSkeleton.update("cellDeps", (cellDeps) => + cellDeps.push(buildCellDep(ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER)), + ); + + return mergeBuildingPacketFromSkeleton(buildingPacket, txSkeleton); +} From bb68434bd9f3483614b9cef6c3906f023d57cc65 Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 9 Feb 2024 16:54:08 +0800 Subject: [PATCH 2/5] :sparkles: Pack verifier for withdrawing --- src/actions/withdraw.js | 6 +- .../withdraw/[txHash]/[index]/form.js | 58 +++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/actions/withdraw.js b/src/actions/withdraw.js index bb2db8c..68ae07d 100644 --- a/src/actions/withdraw.js +++ b/src/actions/withdraw.js @@ -4,12 +4,16 @@ import { withdrawDao } from "@/lib/cobuild/publishers"; import { getConfig } from "@/lib/config"; import { prepareLockActions } from "@/lib/cobuild/lock-actions"; import { payFee } from "@/lib/cobuild/fee-manager"; +import { prepareVerifier } from "@/lib/papps/dao/verifier"; -export default async function withdraw(from, cell, config) { +export default async function withdraw(from, cell, shouldPackVerifier, config) { config = config ?? getConfig(); try { let buildingPacket = await withdrawDao(config)({ cell }); + if (shouldPackVerifier) { + buildingPacket = await prepareVerifier(buildingPacket, from, config); + } buildingPacket = await payFee( buildingPacket, [{ address: from, feeRate: 1200 }], diff --git a/src/app/u/[wallet]/[connection]/withdraw/[txHash]/[index]/form.js b/src/app/u/[wallet]/[connection]/withdraw/[txHash]/[index]/form.js index 8e2face..e8e40be 100644 --- a/src/app/u/[wallet]/[connection]/withdraw/[txHash]/[index]/form.js +++ b/src/app/u/[wallet]/[connection]/withdraw/[txHash]/[index]/form.js @@ -1,13 +1,14 @@ "use client"; -import { useState } from "react"; +import { Alert, Button, Checkbox, Label } from "flowbite-react"; import { useRouter } from "next/navigation"; -import { Alert, Button } from "flowbite-react"; +import { useState } from "react"; import withdraw from "@/actions/withdraw"; -import useTipHeader from "@/hooks/use-tip-header"; -import useHeader from "@/hooks/use-header"; +import PackingVerifierHelpText from "@/components/packing-verifier-help-text"; import useCell from "@/hooks/use-cell"; +import useHeader from "@/hooks/use-header"; +import useTipHeader from "@/hooks/use-tip-header"; import Capacity from "@/components/capacity"; import { @@ -16,9 +17,9 @@ import { daoCycleProgressColor, } from "@/components/dao-cycle-progress"; import * as dao from "@/lib/dao"; -import Loading from "./loading"; import SignForm from "../../../sign-form"; import SubmitBuildingPacket from "../../../submit-building-packet"; +import Loading from "./loading"; function CellDetailsDisplay({ progress, cell, depositHeader, tipHeader }) { return ( @@ -44,7 +45,13 @@ function CellDetailsDisplay({ progress, cell, depositHeader, tipHeader }) { ); } -function CellDetails({ cell, pending, onConfirm }) { +function CellDetails({ + cell, + pending, + onConfirm, + shouldPackVerifier, + setShouldPackVerifier, +}) { const tipHeader = useTipHeader(); const depositHeader = useHeader(cell.blockHash); @@ -66,15 +73,39 @@ function CellDetails({ cell, pending, onConfirm }) { > Confirm Withdraw + { + setShouldPackVerifier(e.target.checked); + }} + /> +

); } -function LoadCell({ outPoint, pending, onConfirm }) { +function LoadCell({ + outPoint, + pending, + onConfirm, + shouldPackVerifier, + setShouldPackVerifier, +}) { const cell = useCell(outPoint); - const childProps = { cell, pending, onConfirm }; + const childProps = { + cell, + pending, + onConfirm, + shouldPackVerifier, + setShouldPackVerifier, + }; return cell ? : ; } @@ -88,12 +119,13 @@ export default function WithdrawForm({ const router = useRouter(); const [formState, setFormState] = useState({}); const [pending, setPending] = useState(false); + const [shouldPackVerifier, setShouldPackVerifier] = useState(false); const [signedBuildingPacket, setSignedBuildingPacket] = useState(null); const back = () => router.back(); const onConfirm = async (cell) => { setPending(true); try { - setFormState(await withdraw(address, cell)); + setFormState(await withdraw(address, cell, shouldPackVerifier)); } catch (err) { setFormState({ error: err.toString() }); } @@ -104,7 +136,13 @@ export default function WithdrawForm({ formState.buildingPacket === null || formState.buildingPacket === undefined ) { - const childProps = { outPoint, pending, onConfirm }; + const childProps = { + outPoint, + pending, + onConfirm, + shouldPackVerifier, + setShouldPackVerifier, + }; return ( <> {formState.error ? ( From 28adb376c2509b482ab27c8687f2ad07cddccd71 Mon Sep 17 00:00:00 2001 From: ian Date: Fri, 9 Feb 2024 16:58:36 +0800 Subject: [PATCH 3/5] :sparkles: Pack verifier for claiming --- src/actions/claim.js | 6 +- .../claim/[txHash]/[index]/form.js | 58 +++++++++++++++---- 2 files changed, 53 insertions(+), 11 deletions(-) diff --git a/src/actions/claim.js b/src/actions/claim.js index 5e12b02..01a10f7 100644 --- a/src/actions/claim.js +++ b/src/actions/claim.js @@ -4,12 +4,16 @@ import { claimDao } from "@/lib/cobuild/publishers"; import { getConfig } from "@/lib/config"; import { prepareLockActions } from "@/lib/cobuild/lock-actions"; import { payFee } from "@/lib/cobuild/fee-manager"; +import { prepareVerifier } from "@/lib/papps/dao/verifier"; -export default async function withdraw(from, cell, config) { +export default async function withdraw(from, cell, shouldPackVerifier, config) { config = config ?? getConfig(); try { let buildingPacket = await claimDao(config)({ cell }); + if (shouldPackVerifier) { + buildingPacket = await prepareVerifier(buildingPacket, from, config); + } buildingPacket = await payFee( buildingPacket, [{ address: from, feeRate: 1200 }], diff --git a/src/app/u/[wallet]/[connection]/claim/[txHash]/[index]/form.js b/src/app/u/[wallet]/[connection]/claim/[txHash]/[index]/form.js index 918eeda..1aef221 100644 --- a/src/app/u/[wallet]/[connection]/claim/[txHash]/[index]/form.js +++ b/src/app/u/[wallet]/[connection]/claim/[txHash]/[index]/form.js @@ -1,20 +1,21 @@ "use client"; -import { useState } from "react"; +import { Alert, Button, Checkbox, Label } from "flowbite-react"; import { useRouter } from "next/navigation"; -import { Alert, Button } from "flowbite-react"; +import { useState } from "react"; import claim from "@/actions/claim"; -import useTipHeader from "@/hooks/use-tip-header"; +import PackingVerifierHelpText from "@/components/packing-verifier-help-text"; +import useCell from "@/hooks/use-cell"; import useHeader from "@/hooks/use-header"; import useHeaderByNumber from "@/hooks/use-header-by-number"; -import useCell from "@/hooks/use-cell"; +import useTipHeader from "@/hooks/use-tip-header"; import Capacity from "@/components/capacity"; import * as dao from "@/lib/dao"; -import Loading from "./loading"; import SignForm from "../../../sign-form"; import SubmitBuildingPacket from "../../../submit-building-packet"; +import Loading from "./loading"; function CellDetailsDisplay({ cell, depositHeader, withdrawHeader }) { return ( @@ -31,7 +32,13 @@ function CellDetailsDisplay({ cell, depositHeader, withdrawHeader }) { ); } -function CellDetails({ cell, pending, onConfirm }) { +function CellDetails({ + cell, + pending, + onConfirm, + shouldPackVerifier, + setShouldPackVerifier, +}) { const tipHeader = useTipHeader(); const depositBlockNumber = dao.getDepositBlockNumberFromWithdrawCell(cell); const depositHeader = useHeaderByNumber(depositBlockNumber); @@ -62,6 +69,18 @@ function CellDetails({ cell, pending, onConfirm }) { Claim Now )} + { + setShouldPackVerifier(e.target.checked); + }} + /> +

: ; } @@ -86,12 +117,13 @@ export default function ClaimForm({ const router = useRouter(); const [formState, setFormState] = useState({}); const [pending, setPending] = useState(false); + const [shouldPackVerifier, setShouldPackVerifier] = useState(false); const [signedBuildingPacket, setSignedBuildingPacket] = useState(null); const back = () => router.back(); const onConfirm = async (cell) => { setPending(true); try { - setFormState(await claim(address, cell)); + setFormState(await claim(address, cell, shouldPackVerifier)); } catch (err) { setFormState({ error: err.toString() }); } @@ -102,7 +134,13 @@ export default function ClaimForm({ formState.buildingPacket === null || formState.buildingPacket === undefined ) { - const childProps = { outPoint, pending, onConfirm }; + const childProps = { + outPoint, + pending, + onConfirm, + shouldPackVerifier, + setShouldPackVerifier, + }; return ( <> {formState.error ? ( From 22b085b27b70444e6b14e68fc68a57136c163322 Mon Sep 17 00:00:00 2001 From: ian Date: Sun, 18 Feb 2024 11:44:29 +0800 Subject: [PATCH 4/5] :sparkles: Reclaim CKB locked by dao verifier cells --- src/actions/get-verifier-cells.js | 29 +++++ src/actions/reclaim.js | 29 +++++ src/app/u/[wallet]/[connection]/assets.js | 56 +++++++++- .../u/[wallet]/[connection]/reclaim/form.js | 103 ++++++++++++++++++ .../[wallet]/[connection]/reclaim/loading.js | 5 + .../u/[wallet]/[connection]/reclaim/page.js | 22 ++++ src/components/packing-verifier-help-text.js | 2 +- src/hooks/use-verifier-cells.js | 12 ++ src/lib/cobuild/assets-manager.js | 32 ++++-- src/lib/cobuild/publishers.js | 4 + .../cobuild/react/building-packet-review.js | 19 ++-- src/lib/config.js | 18 +++ .../lumos-adapter/create-lumos-ckb-builder.js | 59 ++++++++-- .../init-lumos-common-scripts.js | 19 +--- src/lib/papps/dao/lumos-callbacks.js | 26 ++--- src/lib/papps/dao/script-info.js | 8 +- src/lib/papps/dao/verifier.js | 27 +---- src/lib/wallet/btc-wallet.js | 7 +- src/lib/wallet/joyid.js | 9 +- 19 files changed, 383 insertions(+), 103 deletions(-) create mode 100644 src/actions/get-verifier-cells.js create mode 100644 src/actions/reclaim.js create mode 100644 src/app/u/[wallet]/[connection]/reclaim/form.js create mode 100644 src/app/u/[wallet]/[connection]/reclaim/loading.js create mode 100644 src/app/u/[wallet]/[connection]/reclaim/page.js create mode 100644 src/hooks/use-verifier-cells.js diff --git a/src/actions/get-verifier-cells.js b/src/actions/get-verifier-cells.js new file mode 100644 index 0000000..38d200c --- /dev/null +++ b/src/actions/get-verifier-cells.js @@ -0,0 +1,29 @@ +"use server"; + +import { cache } from "react"; +import { getConfig, buildScript } from "@/lib/config"; +import { addressToScript } from "@ckb-lumos/helpers"; +import { Indexer } from "@ckb-lumos/ckb-indexer"; + +function buildVerifierScript(ckbChainConfig) { + return buildScript(ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER, "0x"); +} + +export async function getVerifierCellsWithoutCache(lockAddress, config) { + const { ckbRpcUrl, ckbChainConfig } = config ?? getConfig(); + const indexer = new Indexer(ckbRpcUrl); + + const lock = addressToScript(lockAddress, { config: ckbChainConfig }); + const collector = indexer.collector({ + lock, + argsLen: (lock.args.length - 2) / 2, + type: buildVerifierScript(ckbChainConfig), + }); + const verifierCells = []; + for await (const cell of collector.collect()) { + verifierCells.push(cell); + } + return verifierCells; +} + +export const getVerifierCellsWithCache = cache(getVerifierCellsWithoutCache); diff --git a/src/actions/reclaim.js b/src/actions/reclaim.js new file mode 100644 index 0000000..9cbd559 --- /dev/null +++ b/src/actions/reclaim.js @@ -0,0 +1,29 @@ +"use server"; + +import { reclaimDaoVerifiers } from "@/lib/cobuild/publishers"; +import { getConfig } from "@/lib/config"; +import { prepareLockActions } from "@/lib/cobuild/lock-actions"; +import { payFee } from "@/lib/cobuild/fee-manager"; + +export default async function reclaim(from, config) { + config = config ?? getConfig(); + + try { + let buildingPacket = await reclaimDaoVerifiers(config)({ from }); + 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(), + }; + } +} diff --git a/src/app/u/[wallet]/[connection]/assets.js b/src/app/u/[wallet]/[connection]/assets.js index 9190655..f4e4868 100644 --- a/src/app/u/[wallet]/[connection]/assets.js +++ b/src/app/u/[wallet]/[connection]/assets.js @@ -1,7 +1,10 @@ -import Link from "next/link"; +import { BI, formatUnit } from "@ckb-lumos/bi"; import { Button } from "flowbite-react"; -import Capacity from "@/components/capacity"; +import Link from "next/link"; + import { fetchAssetsWithCache } from "@/actions/fetch-assets"; +import Capacity from "@/components/capacity"; +import PackingVerifierHelpText from "@/components/packing-verifier-help-text"; import DaoCells from "./dao-cells"; import Loading from "./loading"; @@ -25,7 +28,38 @@ export function CkbSection({ wallet, connection, address, ckbBalance }) { ); } -export function DaoSection({ wallet, connection, address, daoCells }) { +export function VerifierCells({ wallet, connection, verifierCells }) { + let lockedCapacity = BI.from(0); + for (const cell of verifierCells) { + lockedCapacity = lockedCapacity.add(BI.from(cell.cellOutput.capacity)); + } + return ( + <> +

Verifier Cells

+ +

+ You have {formatUnit(lockedCapacity, "ckb")} CKB locked in DAO action + verifier cells. You can reclaim them by destroy the verifier cells. +

+ + + ); +} + +export function DaoSection({ + wallet, + connection, + address, + daoCells, + verifierCells, +}) { return (

DAO

@@ -44,6 +78,13 @@ export function DaoSection({ wallet, connection, address, daoCells }) { address={address} daoCells={daoCells} /> + {verifierCells.length > 0 ? ( + + ) : null}
); } @@ -64,13 +105,18 @@ export function AssetsFallback() { } export default async function Assets({ wallet, connection, address }) { - const { ckbBalance, daoCells } = await fetchAssetsWithCache(address); + const { ckbBalance, daoCells, verifierCells } = + await fetchAssetsWithCache(address); const childProps = { wallet, connection, address }; return ( <> - + ); } diff --git a/src/app/u/[wallet]/[connection]/reclaim/form.js b/src/app/u/[wallet]/[connection]/reclaim/form.js new file mode 100644 index 0000000..fd3b5c6 --- /dev/null +++ b/src/app/u/[wallet]/[connection]/reclaim/form.js @@ -0,0 +1,103 @@ +"use client"; + +import { BI, formatUnit } from "@ckb-lumos/bi"; +import { Alert, Button, Checkbox, Label } from "flowbite-react"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; + +import reclaim from "@/actions/reclaim"; +import useVerifierCells from "@/hooks/use-verifier-cells"; + +import SignForm from "../sign-form"; +import SubmitBuildingPacket from "../submit-building-packet"; + +function FormDisplay({ pending, onConfirm, verifierCells }) { + let lockedCapacity = BI.from(0); + if (verifierCells !== undefined) { + for (const cell of verifierCells) { + lockedCapacity = lockedCapacity.add(BI.from(cell.cellOutput.capacity)); + } + } + + return ( + <> +

+ +

+ {lockedCapacity.gt(0) ? ( +

+ You can reclaim {formatUnit(lockedCapacity, "ckb")} CKB from{" "} + {verifierCells.length} {verifierCells.length == 1 ? "cell" : "cells"}. +

+ ) : null} + + ); +} + +export default function WithdrawForm({ wallet, connection, address, config }) { + const router = useRouter(); + const verifierCells = useVerifierCells(address); + const [formState, setFormState] = useState({}); + const [pending, setPending] = useState(false); + const [signedBuildingPacket, setSignedBuildingPacket] = useState(null); + const back = () => router.back(); + const onConfirm = async () => { + setPending(true); + try { + setFormState(await reclaim(address)); + } catch (err) { + setFormState({ error: err.toString() }); + } + setPending(false); + }; + + if ( + formState.buildingPacket === null || + formState.buildingPacket === undefined + ) { + const childProps = { + pending, + onConfirm, + verifierCells, + }; + return ( + <> + {formState.error ? ( + + {formState.error} + + ) : null} + + + ); + } else if ( + signedBuildingPacket === null || + signedBuildingPacket === undefined + ) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/src/app/u/[wallet]/[connection]/reclaim/loading.js b/src/app/u/[wallet]/[connection]/reclaim/loading.js new file mode 100644 index 0000000..d0ed745 --- /dev/null +++ b/src/app/u/[wallet]/[connection]/reclaim/loading.js @@ -0,0 +1,5 @@ +import { Spinner } from "flowbite-react"; + +export default function Loading() { + return ; +} diff --git a/src/app/u/[wallet]/[connection]/reclaim/page.js b/src/app/u/[wallet]/[connection]/reclaim/page.js new file mode 100644 index 0000000..7082e99 --- /dev/null +++ b/src/app/u/[wallet]/[connection]/reclaim/page.js @@ -0,0 +1,22 @@ +import { getConfig } from "@/lib/config"; +import * as walletSelector from "@/lib/wallet/selector"; + +import ReclaimForm from "./form"; + +export default function Withdraw({ params: { wallet, connection }, config }) { + config = config ?? getConfig(); + const address = walletSelector.address( + wallet, + connection, + config.ckbChainConfig, + ); + + const childProps = { wallet, connection, address, config }; + + return ( +
+

Reclaim DAO Verifiers

+ +
+ ); +} diff --git a/src/components/packing-verifier-help-text.js b/src/components/packing-verifier-help-text.js index 6d52a71..3724fe3 100644 --- a/src/components/packing-verifier-help-text.js +++ b/src/components/packing-verifier-help-text.js @@ -5,7 +5,7 @@ export default function PackingVerifierHelpText() { className="hover:cursor-pointer" href="https://github.com/cryptape/ckb-dao-cobuild-poc/blob/develop/docs/packing-verifier.md" > - What's This? + What's This? ); } diff --git a/src/hooks/use-verifier-cells.js b/src/hooks/use-verifier-cells.js new file mode 100644 index 0000000..bf8142b --- /dev/null +++ b/src/hooks/use-verifier-cells.js @@ -0,0 +1,12 @@ +import { useEffect, useState } from "react"; +import { getVerifierCellsWithCache } from "@/actions/get-verifier-cells"; + +export default function useVerifierCells(fromAddress) { + const [verifierCells, setVerifierCells] = useState(); + + useEffect(() => { + getVerifierCellsWithCache(fromAddress).then(setVerifierCells); + }, [fromAddress, setVerifierCells]); + + return verifierCells; +} diff --git a/src/lib/cobuild/assets-manager.js b/src/lib/cobuild/assets-manager.js index dcdca9e..bdc6a1e 100644 --- a/src/lib/cobuild/assets-manager.js +++ b/src/lib/cobuild/assets-manager.js @@ -1,6 +1,7 @@ import { Indexer } from "@ckb-lumos/ckb-indexer"; import { addressToScript } from "@ckb-lumos/helpers"; import { BI } from "@ckb-lumos/bi"; +import { buildScript } from "@/lib/config"; export const DEPOSIT_DAO_DATA = "0x0000000000000000"; @@ -29,14 +30,22 @@ async function reduceDao(collector) { return { deposits, withdraws }; } +async function reduceVerifierCells(collector) { + const cells = []; + + for await (const cell of collector.collect()) { + cells.push(cell); + } + + return cells; +} + function buildDaoScript(ckbChainConfig) { - const template = ckbChainConfig.SCRIPTS.DAO; + return buildScript(ckbChainConfig.SCRIPTS.DAO, "0x"); +} - return { - codeHash: template.CODE_HASH, - hashType: template.HASH_TYPE, - args: "0x", - }; +function buildVerifierScript(ckbChainConfig) { + return buildScript(ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER, "0x"); } export async function fetchAssets(address, { ckbRpcUrl, ckbChainConfig }) { @@ -58,7 +67,14 @@ export async function fetchAssets(address, { ckbRpcUrl, ckbChainConfig }) { }); const daoCells = await reduceDao(daoCollector); - return { ckbBalance, daoCells }; + const verifierCollector = indexer.collector({ + lock, + argsLen: (lock.args.length - 2) / 2, + type: buildVerifierScript(ckbChainConfig), + }); + const verifierCells = await reduceVerifierCells(verifierCollector); + + return { ckbBalance, daoCells, verifierCells }; } export function isDaoWithdrawCell(cellOutput, cellData, ckbChainConfig) { @@ -75,7 +91,7 @@ export function isDaoDepositCell(cellOutput, cellData, ckbChainConfig) { ); } -export function isNoneDaoTypedCell(cellOutput, cellData, ckbChainConfig) { +export function isNoneDaoTypedCell(cellOutput, ckbChainConfig) { const dao = ckbChainConfig.SCRIPTS.DAO; return cellOutput.type && cellOutput.type.codeHash !== dao.CODE_HASH; } diff --git a/src/lib/cobuild/publishers.js b/src/lib/cobuild/publishers.js index 50dff14..6a88f78 100644 --- a/src/lib/cobuild/publishers.js +++ b/src/lib/cobuild/publishers.js @@ -8,6 +8,10 @@ export function transferCkb(config) { return createLumosCkbBuilder(config).transferCkb; } +export function reclaimDaoVerifiers(config) { + return createLumosCkbBuilder(config).reclaimDaoVerifiers; +} + export function depositDao(config) { return createLumosCkbBuilder(config).depositDao; } diff --git a/src/lib/cobuild/react/building-packet-review.js b/src/lib/cobuild/react/building-packet-review.js index 116eca1..8b0fb15 100644 --- a/src/lib/cobuild/react/building-packet-review.js +++ b/src/lib/cobuild/react/building-packet-review.js @@ -3,21 +3,20 @@ import { Accordion, Spinner } from "flowbite-react"; import moment from "moment"; +import { DaoActionData } from "@/lib/papps/dao/schema"; +import { getDaoScriptHash } from "@/lib/papps/dao/script-info"; import { blockchain, utils as lumosBaseUtils } from "@ckb-lumos/base"; import { BI, formatUnit } from "@ckb-lumos/bi"; import * as lumosHelpers from "@ckb-lumos/helpers"; -import { DaoActionData } from "@/lib/papps/dao/schema"; -import { getDaoScriptHash } from "@/lib/papps/dao/script-info"; - -const { ckbHash } = lumosBaseUtils; - import { - isDaoWithdrawCell, isDaoDepositCell, + isDaoWithdrawCell, isNoneDaoTypedCell, } from "../assets-manager"; +const { ckbHash } = lumosBaseUtils; + function BriefAddress({ address }) { const display = `${address.slice(0, 15)}...${address.slice(-14)}`; return {display}; @@ -399,9 +398,9 @@ export function TxSection({ return (
- {process.env.NODE_ENV === "development" ? ( + {process.env.NEXT_PUBLIC_DEBUG ? (
-
Hash
+
Building Packet
               {JSON.stringify(buildingPacket, null, 2)}
@@ -564,7 +563,7 @@ function collectAssets(
       }
     } else if (isDaoDepositCell(cellOutput, cellData, ckbChainConfig)) {
       assets.daoWithdrawn = assets.daoWithdrawn.add(cellCapacity);
-    } else if (isNoneDaoTypedCell(cellOutput, cellData, ckbChainConfig)) {
+    } else if (isNoneDaoTypedCell(cellOutput, ckbChainConfig)) {
       assets.destroyedTypedCells.push({
         cellOutput,
         outPoint: buildingPacket.value.payload.inputs[i].previousOutput,
@@ -580,7 +579,7 @@ function collectAssets(
 
     if (isDaoDepositCell(cellOutput, cellData, ckbChainConfig)) {
       assets.daoDeposited = assets.daoDeposited.add(cellCapacity);
-    } else if (isNoneDaoTypedCell(cellOutput, cellData, ckbChainConfig)) {
+    } else if (isNoneDaoTypedCell(cellOutput, ckbChainConfig)) {
       assets.createdTypedCells.push({
         outPoint: buildingPacket.value.payload.outputs[i].previousOutput,
         data: cellData,
diff --git a/src/lib/config.js b/src/lib/config.js
index f0744ec..d0f69b0 100644
--- a/src/lib/config.js
+++ b/src/lib/config.js
@@ -146,3 +146,21 @@ export function getTestnetConfig() {
     ckbChainConfig: CKB_CHAINS_CONFIGS[DEFAULT_CKB_CHAIN],
   };
 }
+
+export function buildCellDep(scriptInfo) {
+  return {
+    outPoint: {
+      txHash: scriptInfo.TX_HASH,
+      index: scriptInfo.INDEX,
+    },
+    depType: scriptInfo.DEP_TYPE,
+  };
+}
+
+export function buildScript(scriptInfo, args) {
+  return {
+    codeHash: scriptInfo.CODE_HASH,
+    hashType: scriptInfo.HASH_TYPE,
+    args,
+  };
+}
diff --git a/src/lib/lumos-adapter/create-lumos-ckb-builder.js b/src/lib/lumos-adapter/create-lumos-ckb-builder.js
index 6acea5b..bbe0fa5 100644
--- a/src/lib/lumos-adapter/create-lumos-ckb-builder.js
+++ b/src/lib/lumos-adapter/create-lumos-ckb-builder.js
@@ -1,15 +1,16 @@
-import { Indexer } from "@ckb-lumos/ckb-indexer";
-import { TransactionSkeleton } from "@ckb-lumos/helpers";
-import { common as commonScripts } from "@ckb-lumos/common-scripts";
-
+import { buildCellDep, buildScript } from "@/lib/config";
+import { getDaoPappRegistry } from "@/lib/papps/dao/registry";
 import {
-  createBuildingPacketByFormDataCreator,
   createBuildingPacketByCreator,
+  createBuildingPacketByFormDataCreator,
 } from "@/lib/papps/papp";
-import { getDaoPappRegistry } from "@/lib/papps/dao/registry";
+import { BI } from "@ckb-lumos/bi";
+import { Indexer } from "@ckb-lumos/ckb-indexer";
+import { common as commonScripts } from "@ckb-lumos/common-scripts";
+import { TransactionSkeleton, addressToScript } from "@ckb-lumos/helpers";
 
-import initLumosCommonScripts from "./init-lumos-common-scripts";
 import createBuildingPacketFromSkeleton from "./create-building-packet-from-skeleton";
+import initLumosCommonScripts from "./init-lumos-common-scripts";
 
 export default function createLumosCkbBuilder(config) {
   const { ckbRpcUrl, ckbChainConfig } = config;
@@ -58,5 +59,49 @@ export default function createLumosCkbBuilder(config) {
         cellPointer: cell.outPoint,
       });
     },
+
+    reclaimDaoVerifiers: async function ({ from }) {
+      let txSkeleton = TransactionSkeleton({
+        cellProvider: indexer,
+      });
+
+      const verifierScript = buildScript(
+        ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER,
+        "0x",
+      );
+      const lock = addressToScript(from, { config: ckbChainConfig });
+      const collector = indexer.collector({
+        lock,
+        argsLen: (lock.args.length - 2) / 2,
+        type: verifierScript,
+      });
+      let outputCapacity = BI.from(0);
+      for await (const cell of collector.collect()) {
+        outputCapacity = outputCapacity.add(BI.from(cell.cellOutput.capacity));
+        txSkeleton = await commonScripts.setupInputCell(
+          txSkeleton,
+          cell,
+          from,
+          {
+            config: ckbChainConfig,
+          },
+        );
+      }
+      txSkeleton = txSkeleton.update("outputs", (outputs) =>
+        outputs.clear().push({
+          cellOutput: {
+            lock,
+            capacity: outputCapacity.toHexString(),
+          },
+          data: "0x",
+        }),
+      );
+
+      txSkeleton = txSkeleton.update("cellDeps", (cellDeps) =>
+        cellDeps.push(buildCellDep(ckbChainConfig.SCRIPTS.DAO_ACTION_VERIFIER)),
+      );
+
+      return createBuildingPacketFromSkeleton(txSkeleton);
+    },
   };
 }
diff --git a/src/lib/lumos-adapter/init-lumos-common-scripts.js b/src/lib/lumos-adapter/init-lumos-common-scripts.js
index 32d6010..4f1564e 100644
--- a/src/lib/lumos-adapter/init-lumos-common-scripts.js
+++ b/src/lib/lumos-adapter/init-lumos-common-scripts.js
@@ -1,6 +1,7 @@
+import { buildCellDep } from "@/lib/config";
 import {
-  parseFromInfo,
   common as commonScripts,
+  parseFromInfo,
 } from "@ckb-lumos/common-scripts";
 import { addCellDep } from "@ckb-lumos/common-scripts/lib/helper";
 
@@ -105,21 +106,9 @@ export function buildLockInfo(ckbChainConfig, scriptInfo, extraScripts) {
         // II. CellDeps
         //===========================
         // The helper method addCellDep avoids adding duplicated cell deps.
-        addCellDep(txMutable, {
-          outPoint: {
-            txHash: scriptInfo.TX_HASH,
-            index: scriptInfo.INDEX,
-          },
-          depType: scriptInfo.DEP_TYPE,
-        });
+        addCellDep(txMutable, buildCellDep(scriptInfo));
         for (const extraScriptInfo of extraScripts) {
-          addCellDep(txMutable, {
-            outPoint: {
-              txHash: extraScriptInfo.TX_HASH,
-              index: extraScriptInfo.INDEX,
-            },
-            depType: extraScriptInfo.DEP_TYPE,
-          });
+          addCellDep(txMutable, buildCellDep(extraScriptInfo));
         }
 
         return txMutable.asImmutable();
diff --git a/src/lib/papps/dao/lumos-callbacks.js b/src/lib/papps/dao/lumos-callbacks.js
index 792440d..6c3e069 100644
--- a/src/lib/papps/dao/lumos-callbacks.js
+++ b/src/lib/papps/dao/lumos-callbacks.js
@@ -1,10 +1,3 @@
-import { RPC } from "@ckb-lumos/rpc";
-import { BI } from "@ckb-lumos/bi";
-import { common as commonScripts, dao } from "@ckb-lumos/common-scripts";
-import { blockchain } from "@ckb-lumos/base";
-import { bytes, number } from "@ckb-lumos/codec";
-import * as lumosHelpers from "@ckb-lumos/helpers";
-
 import { getCellWithoutCache } from "@/actions/get-cell";
 import {
   getDepositBlockNumberFromWithdrawCell,
@@ -12,24 +5,21 @@ import {
 } from "@/lib/dao";
 import { mergeBuildingPacketFromSkeleton } from "@/lib/lumos-adapter/create-building-packet-from-skeleton";
 import createSkeletonFromBuildingPacket from "@/lib/lumos-adapter/create-skeleton-from-building-packet";
+import { blockchain } from "@ckb-lumos/base";
+import { BI } from "@ckb-lumos/bi";
+import { bytes, number } from "@ckb-lumos/codec";
+import { common as commonScripts, dao } from "@ckb-lumos/common-scripts";
+import * as lumosHelpers from "@ckb-lumos/helpers";
+import { RPC } from "@ckb-lumos/rpc";
+import { buildCellDep } from "@/lib/config";
 
 import { DaoActionData } from "./schema";
 import {
-  getDaoScriptInfo,
   getDaoScriptHash,
+  getDaoScriptInfo,
   getDaoScriptInfoHash,
 } from "./script-info";
 
-function buildCellDep(scriptInfo) {
-  return {
-    outPoint: {
-      txHash: scriptInfo.TX_HASH,
-      index: scriptInfo.INDEX,
-    },
-    depType: scriptInfo.DEP_TYPE,
-  };
-}
-
 function addDistinctCellDep(list, ...items) {
   return pushDistinctBy(
     list,
diff --git a/src/lib/papps/dao/script-info.js b/src/lib/papps/dao/script-info.js
index 8f957c3..489a4cf 100644
--- a/src/lib/papps/dao/script-info.js
+++ b/src/lib/papps/dao/script-info.js
@@ -1,4 +1,4 @@
-import { getConfig } from "@/lib/config";
+import { getConfig, buildScript } from "@/lib/config";
 import {
   createScriptInfoFromHumanTemplate,
   computeScriptInfoHash,
@@ -21,11 +21,7 @@ export function getDaoHumanScriptInfoTemplate() {
 
 function getDaoScriptHashFromConfig(config) {
   const daoInfo = config.ckbChainConfig.SCRIPTS.DAO;
-  const daoScript = {
-    codeHash: daoInfo.CODE_HASH,
-    hashType: daoInfo.HASH_TYPE,
-    args: "0x",
-  };
+  const daoScript = buildScript(daoInfo, "0x");
   return computeScriptHash(daoScript);
 }
 
diff --git a/src/lib/papps/dao/verifier.js b/src/lib/papps/dao/verifier.js
index fcace0a..bcc7e09 100644
--- a/src/lib/papps/dao/verifier.js
+++ b/src/lib/papps/dao/verifier.js
@@ -1,29 +1,12 @@
+import { buildCellDep, buildScript } from "@/lib/config";
+import { mergeBuildingPacketFromSkeleton } from "@/lib/lumos-adapter/create-building-packet-from-skeleton";
+import createSkeletonFromBuildingPacket from "@/lib/lumos-adapter/create-skeleton-from-building-packet";
 import { Indexer } from "@ckb-lumos/ckb-indexer";
+import { common as commonScripts } from "@ckb-lumos/common-scripts";
 import {
-  minimalCellCapacityCompatible,
   addressToScript,
+  minimalCellCapacityCompatible,
 } from "@ckb-lumos/helpers";
-import { common as commonScripts } from "@ckb-lumos/common-scripts";
-import { mergeBuildingPacketFromSkeleton } from "@/lib/lumos-adapter/create-building-packet-from-skeleton";
-import createSkeletonFromBuildingPacket from "@/lib/lumos-adapter/create-skeleton-from-building-packet";
-
-function buildScript(scriptInfo, args) {
-  return {
-    codeHash: scriptInfo.CODE_HASH,
-    hashType: scriptInfo.HASH_TYPE,
-    args,
-  };
-}
-
-function buildCellDep(scriptInfo) {
-  return {
-    outPoint: {
-      txHash: scriptInfo.TX_HASH,
-      index: scriptInfo.INDEX,
-    },
-    depType: scriptInfo.DEP_TYPE,
-  };
-}
 
 export async function prepareVerifier(buildingPacket, fromAddress, config) {
   let txSkeleton = createSkeletonFromBuildingPacket(buildingPacket, config);
diff --git a/src/lib/wallet/btc-wallet.js b/src/lib/wallet/btc-wallet.js
index f17fa58..e02860d 100644
--- a/src/lib/wallet/btc-wallet.js
+++ b/src/lib/wallet/btc-wallet.js
@@ -1,5 +1,6 @@
 import { bytes } from "@ckb-lumos/codec";
 import * as lumosHelpers from "@ckb-lumos/helpers";
+import { buildScript } from "@/lib/config";
 import { bech32 } from "bech32";
 import * as bs58 from "bs58";
 import { packOmnilockWitnessLock } from "./omni-lock";
@@ -49,11 +50,7 @@ export function btcAddressToCkbAddress(btcAddress, scriptInfo, ckbChainConfig) {
   }
   args += "00";
 
-  const script = {
-    codeHash: scriptInfo.CODE_HASH,
-    hashType: scriptInfo.HASH_TYPE,
-    args: args,
-  };
+  const script = buildScript(scriptInfo, args);
   return lumosHelpers.encodeToAddress(script, {
     config: ckbChainConfig,
   });
diff --git a/src/lib/wallet/joyid.js b/src/lib/wallet/joyid.js
index 135e9a8..ce67994 100644
--- a/src/lib/wallet/joyid.js
+++ b/src/lib/wallet/joyid.js
@@ -1,8 +1,9 @@
-import * as joyid from "@joyid/ckb";
 import { bytes } from "@ckb-lumos/codec";
 import * as lumosHelpers from "@ckb-lumos/helpers";
+import * as joyid from "@joyid/ckb";
 
 import { urlSafeBase64Decode } from "../base64";
+import { buildScript } from "../config";
 
 export const title = "Joyid";
 export const lockScriptName = "Joyid Lock";
@@ -19,11 +20,7 @@ export function address(connection, ckbChainConfig) {
     config: ckbChainConfig,
   });
   const scriptInfo = ckbChainConfig.SCRIPTS.JOYID;
-  const script = {
-    codeHash: scriptInfo.CODE_HASH,
-    hashType: scriptInfo.HASH_TYPE,
-    args,
-  };
+  const script = buildScript(scriptInfo, args);
   return lumosHelpers.encodeToAddress(script, {
     config: ckbChainConfig,
   });

From 0cf21c0abf1bbfb1167bd7ce32f4b7fb7289e8e8 Mon Sep 17 00:00:00 2001
From: ian 
Date: Sun, 18 Feb 2024 11:46:15 +0800
Subject: [PATCH 5/5] :memo: Add placeholder for docs about packing verifier

---
 docs/packing-verifier.md | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 docs/packing-verifier.md

diff --git a/docs/packing-verifier.md b/docs/packing-verifier.md
new file mode 100644
index 0000000..a8150a2
--- /dev/null
+++ b/docs/packing-verifier.md
@@ -0,0 +1 @@
+# Packing Verifier