Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Spend Silent Payment Coins #57

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
113 changes: 96 additions & 17 deletions packages/wallet/src/coin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,26 @@ type CoinStatus = {
blockTime?: number;
};

type CoinType = 'P2PWKH' | 'P2Silent';

export class Coin {
txid: string; // previous transaction id
vout: number; // index of the output in the previous transaction
txid: string;
vout: number;
value: number;
address: string;
status: CoinStatus;
type: CoinType;

constructor(partial: Partial<Coin>) {
constructor(partial: Partial<Coin>, type: CoinType) {
Object.assign(this, partial);
this.type = type;
}

static fromJSON(json: string): Coin {
return new Coin(JSON.parse(json));
const data = JSON.parse(json);
if (data.type === 'P2PWKH') return new P2PWKH(data);
if (data.type === 'P2TR') return new P2Silent(data);
throw new Error('Unknown coin type');
}

toJSON(): string {
Expand All @@ -31,6 +38,7 @@ export class Coin {
value: this.value,
address: this.address,
status: this.status,
type: this.type,
});
}

Expand All @@ -45,33 +53,104 @@ export class Coin {
};
}

estimateSpendingSize(): number {
throw new Error("estimateSpendingSize() must be implemented by subclasses");
}

estimateSpendingFee(feeRate: number): number {
return this.estimateSpendingSize() * feeRate;
}
}

export class P2PWKH extends Coin {
constructor(partial: Partial<Coin>) {
super(partial, 'P2PWKH');
}

estimateSpendingSize(): number {
let total = 0;

// Outpoint (hash and index) + sequence
total += 32 + 4 + 4;

// legacy script size (0x00)
total += 1;

// we know our coins is P2WPKH
let size = 0;
// P2WPKH witness size
let witnessSize = 0;

// varint-items-len
size += 1;
witnessSize += 1;
// varint-len [signature]
size += 1 + 73;
witnessSize += 1 + 73;
// varint-len [key]
size += 1 + 33;
// vsize
size = ((size + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR) | 0;
witnessSize += 1 + 33;

total += size;
// Adjust for witness vsize
witnessSize = ((witnessSize + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR) | 0;

total += witnessSize;
return total;
}
}

estimateSpendingFee(feeRate: number): number {
return this.estimateSpendingSize() * feeRate;
export class P2Silent extends Coin {
tweakedData: string;

constructor(partial: Partial<Coin> & { tweakedData: string }) {
super(partial, 'P2Silent');
this.tweakedData = partial.tweakedData;
}

estimateSpendingSize(): number {
let total = 0;

// Outpoint (hash and index) + sequence
total += 32 + 4 + 4;

// P2TR witness size
let witnessSize = 0;

// varint-items-len (P2TR signatures and control block)
witnessSize += 1;
// P2TR Schnorr signature (64 bytes)
witnessSize += 64;
// Optional script path size if relevant (e.g., 33 bytes for pubkey)
witnessSize += 33;

// Adjust for witness vsize
witnessSize = ((witnessSize + WITNESS_SCALE_FACTOR - 1) / WITNESS_SCALE_FACTOR) | 0;

total += witnessSize;
return total;
}

toJSON(): string {
return JSON.stringify({
txid: this.txid,
vout: this.vout,
value: this.value,
address: this.address,
status: this.status,
type: this.type,
tweakedData: this.tweakedData,
});
}

static fromJSON(json: string): P2Silent {
const data = JSON.parse(json);
return new P2Silent({
...data,
tweakedData: data.tweakedData,
});
}
}

// Utility functions to determine coin type
export function isP2PWKH(coin: Coin): coin is P2PWKH {
return coin.type === 'P2PWKH';
}

export function isP2Silent(coin: Coin | Coin[]): coin is P2Silent | P2Silent[] {
if (Array.isArray(coin)) {
return coin.every((item) => item.type === 'P2Silent');
}
return coin.type === 'P2Silent';
}
4 changes: 3 additions & 1 deletion packages/wallet/src/db.interface.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Coin } from './coin.ts';
import { Coin, P2Silent } from './coin.ts';

export type DbInterface = {
open(): Promise<void>;
Expand All @@ -25,6 +25,8 @@ export type DbInterface = {
getAllAddresses(): Promise<string[]>;
saveUnspentCoins(coins: Coin[]): Promise<void>;
getUnspentCoins(): Promise<Coin[]>;
saveUnspentSilentPaymentCoins(coins: P2Silent[]): Promise<void>;
getUnspentSilentPaymentCoins(): Promise<P2Silent[]>;
saveSilentPaymentAddress(address: string): Promise<void>;
getSilentPaymentAddress(): Promise<string>;
};
Loading