Skip to content
Closed
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
48 changes: 41 additions & 7 deletions src/contracts/handlers/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,50 @@ import {
sequenceToTimelock,
timelockToSequence,
} from "./helpers";
import {
normalizeToDescriptor,
extractPubKey,
} from "../../identity/descriptor";

/**
* Typed parameters for DefaultVtxo contracts.
*
* pubKey and serverPubKey are stored as descriptors (tr(...) format).
* For backwards compatibility, hex pubkeys are auto-normalized to tr(pubkey).
*/
export interface DefaultContractParams {
pubKey: Uint8Array;
serverPubKey: Uint8Array;
/** User's public key descriptor (tr(...) format) */
pubKey: string;
/** Server's public key descriptor (tr(...) format) */
serverPubKey: string;
/** CSV timelock for the exit path */
csvTimelock: RelativeTimelock;
}

/**
* Extract a public key as bytes from either descriptor or raw hex.
* Used internally to get the actual pubkey for script creation.
*/
function extractPubKeyBytes(value: string): Uint8Array {
// Normalize first (wraps raw hex as tr(hex))
const descriptor = normalizeToDescriptor(value);
// Extract pubkey from simple descriptor
const pubKeyHex = extractPubKey(descriptor);
return hex.decode(pubKeyHex);
}

/**
* Handler for default wallet VTXOs.
*
* Default contracts use the standard forfeit + exit tapscript:
* - forfeit: (Alice + Server) multisig for collaborative spending
* - exit: (Alice) + CSV timelock for unilateral exit
*
* Public keys are stored as descriptors for future HD wallet support:
* - Simple format: tr(pubkey_hex)
* - HD format: tr([fingerprint/path']xpub/0/{index})
*
* Legacy hex pubkeys are automatically normalized to tr(pubkey) on read.
*/
export const DefaultContractHandler: ContractHandler<
DefaultContractParams,
Expand All @@ -37,13 +65,18 @@ export const DefaultContractHandler: ContractHandler<

createScript(params: Record<string, string>): DefaultVtxo.Script {
const typed = this.deserializeParams(params);
return new DefaultVtxo.Script(typed);
// Extract actual pubkey bytes from descriptors
return new DefaultVtxo.Script({
pubKey: extractPubKeyBytes(typed.pubKey),
serverPubKey: extractPubKeyBytes(typed.serverPubKey),
csvTimelock: typed.csvTimelock,
});
},

serializeParams(params: DefaultContractParams): Record<string, string> {
return {
pubKey: hex.encode(params.pubKey),
serverPubKey: hex.encode(params.serverPubKey),
pubKey: params.pubKey,
serverPubKey: params.serverPubKey,
csvTimelock: timelockToSequence(params.csvTimelock).toString(),
};
},
Expand All @@ -53,8 +86,9 @@ export const DefaultContractHandler: ContractHandler<
? sequenceToTimelock(Number(params.csvTimelock))
: DefaultVtxo.Script.DEFAULT_TIMELOCK;
return {
pubKey: hex.decode(params.pubKey),
serverPubKey: hex.decode(params.serverPubKey),
// Normalize hex pubkeys to descriptors for backwards compat
pubKey: normalizeToDescriptor(params.pubKey),
serverPubKey: normalizeToDescriptor(params.serverPubKey),
csvTimelock,
};
},
Expand Down
56 changes: 47 additions & 9 deletions src/contracts/handlers/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { RelativeTimelock } from "../../script/tapscript";
import * as bip68 from "bip68";
import { Contract, PathContext } from "../types";
import {
isDescriptor,
extractPubKey,
normalizeToDescriptor,
} from "../../identity/descriptor";

/**
* Convert RelativeTimelock to BIP68 sequence number.
Expand Down Expand Up @@ -28,7 +33,22 @@ export function sequenceToTimelock(sequence: number): RelativeTimelock {
}

/**
* Resolve wallet's role from explicit role or by matching pubkey.
* Extract the raw pubkey from a value (descriptor or hex).
* Used for role matching.
*/
function extractRawPubKey(value: string): string {
if (isDescriptor(value)) {
return extractPubKey(value);
}
return value;
}

