diff --git a/packages/core/package.json b/packages/core/package.json index 60488f51..b080f490 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -78,4 +78,4 @@ "ws": "^8.18.0" }, "packageManager": "pnpm@10.8.1" -} \ No newline at end of file +} diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index 64112a74..7d5620c3 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -1,7 +1,7 @@ import { Address } from "../../address/index.js"; import { bytesConcat, bytesFrom } from "../../bytes/index.js"; import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; -import { KnownScript } from "../../client/index.js"; +import { Client, KnownScript } from "../../client/index.js"; import { HexLike, hexFrom } from "../../hex/index.js"; import { numToBytes } from "../../num/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; @@ -14,6 +14,10 @@ import { btcEcdsaPublicKeyHash } from "./verify.js"; * @public */ export abstract class SignerBtc extends Signer { + constructor(client: Client) { + super(client); + } + get type(): SignerType { return SignerType.BTC; } @@ -123,4 +127,21 @@ export abstract class SignerBtc extends Signer { tx.setWitnessArgsAt(info.position, witness); return tx; } + + /** + * Signs a Partially Signed Bitcoin Transaction (PSBT). + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + * @todo Add support for Taproot signing options (useTweakedSigner, etc.) + */ + abstract signPsbt(psbtHex: string): Promise; + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + abstract pushPsbt(psbtHex: string): Promise; } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index 50096db7..ba7e9322 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -70,4 +70,12 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { async getBtcPublicKey(): Promise { return this.publicKey; } + + async signPsbt(_: string): Promise { + throw new Error("Read-only signer does not support signPsbt"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Read-only signer does not support pushPsbt"); + } } diff --git a/packages/demo/package.json b/packages/demo/package.json index d52513a2..43fceab9 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -17,6 +17,7 @@ "dependencies": { "@lit/react": "^1.0.5", "@uiw/react-json-view": "2.0.0-alpha.30", + "bitcoinjs-lib": "6.1.6", "lucide-react": "^0.427.0", "next": "14.2.10", "react": "^18", @@ -25,6 +26,7 @@ "devDependencies": { "@ckb-ccc/connector-react": "workspace:*", "@ckb-ccc/lumos-patches": "workspace:*", + "@ckb-ccc/rgbpp": "workspace:*", "@ckb-ccc/ssri": "workspace:*", "@ckb-ccc/udt": "workspace:*", "@ckb-lumos/ckb-indexer": "^0.24.0-next.1", diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx new file mode 100644 index 00000000..940b9113 --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -0,0 +1,494 @@ +"use client"; + +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Button } from "@/src/components/Button"; +import { + buildNetworkConfig, + isMainnet, + NetworkConfig, + PredefinedNetwork, + RgbppUdtClient, + UtxoSeal, + CkbRgbppUnlockSinger, + getSupportedWallets, + createBrowserRgbppBtcWallet, + BtcAssetApiConfig, + BtcApiUtxo, +} from "@ckb-ccc/rgbpp"; +import { useApp } from "@/src/context"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { ccc, SignerBtc } from "@ckb-ccc/connector-react"; +import { Message } from "@/src/components/Message"; +import { Dropdown } from "@/src/components/Dropdown"; +import { useGetExplorerLink } from "@/src/utils"; + +const issuanceAmount = BigInt(21000000); +const xudtToken = { + name: "Just xUDT", + symbol: "jxUDT", + decimal: 8, +}; + +type ProcessStep = + | "idle" + | "checking-cell" + | "creating-cell" + | "building-tx" + | "signing-btc" + | "waiting-btc" + | "signing-ckb" + | "waiting-ckb" + | "completed"; + +export default function IssueRGBPPXUdt() { + const { signer, createSender } = useApp(); + const sender = useMemo( + () => createSender("Issue RGB++ xUDT"), + [createSender], + ); + const { error, log } = sender; + const { explorerTransaction } = useGetExplorerLink(); + + const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); + const [rgbppCkbTxId, setRgbppCkbTxId] = useState(""); + const [utxos, setUtxos] = useState([]); + const [selectedUtxo, setSelectedUtxo] = useState(""); + const [currentStep, setCurrentStep] = useState("idle"); + const [stepMessage, setStepMessage] = useState(""); + const [isLoadingUtxos, setIsLoadingUtxos] = useState(false); + + const [networkConfig, setNetworkConfig] = useState( + null, + ); + const [ckbClient, setCkbClient] = useState(null); + const [rgbppUdtClient, setRgbppUdtClient] = useState( + null, + ); + + const getBtcExplorerLink = useCallback( + (txId: string) => { + const baseUrl = networkConfig?.isMainnet + ? "https://mempool.space/tx/" + : "https://mempool.space/testnet/tx/"; + return ( + + {txId} + + ); + }, + [networkConfig?.isMainnet], + ); + + useEffect(() => { + if (!signer) { + setNetworkConfig(null); + setCkbClient(null); + setRgbppUdtClient(null); + return; + } + + let network: PredefinedNetwork; + if (signer.client.addressPrefix === "ckb") { + network = PredefinedNetwork.BitcoinMainnet; + } else if (signer.client.addressPrefix === "ckt") { + // * use Testnet3 as default + network = PredefinedNetwork.BitcoinTestnet3; + } else { + error(`Unsupported network prefix: ${signer.client.addressPrefix}`); + return; + } + + const config = buildNetworkConfig(network); + setNetworkConfig(config); + + const client = isMainnet(network) + ? new ccc.ClientPublicMainnet() + : new ccc.ClientPublicTestnet(); + setCkbClient(client); + + const udtClient = new RgbppUdtClient(config, client); + setRgbppUdtClient(udtClient); + }, [signer]); + + const rgbppBtcWallet = useMemo(() => { + if (!signer || !(signer instanceof SignerBtc) || !networkConfig) { + return null; + } + + const config: BtcAssetApiConfig = { + url: process.env.NEXT_PUBLIC_BTC_ASSETS_API_URL!, + token: process.env.NEXT_PUBLIC_BTC_ASSETS_API_TOKEN, + origin: process.env.NEXT_PUBLIC_BTC_ASSETS_API_ORIGIN, + isMainnet: networkConfig.isMainnet, + }; + + return createBrowserRgbppBtcWallet(signer, networkConfig, config); + }, [signer, networkConfig]); + + useEffect(() => { + if ( + signer && + signer instanceof SignerBtc && + networkConfig && + !rgbppBtcWallet + ) { + error( + `Unsupported wallet type: ${signer.constructor.name}. Supported wallets: ${getSupportedWallets().join(", ")}`, + ); + } + }, [signer, networkConfig, rgbppBtcWallet]); + + useEffect(() => { + if (!rgbppBtcWallet) { + setUtxos([]); + setSelectedUtxo(""); + setIsLoadingUtxos(false); + return; + } + + setIsLoadingUtxos(true); + + rgbppBtcWallet + .getAddress() + .then(async (address) => { + const utxoList = await rgbppBtcWallet.getUtxos(address, { + only_non_rgbpp_utxos: true, + }); + + await new Promise((resolve) => setTimeout(resolve, 800)); + + setUtxos(utxoList); + if (utxoList.length > 0) { + setSelectedUtxo(`${utxoList[0].txid}:${utxoList[0].vout}`); + } + setIsLoadingUtxos(false); + }) + .catch((err) => { + error("Failed to get UTXOs:", String(err)); + setUtxos([]); + setSelectedUtxo(""); + setIsLoadingUtxos(false); + }); + }, [rgbppBtcWallet]); + + const [ckbRgbppUnlockSinger, setCkbRgbppUnlockSinger] = + useState(); + + useEffect(() => { + if (!ckbClient || !rgbppBtcWallet || !rgbppUdtClient) { + setCkbRgbppUnlockSinger(undefined); + return; + } + + let mounted = true; + rgbppBtcWallet.getAddress().then((address: string) => { + if (mounted) { + setCkbRgbppUnlockSinger( + new CkbRgbppUnlockSinger( + ckbClient, + address, + rgbppBtcWallet, + rgbppBtcWallet, + rgbppUdtClient.getRgbppScriptInfos(), + ), + ); + } + }); + return () => { + mounted = false; + }; + }, [ckbClient, rgbppBtcWallet, rgbppUdtClient]); + + const signRgbppBtcTx = useCallback(async () => { + if ( + !signer || + !(signer instanceof SignerBtc) || + !rgbppBtcWallet || + !ckbRgbppUnlockSinger || + !rgbppUdtClient || + !selectedUtxo + ) { + return; + } + + try { + setCurrentStep("checking-cell"); + setStepMessage("Checking for existing RGB++ cell..."); + setRgbppBtcTxId(""); + setRgbppCkbTxId(""); + + const btcAccount = await signer.getBtcAccount(); + + const [txId, indexStr] = selectedUtxo.split(":"); + const utxoSeal: UtxoSeal = { + txId, + index: parseInt(indexStr), + }; + const rgbppLockScript = rgbppUdtClient.buildRgbppLockScript(utxoSeal); + + const rgbppCellsGen = + await signer.client.findCellsByLock(rgbppLockScript); + const rgbppIssuanceCells: ccc.Cell[] = []; + for await (const cell of rgbppCellsGen) { + rgbppIssuanceCells.push(cell); + } + + if (rgbppIssuanceCells.length !== 0) { + setStepMessage("Using existing RGB++ cell"); + } else { + setCurrentStep("creating-cell"); + setStepMessage("RGB++ cell not found, creating a new one..."); + + const tx = ccc.Transaction.default(); + // If additional capacity is required when used as an input in a transaction, it can always be supplemented in `completeInputsByCapacity`. + tx.addOutput({ + lock: rgbppLockScript, + }); + + await tx.completeInputsByCapacity(signer); + await tx.completeFeeBy(signer); + const ckbTxId = await signer.sendTransaction(tx); + await signer.client.waitTransaction(ckbTxId); + const cell = await signer.client.getCellLive({ + txHash: ckbTxId, + index: 0, + }); + if (!cell) { + throw new Error("Cell not found"); + } + rgbppIssuanceCells.push(cell); + } + + setCurrentStep("building-tx"); + setStepMessage("Building RGB++ transaction..."); + + const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({ + token: xudtToken, + amount: issuanceAmount, + rgbppLiveCells: rgbppIssuanceCells, + udtScriptInfo: { + name: ccc.KnownScript.XUdt, + script: await ccc.Script.fromKnownScript( + signer.client, + ccc.KnownScript.XUdt, + "", + ), + cellDep: (await signer.client.getKnownScript(ccc.KnownScript.XUdt)) + .cellDeps[0].cellDep, + }, + }); + + setCurrentStep("signing-btc"); + setStepMessage("Building and signing BTC transaction..."); + + // ! indexedCkbPartialTx should be cached in the server side + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx, + ckbClient: signer.client, + rgbppUdtClient, + btcChangeAddress: btcAccount, + receiverBtcAddresses: [btcAccount], + feeRate: 10, + }); + + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); + setRgbppBtcTxId(btcTxId); + + setCurrentStep("waiting-btc"); + setStepMessage(`Waiting for BTC transaction to be confirmed...`); + log("BTC Transaction:", getBtcExplorerLink(btcTxId)); + + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( + indexedCkbPartialTx, + btcTxId, + ); + const rgbppSignedCkbTx = + await ckbRgbppUnlockSinger.signTransaction(ckbPartialTxInjected); + await rgbppSignedCkbTx.completeFeeBy(signer); + + setCurrentStep("waiting-ckb"); + setStepMessage("Waiting for CKB transaction to be confirmed..."); + + const ckbFinalTxId = await signer.sendTransaction(rgbppSignedCkbTx); + setRgbppCkbTxId(ckbFinalTxId); + log("CKB Transaction:", explorerTransaction(ckbFinalTxId)); + + await signer.client.waitTransaction(ckbFinalTxId); + + setCurrentStep("completed"); + setStepMessage("RGB++ xUDT issued successfully!"); + setSelectedUtxo(""); + } catch (err) { + setCurrentStep("idle"); + setStepMessage(""); + error("Transaction failed:", String(err)); + } + }, [ + signer, + selectedUtxo, + rgbppBtcWallet, + rgbppUdtClient, + ckbRgbppUnlockSinger, + log, + error, + explorerTransaction, + getBtcExplorerLink, + ]); + + if (!networkConfig || !ckbClient || !rgbppUdtClient) { + return ( +
+
Initializing network configuration...
+
+ ); + } + + if (signer && signer instanceof SignerBtc && !rgbppBtcWallet) { + return ( +
+ + This wallet is not supported for RGB++ operations. +
+ Supported wallets: {getSupportedWallets().join(", ")} +
+ Please connect with a supported wallet to continue. +
+
+ ); + } + + return ( +
+ + You will need to sign 2 or 3 transactions. +
+ + Current Network:{" "} + {networkConfig.isMainnet + ? "BTC Mainnet & CKB Mainnet" + : "BTC Testnet3 & CKB Testnet"} + +
+ +
+
+ + +
+ {isLoadingUtxos ? ( +
+
+
+

Loading UTXOs...

+
+
+ ) : utxos.length === 0 ? ( +
+

No UTXOs available

+ {!networkConfig?.isMainnet && ( +

+ You're on BTC Testnet3. Get test BTC from a faucet: +
+ + BTC Testnet3 Faucet + +

+ )} +
+ ) : ( + ({ + name: `${utxo.txid}:${utxo.vout}`, + displayName: `${utxo.txid}:${utxo.vout} (${utxo.value} sats)`, + iconName: "Coins", + }))} + selected={selectedUtxo} + onSelect={setSelectedUtxo} + /> + )} +
+ + {/* Status Display */} + {currentStep !== "idle" && ( +
+
+ {currentStep !== "completed" && ( +
+
+
+ )} +
+

{stepMessage}

+ {currentStep === "waiting-btc" && rgbppBtcTxId && ( +

+ BTC Transaction: {getBtcExplorerLink(rgbppBtcTxId)} +

+ )} + {currentStep === "completed" && rgbppBtcTxId && rgbppCkbTxId && ( +
+

BTC Transaction: {getBtcExplorerLink(rgbppBtcTxId)}

+

CKB Transaction: {explorerTransaction(rgbppCkbTxId)}

+
+ )} +
+
+
+ )} + + + + +
+ ); +} diff --git a/packages/demo/src/app/connected/page.tsx b/packages/demo/src/app/connected/page.tsx index 772fef66..03ae0921 100644 --- a/packages/demo/src/app/connected/page.tsx +++ b/packages/demo/src/app/connected/page.tsx @@ -51,6 +51,7 @@ const TABS: [ReactNode, string, keyof typeof icons, string][] = [ ["Hash", "/utils/Hash", "Barcode", "text-violet-500"], ["Mnemonic", "/utils/Mnemonic", "SquareAsterisk", "text-fuchsia-500"], ["Keystore", "/utils/Keystore", "Notebook", "text-rose-500"], + ["Issue RGB++ xUDT", "/connected/IssueRgbppXUdt", "Rss", "text-sky-500"], ]; /* eslint-enable react/jsx-key */ diff --git a/packages/demo/src/components/Notifications.tsx b/packages/demo/src/components/Notifications.tsx index a1fbe739..c7eea8ff 100644 --- a/packages/demo/src/components/Notifications.tsx +++ b/packages/demo/src/components/Notifications.tsx @@ -69,7 +69,7 @@ export function Notifications({ messages }: NotificationProps) { ) : undefined}
{messages diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index b564119e..ba51bd6a 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -25,6 +25,16 @@ export class BitcoinSigner extends ccc.SignerBtc { throw new Error("Not connected"); } + // Additional validation to ensure connection has valid address + if ( + !this.connection.address || + typeof this.connection.address !== "string" + ) { + throw new Error( + "Invalid connection - missing or invalid Bitcoin address", + ); + } + return this.connection; } @@ -198,4 +208,65 @@ export class BitcoinSigner extends ccc.SignerBtc { ); return signature; } + + /** + * Signs a PSBT using JoyID wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + */ + async signPsbt(psbtHex: string): Promise { + const { address } = await this.assertConnection(); + + const config = this.getConfig(); + const { tx: signedPsbtHex } = await createPopup( + buildJoyIDURL( + { + ...config, + tx: psbtHex, + signerAddress: address, + autoFinalized: true, + }, + "popup", + "/sign-psbt", + ), + { ...config, type: DappRequestType.SignPsbt }, + ); + + return signedPsbtHex; + } + + /** + * Signs and broadcasts a PSBT to the Bitcoin network using JoyID wallet. + * + * This method combines both signing and broadcasting in a single operation. + * + * @param psbtHex - The hex string of PSBT to sign and broadcast + * @returns A promise that resolves to the transaction ID + * + * @remarks + * Use this method directly for sign+broadcast operations to avoid double popups. + * While calling signPsbt() then pushPsbt() will still work, it triggers two popups and requires double signing. + */ + async pushPsbt(psbtHex: string): Promise { + const { address } = await this.assertConnection(); + + const config = this.getConfig(); + const { tx: txid } = await createPopup( + buildJoyIDURL( + { + ...config, + tx: psbtHex, + signerAddress: address, + autoFinalized: true, // sendPsbt always finalizes + isSend: true, + }, + "popup", + "/sign-psbt", + ), + { ...config, type: DappRequestType.SignPsbt }, // Use SignPsbt type for both operations + ); + + return txid; + } } diff --git a/packages/okx/src/advancedBarrel.ts b/packages/okx/src/advancedBarrel.ts index 4704b662..bdd6b3e0 100644 --- a/packages/okx/src/advancedBarrel.ts +++ b/packages/okx/src/advancedBarrel.ts @@ -2,8 +2,21 @@ import { Nip07A } from "@ckb-ccc/nip07/advanced"; import { UniSatA } from "@ckb-ccc/uni-sat/advanced"; export interface BitcoinProvider - extends Pick, - Partial> { + extends Pick< + UniSatA.Provider, + "on" | "removeListener" | "signMessage" | "signPsbt" | "pushPsbt" + >, + Partial< + Omit< + UniSatA.Provider, + | "on" + | "removeListener" + | "signMessage" + | "signPsbt" + | "pushPsbt" + | "pushTx" + > + > { connect?(): Promise<{ address: string; publicKey: string; diff --git a/packages/okx/src/btc/index.ts b/packages/okx/src/btc/index.ts index 3ef023d9..400d591d 100644 --- a/packages/okx/src/btc/index.ts +++ b/packages/okx/src/btc/index.ts @@ -176,4 +176,24 @@ export class BitcoinSigner extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + /** + * Signs a PSBT using OKX wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + */ + async signPsbt(psbtHex: string): Promise { + return this.provider.signPsbt(psbtHex); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + async pushPsbt(psbtHex: string): Promise { + return this.provider.pushPsbt(psbtHex); + } } diff --git a/packages/playground/tailwind.config.ts b/packages/playground/tailwind.config.ts index 1af2b12b..2d90dce8 100644 --- a/packages/playground/tailwind.config.ts +++ b/packages/playground/tailwind.config.ts @@ -9,7 +9,7 @@ const config: Config = { safelist: [ { pattern: /./, // Include all Tailwind classes - } + }, ], theme: { extend: { diff --git a/packages/rgbpp/package.json b/packages/rgbpp/package.json index 0eefa5ad..a42509c5 100644 --- a/packages/rgbpp/package.json +++ b/packages/rgbpp/package.json @@ -2,13 +2,21 @@ "name": "@ckb-ccc/rgbpp", "version": "1.0.0", "description": "RGB++ for CKB", - "main": "dist/index.js", "types": "dist/index.d.ts", "source": "src/index.ts", "type": "module", + "main": "dist.commonjs/index.js", + "module": "dist/index.js", + "exports": { + ".": { + "import": "./dist/index.js", + "require": "./dist.commonjs/index.js", + "default": "./dist.commonjs/index.js" + } + }, "scripts": { "test": "jest", - "build": "rimraf ./dist && tsc" + "build": "rimraf ./dist && rimraf ./dist.commonjs && tsc && tsc --project tsconfig.commonjs.json && copyfiles -u 2 misc/basedirs/**/* ." }, "keywords": [], "author": "", @@ -17,6 +25,7 @@ "@types/jest": "^29.5.14", "@types/lodash": "^4.17.14", "@types/node": "^22.10.6", + "copyfiles": "^2.4.1", "eslint": "^9.18.0", "eslint-config-prettier": "^10.0.1", "eslint-plugin-prettier": "^5.2.1", diff --git a/packages/rgbpp/src/bitcoin/service/base.ts b/packages/rgbpp/src/bitcoin/service/base.ts index e13d7d44..3e17d061 100644 --- a/packages/rgbpp/src/bitcoin/service/base.ts +++ b/packages/rgbpp/src/bitcoin/service/base.ts @@ -5,7 +5,6 @@ import { BaseApiRequestOptions, BaseApis, BtcAssetsApiContext, - BtcAssetsApiToken, Json, } from "../types/index.js"; import { isDomain } from "../utils/index.js"; @@ -18,6 +17,7 @@ export class BtcAssetsApiBase implements BaseApis { public domain?: string; public origin?: string; private token?: string; + private isMainnet: boolean; constructor(config: BtcAssetApiConfig) { this.url = config.url; @@ -25,46 +25,51 @@ export class BtcAssetsApiBase implements BaseApis { this.domain = config.domain; this.origin = config.origin; this.token = config.token; + this.isMainnet = config.isMainnet ?? true; // Validation if (this.domain && !isDomain(this.domain, true)) { throw BtcAssetsApiError.withComment( ErrorCodes.ASSETS_API_INVALID_PARAM, - "domain", + `Invalid domain format: "${this.domain}". Please provide a valid domain (e.g., "example.com")`, ); } } async request(route: string, options?: BaseApiRequestOptions): Promise { const { - requireToken = true, + requireToken = this.isMainnet, allow404 = false, method = "GET", headers, params, ...otherOptions } = options ?? {}; - if (requireToken && !this.token && !(this.app && this.domain)) { + + if (requireToken && (!this.token || !this.origin)) { throw BtcAssetsApiError.withComment( ErrorCodes.ASSETS_API_INVALID_PARAM, - "app, domain", + "Missing required parameters: both token and origin are required", ); } - if (requireToken && !this.token) { - await this.init(); - } const pickedParams = pickBy(params, (val) => val !== undefined); const packedParams = params ? "?" + new URLSearchParams(pickedParams).toString() : ""; const url = `${this.url}${route}${packedParams}`; + + const authHeaders: Record = {}; + if (requireToken) { + authHeaders.authorization = `Bearer ${this.token}`; + authHeaders.origin = this.origin!; + } + const res = await fetch(url, { method, headers: { - authorization: this.token ? `Bearer ${this.token}` : undefined, - origin: this.origin, - ...headers, + ...authHeaders, + ...(headers || {}), }, ...otherOptions, } as RequestInit); @@ -157,33 +162,6 @@ export class BtcAssetsApiBase implements BaseApis { }, } as BaseApiRequestOptions); } - - async generateToken() { - if (!this.app || !this.domain) { - throw BtcAssetsApiError.withComment( - ErrorCodes.ASSETS_API_INVALID_PARAM, - "app, domain", - ); - } - - return this.post("/token/generate", { - requireToken: false, - body: JSON.stringify({ - app: this.app!, - domain: this.domain!, - }), - }); - } - - async init(force?: boolean) { - // If the token exists and not a force action, do nothing - if (this.token && !force) { - return; - } - - const token = await this.generateToken(); - this.token = token.token; - } } function tryParseBody(body: unknown): Record | undefined { diff --git a/packages/rgbpp/src/bitcoin/types/base.ts b/packages/rgbpp/src/bitcoin/types/base.ts index 974e178f..db29b985 100644 --- a/packages/rgbpp/src/bitcoin/types/base.ts +++ b/packages/rgbpp/src/bitcoin/types/base.ts @@ -4,8 +4,6 @@ export type Json = Record; export interface BaseApis { request(route: string, options?: BaseApiRequestOptions): Promise; post(route: string, options?: BaseApiRequestOptions): Promise; - generateToken(): Promise; - init(force?: boolean): Promise; } export interface BaseApiRequestOptions extends RequestInit { diff --git a/packages/rgbpp/src/bitcoin/types/btc-assets-api.ts b/packages/rgbpp/src/bitcoin/types/btc-assets-api.ts index 211db17c..1e1fa4f1 100644 --- a/packages/rgbpp/src/bitcoin/types/btc-assets-api.ts +++ b/packages/rgbpp/src/bitcoin/types/btc-assets-api.ts @@ -4,4 +4,5 @@ export interface BtcAssetApiConfig { domain?: string; origin?: string; token?: string; + isMainnet?: boolean; } diff --git a/packages/rgbpp/src/bitcoin/types/tx.ts b/packages/rgbpp/src/bitcoin/types/tx.ts index 2cd14e1a..e50ae4a1 100644 --- a/packages/rgbpp/src/bitcoin/types/tx.ts +++ b/packages/rgbpp/src/bitcoin/types/tx.ts @@ -34,6 +34,23 @@ export interface BtcApiUtxoParams { no_cache?: boolean; } +export interface BtcApiBalanceParams { + min_satoshi?: number; + no_cache?: boolean; +} + +export interface BtcApiBalance { + address: string; + total_satoshi: number; + pending_satoshi: number; + /** @deprecated Use available_satoshi instead */ + satoshi: number; + available_satoshi: number; + dust_satoshi: number; + rgbpp_satoshi: number; + utxo_count: number; +} + export interface BtcApiRecommendedFeeRates { fastestFee: number; halfHourFee: number; diff --git a/packages/rgbpp/src/bitcoin/wallet/browser/index.ts b/packages/rgbpp/src/bitcoin/wallet/browser/index.ts new file mode 100644 index 00000000..b98a98b6 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/wallet/browser/index.ts @@ -0,0 +1,60 @@ +import { ccc } from "@ckb-ccc/shell"; +import { Psbt } from "bitcoinjs-lib"; +import { NetworkConfig } from "../../../types/network.js"; +import { BtcAssetApiConfig } from "../../types/btc-assets-api.js"; +import { RgbppBtcWallet } from "../wallet.js"; + +export class BrowserRgbppBtcWallet extends RgbppBtcWallet { + constructor( + protected signer: ccc.SignerBtc, + networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, + ) { + super(networkConfig, btcAssetApiConfig); + } + + async getAddress(): Promise { + return this.signer.getBtcAccount(); + } + + async signAndBroadcast(psbt: Psbt): Promise { + // JoyID uses different signing method + if ( + this.signer.constructor.name === "BitcoinSigner" && + "name" in this.signer + ) { + return this.signer.pushPsbt(psbt.toHex()); + } + + // UniSat and OKX use standard method + const signedPsbt = await this.signer.signPsbt(psbt.toHex()); + return this.signer.pushPsbt(signedPsbt); + } +} + +export function createBrowserRgbppBtcWallet( + signer: ccc.SignerBtc, + networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, +): BrowserRgbppBtcWallet | null { + const signerName = signer.constructor.name; + + // Check if wallet is supported + const isSupported = + (signerName === "Signer" && "provider" in signer) || // UniSat + (signerName === "BitcoinSigner" && "providers" in signer) || // OKX + (signerName === "BitcoinSigner" && "name" in signer); // JoyID + + if (isSupported) { + return new BrowserRgbppBtcWallet(signer, networkConfig, btcAssetApiConfig); + } + + return null; +} + +/** + * Get supported wallet names + */ +export function getSupportedWallets(): string[] { + return ["UniSat", "OKX", "JoyID"]; +} diff --git a/packages/rgbpp/src/bitcoin/wallet/index.ts b/packages/rgbpp/src/bitcoin/wallet/index.ts index df83b8bb..47578c72 100644 --- a/packages/rgbpp/src/bitcoin/wallet/index.ts +++ b/packages/rgbpp/src/bitcoin/wallet/index.ts @@ -1,2 +1,4 @@ -export * from "./account.js"; +export * from "./browser/index.js"; +export * from "./pk/account.js"; +export * from "./pk/wallet.js"; export * from "./wallet.js"; diff --git a/packages/rgbpp/src/bitcoin/wallet/account.ts b/packages/rgbpp/src/bitcoin/wallet/pk/account.ts similarity index 96% rename from packages/rgbpp/src/bitcoin/wallet/account.ts rename to packages/rgbpp/src/bitcoin/wallet/pk/account.ts index ea52151c..11b00471 100644 --- a/packages/rgbpp/src/bitcoin/wallet/account.ts +++ b/packages/rgbpp/src/bitcoin/wallet/pk/account.ts @@ -2,14 +2,14 @@ import ecc from "@bitcoinerlab/secp256k1"; import * as bitcoin from "bitcoinjs-lib"; import { ECPairFactory } from "ecpair"; -import { trimHexPrefix } from "../../utils/index.js"; -import { AddressType } from "../types/tx.js"; +import { trimHexPrefix } from "../../../utils/index.js"; +import { AddressType } from "../../types/tx.js"; import { isSupportedAddressType, SUPPORTED_ADDRESS_TYPES, toBtcNetwork, toXOnly, -} from "../utils/index.js"; +} from "../../utils/index.js"; bitcoin.initEccLib(ecc); const ECPair = ECPairFactory(ecc); diff --git a/packages/rgbpp/src/bitcoin/wallet/pk/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/pk/wallet.ts new file mode 100644 index 00000000..797da4d8 --- /dev/null +++ b/packages/rgbpp/src/bitcoin/wallet/pk/wallet.ts @@ -0,0 +1,45 @@ +import { Psbt, Transaction } from "bitcoinjs-lib"; + +import { BtcAccount, createBtcAccount, signPsbt } from "../../index.js"; +import { BtcAssetApiConfig } from "../../types/btc-assets-api.js"; +import { AddressType } from "../../types/tx.js"; + +import { NetworkConfig } from "../../../types/network.js"; +import { RgbppBtcWallet } from "../wallet.js"; + +export class PrivateKeyRgbppBtcWallet extends RgbppBtcWallet { + private account: BtcAccount; + + constructor( + privateKey: string, + addressType: AddressType, + protected networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, + ) { + super(networkConfig, btcAssetApiConfig); + this.account = createBtcAccount( + privateKey, + addressType, + networkConfig.name, + ); + } + + async getAddress(): Promise { + return this.account.from; + } + + async signTx(psbt: Psbt): Promise { + signPsbt(psbt, this.account); + psbt.finalizeAllInputs(); + return psbt.extractTransaction(true); + } + + async sendTx(tx: Transaction): Promise { + const txHex = tx.toHex(); + return this.sendTransaction(txHex); + } + + async signAndBroadcast(psbt: Psbt): Promise { + return this.sendTx(await this.signTx(psbt)); + } +} diff --git a/packages/rgbpp/src/bitcoin/wallet/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/wallet.ts index c6dcfd7f..69830b1f 100644 --- a/packages/rgbpp/src/bitcoin/wallet/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/wallet.ts @@ -21,17 +21,12 @@ import { import { UtxoSeal } from "../../types/rgbpp/rgbpp.js"; -import { - BtcAccount, - createBtcAccount, - signPsbt, - transactionToHex, -} from "../index.js"; import { BtcAssetsApiBase } from "../service/base.js"; import { BtcAssetApiConfig } from "../types/btc-assets-api.js"; import { RgbppBtcTxParams } from "../types/rgbpp.js"; import { - AddressType, + BtcApiBalance, + BtcApiBalanceParams, BtcApiRecommendedFeeRates, BtcApiSentTransaction, BtcApiTransaction, @@ -49,32 +44,22 @@ import { toBtcNetwork, utxoToInputData, } from "../utils/index.js"; +import { transactionToHex } from "./pk/account.js"; import { NetworkConfig } from "../../types/network.js"; import { RgbppApiSpvProof } from "../../types/spv.js"; const DEFAULT_VIRTUAL_SIZE_BUFFER = 20; -export class RgbppBtcWallet extends BtcAssetsApiBase { - private account: BtcAccount; - +export abstract class RgbppBtcWallet extends BtcAssetsApiBase { constructor( - privateKey: string, - addressType: AddressType, - private networkConfig: NetworkConfig, + protected networkConfig: NetworkConfig, btcAssetApiConfig: BtcAssetApiConfig, ) { super(btcAssetApiConfig); - this.account = createBtcAccount( - privateKey, - addressType, - networkConfig.name, - ); } - getAddress() { - return this.account.from; - } + abstract getAddress(): Promise; async buildPsbt( params: RgbppBtcTxParams, @@ -190,11 +175,7 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { return { psbt, indexedCkbPartialTx: indexedTx }; } - async signTx(psbt: Psbt): Promise { - signPsbt(psbt, this.account); - psbt.finalizeAllInputs(); - return psbt.extractTransaction(true); - } + abstract signAndBroadcast(psbt: Psbt): Promise; async buildInputs(utxoSeals: UtxoSeal[]): Promise { const inputs: TxInputData[] = []; @@ -236,20 +217,10 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { return inputs; } - async sendTx(tx: Transaction): Promise { - const txHex = tx.toHex(); - return this.sendTransaction(txHex); - } - rawTxHex(tx: Transaction): string { return transactionToHex(tx, false); } - async signAndSendTx(psbt: Psbt): Promise { - const tx = await this.signTx(psbt); - return this.sendTx(tx); - } - async balanceInputsOutputs( inputs: TxInputData[], outputs: TxOutput[], @@ -289,7 +260,7 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { if (changeValue >= this.networkConfig.btcDustLimit) { outputs.push({ - address: this.account.from, + address: await this.getAddress(), value: changeValue, }); } @@ -305,7 +276,7 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { params?: BtcApiUtxoParams, knownInputs?: TxInputData[], ): Promise<{ inputs: TxInputData[]; changeValue: number }> { - const utxos = await this.getUtxos(this.account.from, params); + const utxos = await this.getUtxos(await this.getAddress(), params); let filteredUtxos = utxos; if (knownInputs) { @@ -349,17 +320,16 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { }; } + /** + * Estimate transaction fee without requiring actual signing + * This avoids triggering wallet confirmation dialogs for fee estimation + */ async estimateFee( inputs: TxInputData[], outputs: TxOutput[], feeRate?: number, ) { - // Create a temporary PSBT to calculate the fee - const psbt = new Psbt({ network: toBtcNetwork(this.networkConfig.name) }); - inputs.forEach((input) => psbt.addInput(input)); - outputs.forEach((output) => psbt.addOutput(output)); - - // * signTx will fail if inputs value is smaller than outputs value + // Ensure we have enough inputs to cover outputs let totalInputValue = inputs.reduce( (acc, input) => acc + input.witnessUtxo.value, 0, @@ -368,6 +338,8 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { (acc, output) => acc + output.value, 0, ); + + let balancedInputs = [...inputs]; if (totalInputValue < totalOutputValue) { const { inputs: extraInputs } = await this.collectUtxos( totalOutputValue - totalInputValue, @@ -376,16 +348,11 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { min_satoshi: 1000, }, ); - extraInputs.forEach((input) => psbt.addInput(input)); + balancedInputs = [...inputs, ...extraInputs]; } - const tx = await this.signTx(psbt); - - // Calculate virtual size - const weightWithWitness = tx.byteLength(true); - const weightWithoutWitness = tx.byteLength(false); - const weight = weightWithoutWitness * 3 + weightWithWitness + tx.ins.length; - const virtualSize = Math.ceil(weight / 4); + // Estimate transaction size based on input/output types without signing + const virtualSize = this.estimateVirtualSize(balancedInputs, outputs); const bufferedVirtualSize = virtualSize + DEFAULT_VIRTUAL_SIZE_BUFFER; if (!feeRate) { @@ -402,6 +369,153 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { return Math.ceil(bufferedVirtualSize * feeRate); } + /** + * Estimate virtual size of a transaction + * Based on Bitcoin transaction structure and different address types + */ + private estimateVirtualSize( + inputs: TxInputData[], + outputs: TxOutput[], + ): number { + // Base transaction size (version + locktime + input count + output count) + let baseSize = + 4 + + 4 + + this.getVarIntSize(inputs.length) + + this.getVarIntSize(outputs.length); + + // Calculate input sizes + let witnessSize = 0; + for (const input of inputs) { + // Each input: txid (32) + vout (4) + scriptSig length + scriptSig + sequence (4) + baseSize += 32 + 4 + 4; // txid + vout + sequence + + // Determine address type from the input + const addressType = this.getInputAddressType(input); + + switch (addressType) { + case "P2WPKH": + // P2WPKH: scriptSig is empty, witness has 2 items (signature + pubkey) + baseSize += 1; // empty scriptSig + witnessSize += 1 + 1 + 72 + 1 + 33; // witness stack count + sig length + sig + pubkey length + pubkey + break; + case "P2TR": + // P2TR: scriptSig is empty, witness has 1 item (signature) + baseSize += 1; // empty scriptSig + witnessSize += 1 + 1 + 64; // witness stack count + sig length + sig + break; + case "P2PKH": + // P2PKH: scriptSig has signature + pubkey, no witness + baseSize += 1 + 72 + 33; // scriptSig length + sig + pubkey + break; + default: + // Default estimation for unknown types + baseSize += 1 + 107; // average scriptSig size + break; + } + } + + // Calculate output sizes + for (const output of outputs) { + // Each output: value (8) + scriptPubKey length + scriptPubKey + baseSize += 8; // value + + if ("address" in output && output.address) { + const addressType = this.getOutputAddressType(output.address); + switch (addressType) { + case "P2WPKH": + baseSize += 1 + 22; // length + scriptPubKey + break; + case "P2TR": + baseSize += 1 + 34; // length + scriptPubKey + break; + case "P2PKH": + baseSize += 1 + 25; // length + scriptPubKey + break; + default: + baseSize += 1 + 25; // default size + break; + } + } else if ("script" in output && output.script) { + // For script outputs, use the actual script length + baseSize += + this.getVarIntSize(output.script.length) + output.script.length; + } else { + // Default for unknown output types + baseSize += 1 + 25; + } + } + + // Add witness header if there are witness inputs + if (witnessSize > 0) { + witnessSize += 2; // witness marker + flag + } + + // Calculate weight: base_size * 4 + witness_size + const weight = baseSize * 4 + witnessSize; + + // Virtual size is weight / 4, rounded up + return Math.ceil(weight / 4); + } + + /** + * Get the size of a variable integer + */ + private getVarIntSize(value: number): number { + if (value < 0xfd) return 1; + if (value <= 0xffff) return 3; + if (value <= 0xffffffff) return 5; + return 9; + } + + /** + * Determine address type from input data + */ + private getInputAddressType(input: TxInputData): string { + // Check if it's a Taproot input + if (input.tapInternalKey) { + return "P2TR"; + } + + // Check if it has witness data (P2WPKH or P2WSH) + if (input.witnessUtxo) { + const script = input.witnessUtxo.script; + if (script.length === 22 && script[0] === 0x00 && script[1] === 0x14) { + return "P2WPKH"; + } + if (script.length === 34 && script[0] === 0x00 && script[1] === 0x20) { + return "P2WSH"; + } + } + + // Default to P2PKH for legacy inputs + return "P2PKH"; + } + + /** + * Determine address type from output address + */ + private getOutputAddressType(address: string): string { + if ( + address.startsWith("bc1p") || + address.startsWith("tb1p") || + address.startsWith("bcrt1p") + ) { + return "P2TR"; + } + if ( + address.startsWith("bc1") || + address.startsWith("tb1") || + address.startsWith("bcrt1") + ) { + return "P2WPKH"; + } + if (address.startsWith("3") || address.startsWith("2")) { + return "P2SH"; + } + return "P2PKH"; + } + isCommitmentMatched( commitment: string, ckbPartialTx: ccc.Transaction, @@ -431,12 +545,12 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { const outputs = [ { - address: this.account.from, + address: await this.getAddress(), value: targetValue, }, ]; - const utxos = await this.getUtxos(this.account.from, btcUtxoParams); + const utxos = await this.getUtxos(await this.getAddress(), btcUtxoParams); if (utxos.length === 0) { throw new Error("Insufficient funds"); } @@ -461,18 +575,16 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { psbt.addOutput(output); }); - // TODO: separate construction, signing, and sending - const signedTx = await this.signTx(psbt); - const txId = await this.sendTx(signedTx); + const txId = await this.signAndBroadcast(psbt); console.log(`[prepareUtxoSeal] Transaction ${txId} sent`); - let tx = await this.getTransaction(txId); - while (!tx.status.confirmed) { + let btcTx = await this.getTransaction(txId); + while (!btcTx.status.confirmed) { console.log( `[prepareUtxoSeal] Transaction ${txId} not confirmed, waiting 30 seconds...`, ); await new Promise((resolve) => setTimeout(resolve, 30 * 1000)); - tx = await this.getTransaction(txId); + btcTx = await this.getTransaction(txId); } return { @@ -501,6 +613,21 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { ); } + /** + * Get the balance of a Bitcoin address + * @param address The Bitcoin address + * @param params Optional parameters for balance query + * @returns Balance information including total, available, pending, dust, and RGB++ satoshi amounts + */ + getBalance(address: string, params?: BtcApiBalanceParams) { + return this.request( + `/bitcoin/v1/address/${address}/balance`, + { + params, + }, + ); + } + async getRgbppSpvProof(btcTxId: string, confirmations: number) { const spvProof: RgbppApiSpvProof | null = await this.request("/rgbpp/v1/btc-spv/proof", { diff --git a/packages/rgbpp/src/examples/.env.example b/packages/rgbpp/src/examples/.env.example index aef77d42..459b21b4 100644 --- a/packages/rgbpp/src/examples/.env.example +++ b/packages/rgbpp/src/examples/.env.example @@ -12,5 +12,7 @@ UTXO_BASED_CHAIN_ADDRESS_TYPE= # btc-assets-api BTC_ASSETS_API_URL= + +# JWT Authentication - ONLY REQUIRED FOR BTC MAINNET BTC_ASSETS_API_TOKEN= BTC_ASSETS_API_ORIGIN= diff --git a/packages/rgbpp/src/examples/common/env.ts b/packages/rgbpp/src/examples/common/env.ts index 4d7896a6..aa30b16b 100644 --- a/packages/rgbpp/src/examples/common/env.ts +++ b/packages/rgbpp/src/examples/common/env.ts @@ -1,20 +1,14 @@ import { ccc } from "@ckb-ccc/shell"; -import dotenv from "dotenv"; - -import { dirname } from "path"; -import { fileURLToPath } from "url"; - -import { parseAddressType, RgbppBtcWallet } from "../../bitcoin/index.js"; +import { PrivateKeyRgbppBtcWallet } from "../../bitcoin/wallet/pk/wallet.js"; import { ScriptInfo } from "../../types/rgbpp/index.js"; +import { parseAddressType } from "../../bitcoin/index.js"; import { CkbRgbppUnlockSinger } from "../../signer/index.js"; import { NetworkConfig, PredefinedNetwork } from "../../types/network.js"; import { RgbppUdtClient } from "../../udt/index.js"; import { buildNetworkConfig, isMainnet } from "../../utils/index.js"; -dotenv.config({ path: dirname(fileURLToPath(import.meta.url)) + "/../.env" }); - const utxoBasedChainName = process.env.UTXO_BASED_CHAIN_NAME!; const ckbPrivateKey = process.env.CKB_SECP256K1_PRIVATE_KEY!; const utxoBasedChainPrivateKey = process.env.UTXO_BASED_CHAIN_PRIVATE_KEY!; @@ -23,22 +17,15 @@ const btcAssetsApiUrl = process.env.BTC_ASSETS_API_URL!; const btcAssetsApiToken = process.env.BTC_ASSETS_API_TOKEN!; const btcAssetsApiOrigin = process.env.BTC_ASSETS_API_ORIGIN!; -export const ckbClient = isMainnet(utxoBasedChainName) - ? new ccc.ClientPublicMainnet() - : new ccc.ClientPublicTestnet(); - -const addressType = parseAddressType(utxoBasedChainAddressType); - -export const ckbSigner = new ccc.SignerCkbPrivateKey(ckbClient, ckbPrivateKey); -// export const ckbAddress = await ckbSigner.getRecommendedAddress(); - -export function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): { +export async function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): Promise<{ + ckbClient: ccc.Client; + ckbSigner: ccc.SignerCkbPrivateKey; networkConfig: NetworkConfig; utxoBasedAccountAddress: string; rgbppUdtClient: RgbppUdtClient; - rgbppBtcWallet: RgbppBtcWallet; + rgbppBtcWallet: PrivateKeyRgbppBtcWallet; ckbRgbppUnlockSinger: CkbRgbppUnlockSinger; -} { +}> { const scripts = scriptInfos?.reduce( (acc: Record, { name, script, cellDep }) => { acc.scripts[name] = script; @@ -48,6 +35,14 @@ export function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): { { scripts: {}, cellDeps: {} }, ); + const ckbClient = isMainnet(utxoBasedChainName) + ? new ccc.ClientPublicMainnet() + : new ccc.ClientPublicTestnet(); + + const ckbSigner = new ccc.SignerCkbPrivateKey(ckbClient, ckbPrivateKey); + + const addressType = parseAddressType(utxoBasedChainAddressType); + const networkConfig = buildNetworkConfig( utxoBasedChainName as PredefinedNetwork, scripts, @@ -55,7 +50,7 @@ export function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): { const rgbppUdtClient = new RgbppUdtClient(networkConfig, ckbClient); - const rgbppBtcWallet = new RgbppBtcWallet( + const rgbppBtcWallet = new PrivateKeyRgbppBtcWallet( utxoBasedChainPrivateKey, addressType, networkConfig, @@ -63,17 +58,20 @@ export function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): { url: btcAssetsApiUrl, token: btcAssetsApiToken, origin: btcAssetsApiOrigin, + isMainnet: networkConfig.isMainnet, }, ); return { + ckbClient, + ckbSigner, networkConfig, - utxoBasedAccountAddress: rgbppBtcWallet.getAddress(), + utxoBasedAccountAddress: await rgbppBtcWallet.getAddress(), rgbppUdtClient, rgbppBtcWallet, ckbRgbppUnlockSinger: new CkbRgbppUnlockSinger( ckbClient, - rgbppBtcWallet.getAddress(), + await rgbppBtcWallet.getAddress(), rgbppBtcWallet, rgbppBtcWallet, rgbppUdtClient.getRgbppScriptInfos(), diff --git a/packages/rgbpp/src/examples/common/index.ts b/packages/rgbpp/src/examples/common/index.ts new file mode 100644 index 00000000..706daa2f --- /dev/null +++ b/packages/rgbpp/src/examples/common/index.ts @@ -0,0 +1,3 @@ +export * from "./assets.js"; +export * from "./env.js"; +export * from "./utils.js"; diff --git a/packages/rgbpp/src/examples/common/load-env.ts b/packages/rgbpp/src/examples/common/load-env.ts new file mode 100644 index 00000000..46dc5dbe --- /dev/null +++ b/packages/rgbpp/src/examples/common/load-env.ts @@ -0,0 +1,5 @@ +import dotenv from "dotenv"; +import { dirname } from "path"; +import { fileURLToPath } from "url"; + +dotenv.config({ path: dirname(fileURLToPath(import.meta.url)) + "/../.env" }); diff --git a/packages/rgbpp/src/examples/common/utils.ts b/packages/rgbpp/src/examples/common/utils.ts index aff569c3..22a98509 100644 --- a/packages/rgbpp/src/examples/common/utils.ts +++ b/packages/rgbpp/src/examples/common/utils.ts @@ -3,9 +3,9 @@ import { ccc } from "@ckb-ccc/shell"; import { UtxoSeal } from "../../types/rgbpp/index.js"; import { RgbppUdtClient } from "../../udt/index.js"; -import { ckbClient, ckbSigner } from "./env.js"; - export async function prepareRgbppCells( + ckbClient: ccc.Client, + ckbSigner: ccc.SignerCkbPrivateKey, utxoSeal: UtxoSeal, rgbppUdtClient: RgbppUdtClient, ): Promise { @@ -49,6 +49,7 @@ export async function prepareRgbppCells( } export async function collectRgbppCells( + ckbClient: ccc.Client, utxoSeals: UtxoSeal[], typeScript: ccc.Script, rgbppUdtClient: RgbppUdtClient, @@ -76,6 +77,7 @@ export async function collectRgbppCells( } export async function collectBtcTimeLockCells( + ckbClient: ccc.Client, btcTimeLockArgs: string, rgbppUdtClient: RgbppUdtClient, ): Promise { @@ -91,6 +93,7 @@ export async function collectBtcTimeLockCells( } export async function collectUdtCells( + ckbClient: ccc.Client, ckbAddress: string, udtTypeScript: ccc.Script, ): Promise { diff --git a/packages/rgbpp/src/examples/spore/1-cluster-creation.ts b/packages/rgbpp/src/examples/spore/1-cluster-creation.ts index 43fd9915..5bef88ec 100644 --- a/packages/rgbpp/src/examples/spore/1-cluster-creation.ts +++ b/packages/rgbpp/src/examples/spore/1-cluster-creation.ts @@ -2,8 +2,10 @@ import { ccc, spore } from "@ckb-ccc/shell"; import { UtxoSeal } from "../../types/rgbpp/index.js"; +import "../common/load-env.js"; + import { clusterData } from "../common/assets.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; import { prepareRgbppCells } from "../common/utils.js"; @@ -13,13 +15,20 @@ async function createSporeCluster(utxoSeal?: UtxoSeal) { rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); if (!utxoSeal) { utxoSeal = await rgbppBtcWallet.prepareUtxoSeal({ feeRate: 28 }); } - const rgbppCells = await prepareRgbppCells(utxoSeal, rgbppUdtClient); + const rgbppCells = await prepareRgbppCells( + ckbClient, + ckbSigner, + utxoSeal, + rgbppUdtClient, + ); const tx = ccc.Transaction.default(); // manually add specified inputs rgbppCells.forEach((cell) => { @@ -50,7 +59,7 @@ async function createSporeCluster(utxoSeal?: UtxoSeal) { }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( @@ -70,8 +79,8 @@ async function createSporeCluster(utxoSeal?: UtxoSeal) { const logger = new RgbppTxLogger({ opType: "cluster-creation" }); createSporeCluster({ - txId: "a8598f3b9c6b8a15529ecfd2d6c7c2897b4d4efcf88414270bce0e16b961a404", - index: 3, + txId: "56dea2d2cf703e8f30dee51115419b5af54545878af39873de50ddbb1ec5596e", + index: 2, }) .then(() => { logger.saveOnSuccess(); @@ -85,4 +94,8 @@ createSporeCluster({ /* pnpm tsx packages/rgbpp/src/examples/spore/1-cluster-creation.ts + +cluster id: 0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc +btcTxId: b78ba51aca245436cc94df592adcfd763e835f1916e63a56e0856558f3b3f475 +ckbTxId: 0xc7cf9f775e3fa3d49ed8e18ce5f97048177e6766558d677d07912eed9dc453d8 */ diff --git a/packages/rgbpp/src/examples/spore/2-spore-creation.ts b/packages/rgbpp/src/examples/spore/2-spore-creation.ts index a4a45246..fd637ac7 100644 --- a/packages/rgbpp/src/examples/spore/2-spore-creation.ts +++ b/packages/rgbpp/src/examples/spore/2-spore-creation.ts @@ -1,7 +1,9 @@ import { ccc, spore } from "@ckb-ccc/shell"; import { SporeDataView } from "@ckb-ccc/spore/advanced"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { inspect } from "util"; import { RgbppTxLogger } from "../common/logger.js"; @@ -12,34 +14,39 @@ async function createSpore({ receiverInfo: { btcAddress: string; rawSporeData: SporeDataView; - }; + }[]; }) { const { rgbppBtcWallet, rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); const { tx: transferClusterTx } = await spore.transferSporeCluster({ signer: ckbSigner, - id: receiverInfo.rawSporeData.clusterId!, + id: receiverInfo[0].rawSporeData.clusterId!, to: rgbppUdtClient.buildPseudoRgbppLockScript(), // new cluster output }); - // ? API for creating multiple spores - const { tx: ckbPartialTx, id } = await spore.createSpore({ - signer: ckbSigner, - data: receiverInfo.rawSporeData, - to: rgbppUdtClient.buildPseudoRgbppLockScript(), - // cannot use cluster mode here as cluster's lock needs to be updated - clusterMode: "skip", - tx: transferClusterTx, - }); + let ckbPartialTx: ccc.Transaction = transferClusterTx; + for (const receiver of receiverInfo) { + const { tx: _ckbPartialTx, id } = await spore.createSpore({ + signer: ckbSigner, + data: receiver.rawSporeData, + to: rgbppUdtClient.buildPseudoRgbppLockScript(), + // cannot use cluster mode here as cluster's lock needs to be updated + clusterMode: "skip", + tx: ckbPartialTx, + }); - logger.add("spore id", id, true); + console.log("spore id", id); + ckbPartialTx = _ckbPartialTx; + } - console.log(inspect(ckbPartialTx.witnesses, { depth: null, colors: true })); + console.log(inspect(ckbPartialTx, { depth: null, colors: true })); const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ ckbPartialTx, @@ -51,7 +58,7 @@ async function createSpore({ }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( @@ -72,15 +79,80 @@ async function createSpore({ const logger = new RgbppTxLogger({ opType: "spore-creation" }); createSpore({ - receiverInfo: { - btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", - rawSporeData: { - contentType: "text/plain", - content: ccc.bytesFrom("First Spore Live", "utf8"), - clusterId: - "0xaa116bb68f7461a8bf42f51bdc4ae130da3546088a42b587ade53369e39e28d6", + receiverInfo: [ + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("First Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Second Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Third Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, }, - }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Fourth Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Fifth Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Sixth Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Seventh Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + { + btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", + rawSporeData: { + contentType: "text/plain", + content: ccc.bytesFrom("Eighth Spore Live", "utf8"), + clusterId: + "0x82993b95c82bd0734836a90912bbc46c1ddee4a7a7529eb889393647362105dc", + }, + }, + ], }) .then(() => { logger.saveOnSuccess(); @@ -94,4 +166,15 @@ createSpore({ /* pnpm tsx packages/rgbpp/src/examples/spore/2-spore-creation.ts + +https://testnet.explorer.nervos.org/transaction/0x2b7aa75f9d5358d5ff16f93fca7691a5db1f4e24919def0e8474de4106fb65cb + +spore id 0x8d814f7306d31bdfa40ddec0d3c9391c5505a7e9c0917596a8535e2a81ef3ab2 +spore id 0x01eb873a190a200cdf3a21ee823663e3f2d5d220b0dee6033fd06a67c43cb733 +spore id 0x9dcefeaa8018174caa4666a0efcd2e07db4d19ea698f4849ad2daf5ff973cec1 +spore id 0x32921942cbebbdf2608e4155527c27d28b7f270bedf28cb0102ee54c73851942 +spore id 0x10eff45b53a790d7d90ce079f1ca8ef0043db9bd778f4fdac7b83ffabb2525dc +spore id 0xb36e75044a8beee916255b3391e3d3373188cecafde56842f18804930934b73a +spore id 0xb17a0db52e5e3ede6ee41e79ed68a883d827efa27737a5e11f8f0ac710f23567 +spore id 0x498951a02762ae2655a1a822cd0ec5e475b3d5686730d10f59da719ace75d2af */ diff --git a/packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts b/packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts index ed7944c6..88f02d53 100644 --- a/packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts +++ b/packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts @@ -1,40 +1,48 @@ -import { spore } from "@ckb-ccc/shell"; +import { ccc, spore } from "@ckb-ccc/shell"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; -async function transferSpore({ - btcAddress, - sporeTypeArgs, -}: { - btcAddress: string; - sporeTypeArgs: string; -}) { +async function transferSpore( + transfers: Array<{ + btcAddress: string; + sporeTypeArgs: string; + }>, +) { const { rgbppBtcWallet, rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); - const { tx: ckbPartialTx } = await spore.transferSpore({ - signer: ckbSigner, - id: sporeTypeArgs, - to: rgbppUdtClient.buildPseudoRgbppLockScript(), - }); + let ckbPartialTx = ccc.Transaction.from({}); + for (const { sporeTypeArgs } of transfers) { + const { tx: _ckbPartialTx } = await spore.transferSpore({ + signer: ckbSigner, + id: sporeTypeArgs, + to: rgbppUdtClient.buildPseudoRgbppLockScript(), + tx: ckbPartialTx, + }); + ckbPartialTx = _ckbPartialTx; + } const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ ckbPartialTx, ckbClient, rgbppUdtClient, btcChangeAddress: utxoBasedAccountAddress, - receiverBtcAddresses: [btcAddress], + receiverBtcAddresses: transfers.map((t) => t.btcAddress), feeRate: 28, }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( @@ -53,11 +61,18 @@ async function transferSpore({ const logger = new RgbppTxLogger({ opType: "spore-transfer" }); -transferSpore({ - btcAddress: "tb1qjkdqj8zk6gl7pwuw2d2jp9e6wgf26arjl8pcys", - sporeTypeArgs: - "0xd98234035b2275b9abf1e9d87da53814c5f310aabdf3c6e06084e6e4e8d9d8e2", -}) +transferSpore([ + { + btcAddress: "tb1q4vkt8486w7syqyvz3a4la0f3re5vvj9zw4henw", + sporeTypeArgs: + "0x8d814f7306d31bdfa40ddec0d3c9391c5505a7e9c0917596a8535e2a81ef3ab2", + }, + { + btcAddress: "tb1q4vkt8486w7syqyvz3a4la0f3re5vvj9zw4henw", + sporeTypeArgs: + "0x01eb873a190a200cdf3a21ee823663e3f2d5d220b0dee6033fd06a67c43cb733", + }, +]) .then(() => { logger.saveOnSuccess(); process.exit(0); @@ -70,4 +85,6 @@ transferSpore({ /* pnpm tsx packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts + +https://testnet.explorer.nervos.org/transaction/0x43923c45d214bab0fbbd1b90b15197147bb4a47aaae01c1a36e81585aa84aa78 */ diff --git a/packages/rgbpp/src/examples/spore/4-spore-btc-to-ckb.ts b/packages/rgbpp/src/examples/spore/4-spore-btc-to-ckb.ts index 4888d825..ca664f5d 100644 --- a/packages/rgbpp/src/examples/spore/4-spore-btc-to-ckb.ts +++ b/packages/rgbpp/src/examples/spore/4-spore-btc-to-ckb.ts @@ -1,6 +1,8 @@ import { spore } from "@ckb-ccc/shell"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -16,7 +18,9 @@ async function btcSporeToCkb({ rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); const { tx: ckbPartialTx } = await spore.transferSpore({ signer: ckbSigner, @@ -34,7 +38,7 @@ async function btcSporeToCkb({ }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( diff --git a/packages/rgbpp/src/examples/spore/5-spore-ckb-to-btc.ts b/packages/rgbpp/src/examples/spore/5-spore-ckb-to-btc.ts index 6f538b85..e0629fef 100644 --- a/packages/rgbpp/src/examples/spore/5-spore-ckb-to-btc.ts +++ b/packages/rgbpp/src/examples/spore/5-spore-ckb-to-btc.ts @@ -2,7 +2,9 @@ import { spore } from "@ckb-ccc/shell"; import { UtxoSeal } from "../../types/rgbpp/index.js"; -import { ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -13,7 +15,8 @@ async function ckbSporeToBtc({ utxoSeal?: UtxoSeal; sporeTypeArgs: string; }) { - const { rgbppBtcWallet, rgbppUdtClient } = initializeRgbppEnv(); + const { rgbppBtcWallet, rgbppUdtClient, ckbSigner } = + await initializeRgbppEnv(); if (!utxoSeal) { utxoSeal = await rgbppBtcWallet.prepareUtxoSeal({ feeRate: 28 }); diff --git a/packages/rgbpp/src/examples/udt/1-rgbpp-udt-issuance.ts b/packages/rgbpp/src/examples/udt/1-rgbpp-udt-issuance.ts index 035df858..e4ce7062 100644 --- a/packages/rgbpp/src/examples/udt/1-rgbpp-udt-issuance.ts +++ b/packages/rgbpp/src/examples/udt/1-rgbpp-udt-issuance.ts @@ -1,10 +1,22 @@ import { ScriptInfo, UtxoSeal } from "../../types/rgbpp/index.js"; -import { issuanceAmount, testnetSudtInfo, udtToken } from "../common/assets.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { ccc } from "@ckb-ccc/shell"; +import { issuanceAmount, udtToken } from "../common/assets.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; import { prepareRgbppCells } from "../common/utils.js"; +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSinger, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + async function issueUdt({ udtScriptInfo, utxoSeal, @@ -12,18 +24,16 @@ async function issueUdt({ udtScriptInfo: ScriptInfo; utxoSeal?: UtxoSeal; }) { - const { - rgbppBtcWallet, - rgbppUdtClient, - utxoBasedAccountAddress, - ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); - if (!utxoSeal) { utxoSeal = await rgbppBtcWallet.prepareUtxoSeal({ feeRate: 10 }); } - const rgbppIssuanceCells = await prepareRgbppCells(utxoSeal, rgbppUdtClient); + const rgbppIssuanceCells = await prepareRgbppCells( + ckbClient, + ckbSigner, + utxoSeal, + rgbppUdtClient, + ); const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({ token: udtToken, @@ -46,7 +56,7 @@ async function issueUdt({ }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( @@ -69,21 +79,21 @@ async function issueUdt({ const logger = new RgbppTxLogger({ opType: "udt-issuance" }); issueUdt({ - // udtScriptInfo: { - // name: ccc.KnownScript.XUdt, - // script: await ccc.Script.fromKnownScript( - // ckbClient, - // ccc.KnownScript.XUdt, - // "", - // ), - // cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0] - // .cellDep, - // }, + udtScriptInfo: { + name: ccc.KnownScript.XUdt, + script: await ccc.Script.fromKnownScript( + ckbClient, + ccc.KnownScript.XUdt, + "", + ), + cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0] + .cellDep, + }, - udtScriptInfo: testnetSudtInfo, + // udtScriptInfo: testnetSudtInfo, utxoSeal: { - txId: "f4714d1c4a6d2528a3949db56977f569b62b9735dc008a65d922940ab3580bcd", + txId: "45a32a70556205a6f0523448406218ea12c1b61c10a2df8f844ec0a2609ccb6c", index: 2, }, }) diff --git a/packages/rgbpp/src/examples/udt/2-udt-transfer-on-btc.ts b/packages/rgbpp/src/examples/udt/2-udt-transfer-on-btc.ts index b0daecf6..d3d158b8 100644 --- a/packages/rgbpp/src/examples/udt/2-udt-transfer-on-btc.ts +++ b/packages/rgbpp/src/examples/udt/2-udt-transfer-on-btc.ts @@ -1,12 +1,23 @@ import { ccc } from "@ckb-ccc/shell"; +import "../common/load-env.js"; + import { RgbppBtcReceiver, ScriptInfo } from "../../types/rgbpp/index.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { testnetSudtInfo } from "../common/assets.js"; import { RgbppTxLogger } from "../common/logger.js"; +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSinger, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + async function transferUdt({ udtScriptInfo, receivers, @@ -14,13 +25,6 @@ async function transferUdt({ udtScriptInfo: ScriptInfo; receivers: RgbppBtcReceiver[]; }) { - const { - rgbppBtcWallet, - rgbppUdtClient, - utxoBasedAccountAddress, - ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); - const udt = new ccc.udt.Udt( udtScriptInfo.cellDep.outPoint, udtScriptInfo.script, @@ -53,7 +57,7 @@ async function transferUdt({ }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( diff --git a/packages/rgbpp/src/examples/udt/3-udt-transfer-btc-to-ckb.ts b/packages/rgbpp/src/examples/udt/3-udt-transfer-btc-to-ckb.ts index 9f96a734..410c137f 100644 --- a/packages/rgbpp/src/examples/udt/3-udt-transfer-btc-to-ckb.ts +++ b/packages/rgbpp/src/examples/udt/3-udt-transfer-btc-to-ckb.ts @@ -2,11 +2,22 @@ import { ccc } from "@ckb-ccc/shell"; import { ScriptInfo } from "../../types/rgbpp/index.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { testnetSudtInfo } from "../common/assets.js"; import { RgbppTxLogger } from "../common/logger.js"; +const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSinger, + ckbClient, + ckbSigner, +} = await initializeRgbppEnv(); + async function btcUdtToCkb({ udtScriptInfo, receivers, @@ -14,13 +25,6 @@ async function btcUdtToCkb({ udtScriptInfo: ScriptInfo; receivers: { address: string; amount: bigint }[]; }) { - const { - rgbppBtcWallet, - rgbppUdtClient, - utxoBasedAccountAddress, - ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); - const udt = new ccc.udt.Udt( udtScriptInfo.cellDep.outPoint, udtScriptInfo.script, @@ -53,7 +57,7 @@ async function btcUdtToCkb({ }); logger.logCkbTx("indexedCkbPartialTx", indexedCkbPartialTx); - const btcTxId = await rgbppBtcWallet.signAndSendTx(psbt); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); logger.add("btcTxId", btcTxId, true); const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( diff --git a/packages/rgbpp/src/examples/udt/4-unlock-btc-time-lock.ts b/packages/rgbpp/src/examples/udt/4-unlock-btc-time-lock.ts index 23d03ad9..a73d11c4 100644 --- a/packages/rgbpp/src/examples/udt/4-unlock-btc-time-lock.ts +++ b/packages/rgbpp/src/examples/udt/4-unlock-btc-time-lock.ts @@ -10,14 +10,19 @@ import { import { PredefinedScriptName } from "../../types/script.js"; import { testnetSudtCellDep } from "../common/assets.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; + +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; import { collectBtcTimeLockCells } from "../common/utils.js"; async function unlockBtcTimeLock(btcTimeLockArgs: string) { - const { rgbppBtcWallet, rgbppUdtClient } = initializeRgbppEnv(); + const { rgbppBtcWallet, rgbppUdtClient, ckbClient, ckbSigner } = + await initializeRgbppEnv(); const btcTimeLockCells = await collectBtcTimeLockCells( + ckbClient, btcTimeLockArgs, rgbppUdtClient, ); diff --git a/packages/rgbpp/src/examples/udt/5-udt-transfer-ckb-to-btc.ts b/packages/rgbpp/src/examples/udt/5-udt-transfer-ckb-to-btc.ts index 2931c555..e64c9f94 100644 --- a/packages/rgbpp/src/examples/udt/5-udt-transfer-ckb-to-btc.ts +++ b/packages/rgbpp/src/examples/udt/5-udt-transfer-ckb-to-btc.ts @@ -2,10 +2,15 @@ import { ccc } from "@ckb-ccc/shell"; import { ScriptInfo, UtxoSeal } from "../../types/rgbpp/index.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import "../common/load-env.js"; + +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; +const { rgbppBtcWallet, rgbppUdtClient, ckbClient, ckbSigner } = + await initializeRgbppEnv(); + async function ckbUdtToBtc({ utxoSeal, udtScriptInfo, @@ -16,8 +21,6 @@ async function ckbUdtToBtc({ amount: bigint; }) { - const { rgbppBtcWallet, rgbppUdtClient } = initializeRgbppEnv(); - if (!utxoSeal) { utxoSeal = await rgbppBtcWallet.prepareUtxoSeal({ feeRate: 28 }); } diff --git a/packages/rgbpp/tsconfig.base.json b/packages/rgbpp/tsconfig.base.json new file mode 100644 index 00000000..7e5ac952 --- /dev/null +++ b/packages/rgbpp/tsconfig.base.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "es2020", + "incremental": true, + "allowJs": true, + "importHelpers": false, + "declaration": true, + "declarationMap": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "esModuleInterop": true, + "strict": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "strictNullChecks": true, + "alwaysStrict": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/rgbpp/tsconfig.commonjs.json b/packages/rgbpp/tsconfig.commonjs.json new file mode 100644 index 00000000..76a25e98 --- /dev/null +++ b/packages/rgbpp/tsconfig.commonjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "./dist.commonjs" + } +} diff --git a/packages/rgbpp/tsconfig.json b/packages/rgbpp/tsconfig.json index 031b3faf..df22faec 100644 --- a/packages/rgbpp/tsconfig.json +++ b/packages/rgbpp/tsconfig.json @@ -1,18 +1,8 @@ { + "extends": "./tsconfig.base.json", "compilerOptions": { - /* Visit https://aka.ms/tsconfig to read more about this file */ - "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Bundler", "outDir": "./dist", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "NodeNext", - "module": "NodeNext", - "declarationDir": "dist", - "sourceMap": true, - "declaration": true, - "declarationMap": true - }, - "include": ["src/**/*"] + } } diff --git a/packages/uni-sat/src/advancedBarrel.ts b/packages/uni-sat/src/advancedBarrel.ts index e6ae56b5..05292a8e 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -2,6 +2,30 @@ * Interface representing a provider for interacting with accounts and signing messages. */ export interface Provider { + // TODO: tweaked signer for taproot + signPsbt(psbtHex: string): Promise; + + pushPsbt(psbtHex: string): Promise; + + pushTx(tx: { rawtx: string }): Promise; + + /** + * Signs a PSBT using UniSat wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + * @todo Add support for Taproot signing options (useTweakedSigner, etc.) + */ + signPsbt(psbtHex: string): Promise; + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + pushPsbt(psbtHex: string): Promise; + /** * Requests user accounts. * @returns A promise that resolves to an array of account addresses. diff --git a/packages/uni-sat/src/signer.ts b/packages/uni-sat/src/signer.ts index 653bba8e..9dc8d03f 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -107,7 +107,11 @@ export class Signer extends ccc.SignerBtc { * @returns A promise that resolves when the connection is established. */ async connect(): Promise { - await this.provider.requestAccounts(); + const accounts = await this.provider.requestAccounts(); + console.log("connected accounts", accounts); + if (accounts.length === 0) { + throw new Error("No accounts found"); + } await this.ensureNetwork(); } @@ -150,4 +154,24 @@ export class Signer extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + /** + * Signs a PSBT using UniSat wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + */ + async signPsbt(psbtHex: string): Promise { + return this.provider.signPsbt(psbtHex); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + async pushPsbt(psbtHex: string): Promise { + return this.provider.pushPsbt(psbtHex); + } } diff --git a/packages/utxo-global/src/btc/index.ts b/packages/utxo-global/src/btc/index.ts index 57e73594..c123b2a9 100644 --- a/packages/utxo-global/src/btc/index.ts +++ b/packages/utxo-global/src/btc/index.ts @@ -127,4 +127,26 @@ export class SignerBtc extends ccc.SignerBtc { this.accountCache ?? (await this.getBtcAccount()), ); } + + /** + * Signs a PSBT using UTXO Global wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + * @todo Implement PSBT signing with UTXO Global + */ + async signPsbt(_: string): Promise { + throw new Error("UTXO Global PSBT signing not implemented yet"); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + * @todo Implement PSBT broadcasting with UTXO Global + */ + async pushPsbt(_: string): Promise { + throw new Error("UTXO Global PSBT broadcasting not implemented yet"); + } } diff --git a/packages/xverse/src/signer.ts b/packages/xverse/src/signer.ts index bf6df9c0..e9fd211b 100644 --- a/packages/xverse/src/signer.ts +++ b/packages/xverse/src/signer.ts @@ -167,4 +167,12 @@ export class Signer extends ccc.SignerBtc { ) ).signature; } + + async signPsbt(_: string): Promise { + throw new Error("Not implemented"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Not implemented"); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b0e8c1c..6ff6ef44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -318,6 +318,9 @@ importers: '@uiw/react-json-view': specifier: 2.0.0-alpha.30 version: 2.0.0-alpha.30(@babel/runtime@7.26.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + bitcoinjs-lib: + specifier: 6.1.6 + version: 6.1.6 lucide-react: specifier: ^0.427.0 version: 0.427.0(react@18.3.1) @@ -337,6 +340,9 @@ importers: '@ckb-ccc/lumos-patches': specifier: workspace:* version: link:../lumos-patches + '@ckb-ccc/rgbpp': + specifier: workspace:* + version: link:../rgbpp '@ckb-ccc/ssri': specifier: workspace:* version: link:../ssri @@ -975,6 +981,9 @@ importers: '@types/node': specifier: ^22.10.6 version: 22.13.1 + copyfiles: + specifier: ^2.4.1 + version: 2.4.1 dotenv: specifier: ^16.4.7 version: 16.4.7