From 938d0093253701a5a7b0be7d80f1dec3f7dfce62 Mon Sep 17 00:00:00 2001 From: ian Date: Thu, 28 Dec 2023 15:46:59 +0800 Subject: [PATCH] dao: withdraw phase 2 --- src/actions/claim.js | 19 +++ .../[address]/claim/[txHash]/[index]/form.js | 137 ++++++++++++++++++ .../claim/[txHash]/[index]/loading.js | 5 + .../[address]/claim/[txHash]/[index]/page.js | 21 +++ src/app/accounts/[address]/dao-cells.js | 5 +- src/lib/cobuild/publishers.js | 4 + src/lib/dao.js | 12 +- .../lumos-adapter/create-lumos-ckb-builder.js | 77 ++++++++++ 8 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 src/actions/claim.js create mode 100644 src/app/accounts/[address]/claim/[txHash]/[index]/form.js create mode 100644 src/app/accounts/[address]/claim/[txHash]/[index]/loading.js create mode 100644 src/app/accounts/[address]/claim/[txHash]/[index]/page.js diff --git a/src/actions/claim.js b/src/actions/claim.js new file mode 100644 index 0000000..e576e7a --- /dev/null +++ b/src/actions/claim.js @@ -0,0 +1,19 @@ +"use server"; + +import { claimDao } from "@/lib/cobuild/publishers"; +import { useConfig } from "@/lib/config"; + +export default async function withdraw(from, cell, config) { + config = config ?? useConfig(); + + try { + const buildingPacket = await claimDao(config)({ from, cell }); + return { + buildingPacket, + }; + } catch (err) { + return { + error: err.toString(), + }; + } +} diff --git a/src/app/accounts/[address]/claim/[txHash]/[index]/form.js b/src/app/accounts/[address]/claim/[txHash]/[index]/form.js new file mode 100644 index 0000000..8edde11 --- /dev/null +++ b/src/app/accounts/[address]/claim/[txHash]/[index]/form.js @@ -0,0 +1,137 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Alert, Button } from "flowbite-react"; + +import claim from "@/actions/claim"; +import useTipHeader from "@/hooks/use-tip-header"; +import useHeader from "@/hooks/use-header"; +import useHeaderByNumber from "@/hooks/use-header-by-number"; +import useCell from "@/hooks/use-cell"; + +import Capacity from "@/components/capacity"; +import { + DaoCycleProgress, + DaoCycleProgressHint, + 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"; + +function CellDetailsDisplay({ cell, depositHeader, withdrawHeader }) { + return ( +
+
Base
+
+ +
+
Reward
+
+ + +
+
+ ); +} + +function CellDetails({ cell, pending, onConfirm }) { + const tipHeader = useTipHeader(); + const depositBlockNumber = dao.getDepositBlockNumberFromWithdrawCell(cell); + const depositHeader = useHeaderByNumber(depositBlockNumber); + const withdrawHeader = useHeader(cell.blockHash); + + if (!tipHeader || !depositHeader || !withdrawHeader) { + return ; + } + + const waitingDuration = dao.estimateWithdrawWaitingDurationUntil( + tipHeader, + depositHeader, + withdrawHeader, + ); + + return ( + <> +

+ {waitingDuration ? ( + `Waiting for ${waitingDuration.humanize()}` + ) : ( + + )} +

+ + + ); +} + +function LoadCell({ outPoint, pending, onConfirm }) { + const cell = useCell(outPoint); + const childProps = { cell, pending, onConfirm }; + return cell ? : ; +} + +export default function ClaimForm({ address, outPoint, config }) { + const router = useRouter(); + const [formState, setFormState] = useState({}); + const [pending, setPending] = useState(false); + const [signedBuildingPacket, setSignedBuildingPacket] = useState(null); + const back = () => router.back(); + const onConfirm = async (cell) => { + setPending(true); + try { + setFormState(await claim(address, cell)); + } catch (err) { + setFormState({ error: err.toString() }); + } + setPending(false); + }; + + if ( + formState.buildingPacket === null || + formState.buildingPacket === undefined + ) { + const childProps = { outPoint, pending, onConfirm }; + return ( + <> + {formState.error ? ( + + {formState.error} + + ) : null} + + + ); + } else if ( + signedBuildingPacket === null || + signedBuildingPacket === undefined + ) { + return ( + + ); + } else { + return ( + + ); + } +} diff --git a/src/app/accounts/[address]/claim/[txHash]/[index]/loading.js b/src/app/accounts/[address]/claim/[txHash]/[index]/loading.js new file mode 100644 index 0000000..d0ed745 --- /dev/null +++ b/src/app/accounts/[address]/claim/[txHash]/[index]/loading.js @@ -0,0 +1,5 @@ +import { Spinner } from "flowbite-react"; + +export default function Loading() { + return ; +} diff --git a/src/app/accounts/[address]/claim/[txHash]/[index]/page.js b/src/app/accounts/[address]/claim/[txHash]/[index]/page.js new file mode 100644 index 0000000..605b485 --- /dev/null +++ b/src/app/accounts/[address]/claim/[txHash]/[index]/page.js @@ -0,0 +1,21 @@ +import { useConfig } from "@/lib/config"; + +import ClaimForm from "./form"; + +export default function Claim({ params: { address, txHash, index }, config }) { + config = config ?? useConfig(); + + const outPoint = { + txHash: `0x${txHash}`, + index: `0x${index.toString(16)}`, + }; + + const childProps = { address, outPoint, config }; + + return ( +
+

Claim

+ +
+ ); +} diff --git a/src/app/accounts/[address]/dao-cells.js b/src/app/accounts/[address]/dao-cells.js index c8dca2f..b68ed1a 100644 --- a/src/app/accounts/[address]/dao-cells.js +++ b/src/app/accounts/[address]/dao-cells.js @@ -99,7 +99,10 @@ export function WithdrawRow({ address, cell, tipHeader }) { <> <> - + + + + diff --git a/src/lib/cobuild/publishers.js b/src/lib/cobuild/publishers.js index d5923f6..50dff14 100644 --- a/src/lib/cobuild/publishers.js +++ b/src/lib/cobuild/publishers.js @@ -15,3 +15,7 @@ export function depositDao(config) { export function withdrawDao(config) { return createLumosCkbBuilder(config).withdrawDao; } + +export function claimDao(config) { + return createLumosCkbBuilder(config).claimDao; +} diff --git a/src/lib/dao.js b/src/lib/dao.js index 218bd6a..12c9d97 100644 --- a/src/lib/dao.js +++ b/src/lib/dao.js @@ -1,6 +1,7 @@ import moment from "moment"; import { BI } from "@ckb-lumos/bi"; -import { number } from "@ckb-lumos/codec"; +import { number, bytes } from "@ckb-lumos/codec"; +import { blockchain } from "@ckb-lumos/base"; import { dao } from "@ckb-lumos/common-scripts"; const DAO_CYCLE_EPOCHS = BI.from(180); @@ -35,6 +36,15 @@ export function getDepositBlockNumberFromWithdrawCell(cell) { return BI.from(number.Uint64.unpack(cell.data)).toHexString(); } +// Pack witness for withdraw phase 2 tx. +// depositHeaderIndex - cellDeps index of the block hash in which the deposit tx is committed. +export function packDaoWitnessArgs(depositHeaderIndex) { + const witnessArgs = { + inputType: bytes.hexify(number.Uint64LE.pack(depositHeaderIndex)), + }; + return bytes.hexify(blockchain.WitnessArgs.pack(witnessArgs)); +} + export function currentCycleProgress(tipHeader, depositHeader) { const start = decomposeEpoch(depositHeader.epoch); const end = decomposeEpoch(tipHeader.epoch); diff --git a/src/lib/lumos-adapter/create-lumos-ckb-builder.js b/src/lib/lumos-adapter/create-lumos-ckb-builder.js index b918080..ee70604 100644 --- a/src/lib/lumos-adapter/create-lumos-ckb-builder.js +++ b/src/lib/lumos-adapter/create-lumos-ckb-builder.js @@ -1,9 +1,14 @@ +import { RPC } from "@ckb-lumos/rpc"; import { Indexer } from "@ckb-lumos/ckb-indexer"; import { TransactionSkeleton } from "@ckb-lumos/helpers"; import { common as commonScripts, dao } from "@ckb-lumos/common-scripts"; import initLumosCommonScripts from "./init-lumos-common-scripts"; import createBuildingPacketFromSkeleton from "./create-building-packet-from-skeleton"; +import { + getDepositBlockNumberFromWithdrawCell, + 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) { @@ -18,8 +23,19 @@ async function payFee(txSkeleton, from, ckbChainConfig) { ); } +function buildCellDep(scriptInfo) { + return { + outPoint: { + txHash: scriptInfo.TX_HASH, + index: scriptInfo.INDEX, + }, + depType: scriptInfo.DEP_TYPE, + }; +} + export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) { initLumosCommonScripts(ckbChainConfig); + const rpc = new RPC(ckbRpcUrl); const indexer = new Indexer(ckbRpcUrl); return { @@ -83,5 +99,66 @@ export default function createLumosCkbBuilder({ ckbRpcUrl, ckbChainConfig }) { txSkeleton = await payFee(txSkeleton, from, ckbChainConfig); return createBuildingPacketFromSkeleton(txSkeleton); }, + + claimDao: async function ({ from, cell }) { + const depositBlockNumber = getDepositBlockNumberFromWithdrawCell(cell); + const depositBlockHash = await rpc.getBlockHash(depositBlockNumber); + const depositHeader = await rpc.getHeader(depositBlockHash); + const withdrawHeader = await rpc.getHeader(cell.blockHash); + + const txSkeletonMutable = TransactionSkeleton({ + cellProvider: indexer, + }).asMutable(); + + // add input + const since = + "0x" + + dao + .calculateDaoEarliestSince(depositHeader.epoch, withdrawHeader.epoch) + .toString(16); + txSkeletonMutable.update("inputs", (inputs) => inputs.push(cell)); + txSkeletonMutable.update("inputSinces", (inputSinces) => + inputSinces.set(0, since), + ); + + // add output + const outCapacity = + "0x" + + dao + .calculateMaximumWithdraw(cell, depositHeader.dao, withdrawHeader.dao) + .toString(16); + txSkeletonMutable.update("outputs", (outputs) => + outputs.push({ + cellOutput: { + capacity: outCapacity, + type: null, + lock: cell.cellOutput.lock, + }, + data: "0x", + }), + ); + + // add cell deps + txSkeletonMutable.update("cellDeps", (cellDeps) => + cellDeps.push( + buildCellDep(ckbChainConfig.SCRIPTS.DAO), + buildCellDep(ckbChainConfig.SCRIPTS.JOYID_COBUILD_POC), + ), + ); + // add header deps + txSkeletonMutable.update("headerDeps", (headerDeps) => + headerDeps.push(depositBlockHash, cell.blockHash), + ); + + // add witness + txSkeletonMutable.update("witnesses", (witnesses) => + witnesses.push(packDaoWitnessArgs(0)), + ); + + let txSkeleton = txSkeletonMutable.asImmutable(); + + txSkeleton = await payFee(txSkeleton, from, ckbChainConfig); + return createBuildingPacketFromSkeleton(txSkeleton); + }, }; }