/**
* Resolve wallet's role from explicit role or by matching pubkey/descriptor.
*
* Checks both walletDescriptor (preferred) and walletPubKey (deprecated fallback)
* against contract params. Contract params may be stored as either descriptors
* or raw hex pubkeys, so we normalize both for comparison.
*/
export function resolveRole(
contract: Contract,
Expand All @@ -39,14 +59,32 @@ export function resolveRole(
return context.role;
}

// Try to match wallet pubkey against contract params
if (context.walletPubKey) {
if (context.walletPubKey === contract.params.sender) {
return "sender";
}
if (context.walletPubKey === contract.params.receiver) {
return "receiver";
}
// Get wallet's pubkey from descriptor or legacy field
let walletPubKey: string | undefined;
if (context.walletDescriptor) {
walletPubKey = extractRawPubKey(context.walletDescriptor);
} else if (context.walletPubKey) {
// Deprecated fallback
walletPubKey = context.walletPubKey;
}

if (!walletPubKey) {
return undefined;
}

// Extract pubkeys from contract params (may be descriptors or raw hex)
const senderPubKey = contract.params.sender
? extractRawPubKey(contract.params.sender)
: undefined;
const receiverPubKey = contract.params.receiver
? extractRawPubKey(contract.params.receiver)
: undefined;

if (senderPubKey && walletPubKey === senderPubKey) {
return "sender";
}
if (receiverPubKey && walletPubKey === receiverPubKey) {
return "receiver";
}

return undefined;
Expand Down
10 changes: 9 additions & 1 deletion src/contracts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,17 @@ export interface PathContext {
blockHeight?: number;

/**
* Wallet's public key (x-only, 32 bytes hex).
* Wallet's descriptor for signing.
* Format: tr(pubkey) for static keys, tr([fingerprint/path']xpub/0/{index}) for HD.
* Used by handlers to determine wallet's role in multi-party contracts.
*/
walletDescriptor?: string;

/**
* Wallet's public key (x-only, 32 bytes hex).
* @deprecated Use walletDescriptor instead. This field is provided for
* backwards compatibility and will be removed in a future version.
*/
walletPubKey?: string;

/**
Expand Down
143 changes: 143 additions & 0 deletions src/identity/descriptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/**
* Descriptor utility functions for working with output descriptors.
*
* Output descriptors provide a standardized way to represent Bitcoin addresses
* and their spending conditions. This module supports:
* - Simple descriptors: tr(pubkey) - for static/external keys
* - HD descriptors: tr([fingerprint/path']xpub/derivation) - for HD wallets
*
* @module
*/

/**
* Check if a string is a descriptor (starts with "tr(").
*
* @param value - String to check
* @returns true if the value is in descriptor format
*
* @example
* ```typescript
* isDescriptor("tr(abc123)") // true
* isDescriptor("abc123") // false
* ```
*/
export function isDescriptor(value: string): boolean {
return value.startsWith("tr(");
}

/**
* Normalize a value to descriptor format.
*
* - If already a descriptor, return as-is
* - If hex pubkey, wrap as tr(pubkey)
*
* This provides backwards compatibility for legacy hex pubkeys
* stored in contract parameters.
*
* @param value - Descriptor or hex pubkey
* @returns Value in descriptor format
*
* @example
* ```typescript
* normalizeToDescriptor("tr(abc123)") // "tr(abc123)"
* normalizeToDescriptor("abc123") // "tr(abc123)"
* ```
*/
export function normalizeToDescriptor(value: string): string {
if (isDescriptor(value)) {
return value;
}
return `tr(${value})`;
}

/**
* Extract the public key from a descriptor.
*
* For simple descriptors (tr(pubkey)), extracts the pubkey directly.
* For HD descriptors, throws an error - use the DescriptorProvider
* to derive the key from the xpub instead.
*
* @param descriptor - Descriptor or raw hex pubkey
* @returns 64-character hex public key
* @throws Error if descriptor is HD format (cannot derive without xpub)
*
* @example
* ```typescript
* extractPubKey("tr(abc...)") // "abc..."
* extractPubKey("abc...") // "abc..."
* extractPubKey("tr([fp/path]xpub/0/5)") // throws
* ```
*/
export function extractPubKey(descriptor: string): string {
if (!isDescriptor(descriptor)) {
// Already a raw pubkey
return descriptor;
}

// Simple descriptor: tr(pubkey) - 64 hex chars
const simpleMatch = descriptor.match(/^tr\(([0-9a-fA-F]{64})\)$/);
if (simpleMatch) {
return simpleMatch[1];
}

throw new Error(
"Cannot extract pubkey from HD descriptor without derivation. " +
"Use DescriptorProvider to derive the key from the xpub."
);
}

/**
* Parsed HD descriptor components.
*/
export interface ParsedHDDescriptor {
/** 8-character hex fingerprint of the master key */
fingerprint: string;

/** BIP32 derivation path (e.g., "86'/0'/0'") */
basePath: string;

/** Extended public key (xpub or tpub) */
xpub: string;

/** Final derivation path from xpub (e.g., "0/5") */
derivationPath: string;
}

/**
* Parse an HD descriptor into its components.
*
* HD descriptors have the format: tr([fingerprint/path']xpub/derivation)
*
* @param descriptor - Descriptor string to parse
* @returns Parsed components, or null if not an HD descriptor
*
* @example
* ```typescript
* const parsed = parseHDDescriptor("tr([12345678/86'/0'/0']xpub.../0/5)");
* // {
* // fingerprint: "12345678",
* // basePath: "86'/0'/0'",
* // xpub: "xpub...",
* // derivationPath: "0/5"
* // }
* ```
*/
export function parseHDDescriptor(
descriptor: string
): ParsedHDDescriptor | null {
// tr([fingerprint/path]xpub/derivation)
const match = descriptor.match(
/^tr\(\[([0-9a-fA-F]{8})\/([^\]]+)\]([a-zA-Z0-9]+)\/(.+)\)$/
);

if (!match) {
return null;
}

return {
fingerprint: match[1],
basePath: match[2],
xpub: match[3],
derivationPath: match[4],
};
}
Loading