Skip to content

Commit 4f52dde

Browse files
committed
chore(p2sp): Move main function to top and add lazy prop
Added lazy prop for outputs.
1 parent c8518f3 commit 4f52dde

File tree

1 file changed

+173
-63
lines changed

1 file changed

+173
-63
lines changed

ts_src/payments/p2sp.ts

Lines changed: 173 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,185 @@ import { bech32m } from 'bech32';
99
import { Payment, PaymentOpts } from './index';
1010
import * as lazy from './lazy';
1111
import { taggedHash } from '../crypto';
12-
import { Input } from '../transaction';
12+
import { Input, Output } from '../transaction';
1313

1414
// --- TYPE DEFINITIONS & UTILITIES ---
1515
export const BECH32_SP_LIMIT = 150;
1616

17-
// Extend the Payment interface for silent payments
17+
/**
18+
* @property {Uint8Array} [S] - Shared secret between you and recipient
19+
* @property {Uint8Array} [B_spend_pub] - Recipients spend pubkey B_scan
20+
*/
21+
interface Recipient {
22+
S?: Uint8Array;
23+
B_spend_pub?: Uint8Array;
24+
}
25+
/**
26+
* Represents a Silent Payment transaction structure that extends a standard {@link Payment}.
27+
* Includes additional cryptographic and metadata fields used for constructing
28+
* or parsing silent payments.
29+
*
30+
* @property {Uint8Array} [spendPubkey] - Optional spend public key for the sender.
31+
* @property {Uint8Array} [scanPubkey] - Optional scan public key used for recipient address derivation.
32+
* @property {Input[]} [inputs] - Optional array of input UTXOs used in the transaction.
33+
* @property {Output[]} [outputs] - Optional array of outputs generated in the transaction.
34+
* @property {number} [version] - Optional version number of the silent payment scheme.
35+
* @property {Uint8Array} [aSum] - Optional summed private key (see `calculateSumA`).
36+
* @property {Uint8Array} [outpointL] - Optional first result of lexicographically sorted input transaction IDs.
37+
* @property {{ priv: Uint8Array; isXOnly: boolean }[]} [privKeys] - Optional array of private keys associated with the transaction.
38+
* Each object includes the private key and a flag indicating if it's x-only.
39+
* @property {Recipient[]} recipients - Array of recipients for this silent payment.
40+
*/
1841
export interface SilentPayment extends Payment {
1942
spendPubkey?: Uint8Array;
2043
scanPubkey?: Uint8Array;
44+
inputs?: Input[];
45+
outputs?: Output[];
2146
version?: number;
47+
aSum?: Uint8Array;
48+
outpointL?: Uint8Array;
49+
privKeys?: Array<{ priv: Uint8Array; isXOnly: boolean }>;
50+
recipients: Array<Recipient>;
51+
}
52+
53+
/**
54+
* Main function for creating a Pay-to-Silent-Payment (P2SP) payment object.
55+
* This function encapsulates the logic for handling silent payment addresses and keys.
56+
*
57+
* @param a - The payment object containing the necessary data for P2SP.
58+
* @param opts - Optional payment options.
59+
* @returns The P2SP payment object.
60+
*/
61+
export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment {
62+
if (!a.address && !(a.spendPubkey && a.scanPubkey)) {
63+
throw new TypeError('Not enough data');
64+
}
65+
opts = Object.assign({ validate: true }, opts || {});
66+
67+
const network = a.network || BITCOIN_NETWORK;
68+
const o: SilentPayment = { name: 'p2sp', network };
69+
70+
// Lazy load silent payment specific properties
71+
lazy.prop(o, 'spendPubkey', () => {
72+
if (a.address) return decodeSilentPaymentAddress(a.address).B_spend;
73+
return a.spendPubkey!;
74+
});
75+
lazy.prop(o, 'scanPubkey', () => {
76+
if (a.address) return decodeSilentPaymentAddress(a.address).B_scan;
77+
return a.scanPubkey!;
78+
});
79+
lazy.prop(o, 'address', () => {
80+
if (a.address) return a.address;
81+
const version = a.version !== undefined ? a.version : 0;
82+
return encodeSilentPaymentAddress(
83+
o.spendPubkey!,
84+
o.scanPubkey!,
85+
version,
86+
network,
87+
);
88+
});
89+
lazy.prop(o, 'outputs', () => {
90+
if (a.outputs) return a.outputs;
91+
const allRecipientsComplete = a.recipients.every(
92+
r => r.S.length > 0 && r.B_spend_pub.length > 0,
93+
);
94+
const allRecipientsHaveBSpend = a.recipients.every(
95+
r => r.B_spend_pub.length > 0,
96+
);
97+
// If we have both the secret and B_spend for each key we can derive directly
98+
if (allRecipientsComplete) {
99+
return a.recipients?.map((value, index) => {
100+
deriveOutput(value.S, value.B_spend_pub, index);
101+
});
102+
}
103+
// If we have outpointL, aSum and only the spend keys for the recipients we need to calculate the input hash and secret
104+
else if (
105+
a.outpointL != null &&
106+
a.outpointL?.length > 0 &&
107+
a.aSum != null &&
108+
a.aSum?.length > 0 &&
109+
a.recipients?.length > 0 &&
110+
allRecipientsHaveBSpend
111+
) {
112+
const A: Uint8Array = ecc.pointFromScalar(a.aSum, true); // compressed 33B
113+
const inputHashTweak: Uint8Array = calculateInputHashTweak(
114+
a.outpointL,
115+
A,
116+
);
117+
return a.recipients?.map((value, index) => {
118+
const S = calculateSharedSecret(
119+
inputHashTweak,
120+
value.B_spend_pub,
121+
aSum,
122+
);
123+
deriveOutput(S, value.B_spend_pub, index);
124+
});
125+
}
126+
// If we have all the inputs, aSum and only the spend keys for the recipients we need to calculate the input hash and secret
127+
else if (
128+
a.inputs != null &&
129+
a.inputs?.length > 0 &&
130+
a.aSum != null &&
131+
a.aSum?.length > 0 &&
132+
a?.recipients.length > 0 &&
133+
allRecipientsHaveBSpend
134+
) {
135+
const outpointL = findSmallestOutpoint(a.inputs);
136+
const A: Uint8Array = ecc.pointFromScalar(a.aSum, true); // compressed 33B
137+
const inputHashTweak: Uint8Array = calculateInputHashTweak(outpointL, A);
138+
return a?.recipients.map((value, index) => {
139+
const S = calculateSharedSecret(
140+
inputHashTweak,
141+
value.B_spend_pub,
142+
a.aSum,
143+
);
144+
deriveOutput(S, value.B_spend_pub, index);
145+
});
146+
}
147+
// If we have all the inputs, privKeys and only the spend keys for the recipients we need to calculate the Sum, the input hash and secret
148+
else if (
149+
a.inputs != null &&
150+
a.inputs?.length > 0 &&
151+
a.privKeys != null &&
152+
a.privKeys?.length > 0 &&
153+
a?.recipients.length > 0 &&
154+
allRecipientsHaveBSpend
155+
) {
156+
const aSum: Uint8Array = calculateSumA(a.privKeys);
157+
const outpointL = findSmallestOutpoint(a.inputs);
158+
const A: Uint8Array = ecc.pointFromScalar(aSum, true); // compressed 33B
159+
const inputHashTweak: Uint8Array = calculateInputHashTweak(outpointL, A);
160+
return a?.recipients.map((value, index) => {
161+
const S = calculateSharedSecret(
162+
inputHashTweak,
163+
value.B_spend_pub,
164+
aSum,
165+
);
166+
deriveOutput(S, value.B_spend_pub, index);
167+
});
168+
} else throw Error('Not enough data to derive outputs');
169+
});
170+
171+
if (opts.validate) {
172+
if (a.address) {
173+
const decoded = decodeSilentPaymentAddress(a.address);
174+
if (a.spendPubkey && tools.compare(a.spendPubkey, decoded.B_spend) !== 0)
175+
throw new TypeError('Spend pubkey mismatch');
176+
if (a.scanPubkey && tools.compare(a.scanPubkey, decoded.B_scan) !== 0)
177+
throw new TypeError('Scan pubkey mismatch');
178+
179+
const HRP = network.bech32 === 'bc' ? 'sp' : 'tsp';
180+
if (!a.address.startsWith(HRP)) {
181+
throw new TypeError('Invalid prefix or Network mismatch');
182+
}
183+
}
184+
if (o.spendPubkey && o.spendPubkey.length !== 33)
185+
throw new TypeError('Invalid spend pubkey length');
186+
if (o.scanPubkey && o.scanPubkey.length !== 33)
187+
throw new TypeError('Invalid scan pubkey length');
188+
}
189+
190+
return Object.assign(o, a);
22191
}
23192

