diff --git a/src/index.ts b/src/index.ts index 50e0793..23fae51 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ export type Target = { export type SilentPaymentGroup = { Bscan: Buffer; - BmValues: Array<[Buffer, number | undefined]>; + BmValues: Array<[Buffer, number | undefined, number]>; }; function taggedHash(tag: string, data: Buffer): Buffer { @@ -45,12 +45,13 @@ export class SilentPayment { * Numeric values (if present) for targets are passed through. */ createTransaction(utxos: UTXO[], targets: Target[]): Target[] { - const ret: Target[] = []; + const ret: Target[] = new Array(targets.length); const silentPaymentGroups: Array = []; - for (const target of targets) { + for (let i = 0; i < targets.length; i++) { + const target = targets[i]; if (!target.address?.startsWith("sp1")) { - ret.push(target); // passthrough + ret[i] = target; // passthrough continue; } @@ -66,11 +67,11 @@ export class SilentPayment { // Addresses with the same Bscan key all belong to the same recipient const recipient = silentPaymentGroups.find((group) => Buffer.compare(group.Bscan, Bscan) === 0); if (recipient) { - recipient.BmValues.push([Bm, target.value]); + recipient.BmValues.push([Bm, target.value, i]); } else { silentPaymentGroups.push({ Bscan: Bscan, - BmValues: [[Bm, target.value]], + BmValues: [[Bm, target.value, i]], }); } } @@ -84,10 +85,10 @@ export class SilentPayment { for (const group of silentPaymentGroups) { // Bscan * a * outpoint_hash const ecdh_shared_secret_step1 = Buffer.from(ecc.privateMultiply(outpoint_hash, a) as Uint8Array); - const ecdh_shared_secret = ecc.pointMultiply(group.Bscan, ecdh_shared_secret_step1); + const ecdh_shared_secret = Buffer.from(ecc.getSharedSecret(ecdh_shared_secret_step1, group.Bscan) as Uint8Array); let k = 0; - for (const [Bm, amount] of group.BmValues) { + for (const [Bm, amount, i] of group.BmValues) { const tk = taggedHash("BIP0352/SharedSecret", Buffer.concat([ecdh_shared_secret!, SilentPayment._ser32(k)])); // Let Pmk = tk·G + Bm @@ -97,7 +98,7 @@ export class SilentPayment { const address = SilentPayment.pubkeyToAddress(Pmk.slice(1).toString("hex")); const newTarget: Target = { address }; newTarget.value = amount; - ret.push(newTarget); + ret[i] = newTarget; k += 1; } } diff --git a/src/noble_ecc.ts b/src/noble_ecc.ts index 878351c..f1b30b9 100644 --- a/src/noble_ecc.ts +++ b/src/noble_ecc.ts @@ -58,6 +58,10 @@ const ecc = { return { parity, xOnlyPubkey: P.slice(1) }; }), + getSharedSecret: (sk: Uint8Array, pk: Uint8Array, compressed?: boolean): Uint8Array => { + return necc.getSharedSecret(sk, pk, defaultTrue(compressed)); + }, + pointFromScalar: (sk: Uint8Array, compressed?: boolean): Uint8Array | null => throwToNull(() => necc.getPublicKey(sk, defaultTrue(compressed))), pointCompress: (p: Uint8Array, compressed?: boolean): Uint8Array => { diff --git a/tests/silent-payment.test.ts b/tests/silent-payment.test.ts index 27a0336..8a28c68 100644 --- a/tests/silent-payment.test.ts +++ b/tests/silent-payment.test.ts @@ -118,6 +118,56 @@ it("2 inputs - 0 SP outputs (just a passthrough)", () => { ); }); +it("2 inputs - 1 SP output, 1 legacy, 1change (should not rearrange order of inputs )", () => { + const sp = new SilentPayment(); + assert.deepStrictEqual( + sp.createTransaction( + [ + { + txid: "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16", + vout: 0, + wif: ECPair.fromPrivateKey(Buffer.from("1cd5e8f6b3f29505ed1da7a5806291ebab6491c6a172467e44debe255428a192", "hex")).toWIF(), + utxoType: "p2wpkh", + }, + { + txid: "a1075db55d416d3ca199f55b6084e2115b9345e16c5cf302fc80e9d5fbf5d48d", + vout: 0, + wif: ECPair.fromPrivateKey(Buffer.from("7416ef4d92e4dd09d680af6999d1723816e781c030f4b4ecb5bf46939ca30056", "hex")).toWIF(), + utxoType: "p2wpkh", + }, + ], + [ + { + address: "3FiYaHYHQTmD8n2SJxVYobDeN1uQKvzkLe", + value: 11_111, + }, + { + address: "sp1qqgste7k9hx0qftg6qmwlkqtwuy6cycyavzmzj85c6qdfhjdpdjtdgqjuexzk6murw56suy3e0rd2cgqvycxttddwsvgxe2usfpxumr70xc9pkqwv", + value: 22_222, + }, + { + // no address, which should be interpreted as change + value: 33_333, + }, + ] + ), + [ + { + address: "3FiYaHYHQTmD8n2SJxVYobDeN1uQKvzkLe", + value: 11_111, + }, + { + address: "bc1pszgngkje7t5j3mvdw8xc5l3q7n28awdwl8pena6hrvxgg83lnpmsme6u6j", // unwrapped from SP + value: 22_222, + }, + { + // no address, which should be interpreted as change + value: 33_333, + }, + ] + ); +}); + it("SilentPayment._outpointHash() works", () => { const A = ECPair.fromWIF("L4cJGJp4haLbS46ZKMKrjt7HqVuYTSHkChykdMrni955Fs3Sb8vq").publicKey; assert.deepStrictEqual(