diff --git a/.changeset/nasty-frogs-wonder.md b/.changeset/nasty-frogs-wonder.md new file mode 100644 index 00000000..669a7f9c --- /dev/null +++ b/.changeset/nasty-frogs-wonder.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": patch +--- + +feat(core): ckb proxy locks diff --git a/packages/demo/src/app/connected/(tools)/TimeLockedTransfer/page.tsx b/packages/demo/src/app/connected/(tools)/TimeLockedTransfer/page.tsx new file mode 100644 index 00000000..e1af89fe --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/TimeLockedTransfer/page.tsx @@ -0,0 +1,233 @@ +"use client"; + +import React, { useEffect, useState, useCallback } from "react"; +import { TextInput } from "@/src/components/Input"; +import { Button } from "@/src/components/Button"; +import { ccc } from "@ckb-ccc/connector-react"; +import { useGetExplorerLink } from "@/src/utils"; +import { useApp } from "@/src/context"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { BigButton } from "@/src/components/BigButton"; + +function ClaimButton({ cell, lock }: { cell: ccc.Cell; lock: ccc.Script }) { + const { signer, createSender } = useApp(); + const { log, error } = createSender("Claim Time Locked"); + + const { explorerTransaction } = useGetExplorerLink(); + + return ( + { + if (!signer) { + return; + } + + (async () => { + const toAddress = await signer.getRecommendedAddressObj(); + const { value: ownerCell, done } = await signer.client + .findCells( + { + script: lock, + scriptSearchMode: "exact", + scriptType: "lock", + filter: { + scriptLenRange: [0, 1], + outputDataLenRange: [0, 1], + }, + withData: true, + }, + undefined, + 1, + ) + .next(); + if (done) { + error( + "A existed owner cell from", + ccc.Address.fromScript(lock, signer.client).toString(), + "is required", + ); + return; + } + + const tx = ccc.Transaction.from({ + inputs: [ + { + previousOutput: ownerCell.outPoint, + cellOutput: ownerCell.cellOutput, + outputData: ownerCell.outputData, + }, + { + previousOutput: cell.outPoint, + since: ccc.numFromBytes( + ccc.bytesFrom(cell.cellOutput.lock.args).slice(32, 40), + ), + cellOutput: cell.cellOutput, + outputData: cell.outputData, + }, + ], + outputs: [{ lock: toAddress.script }], + }); + console.log( + tx.inputs[1].since, + + ccc.bytesFrom(cell.cellOutput.lock.args).slice(32, 40), + ); + await tx.addCellDepsOfKnownScripts( + signer.client, + ccc.KnownScript.TimeLock, + ); + + await tx.completeInputsByCapacity(signer); + await tx.completeFeeChangeToOutput(signer, 0, 1000); + + log( + "Transaction sent:", + explorerTransaction(await signer.sendTransaction(tx)), + ); + })(); + }} + className="align-center text-yellow-400" + > + {ccc.fixedPointToString( + (cell.cellOutput.capacity / ccc.fixedPointFrom("0.01")) * + ccc.fixedPointFrom("0.01"), + )} + CKB + + ); +} + +export default function TimeLockedTransfer() { + const { signer, createSender } = useApp(); + const { log, error } = createSender("Time Locked Transfer"); + + const { explorerTransaction } = useGetExplorerLink(); + + const [transferTo, setTransferTo] = useState(""); + const [amount, setAmount] = useState(""); + const [lockedForBlocks, setLockedForBlocks] = useState(""); + + const [liveTimeLockCells, setLiveTimeLockCells] = useState< + { cell: ccc.Cell; lock: ccc.Script }[] + >([]); + + const handleTimeLockedTransfer = useCallback(async () => { + if (!signer) { + return; + } + + // Verify destination addresses + const toAddress = await ccc.Address.fromString(transferTo, signer.client); + + const tip = await signer.client.getTip(); + const lockedUntil = ccc.Since.from({ + relative: "absolute", + metric: "blockNumber", + value: tip + ccc.numFrom(lockedForBlocks), + }); + const timeLockScript = await ccc.Script.fromKnownScript( + signer.client, + ccc.KnownScript.TimeLock, + buildTimeLockArgs(toAddress.script.hash(), lockedUntil.toNum()), + ); + + const tx = ccc.Transaction.from({ + outputs: [{ lock: timeLockScript }], + }); + + const minimumCapacity = tx.getOutputsCapacity(); + if (minimumCapacity > ccc.fixedPointFrom(amount)) { + error("Insufficient capacity to store data"); + return; + } + tx.outputs[0].capacity = ccc.fixedPointFrom(amount); + + // Complete missing parts for transaction + await tx.completeInputsByCapacity(signer); + await tx.completeFeeBy(signer, 1000); + + log( + "Transaction sent:", + explorerTransaction(await signer.sendTransaction(tx)), + ); + }, [ + signer, + amount, + error, + explorerTransaction, + lockedForBlocks, + log, + transferTo, + ]); + + useEffect(() => { + if (!signer) { + return; + } + + (async () => { + const cells = []; + + for await (const { script: lock } of await signer.getAddressObjs()) { + for await (const cell of signer.client.findCells({ + script: await ccc.Script.fromKnownScript( + signer.client, + ccc.KnownScript.TimeLock, + lock.hash(), + ), + scriptType: "lock", + scriptSearchMode: "prefix", + })) { + cells.push({ cell, lock }); + } + } + + setLiveTimeLockCells(cells); + })(); + }, [signer]); + + return ( +
+ + + +
+ {liveTimeLockCells.map(({ cell, lock }) => ( + + ))} +
+ + + +
+ ); +} + +function buildTimeLockArgs( + requiredScriptHash: ccc.HexLike, + lockedUntil: ccc.NumLike, +) { + const lockedUntilBytes8 = ccc.numToBytes(lockedUntil, 8); + return ccc.bytesConcat(requiredScriptHash, lockedUntilBytes8); +} diff --git a/packages/demo/src/app/connected/page.tsx b/packages/demo/src/app/connected/page.tsx index b61cd512..e0707069 100644 --- a/packages/demo/src/app/connected/page.tsx +++ b/packages/demo/src/app/connected/page.tsx @@ -16,6 +16,12 @@ const TABS: [ReactNode, string, keyof typeof icons, string][] = [ "LampWallDown", "text-yellow-500", ], + [ + "Time Locked Transfer", + "/connected/TimeLockedTransfer", + "Clock", + "text-amber-500", + ], ["Transfer xUDT", "/connected/TransferXUdt", "BadgeCent", "text-emerald-500"], ["Issue xUDT (SUS)", "/connected/IssueXUdtSus", "Rss", "text-sky-500"], [