From 5d46a245d35867c226f50e50d023e81246fc5354 Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 29 May 2025 13:34:40 -0500 Subject: [PATCH 01/15] pushPsbt ok --- packages/demo/package.json | 1 + .../connected/(tools)/IssueRgbppXUdt/page.tsx | 141 ++++++++++++++++++ packages/demo/src/app/connected/page.tsx | 1 + packages/rgbpp/src/barrel.ts | 1 + packages/rgbpp/src/examples/common/env.ts | 33 ++-- packages/rgbpp/src/examples/common/index.ts | 3 + packages/rgbpp/src/examples/index.ts | 1 + packages/uni-sat/src/advancedBarrel.ts | 7 + packages/uni-sat/src/signer.ts | 41 ++++- pnpm-lock.yaml | 9 +- 10 files changed, 223 insertions(+), 15 deletions(-) create mode 100644 packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx create mode 100644 packages/rgbpp/src/examples/common/index.ts create mode 100644 packages/rgbpp/src/examples/index.ts diff --git a/packages/demo/package.json b/packages/demo/package.json index d52513a2..3b56321a 100644 --- a/packages/demo/package.json +++ b/packages/demo/package.json @@ -27,6 +27,7 @@ "@ckb-ccc/lumos-patches": "workspace:*", "@ckb-ccc/ssri": "workspace:*", "@ckb-ccc/udt": "workspace:*", + "@ckb-ccc/rgbpp": "workspace:*", "@ckb-lumos/ckb-indexer": "^0.24.0-next.1", "@ckb-lumos/common-scripts": "^0.24.0-next.1", "@ckb-lumos/config-manager": "^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..579ab2f0 --- /dev/null +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -0,0 +1,141 @@ +"use client"; + +import React, { useCallback, useEffect, useState } from "react"; +import { TextInput } from "@/src/components/Input"; +import { Button } from "@/src/components/Button"; +import { useGetExplorerLink } from "@/src/utils"; +import { useApp } from "@/src/context"; +import { ButtonsPanel } from "@/src/components/ButtonsPanel"; +import { Dropdown } from "@/src/components/Dropdown"; +import { ccc, spore } from "@ckb-ccc/connector-react"; +import { Message } from "@/src/components/Message"; +import Link from "next/link"; + +import { initializeRgbppEnv, issuanceAmount, prepareRgbppCells, udtToken, UtxoSeal, ckbClient } from "@ckb-ccc/rgbpp"; + +export default function IssueRGBPPXUdt() { + const { signer, createSender } = useApp(); + const { log, warn } = createSender("Issue RGB++ xUDT"); + const { explorerTransaction } = useGetExplorerLink(); + const [contentType, setContentType] = useState("dob/1"); + const [content, setContent] = useState( + '{ "dna": "0123456789abcdef" }', + ); + const [clusterId, setClusterId] = useState(""); + const [clusterList, setClusterList] = useState([ + { + id: "", + name: "Mint Without Cluster", + }, + ]); + + useEffect(() => { + if (!signer) { + return; + } + + (async () => { + const list = [ + { + id: "", + name: "Mint Without Cluster", + }, + ]; + + for await (const { + cluster, + clusterData, + } of spore.findSporeClustersBySigner({ + signer, + order: "desc", + })) { + if (!cluster.cellOutput.type?.args) { + continue; + } + + list.push({ + id: cluster.cellOutput.type.args, + name: `${clusterData.name} (${cluster.cellOutput.type.args.slice(0, 10)})`, + }); + } + + setClusterList(list); + })(); + }, [signer]); + + const { + rgbppBtcWallet, + rgbppUdtClient, + utxoBasedAccountAddress, + ckbRgbppUnlockSinger, + } = initializeRgbppEnv(); + const utxoSeal: UtxoSeal = { + txId: "a01064157fa765ceabf5673bcbd9781ed54a66b78274480d9c63e7c94c3b093c", + index: 2, + } + + const signRgbppBtcTx = useCallback(async () => { + if (!signer) { + return; + } + + const rgbppIssuanceCells = await prepareRgbppCells(utxoSeal, rgbppUdtClient); + + const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({ + token: udtToken, + amount: issuanceAmount, + rgbppLiveCells: rgbppIssuanceCells, + udtScriptInfo: { + name: ccc.KnownScript.XUdt, + script: await ccc.Script.fromKnownScript( + ckbClient, + ccc.KnownScript.XUdt, + "", + ), + cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0] + .cellDep, + }, + }); + console.log( + "Unique ID of issued udt token", + ckbPartialTx.outputs[0].type!.args, + ); + + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx, + ckbClient, + rgbppUdtClient, + btcChangeAddress: utxoBasedAccountAddress, + receiverBtcAddresses: [utxoBasedAccountAddress], + feeRate: 28, + }); + + const psbtHex = psbt.toHex(); + console.log("psbt hex demo page\n", psbtHex); + + // const signedTx_ = await rgbppBtcWallet.signTx(psbt); + // const signedTxHex_ = signedTx_.toHex(); + // console.log("signedTxHex RGB++\n", signedTxHex_); + + const pseudoCkbTx = ccc.Transaction.from({ + version: 1010101, + outputsData: [psbtHex], + }); + + const signedTx = await signer.signTransaction(pseudoCkbTx); + const txHash = await signer.sendTransaction(signedTx); + console.log("txHash uni-sat\n", txHash); + + // const txHash = await rgbppBtcWallet.sendTransaction(signedTx.outputsData[0]); + // console.log("btc tx id:", txHash); + + }, [signer]); + + return ( +
+ + + +
+ ); +} 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/rgbpp/src/barrel.ts b/packages/rgbpp/src/barrel.ts index da9337ff..342097dd 100644 --- a/packages/rgbpp/src/barrel.ts +++ b/packages/rgbpp/src/barrel.ts @@ -1,6 +1,7 @@ export * from "./bitcoin/index.js"; export * from "./configs/index.js"; export * from "./constants/index.js"; +export * from "./examples/index.js"; export * from "./interfaces/index.js"; export * from "./signer/index.js"; export * from "./types/index.js"; diff --git a/packages/rgbpp/src/examples/common/env.ts b/packages/rgbpp/src/examples/common/env.ts index 4d7896a6..ec49c17d 100644 --- a/packages/rgbpp/src/examples/common/env.ts +++ b/packages/rgbpp/src/examples/common/env.ts @@ -2,9 +2,6 @@ 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 { ScriptInfo } from "../../types/rgbpp/index.js"; @@ -13,15 +10,29 @@ 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" }); +// dotenv.config({ path: dirname(fileURLToPath(import.meta.url)) + "/../.env" }); +dotenv.config({ + path: "/root/ckb/ccc/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/.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!; +// const utxoBasedChainAddressType = process.env.UTXO_BASED_CHAIN_ADDRESS_TYPE!; +// const btcAssetsApiUrl = process.env.BTC_ASSETS_API_URL!; +// const btcAssetsApiToken = process.env.BTC_ASSETS_API_TOKEN!; +// const btcAssetsApiOrigin = process.env.BTC_ASSETS_API_ORIGIN!; -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!; -const utxoBasedChainAddressType = process.env.UTXO_BASED_CHAIN_ADDRESS_TYPE!; -const btcAssetsApiUrl = process.env.BTC_ASSETS_API_URL!; -const btcAssetsApiToken = process.env.BTC_ASSETS_API_TOKEN!; -const btcAssetsApiOrigin = process.env.BTC_ASSETS_API_ORIGIN!; +const utxoBasedChainName = "BitcoinTestnet3"; +const ckbPrivateKey = + "0x86f9dc9ce6218579ba9a0eb72b71ee747ed536c5866895e54a16dc3fdcfde9b9"; +const utxoBasedChainPrivateKey = + "4f2d2b8c36634c1fa14494951a2d70d12883dec0f400d2adfb55f04d4fc4f7ea"; +const utxoBasedChainAddressType = "P2WPKH"; +const btcAssetsApiUrl = "http://localhost:3003"; +const btcAssetsApiToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbi10ZXN0IiwiYXVkIjoibG9jYWxob3N0IiwianRpIjoiN2I2ODNkNzgtY2U4My00NWFlLTgxZTQtNTBhYWM4MWI1MThhIiwiaWF0IjoxNzQ4NDE3MjY1fQ.vN9jFiJJy_dzIMJKltfrAWxd4TmaJD-v8a5q-t7Qqew"; +const btcAssetsApiOrigin = "localhost"; export const ckbClient = isMainnet(utxoBasedChainName) ? new ccc.ClientPublicMainnet() 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/index.ts b/packages/rgbpp/src/examples/index.ts new file mode 100644 index 00000000..027f648c --- /dev/null +++ b/packages/rgbpp/src/examples/index.ts @@ -0,0 +1 @@ +export * from "./common/index.js"; diff --git a/packages/uni-sat/src/advancedBarrel.ts b/packages/uni-sat/src/advancedBarrel.ts index e6ae56b5..c3f9eea3 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -2,6 +2,13 @@ * Interface representing a provider for interacting with accounts and signing messages. */ export interface Provider { + // TODO: tweaked signer for taproot + signPsbt(psbtHex: string): Promise; + + pushTx({ rawtx }: { rawtx: string }): Promise; + + 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..2b654dff 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,39 @@ export class Signer extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + // TODO: move to SignerBtc + async signTransaction(tx_: ccc.TransactionLike): Promise { + const tx = ccc.Transaction.from(tx_); + if (tx.version !== 1010101n) { + console.log("signing normal ckb tx"); + return super.signTransaction(tx); + } + + console.log("signing btc tx"); + const psbtHex = tx.outputsData[0].slice(2); + console.log("psbtHex uni-sat before signing\n", psbtHex); + const signedPsbtHex = await this.provider.signPsbt(psbtHex); + console.log("signedPsbtHex uni-sat\n", signedPsbtHex); + tx.outputsData[0] = signedPsbtHex as ccc.Hex; + return tx; + } + + // TODO: align with other signers: sign and send + async sendTransaction(tx_: ccc.TransactionLike): Promise { + const tx = ccc.Transaction.from(tx_); + if (tx.version !== 1010101n) { + console.log("send normal ckb tx"); + return super.sendTransaction(tx); + } + + console.log("send btc tx", tx.outputsData[0]); + try { + const txHash = await this.provider.pushPsbt(tx.outputsData[0]); + return txHash as ccc.Hex; + } catch (error: any) { + console.error("Push transaction error:", JSON.stringify(error, null, 2)); + throw error; + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7b0e8c1c..6c9a41b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: bs58check@4.0.0: - hash: 0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26 + hash: xkju25h33uk7ay46aspcygifve path: patches/bs58check@4.0.0.patch importers: @@ -244,7 +244,7 @@ importers: version: 2.2.0 bs58check: specifier: ^4.0.0 - version: 4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26) + version: 4.0.0(patch_hash=xkju25h33uk7ay46aspcygifve) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -337,6 +337,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 @@ -14447,7 +14450,7 @@ snapshots: '@noble/hashes': 1.7.1 bs58: 5.0.0 - bs58check@4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26): + bs58check@4.0.0(patch_hash=xkju25h33uk7ay46aspcygifve): dependencies: '@noble/hashes': 1.7.1 bs58: 6.0.0 From 1ebfce83e408bdbaceb7c49a30a69930fff1e64d Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 29 May 2025 14:37:03 -0500 Subject: [PATCH 02/15] up to signerBtc --- packages/core/src/signer/btc/signerBtc.ts | 53 +++++++++++++++- .../signer/btc/signerBtcPublicKeyReadonly.ts | 6 +- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 60 +++---------------- packages/uni-sat/src/advancedBarrel.ts | 2 - packages/uni-sat/src/signer.ts | 39 ++---------- 5 files changed, 68 insertions(+), 92 deletions(-) diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index 64112a74..2bd51ec8 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -1,12 +1,19 @@ 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 { HexLike, hexFrom } from "../../hex/index.js"; +import { Client, KnownScript } from "../../client/index.js"; +import { Hex, HexLike, hexFrom } from "../../hex/index.js"; import { numToBytes } from "../../num/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; import { btcEcdsaPublicKeyHash } from "./verify.js"; +export interface Provider { + // TODO: tweaked signer for taproot + signPsbt(psbtHex: string): Promise; + + pushPsbt(psbtHex: string): Promise; +} + /** * An abstract class extending the Signer class for Bitcoin-like signing operations. * This class provides methods to get Bitcoin account, public key, and internal address, @@ -14,6 +21,12 @@ import { btcEcdsaPublicKeyHash } from "./verify.js"; * @public */ export abstract class SignerBtc extends Signer { + constructor(client: Client) { + super(client); + } + + protected abstract getProvider(): Provider; + get type(): SignerType { return SignerType.BTC; } @@ -123,4 +136,40 @@ export abstract class SignerBtc extends Signer { tx.setWitnessArgsAt(info.position, witness); return tx; } + + async signTransaction(tx_: TransactionLike): Promise { + const tx = Transaction.from(tx_); + + console.log("signTransaction, tx version", tx.version); + + if (tx.version !== 668467n) { + const preparedTx = await this.prepareTransaction(tx_); + return this.signOnlyTransaction(preparedTx); + } + + console.log("signing btc tx"); + const psbtHex = tx.outputsData[0].slice(2); + const signedPsbtHex = await this.getProvider().signPsbt(psbtHex); + tx.outputsData[0] = signedPsbtHex as Hex; + return tx; + } + + async sendTransaction(tx_: TransactionLike): Promise { + const tx = Transaction.from(tx_); + + console.log("sendTransaction, tx version", tx.version); + + if (tx.version !== 668467n) { + return super.sendTransaction(tx_); + } + + console.log("sending btc tx", tx.outputsData[0]); + try { + const txHash = await this.getProvider().pushPsbt(tx.outputsData[0]); + return txHash as Hex; + } catch (error: any) { + console.error("Push transaction error:", JSON.stringify(error, null, 2)); + throw error; + } + } } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index 50096db7..9ca6ca3c 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -1,6 +1,6 @@ import { Client } from "../../client/index.js"; import { Hex, HexLike, hexFrom } from "../../hex/index.js"; -import { SignerBtc } from "./signerBtc.js"; +import { Provider, SignerBtc } from "./signerBtc.js"; /** * A class extending SignerBtc that provides read-only access to a Bitcoin public key and account. @@ -27,6 +27,10 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { this.publicKey = hexFrom(publicKey); } + protected getProvider(): Provider { + throw new Error("Read-only signer does not support provider operations"); + } + /** * Connects to the client. This implementation does nothing as the class is read-only. * diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 579ab2f0..86cef5ae 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -16,51 +16,12 @@ import { initializeRgbppEnv, issuanceAmount, prepareRgbppCells, udtToken, UtxoSe export default function IssueRGBPPXUdt() { const { signer, createSender } = useApp(); const { log, warn } = createSender("Issue RGB++ xUDT"); - const { explorerTransaction } = useGetExplorerLink(); - const [contentType, setContentType] = useState("dob/1"); - const [content, setContent] = useState( - '{ "dna": "0123456789abcdef" }', - ); - const [clusterId, setClusterId] = useState(""); - const [clusterList, setClusterList] = useState([ - { - id: "", - name: "Mint Without Cluster", - }, - ]); + const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); useEffect(() => { if (!signer) { return; } - - (async () => { - const list = [ - { - id: "", - name: "Mint Without Cluster", - }, - ]; - - for await (const { - cluster, - clusterData, - } of spore.findSporeClustersBySigner({ - signer, - order: "desc", - })) { - if (!cluster.cellOutput.type?.args) { - continue; - } - - list.push({ - id: cluster.cellOutput.type.args, - name: `${clusterData.name} (${cluster.cellOutput.type.args.slice(0, 10)})`, - }); - } - - setClusterList(list); - })(); }, [signer]); const { @@ -70,7 +31,7 @@ export default function IssueRGBPPXUdt() { ckbRgbppUnlockSinger, } = initializeRgbppEnv(); const utxoSeal: UtxoSeal = { - txId: "a01064157fa765ceabf5673bcbd9781ed54a66b78274480d9c63e7c94c3b093c", + txId: "6cabb6b0c75b8f3331976d7cf8e3ee7915b77e0b0b26331e0dfd3681752a7a6465cc2b7d4931d564667581db9ab46110ca447664112c28ee4d0fc7f66be812ff", index: 2, } @@ -111,28 +72,23 @@ export default function IssueRGBPPXUdt() { }); const psbtHex = psbt.toHex(); - console.log("psbt hex demo page\n", psbtHex); - - // const signedTx_ = await rgbppBtcWallet.signTx(psbt); - // const signedTxHex_ = signedTx_.toHex(); - // console.log("signedTxHex RGB++\n", signedTxHex_); + //* 668467 is the ASCII decimal representation of "BTC" (66, 84, 67) const pseudoCkbTx = ccc.Transaction.from({ - version: 1010101, + version: 668467, outputsData: [psbtHex], }); const signedTx = await signer.signTransaction(pseudoCkbTx); const txHash = await signer.sendTransaction(signedTx); - console.log("txHash uni-sat\n", txHash); - - // const txHash = await rgbppBtcWallet.sendTransaction(signedTx.outputsData[0]); - // console.log("btc tx id:", txHash); - + setRgbppBtcTxId(txHash); }, [signer]); return (
+ + {rgbppBtcTxId &&
Waiting for RGB++ BTC Transaction {rgbppBtcTxId} to be confirmed...
} + diff --git a/packages/uni-sat/src/advancedBarrel.ts b/packages/uni-sat/src/advancedBarrel.ts index c3f9eea3..ec795044 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -5,8 +5,6 @@ export interface Provider { // TODO: tweaked signer for taproot signPsbt(psbtHex: string): Promise; - pushTx({ rawtx }: { rawtx: string }): Promise; - pushPsbt(psbtHex: string): Promise; /** diff --git a/packages/uni-sat/src/signer.ts b/packages/uni-sat/src/signer.ts index 2b654dff..6882beea 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -30,6 +30,10 @@ export class Signer extends ccc.SignerBtc { super(client); } + getProvider(): Provider { + return this.provider; + } + async _getNetworkToChange(): Promise { const currentNetwork = await (async () => { if (this.provider.getChain) { @@ -154,39 +158,4 @@ export class Signer extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } - - // TODO: move to SignerBtc - async signTransaction(tx_: ccc.TransactionLike): Promise { - const tx = ccc.Transaction.from(tx_); - if (tx.version !== 1010101n) { - console.log("signing normal ckb tx"); - return super.signTransaction(tx); - } - - console.log("signing btc tx"); - const psbtHex = tx.outputsData[0].slice(2); - console.log("psbtHex uni-sat before signing\n", psbtHex); - const signedPsbtHex = await this.provider.signPsbt(psbtHex); - console.log("signedPsbtHex uni-sat\n", signedPsbtHex); - tx.outputsData[0] = signedPsbtHex as ccc.Hex; - return tx; - } - - // TODO: align with other signers: sign and send - async sendTransaction(tx_: ccc.TransactionLike): Promise { - const tx = ccc.Transaction.from(tx_); - if (tx.version !== 1010101n) { - console.log("send normal ckb tx"); - return super.sendTransaction(tx); - } - - console.log("send btc tx", tx.outputsData[0]); - try { - const txHash = await this.provider.pushPsbt(tx.outputsData[0]); - return txHash as ccc.Hex; - } catch (error: any) { - console.error("Push transaction error:", JSON.stringify(error, null, 2)); - throw error; - } - } } From 6c3b205475c009272e26e2135a4406fe280714ec Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 29 May 2025 15:29:39 -0500 Subject: [PATCH 03/15] draft --- packages/core/src/signer/btc/signerBtc.ts | 15 +- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 184 ++++++++++++------ 2 files changed, 131 insertions(+), 68 deletions(-) diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index 2bd51ec8..d3f2b7ad 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -140,14 +140,11 @@ export abstract class SignerBtc extends Signer { async signTransaction(tx_: TransactionLike): Promise { const tx = Transaction.from(tx_); - console.log("signTransaction, tx version", tx.version); - if (tx.version !== 668467n) { const preparedTx = await this.prepareTransaction(tx_); return this.signOnlyTransaction(preparedTx); } - console.log("signing btc tx"); const psbtHex = tx.outputsData[0].slice(2); const signedPsbtHex = await this.getProvider().signPsbt(psbtHex); tx.outputsData[0] = signedPsbtHex as Hex; @@ -157,19 +154,11 @@ export abstract class SignerBtc extends Signer { async sendTransaction(tx_: TransactionLike): Promise { const tx = Transaction.from(tx_); - console.log("sendTransaction, tx version", tx.version); - if (tx.version !== 668467n) { return super.sendTransaction(tx_); } - console.log("sending btc tx", tx.outputsData[0]); - try { - const txHash = await this.getProvider().pushPsbt(tx.outputsData[0]); - return txHash as Hex; - } catch (error: any) { - console.error("Push transaction error:", JSON.stringify(error, null, 2)); - throw error; - } + const txHash = await this.getProvider().pushPsbt(tx.outputsData[0]); + return txHash as Hex; } } diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 86cef5ae..3b7c5c83 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -3,20 +3,21 @@ import React, { useCallback, useEffect, useState } from "react"; import { TextInput } from "@/src/components/Input"; import { Button } from "@/src/components/Button"; -import { useGetExplorerLink } from "@/src/utils"; import { useApp } from "@/src/context"; import { ButtonsPanel } from "@/src/components/ButtonsPanel"; -import { Dropdown } from "@/src/components/Dropdown"; -import { ccc, spore } from "@ckb-ccc/connector-react"; +import { ccc } from "@ckb-ccc/connector-react"; import { Message } from "@/src/components/Message"; -import Link from "next/link"; -import { initializeRgbppEnv, issuanceAmount, prepareRgbppCells, udtToken, UtxoSeal, ckbClient } from "@ckb-ccc/rgbpp"; + +import { initializeRgbppEnv, issuanceAmount, udtToken, UtxoSeal } from "@ckb-ccc/rgbpp"; export default function IssueRGBPPXUdt() { const { signer, createSender } = useApp(); const { log, warn } = createSender("Issue RGB++ xUDT"); const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); + const [rgbppCkbTxId, setRgbppCkbTxId] = useState(""); + const [utxoSealTxId, setUtxoSealTxId] = useState(""); + const [utxoSealIndex, setUtxoSealIndex] = useState(""); useEffect(() => { if (!signer) { @@ -30,67 +31,140 @@ export default function IssueRGBPPXUdt() { utxoBasedAccountAddress, ckbRgbppUnlockSinger, } = initializeRgbppEnv(); - const utxoSeal: UtxoSeal = { - txId: "6cabb6b0c75b8f3331976d7cf8e3ee7915b77e0b0b26331e0dfd3681752a7a6465cc2b7d4931d564667581db9ab46110ca447664112c28ee4d0fc7f66be812ff", - index: 2, - } - const signRgbppBtcTx = useCallback(async () => { if (!signer) { return; } - const rgbppIssuanceCells = await prepareRgbppCells(utxoSeal, rgbppUdtClient); - - const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({ - token: udtToken, - amount: issuanceAmount, - rgbppLiveCells: rgbppIssuanceCells, - udtScriptInfo: { - name: ccc.KnownScript.XUdt, - script: await ccc.Script.fromKnownScript( - ckbClient, - ccc.KnownScript.XUdt, - "", - ), - cellDep: (await ckbClient.getKnownScript(ccc.KnownScript.XUdt)).cellDeps[0] - .cellDep, - }, - }); - console.log( - "Unique ID of issued udt token", - ckbPartialTx.outputs[0].type!.args, - ); + const utxoSeal: UtxoSeal = { + txId: utxoSealTxId, + index: parseInt(utxoSealIndex), + } + console.log("utxoSeal", utxoSeal); + const rgbppLockScript = rgbppUdtClient.buildRgbppLockScript(utxoSeal); - const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ - ckbPartialTx, - ckbClient, - rgbppUdtClient, - btcChangeAddress: utxoBasedAccountAddress, - receiverBtcAddresses: [utxoBasedAccountAddress], - feeRate: 28, - }); - - const psbtHex = psbt.toHex(); - - //* 668467 is the ASCII decimal representation of "BTC" (66, 84, 67) - const pseudoCkbTx = ccc.Transaction.from({ - version: 668467, - outputsData: [psbtHex], - }); - - const signedTx = await signer.signTransaction(pseudoCkbTx); - const txHash = await signer.sendTransaction(signedTx); - setRgbppBtcTxId(txHash); - }, [signer]); + const rgbppCellsGen = await signer.client.findCellsByLock(rgbppLockScript); + const rgbppIssuanceCells: ccc.Cell[] = []; + for await (const cell of rgbppCellsGen) { + rgbppIssuanceCells.push(cell); + } + + if (rgbppIssuanceCells.length !== 0) { + console.log("Using existing RGB++ cell"); + } else { + console.log("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); + } + + const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({ + token: udtToken, + 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, + }, + }); + console.log( + "Unique ID of issued udt token", + ckbPartialTx.outputs[0].type!.args, + ); + + const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ + ckbPartialTx, + ckbClient: signer.client, + rgbppUdtClient, + btcChangeAddress: utxoBasedAccountAddress, + receiverBtcAddresses: [utxoBasedAccountAddress], + feeRate: 28, + }); + + const psbtHex = psbt.toHex(); + + //* 668467 is the ASCII decimal representation of "BTC" (66, 84, 67) + const pseudoCkbTx = ccc.Transaction.from({ + version: 668467, + outputsData: [psbtHex], + }); + + const signedTx = await signer.signTransaction(pseudoCkbTx); + const btcTxId = await signer.sendTransaction(signedTx); + setRgbppBtcTxId(btcTxId); + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( + indexedCkbPartialTx, + btcTxId, + ); + const rgbppSignedCkbTx = + await ckbRgbppUnlockSinger.signTransaction(ckbPartialTxInjected); + await rgbppSignedCkbTx.completeFeeBy(signer); + const ckbFinalTxId = await signer.sendTransaction(rgbppSignedCkbTx); + await signer.client.waitTransaction(ckbFinalTxId); + setRgbppCkbTxId(ckbFinalTxId); + }, [signer, utxoSealTxId, utxoSealIndex]); + + useEffect(() => { + if (!rgbppBtcTxId) { + return; + } + }, [rgbppBtcTxId]); return (
+ + You will need to sign 2 to 4 transactions. +
+ Learn more on{" "} +
+ + + + + {rgbppBtcTxId && !rgbppCkbTxId &&
Waiting for RGB++ BTC Transaction {rgbppBtcTxId} to be confirmed...
} + + { + rgbppBtcTxId && rgbppCkbTxId && ( +
+ RGB++ xUDT is issued successfully. +
+ RGB++ BTC Transaction: {rgbppBtcTxId} +
+ RGB++ CKB Transaction: {rgbppCkbTxId} +
+ )} - {rgbppBtcTxId &&
Waiting for RGB++ BTC Transaction {rgbppBtcTxId} to be confirmed...
} - - +
); From 82015ae00a04c00164ee01dc74ec2f619886589c Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 29 May 2025 15:41:53 -0500 Subject: [PATCH 04/15] update btcAssetsApiUrl --- .../src/app/connected/(tools)/IssueRgbppXUdt/page.tsx | 5 +++-- packages/rgbpp/src/examples/common/env.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 3b7c5c83..e9549c7c 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -40,7 +40,6 @@ export default function IssueRGBPPXUdt() { txId: utxoSealTxId, index: parseInt(utxoSealIndex), } - console.log("utxoSeal", utxoSeal); const rgbppLockScript = rgbppUdtClient.buildRgbppLockScript(utxoSeal); const rgbppCellsGen = await signer.client.findCellsByLock(rgbppLockScript); @@ -93,6 +92,7 @@ export default function IssueRGBPPXUdt() { ckbPartialTx.outputs[0].type!.args, ); + // ! indexedCkbPartialTx should be cached in the server side const { psbt, indexedCkbPartialTx } = await rgbppBtcWallet.buildPsbt({ ckbPartialTx, ckbClient: signer.client, @@ -123,6 +123,8 @@ export default function IssueRGBPPXUdt() { const ckbFinalTxId = await signer.sendTransaction(rgbppSignedCkbTx); await signer.client.waitTransaction(ckbFinalTxId); setRgbppCkbTxId(ckbFinalTxId); + setUtxoSealTxId(""); + setUtxoSealIndex(""); }, [signer, utxoSealTxId, utxoSealIndex]); useEffect(() => { @@ -136,7 +138,6 @@ export default function IssueRGBPPXUdt() { You will need to sign 2 to 4 transactions.
- Learn more on{" "}
Date: Mon, 2 Jun 2025 14:17:10 -0500 Subject: [PATCH 05/15] add signPsbt and pushPsbt to SignerBtc class --- packages/core/src/signer/btc/signerBtc.ts | 36 +------- .../signer/btc/signerBtcPublicKeyReadonly.ts | 14 ++- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 92 +++++++++---------- packages/joy-id/src/btc/index.ts | 8 ++ packages/okx/src/btc/index.ts | 8 ++ packages/uni-sat/src/signer.ts | 12 ++- packages/utxo-global/src/btc/index.ts | 8 ++ packages/xverse/src/signer.ts | 8 ++ 8 files changed, 95 insertions(+), 91 deletions(-) diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index d3f2b7ad..59b33115 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -2,18 +2,11 @@ import { Address } from "../../address/index.js"; import { bytesConcat, bytesFrom } from "../../bytes/index.js"; import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js"; import { Client, KnownScript } from "../../client/index.js"; -import { Hex, HexLike, hexFrom } from "../../hex/index.js"; +import { HexLike, hexFrom } from "../../hex/index.js"; import { numToBytes } from "../../num/index.js"; import { Signer, SignerSignType, SignerType } from "../signer/index.js"; import { btcEcdsaPublicKeyHash } from "./verify.js"; -export interface Provider { - // TODO: tweaked signer for taproot - signPsbt(psbtHex: string): Promise; - - pushPsbt(psbtHex: string): Promise; -} - /** * An abstract class extending the Signer class for Bitcoin-like signing operations. * This class provides methods to get Bitcoin account, public key, and internal address, @@ -25,8 +18,6 @@ export abstract class SignerBtc extends Signer { super(client); } - protected abstract getProvider(): Provider; - get type(): SignerType { return SignerType.BTC; } @@ -137,28 +128,7 @@ export abstract class SignerBtc extends Signer { return tx; } - async signTransaction(tx_: TransactionLike): Promise { - const tx = Transaction.from(tx_); + abstract signPsbt(psbtHex: string): Promise; - if (tx.version !== 668467n) { - const preparedTx = await this.prepareTransaction(tx_); - return this.signOnlyTransaction(preparedTx); - } - - const psbtHex = tx.outputsData[0].slice(2); - const signedPsbtHex = await this.getProvider().signPsbt(psbtHex); - tx.outputsData[0] = signedPsbtHex as Hex; - return tx; - } - - async sendTransaction(tx_: TransactionLike): Promise { - const tx = Transaction.from(tx_); - - if (tx.version !== 668467n) { - return super.sendTransaction(tx_); - } - - const txHash = await this.getProvider().pushPsbt(tx.outputsData[0]); - return txHash as Hex; - } + abstract pushPsbt(psbtHex: string): Promise; } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index 9ca6ca3c..ba7e9322 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -1,6 +1,6 @@ import { Client } from "../../client/index.js"; import { Hex, HexLike, hexFrom } from "../../hex/index.js"; -import { Provider, SignerBtc } from "./signerBtc.js"; +import { SignerBtc } from "./signerBtc.js"; /** * A class extending SignerBtc that provides read-only access to a Bitcoin public key and account. @@ -27,10 +27,6 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { this.publicKey = hexFrom(publicKey); } - protected getProvider(): Provider { - throw new Error("Read-only signer does not support provider operations"); - } - /** * Connects to the client. This implementation does nothing as the class is read-only. * @@ -74,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/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index e9549c7c..713d79cf 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -5,41 +5,37 @@ import { TextInput } from "@/src/components/Input"; import { Button } from "@/src/components/Button"; import { useApp } from "@/src/context"; import { ButtonsPanel } from "@/src/components/ButtonsPanel"; -import { ccc } from "@ckb-ccc/connector-react"; +import { ccc, SignerBtc } from "@ckb-ccc/connector-react"; import { Message } from "@/src/components/Message"; - -import { initializeRgbppEnv, issuanceAmount, udtToken, UtxoSeal } from "@ckb-ccc/rgbpp"; +import { + initializeRgbppEnv, + issuanceAmount, + udtToken, + UtxoSeal, +} from "@ckb-ccc/rgbpp"; export default function IssueRGBPPXUdt() { const { signer, createSender } = useApp(); - const { log, warn } = createSender("Issue RGB++ xUDT"); + // const { log, warn } = createSender("Issue RGB++ xUDT"); const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); const [rgbppCkbTxId, setRgbppCkbTxId] = useState(""); const [utxoSealTxId, setUtxoSealTxId] = useState(""); const [utxoSealIndex, setUtxoSealIndex] = useState(""); - useEffect(() => { - if (!signer) { - return; - } - }, [signer]); - - const { - rgbppBtcWallet, - rgbppUdtClient, - utxoBasedAccountAddress, - ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + const { rgbppBtcWallet, rgbppUdtClient, ckbRgbppUnlockSinger } = + initializeRgbppEnv(); const signRgbppBtcTx = useCallback(async () => { - if (!signer) { + if (!signer || !(signer instanceof SignerBtc)) { return; } + const btcAccount = await signer.getBtcAccount(); + const utxoSeal: UtxoSeal = { txId: utxoSealTxId, index: parseInt(utxoSealIndex), - } + }; const rgbppLockScript = rgbppUdtClient.buildRgbppLockScript(utxoSeal); const rgbppCellsGen = await signer.client.findCellsByLock(rgbppLockScript); @@ -77,15 +73,15 @@ export default function IssueRGBPPXUdt() { 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, - }, + 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, + }, }); console.log( "Unique ID of issued udt token", @@ -97,41 +93,36 @@ export default function IssueRGBPPXUdt() { ckbPartialTx, ckbClient: signer.client, rgbppUdtClient, - btcChangeAddress: utxoBasedAccountAddress, - receiverBtcAddresses: [utxoBasedAccountAddress], + btcChangeAddress: btcAccount, + receiverBtcAddresses: [btcAccount], feeRate: 28, }); - const psbtHex = psbt.toHex(); - - //* 668467 is the ASCII decimal representation of "BTC" (66, 84, 67) - const pseudoCkbTx = ccc.Transaction.from({ - version: 668467, - outputsData: [psbtHex], - }); + const signedPsbtHex = await signer.signPsbt(psbt.toHex()); + const btcTxId = await signer.pushPsbt(signedPsbtHex); - const signedTx = await signer.signTransaction(pseudoCkbTx); - const btcTxId = await signer.sendTransaction(signedTx); setRgbppBtcTxId(btcTxId); + const ckbPartialTxInjected = await rgbppUdtClient.injectTxIdToRgbppCkbTx( indexedCkbPartialTx, btcTxId, ); const rgbppSignedCkbTx = - await ckbRgbppUnlockSinger.signTransaction(ckbPartialTxInjected); + await ckbRgbppUnlockSinger.signTransaction(ckbPartialTxInjected); await rgbppSignedCkbTx.completeFeeBy(signer); const ckbFinalTxId = await signer.sendTransaction(rgbppSignedCkbTx); await signer.client.waitTransaction(ckbFinalTxId); setRgbppCkbTxId(ckbFinalTxId); setUtxoSealTxId(""); setUtxoSealIndex(""); - }, [signer, utxoSealTxId, utxoSealIndex]); - - useEffect(() => { - if (!rgbppBtcTxId) { - return; - } - }, [rgbppBtcTxId]); + }, [ + signer, + utxoSealTxId, + utxoSealIndex, + rgbppBtcWallet, + rgbppUdtClient, + ckbRgbppUnlockSinger, + ]); return (
@@ -151,10 +142,13 @@ export default function IssueRGBPPXUdt() { state={[utxoSealIndex, setUtxoSealIndex]} /> - {rgbppBtcTxId && !rgbppCkbTxId &&
Waiting for RGB++ BTC Transaction {rgbppBtcTxId} to be confirmed...
} + {rgbppBtcTxId && !rgbppCkbTxId && ( +
+ Waiting for RGB++ BTC Transaction {rgbppBtcTxId} to be confirmed... +
+ )} - { - rgbppBtcTxId && rgbppCkbTxId && ( + {rgbppBtcTxId && rgbppCkbTxId && (
RGB++ xUDT is issued successfully.
diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index b564119e..96e44169 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -198,4 +198,12 @@ export class BitcoinSigner extends ccc.SignerBtc { ); return signature; } + + async signPsbt(_: string): Promise { + throw new Error("Not implemented"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Not implemented"); + } } diff --git a/packages/okx/src/btc/index.ts b/packages/okx/src/btc/index.ts index 3ef023d9..3d05f81b 100644 --- a/packages/okx/src/btc/index.ts +++ b/packages/okx/src/btc/index.ts @@ -176,4 +176,12 @@ export class BitcoinSigner extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + async signPsbt(_: string): Promise { + throw new Error("Not implemented"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Not implemented"); + } } diff --git a/packages/uni-sat/src/signer.ts b/packages/uni-sat/src/signer.ts index 6882beea..979a7713 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -30,10 +30,6 @@ export class Signer extends ccc.SignerBtc { super(client); } - getProvider(): Provider { - return this.provider; - } - async _getNetworkToChange(): Promise { const currentNetwork = await (async () => { if (this.provider.getChain) { @@ -158,4 +154,12 @@ export class Signer extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + async signPsbt(psbtHex: string): Promise { + return this.provider.signPsbt(psbtHex); + } + + 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..dec7077a 100644 --- a/packages/utxo-global/src/btc/index.ts +++ b/packages/utxo-global/src/btc/index.ts @@ -127,4 +127,12 @@ export class SignerBtc extends ccc.SignerBtc { this.accountCache ?? (await this.getBtcAccount()), ); } + + async signPsbt(_: string): Promise { + throw new Error("Not implemented"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Not implemented"); + } } 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"); + } } From 61d6f84519cd25c6d5e97bdea931ad52d697f3de Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Fri, 13 Jun 2025 01:00:12 -0500 Subject: [PATCH 06/15] refactor RgbppBtcWallet --- packages/core/package.json | 2 +- packages/core/src/signer/btc/signerBtc.ts | 2 + .../signer/btc/signerBtcPublicKeyReadonly.ts | 4 + packages/demo/package.json | 3 +- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 101 +++++++++++++++++- packages/joy-id/src/btc/index.ts | 4 + packages/okx/src/btc/index.ts | 4 + packages/playground/tailwind.config.ts | 2 +- packages/rgbpp/package.json | 13 ++- packages/rgbpp/src/bitcoin/wallet/index.ts | 3 +- .../wallet/{ => privateKey}/account.ts | 6 +- .../src/bitcoin/wallet/privateKey/wallet.ts | 41 +++++++ packages/rgbpp/src/bitcoin/wallet/wallet.ts | 46 +++----- packages/rgbpp/src/examples/common/env.ts | 72 +++++-------- .../rgbpp/src/examples/common/load-env.ts | 5 + packages/rgbpp/src/examples/common/utils.ts | 7 +- .../src/examples/spore/1-cluster-creation.ts | 13 ++- .../src/examples/spore/2-spore-creation.ts | 6 +- .../examples/spore/3-spore-btc-transfer.ts | 6 +- .../src/examples/spore/4-spore-btc-to-ckb.ts | 6 +- .../src/examples/spore/5-spore-ckb-to-btc.ts | 5 +- .../src/examples/udt/1-rgbpp-udt-issuance.ts | 54 ++++++---- .../src/examples/udt/2-udt-transfer-on-btc.ts | 18 ++-- .../examples/udt/3-udt-transfer-btc-to-ckb.ts | 18 ++-- .../examples/udt/4-unlock-btc-time-lock.ts | 6 +- .../examples/udt/5-udt-transfer-ckb-to-btc.ts | 7 +- packages/rgbpp/tsconfig.base.json | 22 ++++ packages/rgbpp/tsconfig.commonjs.json | 8 ++ packages/rgbpp/tsconfig.json | 18 +--- packages/uni-sat/src/advancedBarrel.ts | 2 + packages/uni-sat/src/signer.ts | 6 ++ packages/utxo-global/src/btc/index.ts | 4 + packages/xverse/src/signer.ts | 4 + pnpm-lock.yaml | 6 ++ 34 files changed, 361 insertions(+), 163 deletions(-) rename packages/rgbpp/src/bitcoin/wallet/{ => privateKey}/account.ts (96%) create mode 100644 packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts create mode 100644 packages/rgbpp/src/examples/common/load-env.ts create mode 100644 packages/rgbpp/tsconfig.base.json create mode 100644 packages/rgbpp/tsconfig.commonjs.json 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 59b33115..fe1904e1 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -131,4 +131,6 @@ export abstract class SignerBtc extends Signer { abstract signPsbt(psbtHex: string): Promise; abstract pushPsbt(psbtHex: string): Promise; + + abstract pushTx(txHex: string): Promise; } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index ba7e9322..6ae48c76 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -78,4 +78,8 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Read-only signer does not support pushPsbt"); } + + async pushTx(_: string): Promise { + throw new Error("Read-only signer does not support pushTx"); + } } diff --git a/packages/demo/package.json b/packages/demo/package.json index 3b56321a..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,9 +26,9 @@ "devDependencies": { "@ckb-ccc/connector-react": "workspace:*", "@ckb-ccc/lumos-patches": "workspace:*", + "@ckb-ccc/rgbpp": "workspace:*", "@ckb-ccc/ssri": "workspace:*", "@ckb-ccc/udt": "workspace:*", - "@ckb-ccc/rgbpp": "workspace:*", "@ckb-lumos/ckb-indexer": "^0.24.0-next.1", "@ckb-lumos/common-scripts": "^0.24.0-next.1", "@ckb-lumos/config-manager": "^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 index 713d79cf..8e9f2e9a 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useEffect, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { TextInput } from "@/src/components/Input"; import { Button } from "@/src/components/Button"; import { useApp } from "@/src/context"; @@ -8,13 +8,46 @@ import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { ccc, SignerBtc } from "@ckb-ccc/connector-react"; import { Message } from "@/src/components/Message"; +import { Psbt, Transaction } from "bitcoinjs-lib"; + import { - initializeRgbppEnv, + buildNetworkConfig, + PredefinedNetwork, + RgbppUdtClient, + isMainnet, + RgbppBtcWallet, + BtcAssetApiConfig, + NetworkConfig, + CkbRgbppUnlockSinger, issuanceAmount, udtToken, UtxoSeal, } from "@ckb-ccc/rgbpp"; +class UnisatRgbppWallet extends RgbppBtcWallet { + constructor( + private signer: SignerBtc, + networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, + ) { + super(networkConfig, btcAssetApiConfig); + } + + async getAddress(): Promise { + return this.signer.getBtcAccount(); + } + + async signTx(psbt: Psbt): Promise { + const signedPsbtHex = await this.signer.signPsbt(psbt.toHex()); + const signedPsbt = Psbt.fromHex(signedPsbtHex); + return signedPsbt.extractTransaction(true); + } + + async sendTx(tx: Transaction): Promise { + return this.signer.pushTx(tx.toHex()); + } +} + export default function IssueRGBPPXUdt() { const { signer, createSender } = useApp(); // const { log, warn } = createSender("Issue RGB++ xUDT"); @@ -23,12 +56,70 @@ export default function IssueRGBPPXUdt() { const [utxoSealTxId, setUtxoSealTxId] = useState(""); const [utxoSealIndex, setUtxoSealIndex] = useState(""); - const { rgbppBtcWallet, rgbppUdtClient, ckbRgbppUnlockSinger } = - initializeRgbppEnv(); - const signRgbppBtcTx = useCallback(async () => { + const ckbClient = useMemo( + () => + isMainnet( + process.env.NEXT_PUBLIC_UTXO_BASED_CHAIN_NAME as PredefinedNetwork, + ) + ? new ccc.ClientPublicMainnet() + : new ccc.ClientPublicTestnet(), + [], + ); + const networkConfig = useMemo( + () => + buildNetworkConfig( + process.env.NEXT_PUBLIC_UTXO_BASED_CHAIN_NAME as PredefinedNetwork, + ), + [], + ); + const rgbppUdtClient = useMemo( + () => new RgbppUdtClient(networkConfig, ckbClient), + [networkConfig, ckbClient], + ); + const rgbppBtcWallet = useMemo(() => { if (!signer || !(signer instanceof SignerBtc)) { + return null; + } + return new UnisatRgbppWallet(signer, networkConfig, { + 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!, + }); + }, [signer, networkConfig]); + const [ckbRgbppUnlockSinger, setCkbRgbppUnlockSinger] = + useState(); + + useEffect(() => { + let mounted = true; + rgbppBtcWallet?.getAddress().then((address) => { + 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 + ) { return; } + setRgbppBtcTxId(""); + setRgbppCkbTxId(""); const btcAccount = await signer.getBtcAccount(); diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index 96e44169..50eaf9b4 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -206,4 +206,8 @@ export class BitcoinSigner extends ccc.SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Not implemented"); } + + async pushTx(_: string): Promise { + throw new Error("Not implemented"); + } } diff --git a/packages/okx/src/btc/index.ts b/packages/okx/src/btc/index.ts index 3d05f81b..69a7f0a8 100644 --- a/packages/okx/src/btc/index.ts +++ b/packages/okx/src/btc/index.ts @@ -184,4 +184,8 @@ export class BitcoinSigner extends ccc.SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Not implemented"); } + + async pushTx(_: string): Promise { + throw new Error("Not implemented"); + } } 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/wallet/index.ts b/packages/rgbpp/src/bitcoin/wallet/index.ts index df83b8bb..f4b0846a 100644 --- a/packages/rgbpp/src/bitcoin/wallet/index.ts +++ b/packages/rgbpp/src/bitcoin/wallet/index.ts @@ -1,2 +1,3 @@ -export * from "./account.js"; +export * from "./privateKey/account.js"; +export * from "./privateKey/wallet.js"; export * from "./wallet.js"; diff --git a/packages/rgbpp/src/bitcoin/wallet/account.ts b/packages/rgbpp/src/bitcoin/wallet/privateKey/account.ts similarity index 96% rename from packages/rgbpp/src/bitcoin/wallet/account.ts rename to packages/rgbpp/src/bitcoin/wallet/privateKey/account.ts index ea52151c..11b00471 100644 --- a/packages/rgbpp/src/bitcoin/wallet/account.ts +++ b/packages/rgbpp/src/bitcoin/wallet/privateKey/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/privateKey/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts new file mode 100644 index 00000000..3152380d --- /dev/null +++ b/packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts @@ -0,0 +1,41 @@ +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); + } +} diff --git a/packages/rgbpp/src/bitcoin/wallet/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/wallet.ts index c6dcfd7f..a8328e8d 100644 --- a/packages/rgbpp/src/bitcoin/wallet/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/wallet.ts @@ -21,17 +21,10 @@ 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, BtcApiRecommendedFeeRates, BtcApiSentTransaction, BtcApiTransaction, @@ -49,32 +42,22 @@ import { toBtcNetwork, utxoToInputData, } from "../utils/index.js"; +import { transactionToHex } from "./privateKey/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 +173,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 signTx(psbt: Psbt): Promise; async buildInputs(utxoSeals: UtxoSeal[]): Promise { const inputs: TxInputData[] = []; @@ -236,10 +215,7 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { return inputs; } - async sendTx(tx: Transaction): Promise { - const txHex = tx.toHex(); - return this.sendTransaction(txHex); - } + abstract sendTx(tx: Transaction): Promise; rawTxHex(tx: Transaction): string { return transactionToHex(tx, false); @@ -289,7 +265,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 +281,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,6 +325,8 @@ export class RgbppBtcWallet extends BtcAssetsApiBase { }; } + // TODO: FIX THIS, in extension wallet case, a popup window will be shown to the user to confirm the transaction. + // TODO: API for Sign & Pay async estimateFee( inputs: TxInputData[], outputs: TxOutput[], @@ -431,12 +409,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"); } diff --git a/packages/rgbpp/src/examples/common/env.ts b/packages/rgbpp/src/examples/common/env.ts index 38dd2bd9..89a9500c 100644 --- a/packages/rgbpp/src/examples/common/env.ts +++ b/packages/rgbpp/src/examples/common/env.ts @@ -1,59 +1,31 @@ import { ccc } from "@ckb-ccc/shell"; -import dotenv from "dotenv"; - -import { parseAddressType, RgbppBtcWallet } from "../../bitcoin/index.js"; +import { PrivateKeyRgbppBtcWallet } from "../../bitcoin/wallet/privateKey/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" }); -dotenv.config({ - path: "/root/ckb/ccc/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/.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!; -// const utxoBasedChainAddressType = process.env.UTXO_BASED_CHAIN_ADDRESS_TYPE!; -// const btcAssetsApiUrl = process.env.BTC_ASSETS_API_URL!; -// const btcAssetsApiToken = process.env.BTC_ASSETS_API_TOKEN!; -// const btcAssetsApiOrigin = process.env.BTC_ASSETS_API_ORIGIN!; - -const utxoBasedChainName = "BitcoinTestnet3"; -const ckbPrivateKey = - "0x86f9dc9ce6218579ba9a0eb72b71ee747ed536c5866895e54a16dc3fdcfde9b9"; -const utxoBasedChainPrivateKey = - "4f2d2b8c36634c1fa14494951a2d70d12883dec0f400d2adfb55f04d4fc4f7ea"; -const utxoBasedChainAddressType = "P2WPKH"; -// const btcAssetsApiUrl = "http://localhost:3003"; -// const btcAssetsApiToken = -// "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbi10ZXN0IiwiYXVkIjoibG9jYWxob3N0IiwianRpIjoiN2I2ODNkNzgtY2U4My00NWFlLTgxZTQtNTBhYWM4MWI1MThhIiwiaWF0IjoxNzQ4NDE3MjY1fQ.vN9jFiJJy_dzIMJKltfrAWxd4TmaJD-v8a5q-t7Qqew"; -// const btcAssetsApiOrigin = "localhost"; -const btcAssetsApiUrl = "https://api.testnet.rgbpp.io"; -const btcAssetsApiToken = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJpbnRlZ3JhdGlvbi10ZXN0IiwiYXVkIjoibG9jYWxob3N0IiwianRpIjoiM2VjMTY2OTYtOTE4Yy00NWYzLTkzZjAtNjQ3ZGNlMTc1MjlkIiwiaWF0IjoxNzQ4NTUwODc5fQ.0SA2UHjluxsHZxHw4EYJxVUIuxuVflVaRrkucocg6Og"; -const btcAssetsApiOrigin = "localhost"; - -export const ckbClient = isMainnet(utxoBasedChainName) - ? new ccc.ClientPublicMainnet() - : new ccc.ClientPublicTestnet(); - -const addressType = parseAddressType(utxoBasedChainAddressType); +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!; +const utxoBasedChainAddressType = process.env.UTXO_BASED_CHAIN_ADDRESS_TYPE!; +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 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; @@ -63,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, @@ -70,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, @@ -82,13 +62,15 @@ export function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): { ); 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/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..765443ed 100644 --- a/packages/rgbpp/src/examples/spore/1-cluster-creation.ts +++ b/packages/rgbpp/src/examples/spore/1-cluster-creation.ts @@ -3,7 +3,7 @@ import { ccc, spore } from "@ckb-ccc/shell"; import { UtxoSeal } from "../../types/rgbpp/index.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 +13,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) => { diff --git a/packages/rgbpp/src/examples/spore/2-spore-creation.ts b/packages/rgbpp/src/examples/spore/2-spore-creation.ts index a4a45246..39446b85 100644 --- a/packages/rgbpp/src/examples/spore/2-spore-creation.ts +++ b/packages/rgbpp/src/examples/spore/2-spore-creation.ts @@ -1,7 +1,7 @@ import { ccc, spore } from "@ckb-ccc/shell"; import { SporeDataView } from "@ckb-ccc/spore/advanced"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { inspect } from "util"; import { RgbppTxLogger } from "../common/logger.js"; @@ -19,7 +19,9 @@ async function createSpore({ rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); const { tx: transferClusterTx } = await spore.transferSporeCluster({ signer: ckbSigner, 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..b8c4326e 100644 --- a/packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts +++ b/packages/rgbpp/src/examples/spore/3-spore-btc-transfer.ts @@ -1,6 +1,6 @@ import { spore } from "@ckb-ccc/shell"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -16,7 +16,9 @@ async function transferSpore({ rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); const { tx: ckbPartialTx } = await spore.transferSpore({ signer: ckbSigner, 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..be91b835 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,6 @@ import { spore } from "@ckb-ccc/shell"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -16,7 +16,9 @@ async function btcSporeToCkb({ rgbppUdtClient, utxoBasedAccountAddress, ckbRgbppUnlockSinger, - } = initializeRgbppEnv(); + ckbClient, + ckbSigner, + } = await initializeRgbppEnv(); const { tx: ckbPartialTx } = await spore.transferSpore({ signer: ckbSigner, 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..af8b153d 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,7 @@ import { spore } from "@ckb-ccc/shell"; import { UtxoSeal } from "../../types/rgbpp/index.js"; -import { ckbSigner, initializeRgbppEnv } from "../common/env.js"; +import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -13,7 +13,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..34c87e24 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, @@ -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: "7cc7de993804bf7b18e12c40d3bfd65545e2d1317f2768771405847889632765", 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..cea1b948 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 @@ -2,11 +2,20 @@ import { ccc } from "@ckb-ccc/shell"; 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 +23,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, 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..c4a3de33 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,20 @@ import { ccc } from "@ckb-ccc/shell"; import { 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 btcUdtToCkb({ udtScriptInfo, receivers, @@ -14,13 +23,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, 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..84360f28 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,16 @@ import { import { PredefinedScriptName } from "../../types/script.js"; import { testnetSudtCellDep } 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 { 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..10e549c5 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,13 @@ import { ccc } from "@ckb-ccc/shell"; import { ScriptInfo, UtxoSeal } from "../../types/rgbpp/index.js"; -import { ckbClient, ckbSigner, initializeRgbppEnv } from "../common/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 +19,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 ec795044..e9231e6d 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -7,6 +7,8 @@ export interface Provider { pushPsbt(psbtHex: string): Promise; + pushTx(tx: { rawtx: 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 979a7713..ed920b4a 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -162,4 +162,10 @@ export class Signer extends ccc.SignerBtc { async pushPsbt(psbtHex: string): Promise { return this.provider.pushPsbt(psbtHex); } + + async pushTx(txHex: string): Promise { + return this.provider.pushTx({ + rawtx: txHex, + }); + } } diff --git a/packages/utxo-global/src/btc/index.ts b/packages/utxo-global/src/btc/index.ts index dec7077a..5f14f971 100644 --- a/packages/utxo-global/src/btc/index.ts +++ b/packages/utxo-global/src/btc/index.ts @@ -135,4 +135,8 @@ export class SignerBtc extends ccc.SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Not implemented"); } + + async pushTx(_: string): Promise { + throw new Error("Not implemented"); + } } diff --git a/packages/xverse/src/signer.ts b/packages/xverse/src/signer.ts index e9fd211b..af9d0af0 100644 --- a/packages/xverse/src/signer.ts +++ b/packages/xverse/src/signer.ts @@ -175,4 +175,8 @@ export class Signer extends ccc.SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Not implemented"); } + + async pushTx(_: string): Promise { + throw new Error("Not implemented"); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c9a41b3..3fb6cba5 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) @@ -978,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 From e70f2154da55961cbd03137c0f67512719af921e Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Mon, 23 Jun 2025 23:14:16 -0500 Subject: [PATCH 07/15] refactor(rgbpp): optimize estimateFee to avoid wallet signature prompts --- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 6 +- packages/rgbpp/src/bitcoin/wallet/wallet.ts | 173 ++++++++++++++++-- 2 files changed, 160 insertions(+), 19 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 8e9f2e9a..eae823b5 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -186,7 +186,7 @@ export default function IssueRGBPPXUdt() { rgbppUdtClient, btcChangeAddress: btcAccount, receiverBtcAddresses: [btcAccount], - feeRate: 28, + feeRate: 10, }); const signedPsbtHex = await signer.signPsbt(psbt.toHex()); @@ -223,12 +223,12 @@ export default function IssueRGBPPXUdt() { diff --git a/packages/rgbpp/src/bitcoin/wallet/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/wallet.ts index a8328e8d..f577d4ca 100644 --- a/packages/rgbpp/src/bitcoin/wallet/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/wallet.ts @@ -325,19 +325,16 @@ export abstract class RgbppBtcWallet extends BtcAssetsApiBase { }; } - // TODO: FIX THIS, in extension wallet case, a popup window will be shown to the user to confirm the transaction. - // TODO: API for Sign & Pay + /** + * 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, @@ -346,6 +343,8 @@ export abstract class RgbppBtcWallet extends BtcAssetsApiBase { (acc, output) => acc + output.value, 0, ); + + let balancedInputs = [...inputs]; if (totalInputValue < totalOutputValue) { const { inputs: extraInputs } = await this.collectUtxos( totalOutputValue - totalInputValue, @@ -354,16 +353,11 @@ export abstract 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) { @@ -380,6 +374,153 @@ export abstract 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, From f16e54c337df8d336cba682c91ae096521a09441 Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Tue, 24 Jun 2025 01:36:40 -0500 Subject: [PATCH 08/15] chore(rgbpp): fix patchedDependencies hash in pnpm-lock.yaml --- pnpm-lock.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3fb6cba5..6ff6ef44 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,7 +6,7 @@ settings: patchedDependencies: bs58check@4.0.0: - hash: xkju25h33uk7ay46aspcygifve + hash: 0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26 path: patches/bs58check@4.0.0.patch importers: @@ -244,7 +244,7 @@ importers: version: 2.2.0 bs58check: specifier: ^4.0.0 - version: 4.0.0(patch_hash=xkju25h33uk7ay46aspcygifve) + version: 4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26) buffer: specifier: ^6.0.3 version: 6.0.3 @@ -14456,7 +14456,7 @@ snapshots: '@noble/hashes': 1.7.1 bs58: 5.0.0 - bs58check@4.0.0(patch_hash=xkju25h33uk7ay46aspcygifve): + bs58check@4.0.0(patch_hash=0848a2e3956f24abf1dd8620cba2a3f468393e489185d9536ad109f7e5712d26): dependencies: '@noble/hashes': 1.7.1 bs58: 6.0.0 From 2c090c43e957784e29e1bda87d5cbad2df02dcfe Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Wed, 2 Jul 2025 00:15:12 -0500 Subject: [PATCH 09/15] feat(btc): add signPsbt/pushPsbt methods with UniSat and OKX implementations --- packages/core/src/signer/btc/signerBtc.ts | 15 ++- .../signer/btc/signerBtcPublicKeyReadonly.ts | 4 - packages/joy-id/src/btc/index.ts | 101 +++++++++++++++++- packages/joy-id/src/common/index.ts | 5 +- packages/okx/src/advancedBarrel.ts | 17 ++- packages/okx/src/btc/index.ts | 24 +++-- packages/uni-sat/src/advancedBarrel.ts | 17 +++ packages/uni-sat/src/signer.ts | 18 ++-- packages/utxo-global/src/btc/index.ts | 22 ++-- packages/xverse/src/signer.ts | 4 - 10 files changed, 191 insertions(+), 36 deletions(-) diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index fe1904e1..7d5620c3 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -128,9 +128,20 @@ export abstract class SignerBtc extends Signer { 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; - - abstract pushTx(txHex: string): Promise; } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index 6ae48c76..ba7e9322 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -78,8 +78,4 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Read-only signer does not support pushPsbt"); } - - async pushTx(_: string): Promise { - throw new Error("Read-only signer does not support pushTx"); - } } diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index 50eaf9b4..6871924b 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -9,6 +9,39 @@ import { /** * Class representing a Bitcoin signer that extends SignerBtc + * + * JoyID Bitcoin PSBT Support: + * - Supports both P2WPKH (Wrapped SegWit) and P2TR (Taproot) addresses + * - Automatically detects and signs all inputs matching the current address + * - Provides both simple and advanced signing methods + * - Supports direct transaction broadcasting via sendPsbt + * + * Usage Examples: + * ```typescript + * // Basic PSBT signing (auto-finalized) + * const signedPsbtHex = await signer.signPsbt(psbtHex); + * + * // Advanced PSBT signing with custom options + * const signedPsbtHex = await signer.signPsbtAdvanced(psbtHex, { + * autoFinalized: false, + * toSignInputs: [ + * { + * index: 0, + * address: "bc1qaddress...", + * sighashTypes: [1] + * }, + * { + * index: 1, + * publicKey: "02062...8779693f", + * disableTweakSigner: true + * } + * ] + * }); + * + * // Sign and broadcast in one step + * const txid = await signer.pushPsbt(psbtHex); + * ``` + * * @public */ export class BitcoinSigner extends ccc.SignerBtc { @@ -199,15 +232,77 @@ export class BitcoinSigner extends ccc.SignerBtc { return signature; } + /** + * Signs a PSBT using JoyID wallet. + * + * This method follows JoyID's signPsbt API specification: + * - Automatically traverses all inputs that match the current address to sign + * - Uses autoFinalized: true by default (can be customized with signPsbtAdvanced) + * - Supports both P2WPKH and P2TR address types + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + */ async signPsbt(_: string): Promise { throw new Error("Not implemented"); + + // const { address } = await this.assertConnection(); + + // const config = this.getConfig(); + // const result = await createPopup( + // buildJoyIDURL( + // { + // ...config, + // psbtHex, + // address, + // autoFinalized: true, // Default to finalized for simple usage + // }, + // "popup", + // "/sign-psbt", + // ), + // { ...config, type: DappRequestType.SignPsbt }, + // ); + + // return result.psbt; } + /** + * Signs and broadcasts a PSBT to the Bitcoin network using JoyID wallet. + * + * This method follows JoyID's sendPsbt API specification: + * - Combines signPsbt and broadcast operations + * - Always uses autoFinalized: true + * - Returns the transaction ID upon successful broadcast + * + * @param psbtHex - The hex string of PSBT to sign and broadcast + * @returns A promise that resolves to the transaction ID + */ async pushPsbt(_: string): Promise { throw new Error("Not implemented"); - } - async pushTx(_: string): Promise { - throw new Error("Not implemented"); + // const { address } = await this.assertConnection(); + + // const config = this.getConfig(); + // const result = await createPopup( + // buildJoyIDURL( + // { + // ...config, + // psbtHex, + // address, + // autoFinalized: true, // sendPsbt always finalizes + // broadcast: true, // This tells JoyID to broadcast after signing + // }, + // "popup", + // "/send-psbt", + // ), + // { ...config, type: DappRequestType.SignPsbt }, // Use SignPsbt type for both operations + // ); + + // // For sendPsbt, JoyID should return the transaction ID + // if (result.txid) { + // return result.txid; + // } + + // throw new Error("Failed to broadcast PSBT - no transaction ID returned"); } } diff --git a/packages/joy-id/src/common/index.ts b/packages/joy-id/src/common/index.ts index 4426df84..cf400ff1 100644 --- a/packages/joy-id/src/common/index.ts +++ b/packages/joy-id/src/common/index.ts @@ -25,7 +25,10 @@ export interface PopupReturnType { [DappRequestType.Auth]: AuthResponseData; [DappRequestType.SignMessage]: SignMessageResponseData; [DappRequestType.SignEvm]: SignEvmTxResponseData; - [DappRequestType.SignPsbt]: SignEvmTxResponseData; + [DappRequestType.SignPsbt]: { + psbt: string; + txid?: string; + }; [DappRequestType.BatchSignPsbt]: { psbts: string[]; }; 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 69a7f0a8..400d591d 100644 --- a/packages/okx/src/btc/index.ts +++ b/packages/okx/src/btc/index.ts @@ -177,15 +177,23 @@ export class BitcoinSigner extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } - async signPsbt(_: string): Promise { - throw new Error("Not implemented"); - } - - async pushPsbt(_: string): Promise { - throw new Error("Not implemented"); + /** + * 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); } - async pushTx(_: string): Promise { - throw new Error("Not implemented"); + /** + * 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/uni-sat/src/advancedBarrel.ts b/packages/uni-sat/src/advancedBarrel.ts index e9231e6d..05292a8e 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -9,6 +9,23 @@ export interface Provider { 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 ed920b4a..9dc8d03f 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -155,17 +155,23 @@ 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); } - - async pushTx(txHex: string): Promise { - return this.provider.pushTx({ - rawtx: txHex, - }); - } } diff --git a/packages/utxo-global/src/btc/index.ts b/packages/utxo-global/src/btc/index.ts index 5f14f971..c123b2a9 100644 --- a/packages/utxo-global/src/btc/index.ts +++ b/packages/utxo-global/src/btc/index.ts @@ -128,15 +128,25 @@ export class SignerBtc extends ccc.SignerBtc { ); } + /** + * 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("Not implemented"); + 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("Not implemented"); - } - - async pushTx(_: string): Promise { - throw new Error("Not implemented"); + 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 af9d0af0..e9fd211b 100644 --- a/packages/xverse/src/signer.ts +++ b/packages/xverse/src/signer.ts @@ -175,8 +175,4 @@ export class Signer extends ccc.SignerBtc { async pushPsbt(_: string): Promise { throw new Error("Not implemented"); } - - async pushTx(_: string): Promise { - throw new Error("Not implemented"); - } } From 63afa03e53827ed5c2b296aec996e25251b5750f Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 3 Jul 2025 02:15:14 -0500 Subject: [PATCH 10/15] feat: implement signPsbt and pushPsbt for JoyID BTC P2WPKH signer --- packages/joy-id/src/btc/index.ts | 136 ++++++++++------------------ packages/joy-id/src/common/index.ts | 5 +- 2 files changed, 51 insertions(+), 90 deletions(-) diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index 6871924b..ba51bd6a 100644 --- a/packages/joy-id/src/btc/index.ts +++ b/packages/joy-id/src/btc/index.ts @@ -9,39 +9,6 @@ import { /** * Class representing a Bitcoin signer that extends SignerBtc - * - * JoyID Bitcoin PSBT Support: - * - Supports both P2WPKH (Wrapped SegWit) and P2TR (Taproot) addresses - * - Automatically detects and signs all inputs matching the current address - * - Provides both simple and advanced signing methods - * - Supports direct transaction broadcasting via sendPsbt - * - * Usage Examples: - * ```typescript - * // Basic PSBT signing (auto-finalized) - * const signedPsbtHex = await signer.signPsbt(psbtHex); - * - * // Advanced PSBT signing with custom options - * const signedPsbtHex = await signer.signPsbtAdvanced(psbtHex, { - * autoFinalized: false, - * toSignInputs: [ - * { - * index: 0, - * address: "bc1qaddress...", - * sighashTypes: [1] - * }, - * { - * index: 1, - * publicKey: "02062...8779693f", - * disableTweakSigner: true - * } - * ] - * }); - * - * // Sign and broadcast in one step - * const txid = await signer.pushPsbt(psbtHex); - * ``` - * * @public */ export class BitcoinSigner extends ccc.SignerBtc { @@ -58,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; } @@ -235,74 +212,61 @@ export class BitcoinSigner extends ccc.SignerBtc { /** * Signs a PSBT using JoyID wallet. * - * This method follows JoyID's signPsbt API specification: - * - Automatically traverses all inputs that match the current address to sign - * - Uses autoFinalized: true by default (can be customized with signPsbtAdvanced) - * - Supports both P2WPKH and P2TR address types - * * @param psbtHex - The hex string of PSBT to sign * @returns A promise that resolves to the signed PSBT hex string */ - async signPsbt(_: string): Promise { - throw new Error("Not implemented"); - - // const { address } = await this.assertConnection(); + async signPsbt(psbtHex: string): Promise { + const { address } = await this.assertConnection(); - // const config = this.getConfig(); - // const result = await createPopup( - // buildJoyIDURL( - // { - // ...config, - // psbtHex, - // address, - // autoFinalized: true, // Default to finalized for simple usage - // }, - // "popup", - // "/sign-psbt", - // ), - // { ...config, type: DappRequestType.SignPsbt }, - // ); + 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 result.psbt; + return signedPsbtHex; } /** * Signs and broadcasts a PSBT to the Bitcoin network using JoyID wallet. * - * This method follows JoyID's sendPsbt API specification: - * - Combines signPsbt and broadcast operations - * - Always uses autoFinalized: true - * - Returns the transaction ID upon successful broadcast + * 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(_: string): Promise { - throw new Error("Not implemented"); - - // const { address } = await this.assertConnection(); - - // const config = this.getConfig(); - // const result = await createPopup( - // buildJoyIDURL( - // { - // ...config, - // psbtHex, - // address, - // autoFinalized: true, // sendPsbt always finalizes - // broadcast: true, // This tells JoyID to broadcast after signing - // }, - // "popup", - // "/send-psbt", - // ), - // { ...config, type: DappRequestType.SignPsbt }, // Use SignPsbt type for both operations - // ); + async pushPsbt(psbtHex: string): Promise { + const { address } = await this.assertConnection(); - // // For sendPsbt, JoyID should return the transaction ID - // if (result.txid) { - // return result.txid; - // } + 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 + ); - // throw new Error("Failed to broadcast PSBT - no transaction ID returned"); + return txid; } } diff --git a/packages/joy-id/src/common/index.ts b/packages/joy-id/src/common/index.ts index cf400ff1..4426df84 100644 --- a/packages/joy-id/src/common/index.ts +++ b/packages/joy-id/src/common/index.ts @@ -25,10 +25,7 @@ export interface PopupReturnType { [DappRequestType.Auth]: AuthResponseData; [DappRequestType.SignMessage]: SignMessageResponseData; [DappRequestType.SignEvm]: SignEvmTxResponseData; - [DappRequestType.SignPsbt]: { - psbt: string; - txid?: string; - }; + [DappRequestType.SignPsbt]: SignEvmTxResponseData; [DappRequestType.BatchSignPsbt]: { psbts: string[]; }; From fbab098451578594f9004a4793bb8dbca2b24863 Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 3 Jul 2025 11:58:30 -0500 Subject: [PATCH 11/15] feat(rgbpp): add signAndBroadcast method and PrivateKeyRgbppBtcWallet implementation --- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 82 ++++++++++-- .../demo/src/components/Notifications.tsx | 2 +- .../src/bitcoin/wallet/privateKey/wallet.ts | 4 + packages/rgbpp/src/bitcoin/wallet/wallet.ts | 19 +-- .../src/examples/spore/1-cluster-creation.ts | 12 +- .../src/examples/spore/2-spore-creation.ts | 125 +++++++++++++++--- .../examples/spore/3-spore-btc-transfer.ts | 55 +++++--- .../src/examples/spore/4-spore-btc-to-ckb.ts | 4 +- .../src/examples/spore/5-spore-ckb-to-btc.ts | 2 + .../src/examples/udt/1-rgbpp-udt-issuance.ts | 4 +- .../src/examples/udt/2-udt-transfer-on-btc.ts | 4 +- .../examples/udt/3-udt-transfer-btc-to-ckb.ts | 4 +- .../examples/udt/4-unlock-btc-time-lock.ts | 3 + .../examples/udt/5-udt-transfer-ckb-to-btc.ts | 2 + 14 files changed, 243 insertions(+), 79 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index eae823b5..47059462 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -8,7 +8,7 @@ import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { ccc, SignerBtc } from "@ckb-ccc/connector-react"; import { Message } from "@/src/components/Message"; -import { Psbt, Transaction } from "bitcoinjs-lib"; +import { Psbt } from "bitcoinjs-lib"; import { buildNetworkConfig, @@ -24,9 +24,9 @@ import { UtxoSeal, } from "@ckb-ccc/rgbpp"; -class UnisatRgbppWallet extends RgbppBtcWallet { +abstract class BrowserRgbppWallet extends RgbppBtcWallet { constructor( - private signer: SignerBtc, + protected signer: SignerBtc, networkConfig: NetworkConfig, btcAssetApiConfig: BtcAssetApiConfig, ) { @@ -36,15 +36,47 @@ class UnisatRgbppWallet extends RgbppBtcWallet { async getAddress(): Promise { return this.signer.getBtcAccount(); } +} + +class UnisatRgbppWallet extends BrowserRgbppWallet { + constructor( + signer: SignerBtc, + networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, + ) { + super(signer, networkConfig, btcAssetApiConfig); + } + + async signAndBroadcast(psbt: Psbt): Promise { + return this.signer.pushPsbt(await this.signer.signPsbt(psbt.toHex())); + } +} + +class OkxRgbppWallet extends BrowserRgbppWallet { + constructor( + signer: SignerBtc, + networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, + ) { + super(signer, networkConfig, btcAssetApiConfig); + } - async signTx(psbt: Psbt): Promise { - const signedPsbtHex = await this.signer.signPsbt(psbt.toHex()); - const signedPsbt = Psbt.fromHex(signedPsbtHex); - return signedPsbt.extractTransaction(true); + async signAndBroadcast(psbt: Psbt): Promise { + return this.signer.pushPsbt(await this.signer.signPsbt(psbt.toHex())); + } +} + +class JoyidRgbppWallet extends BrowserRgbppWallet { + constructor( + signer: SignerBtc, + networkConfig: NetworkConfig, + btcAssetApiConfig: BtcAssetApiConfig, + ) { + super(signer, networkConfig, btcAssetApiConfig); } - async sendTx(tx: Transaction): Promise { - return this.signer.pushTx(tx.toHex()); + async signAndBroadcast(psbt: Psbt): Promise { + return this.signer.pushPsbt(psbt.toHex()); } } @@ -80,18 +112,41 @@ export default function IssueRGBPPXUdt() { if (!signer || !(signer instanceof SignerBtc)) { return null; } - return new UnisatRgbppWallet(signer, networkConfig, { + + const config = { 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!, - }); + }; + + // Check signer type and create appropriate wallet + if (signer.constructor.name === "Signer" && "provider" in signer) { + console.log("Unisat wallet"); + return new UnisatRgbppWallet(signer, networkConfig, config); + } else if ( + signer.constructor.name === "BitcoinSigner" && + "providers" in signer + ) { + console.log("OKX wallet"); + return new OkxRgbppWallet(signer, networkConfig, config); + } else if ( + signer.constructor.name === "BitcoinSigner" && + "name" in signer + ) { + console.log("JoyID wallet"); + return new JoyidRgbppWallet(signer, networkConfig, config); + } + + // Default fallback + return new UnisatRgbppWallet(signer, networkConfig, config); }, [signer, networkConfig]); + const [ckbRgbppUnlockSinger, setCkbRgbppUnlockSinger] = useState(); useEffect(() => { let mounted = true; - rgbppBtcWallet?.getAddress().then((address) => { + rgbppBtcWallet?.getAddress().then((address: string) => { if (mounted) { setCkbRgbppUnlockSinger( new CkbRgbppUnlockSinger( @@ -189,8 +244,7 @@ export default function IssueRGBPPXUdt() { feeRate: 10, }); - const signedPsbtHex = await signer.signPsbt(psbt.toHex()); - const btcTxId = await signer.pushPsbt(signedPsbtHex); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); setRgbppBtcTxId(btcTxId); 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/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts index 3152380d..797da4d8 100644 --- a/packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts @@ -38,4 +38,8 @@ export class PrivateKeyRgbppBtcWallet extends RgbppBtcWallet { 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 f577d4ca..761f1259 100644 --- a/packages/rgbpp/src/bitcoin/wallet/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/wallet.ts @@ -173,7 +173,7 @@ export abstract class RgbppBtcWallet extends BtcAssetsApiBase { return { psbt, indexedCkbPartialTx: indexedTx }; } - abstract signTx(psbt: Psbt): Promise; + abstract signAndBroadcast(psbt: Psbt): Promise; async buildInputs(utxoSeals: UtxoSeal[]): Promise { const inputs: TxInputData[] = []; @@ -215,17 +215,10 @@ export abstract class RgbppBtcWallet extends BtcAssetsApiBase { return inputs; } - abstract sendTx(tx: Transaction): Promise; - 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[], @@ -580,18 +573,16 @@ export abstract 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 { diff --git a/packages/rgbpp/src/examples/spore/1-cluster-creation.ts b/packages/rgbpp/src/examples/spore/1-cluster-creation.ts index 765443ed..5bef88ec 100644 --- a/packages/rgbpp/src/examples/spore/1-cluster-creation.ts +++ b/packages/rgbpp/src/examples/spore/1-cluster-creation.ts @@ -2,6 +2,8 @@ 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 { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -57,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( @@ -77,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(); @@ -92,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 39446b85..fd637ac7 100644 --- a/packages/rgbpp/src/examples/spore/2-spore-creation.ts +++ b/packages/rgbpp/src/examples/spore/2-spore-creation.ts @@ -1,6 +1,8 @@ import { ccc, spore } from "@ckb-ccc/shell"; import { SporeDataView } from "@ckb-ccc/spore/advanced"; +import "../common/load-env.js"; + import { initializeRgbppEnv } from "../common/env.js"; import { inspect } from "util"; @@ -12,7 +14,7 @@ async function createSpore({ receiverInfo: { btcAddress: string; rawSporeData: SporeDataView; - }; + }[]; }) { const { rgbppBtcWallet, @@ -25,23 +27,26 @@ async function createSpore({ 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, @@ -53,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( @@ -74,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(); @@ -96,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 b8c4326e..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,16 +1,17 @@ -import { spore } from "@ckb-ccc/shell"; +import { ccc, spore } from "@ckb-ccc/shell"; + +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, @@ -20,23 +21,28 @@ async function transferSpore({ 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( @@ -55,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); @@ -72,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 be91b835..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,5 +1,7 @@ import { spore } from "@ckb-ccc/shell"; +import "../common/load-env.js"; + import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; @@ -36,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 af8b153d..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,6 +2,8 @@ import { spore } from "@ckb-ccc/shell"; import { UtxoSeal } from "../../types/rgbpp/index.js"; +import "../common/load-env.js"; + import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; 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 34c87e24..e4ce7062 100644 --- a/packages/rgbpp/src/examples/udt/1-rgbpp-udt-issuance.ts +++ b/packages/rgbpp/src/examples/udt/1-rgbpp-udt-issuance.ts @@ -56,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( @@ -93,7 +93,7 @@ issueUdt({ // udtScriptInfo: testnetSudtInfo, utxoSeal: { - txId: "7cc7de993804bf7b18e12c40d3bfd65545e2d1317f2768771405847889632765", + 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 cea1b948..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,5 +1,7 @@ import { ccc } from "@ckb-ccc/shell"; +import "../common/load-env.js"; + import { RgbppBtcReceiver, ScriptInfo } from "../../types/rgbpp/index.js"; import { initializeRgbppEnv } from "../common/env.js"; @@ -55,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 c4a3de33..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,6 +2,8 @@ import { ccc } from "@ckb-ccc/shell"; import { ScriptInfo } from "../../types/rgbpp/index.js"; +import "../common/load-env.js"; + import { initializeRgbppEnv } from "../common/env.js"; import { testnetSudtInfo } from "../common/assets.js"; @@ -55,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 84360f28..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,6 +10,9 @@ import { import { PredefinedScriptName } from "../../types/script.js"; import { testnetSudtCellDep } from "../common/assets.js"; + +import "../common/load-env.js"; + import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; import { collectBtcTimeLockCells } from "../common/utils.js"; 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 10e549c5..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,6 +2,8 @@ import { ccc } from "@ckb-ccc/shell"; import { ScriptInfo, UtxoSeal } from "../../types/rgbpp/index.js"; +import "../common/load-env.js"; + import { initializeRgbppEnv } from "../common/env.js"; import { RgbppTxLogger } from "../common/logger.js"; From 4076512713ea1171fe57946389d32f445f73c20e Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Wed, 9 Jul 2025 16:23:20 -0500 Subject: [PATCH 12/15] feat: simplified browser RGB++ wallet implementation --- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 200 +++++++++--------- .../rgbpp/src/bitcoin/wallet/browser/index.ts | 60 ++++++ packages/rgbpp/src/bitcoin/wallet/index.ts | 5 +- .../wallet/{privateKey => pk}/account.ts | 0 .../wallet/{privateKey => pk}/wallet.ts | 0 packages/rgbpp/src/bitcoin/wallet/wallet.ts | 2 +- packages/rgbpp/src/examples/common/env.ts | 2 +- 7 files changed, 163 insertions(+), 106 deletions(-) create mode 100644 packages/rgbpp/src/bitcoin/wallet/browser/index.ts rename packages/rgbpp/src/bitcoin/wallet/{privateKey => pk}/account.ts (100%) rename packages/rgbpp/src/bitcoin/wallet/{privateKey => pk}/wallet.ts (100%) diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 47059462..5a8023f4 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -8,145 +8,110 @@ import { ButtonsPanel } from "@/src/components/ButtonsPanel"; import { ccc, SignerBtc } from "@ckb-ccc/connector-react"; import { Message } from "@/src/components/Message"; -import { Psbt } from "bitcoinjs-lib"; - import { buildNetworkConfig, PredefinedNetwork, RgbppUdtClient, isMainnet, - RgbppBtcWallet, BtcAssetApiConfig, NetworkConfig, CkbRgbppUnlockSinger, issuanceAmount, udtToken, UtxoSeal, + createBrowserRgbppBtcWallet, + getSupportedWallets, } from "@ckb-ccc/rgbpp"; -abstract class BrowserRgbppWallet extends RgbppBtcWallet { - constructor( - protected signer: SignerBtc, - networkConfig: NetworkConfig, - btcAssetApiConfig: BtcAssetApiConfig, - ) { - super(networkConfig, btcAssetApiConfig); - } - - async getAddress(): Promise { - return this.signer.getBtcAccount(); - } -} - -class UnisatRgbppWallet extends BrowserRgbppWallet { - constructor( - signer: SignerBtc, - networkConfig: NetworkConfig, - btcAssetApiConfig: BtcAssetApiConfig, - ) { - super(signer, networkConfig, btcAssetApiConfig); - } - - async signAndBroadcast(psbt: Psbt): Promise { - return this.signer.pushPsbt(await this.signer.signPsbt(psbt.toHex())); - } -} - -class OkxRgbppWallet extends BrowserRgbppWallet { - constructor( - signer: SignerBtc, - networkConfig: NetworkConfig, - btcAssetApiConfig: BtcAssetApiConfig, - ) { - super(signer, networkConfig, btcAssetApiConfig); - } - - async signAndBroadcast(psbt: Psbt): Promise { - return this.signer.pushPsbt(await this.signer.signPsbt(psbt.toHex())); - } -} - -class JoyidRgbppWallet extends BrowserRgbppWallet { - constructor( - signer: SignerBtc, - networkConfig: NetworkConfig, - btcAssetApiConfig: BtcAssetApiConfig, - ) { - super(signer, networkConfig, btcAssetApiConfig); - } - - async signAndBroadcast(psbt: Psbt): Promise { - return this.signer.pushPsbt(psbt.toHex()); - } -} - export default function IssueRGBPPXUdt() { const { signer, createSender } = useApp(); - // const { log, warn } = createSender("Issue RGB++ xUDT"); + const sender = useMemo( + () => createSender("Issue RGB++ xUDT"), + [createSender], + ); + const { error } = sender; const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); const [rgbppCkbTxId, setRgbppCkbTxId] = useState(""); const [utxoSealTxId, setUtxoSealTxId] = useState(""); const [utxoSealIndex, setUtxoSealIndex] = useState(""); - const ckbClient = useMemo( - () => - isMainnet( - process.env.NEXT_PUBLIC_UTXO_BASED_CHAIN_NAME as PredefinedNetwork, - ) - ? new ccc.ClientPublicMainnet() - : new ccc.ClientPublicTestnet(), - [], - ); - const networkConfig = useMemo( - () => - buildNetworkConfig( - process.env.NEXT_PUBLIC_UTXO_BASED_CHAIN_NAME as PredefinedNetwork, - ), - [], + const [networkConfig, setNetworkConfig] = useState( + null, ); - const rgbppUdtClient = useMemo( - () => new RgbppUdtClient(networkConfig, ckbClient), - [networkConfig, ckbClient], + const [ckbClient, setCkbClient] = useState(null); + const [rgbppUdtClient, setRgbppUdtClient] = useState( + null, ); + + 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, error]); + const rgbppBtcWallet = useMemo(() => { - if (!signer || !(signer instanceof SignerBtc)) { + if (!signer || !(signer instanceof SignerBtc) || !networkConfig) { return null; } - const config = { + 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!, }; - // Check signer type and create appropriate wallet - if (signer.constructor.name === "Signer" && "provider" in signer) { - console.log("Unisat wallet"); - return new UnisatRgbppWallet(signer, networkConfig, config); - } else if ( - signer.constructor.name === "BitcoinSigner" && - "providers" in signer - ) { - console.log("OKX wallet"); - return new OkxRgbppWallet(signer, networkConfig, config); - } else if ( - signer.constructor.name === "BitcoinSigner" && - "name" in signer + return createBrowserRgbppBtcWallet(signer, networkConfig, config); + }, [signer, networkConfig]); + + useEffect(() => { + if ( + signer && + signer instanceof SignerBtc && + networkConfig && + !rgbppBtcWallet ) { - console.log("JoyID wallet"); - return new JoyidRgbppWallet(signer, networkConfig, config); + error( + `Unsupported wallet type: ${signer.constructor.name}. Supported wallets: ${getSupportedWallets().join(", ")}`, + ); } - - // Default fallback - return new UnisatRgbppWallet(signer, networkConfig, config); - }, [signer, networkConfig]); + }, [signer, networkConfig, rgbppBtcWallet, error]); const [ckbRgbppUnlockSinger, setCkbRgbppUnlockSinger] = useState(); useEffect(() => { + if (!ckbClient || !rgbppBtcWallet || !rgbppUdtClient) { + setCkbRgbppUnlockSinger(undefined); + return; + } + let mounted = true; - rgbppBtcWallet?.getAddress().then((address: string) => { + rgbppBtcWallet.getAddress().then((address: string) => { if (mounted) { setCkbRgbppUnlockSinger( new CkbRgbppUnlockSinger( @@ -169,7 +134,8 @@ export default function IssueRGBPPXUdt() { !signer || !(signer instanceof SignerBtc) || !rgbppBtcWallet || - !ckbRgbppUnlockSinger + !ckbRgbppUnlockSinger || + !rgbppUdtClient ) { return; } @@ -269,11 +235,39 @@ export default function IssueRGBPPXUdt() { ckbRgbppUnlockSinger, ]); + 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 to 4 transactions.
+ + Current Network:{" "} + {networkConfig.isMainnet + ? "BTC Mainnet & CKB Mainnet" + : "BTC Testnet3 & CKB Testnet"} +
- +
); 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 f4b0846a..47578c72 100644 --- a/packages/rgbpp/src/bitcoin/wallet/index.ts +++ b/packages/rgbpp/src/bitcoin/wallet/index.ts @@ -1,3 +1,4 @@ -export * from "./privateKey/account.js"; -export * from "./privateKey/wallet.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/privateKey/account.ts b/packages/rgbpp/src/bitcoin/wallet/pk/account.ts similarity index 100% rename from packages/rgbpp/src/bitcoin/wallet/privateKey/account.ts rename to packages/rgbpp/src/bitcoin/wallet/pk/account.ts diff --git a/packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/pk/wallet.ts similarity index 100% rename from packages/rgbpp/src/bitcoin/wallet/privateKey/wallet.ts rename to packages/rgbpp/src/bitcoin/wallet/pk/wallet.ts diff --git a/packages/rgbpp/src/bitcoin/wallet/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/wallet.ts index 761f1259..80bdac77 100644 --- a/packages/rgbpp/src/bitcoin/wallet/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/wallet.ts @@ -42,7 +42,7 @@ import { toBtcNetwork, utxoToInputData, } from "../utils/index.js"; -import { transactionToHex } from "./privateKey/account.js"; +import { transactionToHex } from "./pk/account.js"; import { NetworkConfig } from "../../types/network.js"; import { RgbppApiSpvProof } from "../../types/spv.js"; diff --git a/packages/rgbpp/src/examples/common/env.ts b/packages/rgbpp/src/examples/common/env.ts index 89a9500c..580062cf 100644 --- a/packages/rgbpp/src/examples/common/env.ts +++ b/packages/rgbpp/src/examples/common/env.ts @@ -1,6 +1,6 @@ import { ccc } from "@ckb-ccc/shell"; -import { PrivateKeyRgbppBtcWallet } from "../../bitcoin/wallet/privateKey/wallet.js"; +import { PrivateKeyRgbppBtcWallet } from "../../bitcoin/wallet/pk/wallet.js"; import { ScriptInfo } from "../../types/rgbpp/index.js"; import { parseAddressType } from "../../bitcoin/index.js"; From afb6be83557e6e2e21225a88f6d2666b33ef222a Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 10 Jul 2025 00:33:42 -0500 Subject: [PATCH 13/15] feat: complete RGB++ xUDT issuance UI with dropdown selection and progress tracking --- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 436 +++++++++++++----- packages/rgbpp/src/barrel.ts | 1 - packages/rgbpp/src/examples/index.ts | 1 - 3 files changed, 311 insertions(+), 127 deletions(-) delete mode 100644 packages/rgbpp/src/examples/index.ts diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 5a8023f4..15c70106 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -1,27 +1,44 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { TextInput } from "@/src/components/Input"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Button } from "@/src/components/Button"; -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 { buildNetworkConfig, - PredefinedNetwork, - RgbppUdtClient, isMainnet, - BtcAssetApiConfig, NetworkConfig, - CkbRgbppUnlockSinger, - issuanceAmount, - udtToken, + PredefinedNetwork, + RgbppUdtClient, UtxoSeal, - createBrowserRgbppBtcWallet, + 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(); @@ -29,11 +46,16 @@ export default function IssueRGBPPXUdt() { () => createSender("Issue RGB++ xUDT"), [createSender], ); - const { error } = sender; + const { error, log } = sender; + const { explorerTransaction } = useGetExplorerLink(); + const [rgbppBtcTxId, setRgbppBtcTxId] = useState(""); const [rgbppCkbTxId, setRgbppCkbTxId] = useState(""); - const [utxoSealTxId, setUtxoSealTxId] = useState(""); - const [utxoSealIndex, setUtxoSealIndex] = 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, @@ -43,6 +65,25 @@ export default function IssueRGBPPXUdt() { 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); @@ -72,7 +113,7 @@ export default function IssueRGBPPXUdt() { const udtClient = new RgbppUdtClient(config, client); setRgbppUdtClient(udtClient); - }, [signer, error]); + }, [signer]); const rgbppBtcWallet = useMemo(() => { if (!signer || !(signer instanceof SignerBtc) || !networkConfig) { @@ -99,7 +140,40 @@ export default function IssueRGBPPXUdt() { `Unsupported wallet type: ${signer.constructor.name}. Supported wallets: ${getSupportedWallets().join(", ")}`, ); } - }, [signer, networkConfig, rgbppBtcWallet, error]); + }, [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(); @@ -135,104 +209,134 @@ export default function IssueRGBPPXUdt() { !(signer instanceof SignerBtc) || !rgbppBtcWallet || !ckbRgbppUnlockSinger || - !rgbppUdtClient + !rgbppUdtClient || + !selectedUtxo ) { return; } - setRgbppBtcTxId(""); - setRgbppCkbTxId(""); - const btcAccount = await signer.getBtcAccount(); - - const utxoSeal: UtxoSeal = { - txId: utxoSealTxId, - index: parseInt(utxoSealIndex), - }; - const rgbppLockScript = rgbppUdtClient.buildRgbppLockScript(utxoSeal); + 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); + } - 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); + } - if (rgbppIssuanceCells.length !== 0) { - console.log("Using existing RGB++ cell"); - } else { - console.log("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, + 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, + }, }); - 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, + 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, }); - if (!cell) { - throw new Error("Cell not found"); - } - rgbppIssuanceCells.push(cell); - } - const ckbPartialTx = await rgbppUdtClient.issuanceCkbPartialTx({ - token: udtToken, - 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, - }, - }); - console.log( - "Unique ID of issued udt token", - ckbPartialTx.outputs[0].type!.args, - ); - - // ! 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); + const btcTxId = await rgbppBtcWallet.signAndBroadcast(psbt); + setRgbppBtcTxId(btcTxId); - 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); - const ckbFinalTxId = await signer.sendTransaction(rgbppSignedCkbTx); - await signer.client.waitTransaction(ckbFinalTxId); - setRgbppCkbTxId(ckbFinalTxId); - setUtxoSealTxId(""); - setUtxoSealIndex(""); + 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, - utxoSealTxId, - utxoSealIndex, + selectedUtxo, rgbppBtcWallet, rgbppUdtClient, ckbRgbppUnlockSinger, + log, + error, + explorerTransaction, + getBtcExplorerLink, ]); if (!networkConfig || !ckbClient || !rgbppUdtClient) { @@ -260,7 +364,7 @@ export default function IssueRGBPPXUdt() { return (
- You will need to sign 2 to 4 transactions. + You will need to sign 2 or 3 transactions.
Current Network:{" "} @@ -270,36 +374,118 @@ export default function IssueRGBPPXUdt() {
- - - - {rgbppBtcTxId && !rgbppCkbTxId && ( -
- Waiting for RGB++ BTC Transaction {rgbppBtcTxId} to be confirmed... +
+
+ +
- )} + {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} + /> + )} +
- {rgbppBtcTxId && rgbppCkbTxId && ( -
- RGB++ xUDT is issued successfully. -
- RGB++ BTC Transaction: {rgbppBtcTxId} -
- RGB++ CKB Transaction: {rgbppCkbTxId} + {/* 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/rgbpp/src/barrel.ts b/packages/rgbpp/src/barrel.ts index 342097dd..da9337ff 100644 --- a/packages/rgbpp/src/barrel.ts +++ b/packages/rgbpp/src/barrel.ts @@ -1,7 +1,6 @@ export * from "./bitcoin/index.js"; export * from "./configs/index.js"; export * from "./constants/index.js"; -export * from "./examples/index.js"; export * from "./interfaces/index.js"; export * from "./signer/index.js"; export * from "./types/index.js"; diff --git a/packages/rgbpp/src/examples/index.ts b/packages/rgbpp/src/examples/index.ts deleted file mode 100644 index 027f648c..00000000 --- a/packages/rgbpp/src/examples/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./common/index.js"; From 37f3a2a572f210fd0ce28e6b27feaeb3ddf26e69 Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Tue, 29 Jul 2025 11:55:49 -0500 Subject: [PATCH 14/15] feat(rgbpp): implement network-aware JWT authentication for BTC Assets API --- .../connected/(tools)/IssueRgbppXUdt/page.tsx | 5 +- packages/rgbpp/src/bitcoin/service/base.ts | 54 ++++++------------- packages/rgbpp/src/bitcoin/types/base.ts | 2 - .../rgbpp/src/bitcoin/types/btc-assets-api.ts | 1 + packages/rgbpp/src/examples/.env.example | 2 + packages/rgbpp/src/examples/common/env.ts | 1 + 6 files changed, 23 insertions(+), 42 deletions(-) diff --git a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx index 15c70106..940b9113 100644 --- a/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx +++ b/packages/demo/src/app/connected/(tools)/IssueRgbppXUdt/page.tsx @@ -122,8 +122,9 @@ export default function IssueRGBPPXUdt() { 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!, + 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); 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/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 580062cf..aa30b16b 100644 --- a/packages/rgbpp/src/examples/common/env.ts +++ b/packages/rgbpp/src/examples/common/env.ts @@ -58,6 +58,7 @@ export async function initializeRgbppEnv(scriptInfos?: ScriptInfo[]): Promise<{ url: btcAssetsApiUrl, token: btcAssetsApiToken, origin: btcAssetsApiOrigin, + isMainnet: networkConfig.isMainnet, }, ); From 98fa1d06eac2be2e5a9342e02e2ae6eef3a3eec0 Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Tue, 29 Jul 2025 12:33:39 -0500 Subject: [PATCH 15/15] feat(rgbpp): add BTC address balance query interface to `RgbppBtcWallet` --- packages/rgbpp/src/bitcoin/types/tx.ts | 17 +++++++++++++++++ packages/rgbpp/src/bitcoin/wallet/wallet.ts | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) 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/wallet.ts b/packages/rgbpp/src/bitcoin/wallet/wallet.ts index 80bdac77..69830b1f 100644 --- a/packages/rgbpp/src/bitcoin/wallet/wallet.ts +++ b/packages/rgbpp/src/bitcoin/wallet/wallet.ts @@ -25,6 +25,8 @@ import { BtcAssetsApiBase } from "../service/base.js"; import { BtcAssetApiConfig } from "../types/btc-assets-api.js"; import { RgbppBtcTxParams } from "../types/rgbpp.js"; import { + BtcApiBalance, + BtcApiBalanceParams, BtcApiRecommendedFeeRates, BtcApiSentTransaction, BtcApiTransaction, @@ -611,6 +613,21 @@ export abstract 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", {