Skip to content
1 change: 1 addition & 0 deletions packages/core/src/signer/btc/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./psbt.js";
export * from "./signerBtc.js";
export * from "./signerBtcPublicKeyReadonly.js";
export * from "./verify.js";
56 changes: 56 additions & 0 deletions packages/core/src/signer/btc/psbt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Options for signing a PSBT (Partially Signed Bitcoin Transaction)
*/
export type SignPsbtOptions = {
/**
* Whether to finalize the PSBT after signing.
* Default is true.
*/
autoFinalized?: boolean;
/**
* Array of inputs to sign
*/
toSignInputs?: ToSignInput[];
};

/**
* Specification for an input to sign in a PSBT.
* Must specify at least one of: address or pubkey.
*/
export type ToSignInput = {
/**
* Which input to sign (index in the PSBT inputs array)
*/
index: number;
/**
* (Optional) Sighash types to use for signing.
*/
sighashTypes?: number[];
/**
* (Optional) When signing and unlocking Taproot addresses, the tweakSigner is used by default
* for signature generation. Setting this to true allows for signing with the original private key.
* Default value is false.
*/
disableTweakSigner?: boolean;
} & (
| {
/**
* The address whose corresponding private key to use for signing.
*/
address: string;
/**
* The public key whose corresponding private key to use for signing.
*/
publicKey?: string;
}
| {
/**
* The address whose corresponding private key to use for signing.
*/
address?: string;
/**
* The public key whose corresponding private key to use for signing.
*/
publicKey: string;
}
);
41 changes: 40 additions & 1 deletion packages/core/src/signer/btc/signerBtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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 { Hex, HexLike, hexFrom } from "../../hex/index.js";
import { numToBytes } from "../../num/index.js";
import { Signer, SignerSignType, SignerType } from "../signer/index.js";
import { SignPsbtOptions } from "./psbt.js";
import { btcEcdsaPublicKeyHash } from "./verify.js";

/**
Expand All @@ -22,6 +23,21 @@ export abstract class SignerBtc extends Signer {
return SignerSignType.BtcEcdsa;
}

/**
* Sign and broadcast a PSBT.
*
* @param psbtHex - The hex string of PSBT to sign and broadcast.
* @param options - Options for signing the PSBT.
* @returns A promise that resolves to the transaction ID as a Hex string.
*/
async signAndBroadcastPsbt(
psbtHex: HexLike,
options?: SignPsbtOptions,
): Promise<Hex> {
const signedPsbt = await this.signPsbt(psbtHex, options);
return this.broadcastPsbt(signedPsbt, options);
}

/**
* Gets the Bitcoin account associated with the signer.
*
Expand Down Expand Up @@ -123,4 +139,27 @@ 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.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT as a Hex string.
*/
abstract signPsbt(psbtHex: HexLike, options?: SignPsbtOptions): Promise<Hex>;

/**
* Broadcasts a PSBT to the Bitcoin network.
*
* @param psbtHex - The hex string of the PSBT to broadcast.
* @param options - Options for broadcasting the PSBT.
* @returns A promise that resolves to the transaction ID as a Hex string.
*/
async broadcastPsbt(
_psbtHex: HexLike,
_options?: SignPsbtOptions,
): Promise<Hex> {
throw new Error("Not implemented");
}
}
12 changes: 12 additions & 0 deletions packages/core/src/signer/btc/signerBtcPublicKeyReadonly.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Client } from "../../client/index.js";
import { Hex, HexLike, hexFrom } from "../../hex/index.js";
import { SignPsbtOptions } from "./psbt.js";
import { SignerBtc } from "./signerBtc.js";

/**
Expand Down Expand Up @@ -70,4 +71,15 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc {
async getBtcPublicKey(): Promise<Hex> {
return this.publicKey;
}

async signPsbt(_psbtHex: HexLike, _options?: SignPsbtOptions): Promise<Hex> {
throw new Error("Read-only signer does not support signPsbt");
}

async broadcastPsbt(
_psbtHex: HexLike,
_options?: SignPsbtOptions,
): Promise<Hex> {
throw new Error("Read-only signer does not support broadcastPsbt");
}
}
74 changes: 74 additions & 0 deletions packages/joy-id/src/btc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,78 @@ export class BitcoinSigner extends ccc.SignerBtc {
);
return signature;
}

/**
* Signs a PSBT using JoyID wallet.
*
* @param psbtHex - The hex string of PSBT to sign.
* @returns A promise that resolves to the signed PSBT as a Hex string.
*/
async signPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
const { address } = await this.assertConnection();

