Skip to content
Open
Show file tree
Hide file tree
Changes from all 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";
92 changes: 92 additions & 0 deletions packages/core/src/signer/btc/psbt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { HexLike, hexFrom } from "../../hex/index.js";

/**
* Options for signing a PSBT (Partially Signed Bitcoin Transaction)
*/
export type SignPsbtOptionsLike = {
/**
* Whether to finalize the PSBT after signing.
* Default is true.
*/
autoFinalized?: boolean;
/**
* Array of inputs to sign
*/
inputsToSign?: InputToSignLike[];
};

export class SignPsbtOptions {
constructor(
public autoFinalized: boolean,
public inputsToSign: InputToSign[],
) {}

static from(options?: SignPsbtOptionsLike): SignPsbtOptions {
return new SignPsbtOptions(
options?.autoFinalized ?? true,
options?.inputsToSign?.map((i) => InputToSign.from(i)) ?? [],
);
}
}

/**
* Specification for an input to sign in a PSBT.
* Must specify at least one of: address or pubkey.
*/
export type InputToSignLike = {
/**
* 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?: HexLike;
}
| {
/**
* The address whose corresponding private key to use for signing.
*/
address?: string;
/**
* The public key whose corresponding private key to use for signing.
*/
publicKey: HexLike;
}
);

export class InputToSign {
constructor(
public index: number,
public sighashTypes?: number[],
public disableTweakSigner?: boolean,
public address?: string,
public publicKey?: string,
) {}

static from(input: InputToSignLike): InputToSign {
return new InputToSign(
input.index,
input.sighashTypes,
input.disableTweakSigner,
input.address,
input.publicKey ? hexFrom(input.publicKey).slice(2) : undefined,
);
}
}
42 changes: 41 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 { SignPsbtOptionsLike } 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?: SignPsbtOptionsLike,
): 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,28 @@ 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?: SignPsbtOptionsLike,
): 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.
*/
abstract broadcastPsbt(
psbtHex: HexLike,
options?: SignPsbtOptionsLike,
): Promise<Hex>;
}
15 changes: 15 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 { SignPsbtOptionsLike } from "./psbt.js";
import { SignerBtc } from "./signerBtc.js";

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

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

async broadcastPsbt(
_psbtHex: HexLike,
_options?: SignPsbtOptionsLike,
): Promise<Hex> {
throw new Error("Read-only signer does not support broadcastPsbt");
}
}
76 changes: 76 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,80 @@ 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.SignPsbtOptionsLike,
): Promise<ccc.Hex> {
const { address } = await this.assertConnection();
const formattedOptions = ccc.SignPsbtOptions.from(options);

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: formattedOptions,
signerAddress: address,
autoFinalized: formattedOptions.autoFinalized,
},
"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.SignPsbtOptionsLike,
): Promise<ccc.Hex> {
throw new Error(
"JoyID does not support broadcasting signed PSBTs directly. Use signAndBroadcastPsbt instead.",
);
}

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

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: formattedOptions,
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.SignPsbtOptionsLike,
): 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 of signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID as a Hex string
*/
async broadcastPsbt(
psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptionsLike,
): 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.SignPsbtOptionsLike): 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.SignPsbtOptionsLike,
): 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 of signed PSBT to broadcast.
* @returns A promise that resolves to the transaction ID as a Hex string
*/
async broadcastPsbt(
psbtHex: ccc.HexLike,
_options?: ccc.SignPsbtOptionsLike,
): Promise<ccc.Hex> {
const txid = await this.provider.pushPsbt(ccc.hexFrom(psbtHex).slice(2));
return ccc.hexFrom(txid);
}
}
Loading