24193
/**
@@ -148,8 +317,8 @@ export function encodeSilentPaymentAddress(
148317
* @returns { B_spend, B_scan, version }
149318
*/
150319
export function decodeSilentPaymentAddress(address: string): {
151-
B_spend: Uint8Array;
152-
B_scan: Uint8Array;
320+
B_spend: Uint8Array; // pub spend key
321+
B_scan: Uint8Array; // pub scan key
153322
version: number;
154323
} {
155324
// The default bech32 limit is 90, but silent payment addresses are longer.
@@ -200,65 +369,6 @@ export function decodeSilentPaymentAddress(address: string): {
200369
return { B_spend, B_scan, version };
201370
}
202371

203-
/**
204-
* Main function for creating a Pay-to-Silent-Payment (P2SP) payment object.
205-
* This function encapsulates the logic for handling silent payment addresses and keys.
206-
*
207-
* @param a - The payment object containing the necessary data for P2SP.
208-
* @param opts - Optional payment options.
209-
* @returns The P2SP payment object.
210-
*/
211-
export function p2sp(a: SilentPayment, opts?: PaymentOpts): SilentPayment {
212-
if (!a.address && !(a.spendPubkey && a.scanPubkey)) {
213-
throw new TypeError('Not enough data');
214-
}
215-
opts = Object.assign({ validate: true }, opts || {});
216-
217-
const network = a.network || BITCOIN_NETWORK;
218-
const o: SilentPayment = { name: 'p2sp', network };
219-
220-
// Lazy load silent payment specific properties
221-
lazy.prop(o, 'spendPubkey', () => {
222-
if (a.address) return decodeSilentPaymentAddress(a.address).B_spend;
223-
return a.spendPubkey!;
224-
});
225-
lazy.prop(o, 'scanPubkey', () => {
226-
if (a.address) return decodeSilentPaymentAddress(a.address).B_scan;
227-
return a.scanPubkey!;
228-
});
229-
lazy.prop(o, 'address', () => {
230-
if (a.address) return a.address;
231-
const version = a.version !== undefined ? a.version : 0;
232-
return encodeSilentPaymentAddress(
233-
o.spendPubkey!,
234-
o.scanPubkey!,
235-
version,
236-
network,
237-
);
238-
});
239-
240-
if (opts.validate) {
241-
if (a.address) {
242-
const decoded = decodeSilentPaymentAddress(a.address);
243-
if (a.spendPubkey && tools.compare(a.spendPubkey, decoded.B_spend) !== 0)
244-
throw new TypeError('Spend pubkey mismatch');
245-
if (a.scanPubkey && tools.compare(a.scanPubkey, decoded.B_scan) !== 0)
246-
throw new TypeError('Scan pubkey mismatch');
247-
248-
const HRP = network.bech32 === 'bc' ? 'sp' : 'tsp';
249-
if (!a.address.startsWith(HRP)) {
250-
throw new TypeError('Invalid prefix or Network mismatch');
251-
}
252-
}
253-
if (o.spendPubkey && o.spendPubkey.length !== 33)
254-
throw new TypeError('Invalid spend pubkey length');
255-
if (o.scanPubkey && o.scanPubkey.length !== 33)
256-
throw new TypeError('Invalid scan pubkey length');
257-
}
258-
259-
return Object.assign(o, a);
260-
}
261-
262372
/** Calculate Input hash tweak
263373
* input_hash tweak = H_tag(Inputs, outpoint_L || ser_P(A)) -> reduce mod n
264374
* @param smallestOutpoint - output of (findSmallestOutpoint)

0 commit comments

Comments
 (0)