const config = this.getConfig();
const { tx: signedPsbtHex } = await createPopup(
buildJoyIDURL(
{
...config,
tx: ccc.hexFrom(psbtHex).slice(2),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The expression ccc.hexFrom(psbtHex).slice(2) is used to get a non-0x-prefixed hex string. A more direct and readable approach is to use ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"). This avoids the intermediate step of potentially adding and then immediately removing the "0x" prefix.

Suggested change
tx: ccc.hexFrom(psbtHex).slice(2),
tx: ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"),

options,
signerAddress: address,
autoFinalized: options?.autoFinalized ?? true,
},
"popup",
"/sign-psbt",
),
{ ...config, type: DappRequestType.SignPsbt },
);

return ccc.hexFrom(signedPsbtHex);
}

/**
* Broadcasts a PSBT to the Bitcoin network.
*
* @remarks
* JoyID does not support broadcasting a signed PSBT directly.
* It only supports "Sign and Broadcast" as a single atomic operation via `signAndBroadcastPsbt`.
*/
async broadcastPsbt(
_psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
throw new Error(
"JoyID does not support broadcasting signed PSBTs directly. Use signAndBroadcastPsbt instead.",
);
}

async signAndBroadcastPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
const { address } = await this.assertConnection();

const config = this.getConfig();
// ccc.hexFrom adds 0x prefix, but BTC expects non-0x
const { tx: txid } = await createPopup(
buildJoyIDURL(
{
...config,
tx: ccc.hexFrom(psbtHex).slice(2),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The expression ccc.hexFrom(psbtHex).slice(2) is used to get a non-0x-prefixed hex string. A more direct and readable approach is to use ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"). This avoids the intermediate step of potentially adding and then immediately removing the "0x" prefix.

Suggested change
tx: ccc.hexFrom(psbtHex).slice(2),
tx: ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"),

options,
signerAddress: address,
autoFinalized: true, // sendPsbt always finalizes
isSend: true,
},
"popup",
"/sign-psbt",
),
{ ...config, type: DappRequestType.SignPsbt }, // Use SignPsbt type for both operations
);

return ccc.hexFrom(txid);
}
}
17 changes: 15 additions & 2 deletions packages/okx/src/advancedBarrel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@ import { Nip07A } from "@ckb-ccc/nip07/advanced";
import { UniSatA } from "@ckb-ccc/uni-sat/advanced";

export interface BitcoinProvider
extends Pick<UniSatA.Provider, "on" | "removeListener" | "signMessage">,
Partial<Omit<UniSatA.Provider, "on" | "removeListener" | "signMessage">> {
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;
Expand Down
30 changes: 30 additions & 0 deletions packages/okx/src/btc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,34 @@ 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.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT as a Hex string
*/
async signPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
return ccc.hexFrom(
await this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options),
);
}

/**
* Broadcasts a signed PSBT to the Bitcoin network.
*
* @param psbtHex - The hex string (without 0x prefix) of signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID (without 0x prefix)
*/
async broadcastPsbt(
psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
const txid = await this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2));
return ccc.hexFrom(txid);
}
}
19 changes: 19 additions & 0 deletions packages/uni-sat/src/advancedBarrel.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import { ccc } from "@ckb-ccc/core";

/**
* 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
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT hex string
*/
signPsbt(psbtHex: string, options?: ccc.SignPsbtOptions): Promise<string>;

/**
* Broadcasts a signed PSBT to the Bitcoin network.
*
* @param psbtHex - The hex string of the signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID.
*/
pushPsbt(psbtHex: string): Promise<string>;

/**
* Requests user accounts.
* @returns A promise that resolves to an array of account addresses.
Expand Down
30 changes: 30 additions & 0 deletions packages/uni-sat/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,34 @@ 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.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT as a Hex string
*/
async signPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
return ccc.hexFrom(
await this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options),
);
}

/**
* Broadcasts a signed PSBT to the Bitcoin network.
*
* @param psbtHex - The hex string (without 0x prefix) of signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID (without 0x prefix)
*/
async broadcastPsbt(
psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
const txid = await this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2));
return ccc.hexFrom(txid);
}
}
28 changes: 28 additions & 0 deletions packages/utxo-global/src/btc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,32 @@ 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.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT as a Hex string
*/
async signPsbt(
_psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
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 as a Hex string
* @todo Implement PSBT broadcasting with UTXO Global
*/
async broadcastPsbt(
_psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<ccc.Hex> {
throw new Error("UTXO Global PSBT broadcasting not implemented yet");
}
}
1 change: 1 addition & 0 deletions packages/xverse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
},
"dependencies": {
"@ckb-ccc/core": "workspace:*",
"bitcoinjs-lib": "^7.0.0",
"valibot": "^1.1.0"
},
"packageManager": "pnpm@10.8.1"
Expand Down
Loading