From 5db875d0269edf6ac21329c0a218a24fe5c779ff Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Wed, 2 Jul 2025 00:15:12 -0500 Subject: [PATCH 1/2] feat(btc): add signPsbt/pushPsbt methods with UniSat and OKX implementations --- packages/core/src/signer/btc/signerBtc.ts | 17 +++ .../signer/btc/signerBtcPublicKeyReadonly.ts | 8 ++ packages/joy-id/src/btc/index.ts | 107 ++++++++++++++++++ packages/joy-id/src/common/index.ts | 5 +- packages/okx/src/advancedBarrel.ts | 17 ++- packages/okx/src/btc/index.ts | 20 ++++ packages/uni-sat/src/advancedBarrel.ts | 17 +++ packages/uni-sat/src/signer.ts | 20 ++++ packages/utxo-global/src/btc/index.ts | 22 ++++ packages/xverse/src/signer.ts | 8 ++ 10 files changed, 238 insertions(+), 3 deletions(-) diff --git a/packages/core/src/signer/btc/signerBtc.ts b/packages/core/src/signer/btc/signerBtc.ts index 64112a74..cb77de1b 100644 --- a/packages/core/src/signer/btc/signerBtc.ts +++ b/packages/core/src/signer/btc/signerBtc.ts @@ -123,4 +123,21 @@ export abstract class SignerBtc extends Signer { tx.setWitnessArgsAt(info.position, witness); return tx; } + + /** + * Signs a Partially Signed Bitcoin Transaction (PSBT). + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + * @todo Add support for Taproot signing options (useTweakedSigner, etc.) + */ + abstract signPsbt(psbtHex: string): Promise; + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + abstract pushPsbt(psbtHex: string): Promise; } diff --git a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts index 50096db7..ba7e9322 100644 --- a/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts +++ b/packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts @@ -70,4 +70,12 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc { async getBtcPublicKey(): Promise { return this.publicKey; } + + async signPsbt(_: string): Promise { + throw new Error("Read-only signer does not support signPsbt"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Read-only signer does not support pushPsbt"); + } } diff --git a/packages/joy-id/src/btc/index.ts b/packages/joy-id/src/btc/index.ts index b564119e..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 { @@ -198,4 +231,78 @@ 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"); + + // 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 3ef023d9..400d591d 100644 --- a/packages/okx/src/btc/index.ts +++ b/packages/okx/src/btc/index.ts @@ -176,4 +176,24 @@ export class BitcoinSigner extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + /** + * Signs a PSBT using OKX wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + */ + async signPsbt(psbtHex: string): Promise { + return this.provider.signPsbt(psbtHex); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + async pushPsbt(psbtHex: string): Promise { + return this.provider.pushPsbt(psbtHex); + } } diff --git a/packages/uni-sat/src/advancedBarrel.ts b/packages/uni-sat/src/advancedBarrel.ts index e6ae56b5..612c6273 100644 --- a/packages/uni-sat/src/advancedBarrel.ts +++ b/packages/uni-sat/src/advancedBarrel.ts @@ -2,6 +2,23 @@ * Interface representing a provider for interacting with accounts and signing messages. */ export interface Provider { + /** + * Signs a PSBT using UniSat wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + * @todo Add support for Taproot signing options (useTweakedSigner, etc.) + */ + signPsbt(psbtHex: string): Promise; + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + pushPsbt(psbtHex: string): Promise; + /** * Requests user accounts. * @returns A promise that resolves to an array of account addresses. diff --git a/packages/uni-sat/src/signer.ts b/packages/uni-sat/src/signer.ts index 653bba8e..109db82e 100644 --- a/packages/uni-sat/src/signer.ts +++ b/packages/uni-sat/src/signer.ts @@ -150,4 +150,24 @@ export class Signer extends ccc.SignerBtc { return this.provider.signMessage(challenge, "ecdsa"); } + + /** + * Signs a PSBT using UniSat wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + */ + async signPsbt(psbtHex: string): Promise { + return this.provider.signPsbt(psbtHex); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + */ + async pushPsbt(psbtHex: string): Promise { + return this.provider.pushPsbt(psbtHex); + } } diff --git a/packages/utxo-global/src/btc/index.ts b/packages/utxo-global/src/btc/index.ts index 57e73594..c123b2a9 100644 --- a/packages/utxo-global/src/btc/index.ts +++ b/packages/utxo-global/src/btc/index.ts @@ -127,4 +127,26 @@ export class SignerBtc extends ccc.SignerBtc { this.accountCache ?? (await this.getBtcAccount()), ); } + + /** + * Signs a PSBT using UTXO Global wallet. + * + * @param psbtHex - The hex string of PSBT to sign + * @returns A promise that resolves to the signed PSBT hex string + * @todo Implement PSBT signing with UTXO Global + */ + async signPsbt(_: string): Promise { + throw new Error("UTXO Global PSBT signing not implemented yet"); + } + + /** + * Broadcasts a signed PSBT to the Bitcoin network. + * + * @param psbtHex - The hex string of signed PSBT to broadcast + * @returns A promise that resolves to the transaction ID + * @todo Implement PSBT broadcasting with UTXO Global + */ + async pushPsbt(_: string): Promise { + throw new Error("UTXO Global PSBT broadcasting not implemented yet"); + } } diff --git a/packages/xverse/src/signer.ts b/packages/xverse/src/signer.ts index bf6df9c0..e9fd211b 100644 --- a/packages/xverse/src/signer.ts +++ b/packages/xverse/src/signer.ts @@ -167,4 +167,12 @@ export class Signer extends ccc.SignerBtc { ) ).signature; } + + async signPsbt(_: string): Promise { + throw new Error("Not implemented"); + } + + async pushPsbt(_: string): Promise { + throw new Error("Not implemented"); + } } From 390411a26d9eb60774c326d069598a164c84a055 Mon Sep 17 00:00:00 2001 From: fgh_ssh Date: Thu, 3 Jul 2025 02:15:14 -0500 Subject: [PATCH 2/2] 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[]; };