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/ccc/src/signersController.ts b/packages/ccc/src/signersController.ts index 7858db7c..dd8b7f4d 100644 --- a/packages/ccc/src/signersController.ts +++ b/packages/ccc/src/signersController.ts @@ -147,10 +147,12 @@ export class SignersController { context, ); - await Promise.all(Xverse.getXverseSigners(client, preferredNetworks).map( - ({ wallet, signerInfo }) => - this.addSigner(wallet.name, wallet.icon, signerInfo, context), - )); + await Promise.all( + Xverse.getXverseSigners(client, preferredNetworks).map( + ({ wallet, signerInfo }) => + this.addSigner(wallet.name, wallet.icon, signerInfo, context), + ), + ); const nostrSigner = Nip07.getNip07Signer(client); if (nostrSigner) { diff --git a/packages/connector-react/src/hooks/useCcc.tsx b/packages/connector-react/src/hooks/useCcc.tsx index 1ec2ec92..6c5a6d10 100644 --- a/packages/connector-react/src/hooks/useCcc.tsx +++ b/packages/connector-react/src/hooks/useCcc.tsx @@ -66,7 +66,7 @@ export function Provider({ }: { children: ReactNode; connectorProps?: HTMLAttributes<{}>; - hideMark?: boolean, + hideMark?: boolean; name?: string; icon?: string; signerFilter?: ( diff --git a/packages/connector/src/scenes/connected.ts b/packages/connector/src/scenes/connected.ts index 0f924b56..e70903a9 100644 --- a/packages/connector/src/scenes/connected.ts +++ b/packages/connector/src/scenes/connected.ts @@ -111,7 +111,9 @@ export class ConnectedScene extends LitElement { ${this.hideMark == null - ? html`Powered by CCC` + ? html`Powered by CCC` : ""}
& { cellDeps: CellDepInfoLike[] }) | undefined > = Object.freeze({ + [KnownScript.NervosDao]: { + codeHash: + "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xe2fb199810d49a4d8beec56718ba2593b665db9d52299a0f9e6e75416d73ff5c", + index: 2, + }, + depType: "code", + }, + }, + ], + }, [KnownScript.Secp256k1Blake160]: { codeHash: "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", @@ -220,9 +237,9 @@ export const MAINNET_SCRIPTS: Record< }, ], }, - [KnownScript.SingleUseLock]: { + [KnownScript.AlwaysSuccess]: { codeHash: - "0x8290467a512e5b9a6b816469b0edabba1f4ac474e28ffdd604c2a7c76446bbaf", + "0x3b521cc4b552f109d092d8cc468a8048acb53c5952dbe769d2b2f9cf6e47f7f1", hashType: "data1", cellDeps: [ { @@ -230,7 +247,24 @@ export const MAINNET_SCRIPTS: Record< outPoint: { txHash: "0x10d63a996157d32c01078058000052674ca58d15f921bec7f1dcdac2160eb66b", - index: 4, + index: 0, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.InputTypeProxyLock]: { + codeHash: + "0x5123908965c711b0ffd8aec642f1ede329649bda1ebdca6bd24124d3796f768a", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x10d63a996157d32c01078058000052674ca58d15f921bec7f1dcdac2160eb66b", + index: 1, }, depType: "code", }, @@ -254,17 +288,85 @@ export const MAINNET_SCRIPTS: Record< }, ], }, - [KnownScript.NervosDao]: { + [KnownScript.LockProxyLock]: { codeHash: - "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", - hashType: "type", + "0x5d41e32e224c15f152b7e6529100ebeac83b162f5f692a5365774dad2c1a1d02", + hashType: "data1", cellDeps: [ { cellDep: { outPoint: { txHash: - "0xe2fb199810d49a4d8beec56718ba2593b665db9d52299a0f9e6e75416d73ff5c", - index: 2, + "0x10d63a996157d32c01078058000052674ca58d15f921bec7f1dcdac2160eb66b", + index: 3, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.SingleUseLock]: { + codeHash: + "0x8290467a512e5b9a6b816469b0edabba1f4ac474e28ffdd604c2a7c76446bbaf", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x10d63a996157d32c01078058000052674ca58d15f921bec7f1dcdac2160eb66b", + index: 4, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.TypeBurnLock]: { + codeHash: + "0xff78bae0abf17d7a404c0be0f9ad9c9185b3f88dcc60403453d5ba8e1f22f53a", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x10d63a996157d32c01078058000052674ca58d15f921bec7f1dcdac2160eb66b", + index: 5, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.EasyToDiscoverType]: { + codeHash: + "0xaba4430cc7110d699007095430a1faa72973edf2322ddbfd4d1d219cacf237af", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xb0ed754fb27d67fd8388c97fed914fb7998eceaa01f3e6f967e498de1ba0ac9b", + index: 0, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.TimeLock]: { + codeHash: + "0x6fac4b2e89360a1e692efcddcb3a28656d8446549fb83da6d896db8b714f4451", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xb0ed754fb27d67fd8388c97fed914fb7998eceaa01f3e6f967e498de1ba0ac9b", + index: 1, }, depType: "code", }, diff --git a/packages/core/src/client/clientPublicTestnet.advanced.ts b/packages/core/src/client/clientPublicTestnet.advanced.ts index 166112bc..8973b590 100644 --- a/packages/core/src/client/clientPublicTestnet.advanced.ts +++ b/packages/core/src/client/clientPublicTestnet.advanced.ts @@ -5,6 +5,23 @@ export const TESTNET_SCRIPTS: Record< KnownScript, Pick & { cellDeps: CellDepInfoLike[] } > = Object.freeze({ + [KnownScript.NervosDao]: { + codeHash: + "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", + hashType: "type", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f", + index: 2, + }, + depType: "code", + }, + }, + ], + }, [KnownScript.Secp256k1Blake160]: { codeHash: "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", @@ -231,9 +248,9 @@ export const TESTNET_SCRIPTS: Record< }, ], }, - [KnownScript.SingleUseLock]: { + [KnownScript.AlwaysSuccess]: { codeHash: - "0x8290467a512e5b9a6b816469b0edabba1f4ac474e28ffdd604c2a7c76446bbaf", + "0x3b521cc4b552f109d092d8cc468a8048acb53c5952dbe769d2b2f9cf6e47f7f1", hashType: "data1", cellDeps: [ { @@ -241,7 +258,24 @@ export const TESTNET_SCRIPTS: Record< outPoint: { txHash: "0xb4f171c9c9caf7401f54a8e56225ae21d95032150a87a4678eac3f66a3137b93", - index: 4, + index: 0, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.InputTypeProxyLock]: { + codeHash: + "0x5123908965c711b0ffd8aec642f1ede329649bda1ebdca6bd24124d3796f768a", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xb4f171c9c9caf7401f54a8e56225ae21d95032150a87a4678eac3f66a3137b93", + index: 1, }, depType: "code", }, @@ -265,17 +299,85 @@ export const TESTNET_SCRIPTS: Record< }, ], }, - [KnownScript.NervosDao]: { + [KnownScript.LockProxyLock]: { codeHash: - "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", - hashType: "type", + "0x5d41e32e224c15f152b7e6529100ebeac83b162f5f692a5365774dad2c1a1d02", + hashType: "data1", cellDeps: [ { cellDep: { outPoint: { txHash: - "0x8f8c79eb6671709633fe6a46de93c0fedc9c1b8a6527a18d3983879542635c9f", - index: 2, + "0xb4f171c9c9caf7401f54a8e56225ae21d95032150a87a4678eac3f66a3137b93", + index: 3, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.SingleUseLock]: { + codeHash: + "0x8290467a512e5b9a6b816469b0edabba1f4ac474e28ffdd604c2a7c76446bbaf", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xb4f171c9c9caf7401f54a8e56225ae21d95032150a87a4678eac3f66a3137b93", + index: 4, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.TypeBurnLock]: { + codeHash: + "0xff78bae0abf17d7a404c0be0f9ad9c9185b3f88dcc60403453d5ba8e1f22f53a", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0xb4f171c9c9caf7401f54a8e56225ae21d95032150a87a4678eac3f66a3137b93", + index: 5, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.EasyToDiscoverType]: { + codeHash: + "0xaba4430cc7110d699007095430a1faa72973edf2322ddbfd4d1d219cacf237af", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x1b4ffcad55ecd36ffb2715b6816b83da73851f1a24fe594f263c4f34dad90792", + index: 0, + }, + depType: "code", + }, + }, + ], + }, + [KnownScript.TimeLock]: { + codeHash: + "0x6fac4b2e89360a1e692efcddcb3a28656d8446549fb83da6d896db8b714f4451", + hashType: "data1", + cellDeps: [ + { + cellDep: { + outPoint: { + txHash: + "0x1b4ffcad55ecd36ffb2715b6816b83da73851f1a24fe594f263c4f34dad90792", + index: 1, }, depType: "code", }, diff --git a/packages/core/src/client/jsonRpc/index.ts b/packages/core/src/client/jsonRpc/index.ts index da8308e2..295b3378 100644 --- a/packages/core/src/client/jsonRpc/index.ts +++ b/packages/core/src/client/jsonRpc/index.ts @@ -37,15 +37,12 @@ import { JsonRpcCellOutput, JsonRpcTransformers } from "./advanced.js"; * * @example * ```typescript - * const result = await transform(5, (x) => x * 2); // Outputs 10 - * const resultWithoutTransformer = await transform(5); // Outputs 5 + * const result = transform(5, (x) => x * 2); // Outputs 10 + * const resultWithoutTransformer = transform(5); // Outputs 5 * ``` */ -async function transform( - value: unknown, - transformer?: (i: unknown) => unknown, -) { +function transform(value: unknown, transformer?: (i: unknown) => unknown) { if (transformer) { return transformer(value); } @@ -367,15 +364,13 @@ export abstract class ClientJsonRpc extends Client { return async (...req: unknown[]) => { const payload = this.buildPayload( rpcMethod, - await Promise.all( - req - .concat( - Array.from( - new Array(Math.max(inTransformers.length - req.length, 0)), - ), - ) - .map((v, i) => transform(v, inTransformers[i])), - ), + req + .concat( + Array.from( + new Array(Math.max(inTransformers.length - req.length, 0)), + ), + ) + .map((v, i) => transform(v, inTransformers[i])), ); try { diff --git a/packages/core/src/client/transports/webSocket.ts b/packages/core/src/client/transports/webSocket.ts index e82f1eca..28f89854 100644 --- a/packages/core/src/client/transports/webSocket.ts +++ b/packages/core/src/client/transports/webSocket.ts @@ -17,7 +17,7 @@ export class TransportWebSocket implements Transport { private readonly timeout = 30000, ) {} - async request(data: JsonRpcPayload) { + request(data: JsonRpcPayload) { const socket = (() => { if (this.socket) { return this.socket; diff --git a/packages/demo/src/app/connected/(tools)/NervosDao/page.tsx b/packages/demo/src/app/connected/(tools)/NervosDao/page.tsx index 5ead73dd..7b2e9b47 100644 --- a/packages/demo/src/app/connected/(tools)/NervosDao/page.tsx +++ b/packages/demo/src/app/connected/(tools)/NervosDao/page.tsx @@ -186,7 +186,7 @@ function DaoButton({ dao }: { dao: ccc.Cell }) { } const [withdrawTx, withdrawHeader] = infos[3]; if (!withdrawTx?.blockHash) { - error("Unexpected empty withdraw tx block info"); + error("Unexpected empty redeem tx block info"); return; } if (!depositTx.blockHash) { @@ -267,7 +267,7 @@ function DaoButton({ dao }: { dao: ccc.Cell }) { epoch
) : undefined} - {isNew ? "Withdraw" : "Claim"} + {isNew ? "Redeem" : "Withdraw"} ); 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"], [ diff --git a/packages/xverse/src/sat-connect-core/provider.advanced.ts b/packages/xverse/src/sat-connect-core/provider.advanced.ts index 0ded4006..04b49775 100644 --- a/packages/xverse/src/sat-connect-core/provider.advanced.ts +++ b/packages/xverse/src/sat-connect-core/provider.advanced.ts @@ -1,5 +1,5 @@ import * as v from "valibot"; -import { Requests, Params } from "./requests.advanced"; +import { Params, Requests } from "./requests.advanced"; import { RpcResponse } from "./types.advanced"; // accountChange