@@ -9,16 +9,185 @@ import { bech32m } from 'bech32';
99import { Payment , PaymentOpts } from './index' ;
1010import * as lazy from './lazy' ;
1111import { taggedHash } from '../crypto' ;
12- import { Input } from '../transaction' ;
12+ import { Input , Output } from '../transaction' ;
1313
1414// --- TYPE DEFINITIONS & UTILITIES ---
1515export 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+ */
1841export 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 */
150319export 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