Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = {
Copy link
Member

Choose a reason for hiding this comment

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

I suggest having SignPsbtOptionsLike and ToSignInputLike types to simplify the type conversion tasks needed for developers, just like other CCC types.

BTW, I'm curious about why it is ToSignInput instead of InputToSign?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good suggestion. It does help keep the API style consistent. Modified.

BTW, I'm curious about why it is ToSignInput instead of InputToSign?

I copied it from the wallet API docs without second thought. I’ve switched it to InputToSign as well since it reads more clearly.

/**
* 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;
}
);
43 changes: 43 additions & 0 deletions packages/core/src/signer/btc/signerBtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { KnownScript } from "../../client/index.js";
import { HexLike, hexFrom } from "../../hex/index.js";
import { numToBytes } from "../../num/index.js";
import { Signer, SignerSignType, SignerType } from "../signer/index.js";
import { SignPsbtOptions } from "./psbt.js";
import { btcEcdsaPublicKeyHash } from "./verify.js";

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

/**
* Sign and broadcast a PSBT.
*
* @param psbtHex - The hex string (without 0x prefix) of PSBT to sign and broadcast.
* @param options - Options for signing the PSBT.
* @returns A promise that resolves to the transaction ID (non-0x prefixed hex).
*/
async signAndBroadcastPsbt(
psbtHex: HexLike,
options?: SignPsbtOptions,
): Promise<string> {
Copy link
Member

Choose a reason for hiding this comment

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

It's better to use clearer types like ccc.HexLike for the parameter and ccc.Hex for returns, so that we won't have to worry about whether a hex string is 0x-prefixed or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great suggestion! However, for return values specifically, BTC transactions default to non-0x prefixed hex. Following convention, I think it's better to return string type directly and document this in comments.

Copy link
Member

Choose a reason for hiding this comment

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

That doesn't matter. It's because CCC uses 0x-prefixed hex strings as default. If BTC transactions return non-0x hex, we should use ccc.hexFrom to convert it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Got it. Modified.

// ccc.hexFrom adds 0x prefix, but BTC expects non-0x
const signedPsbt = await this.signPsbt(hexFrom(psbtHex).slice(2), options);
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 hexFrom(psbtHex).slice(2) is used to get a non-0x-prefixed hex string. A more direct and readable approach is to use bytesTo(bytesFrom(psbtHex), "hex"). This avoids the intermediate step of potentially adding and then immediately removing the "0x" prefix, making the intent clearer.

Suggested change
const signedPsbt = await this.signPsbt(hexFrom(psbtHex).slice(2), options);
const signedPsbt = await this.signPsbt(bytesTo(bytesFrom(psbtHex), "hex"), options);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the signAndBroadcastPsbt flow, there are no other intermediate steps that could modify it. The current approach remains direct, clear.

Copy link
Member

Choose a reason for hiding this comment

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

That's actually a problem. signPsbt should (also) accept a HexLike and automatically convert it. We shouldn't have to handle these type conversions here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True that. Fixed.

return this.broadcastPsbt(signedPsbt, options);
}

/**
* Gets the Bitcoin account associated with the signer.
*
Expand Down Expand Up @@ -123,4 +140,30 @@ 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 (without 0x prefix) of PSBT to sign.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT hex string (without 0x prefix)
*/
abstract signPsbt(
psbtHex: HexLike,
options?: SignPsbtOptions,
): Promise<string>;

/**
* Broadcasts a PSBT to the Bitcoin network.
*
* @param psbtHex - The hex string (without 0x prefix) of the PSBT to broadcast.
* @param options - Options for broadcasting the PSBT.
* @returns A promise that resolves to the transaction ID (without 0x prefix).
*/
async broadcastPsbt(
_psbtHex: HexLike,
_options?: SignPsbtOptions,
): Promise<string> {
throw new Error("Not implemented");
Copy link
Member

Choose a reason for hiding this comment

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

I'm confused why the signPsbt is abstract but the broadcastPsbt is Not implemented.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch! Changed to abstract for better type safety and consistency.

}
}
9 changes: 9 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,12 @@ export class SignerBtcPublicKeyReadonly extends SignerBtc {
async getBtcPublicKey(): Promise<Hex> {
return this.publicKey;
}

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

async broadcastPsbt(_: HexLike, __?: SignPsbtOptions): Promise<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For consistency and readability, it's better to use named parameters prefixed with an underscore (e.g., _psbtHex) to indicate they are unused, rather than _ and __. This aligns with the convention used in other parts of the codebase.

Suggested change
async broadcastPsbt(_: HexLike, __?: SignPsbtOptions): Promise<string> {
async broadcastPsbt(_psbtHex: HexLike, _options?: SignPsbtOptions): Promise<string> {

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 (without 0x prefix) of PSBT to sign.
* @returns A promise that resolves to the signed PSBT hex string (without 0x prefix)
*/
async signPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<string> {
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 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<string> {
throw new Error(
"JoyID does not support broadcasting signed PSBTs directly. Use signAndBroadcastPsbt instead.",
);
}

async signAndBroadcastPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<string> {
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 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
27 changes: 27 additions & 0 deletions packages/okx/src/btc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,31 @@ export class BitcoinSigner extends ccc.SignerBtc {

return this.provider.signMessage(challenge, "ecdsa");
}

/**
* Signs a PSBT using OKX wallet.
*
* @param psbtHex - The hex string (without 0x prefix) of PSBT to sign.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT hex string (without 0x prefix)
*/
async signPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<string> {
return this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options);
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
return this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options);
return this.provider.signPsbt(ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"), 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)
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 documentation for @param psbtHex and @returns is slightly misleading. The psbtHex parameter is of type ccc.HexLike, which can be a hex string with or without a "0x" prefix, as ccc.hexFrom handles both cases. The documentation, however, states it must be without the prefix. Similarly, the return value is a ccc.Hex string, which always includes a "0x" prefix, but the documentation states it's without one. To improve clarity and align with the implementation, I recommend updating the JSDoc.

Suggested change
* @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)
* @param psbtHex - The hex string of the signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID as a Hex string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

*/
async broadcastPsbt(
psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<string> {
return this.provider.pushPsbt(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
return this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2));
return this.provider.pushPsbt(ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"));

}
}
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
27 changes: 27 additions & 0 deletions packages/uni-sat/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,31 @@ export class Signer extends ccc.SignerBtc {

return this.provider.signMessage(challenge, "ecdsa");
}

/**
* Signs a PSBT using UniSat wallet.
*
* @param psbtHex - The hex string (without 0x prefix) of PSBT to sign.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT hex string (without 0x prefix)
*/
async signPsbt(
psbtHex: ccc.HexLike,
options?: ccc.SignPsbtOptions,
): Promise<string> {
return this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options);
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
return this.provider.signPsbt(ccc.hexFrom(psbtHex).slice(2), options);
return this.provider.signPsbt(ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"), 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)
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 JSDoc for this method could be more precise. The @param psbtHex is documented as a hex string "without 0x prefix", but the implementation correctly handles hex strings both with and without the prefix thanks to ccc.hexFrom. Also, the @returns JSDoc states the transaction ID is returned "without 0x prefix", but ccc.hexFrom(txid) will ensure it is prefixed. I suggest updating the documentation to reflect the actual behavior.

Suggested change
* @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)
* @param psbtHex - The hex string of the signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID as a Hex string.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

*/
async broadcastPsbt(
psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<string> {
return this.provider.pushPsbt(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
return this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2));
return this.provider.pushPsbt(ccc.bytesTo(ccc.bytesFrom(psbtHex), "hex"));

}
}
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 (without 0x prefix) of PSBT to sign.
* @param options - Options for signing the PSBT
* @returns A promise that resolves to the signed PSBT hex string (without 0x prefix)
*/
async signPsbt(
_psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,
): Promise<string> {
throw new Error("UTXO Global PSBT signing not implemented yet");
}

/**
* 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)
* @todo Implement PSBT broadcasting with UTXO Global
*/
async broadcastPsbt(
_: ccc.HexLike,
__?: ccc.SignPsbtOptions,
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 parameter names _ and __ are obscure and inconsistent with signPsbt in the same file, which uses _psbtHex and _options. For better readability and consistency, it's better to use named parameters prefixed with an underscore to indicate they are unused.

Suggested change
_: ccc.HexLike,
__?: ccc.SignPsbtOptions,
_psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptions,

): Promise<string> {
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