Split Payments - #128
Open
notTanveer wants to merge 3 commits into
Open
Conversation
notTanveer
force-pushed
the
feat/split-payments-v2
branch
from
July 16, 2026 16:16
93a3b33 to
4913e75
Compare
chaitika
reviewed
Jul 22, 2026
chaitika
reviewed
Jul 22, 2026
chaitika
reviewed
Jul 22, 2026
Comment on lines
+987
to
+1075
| private async createSplitRegularToSPTransaction( | ||
| regularUtxos: CreateTransactionUtxo[], | ||
| targets: CreateTransactionTarget[], | ||
| feeRate: number, | ||
| changeAddress: string, | ||
| sequence: number, | ||
| skipSigning: boolean, | ||
| masterFingerprint: number, | ||
| precalculatedPaymentAmounts?: number[], | ||
| ): Promise<CreateTransactionResult | null> { | ||
| const spAddress = targets[0].address!; | ||
| let { inputs, outputs: rawOutputs } = this.coinselect(regularUtxos, targets, feeRate); | ||
|
|
||
| const obf = this.obfuscateUnnecessaryInputHeuristic(inputs, rawOutputs, regularUtxos, feeRate); | ||
| inputs = obf.inputs; | ||
| rawOutputs = obf.rawOutputs; | ||
|
|
||
| const changeValue = rawOutputs.find(o => !o.address)?.value ?? 0; | ||
| if (changeValue <= 0) return null; | ||
|
|
||
| const planned = await this.planSplitTransaction( | ||
| spAddress, | ||
| targets[0].value!, | ||
| changeValue, | ||
| feeRate, | ||
| rawOutputs.length, | ||
| precalculatedPaymentAmounts, | ||
| ); | ||
| const paymentCount = planned.outputs.filter(o => o.address === spAddress).length; | ||
| if (paymentCount < 2) return null; // planner declined to split; let the caller fall back | ||
|
|
||
| const inputWifs = inputs.map(input => ({ | ||
| txid: input.txid, | ||
| vout: input.vout, | ||
| wif: this._getWifForAddress(String(input.address)), | ||
| })); | ||
| const outputs = this.resolveSPOutputs(inputWifs, planned.outputs); | ||
|
|
||
| let masterFingerprintBuffer: Buffer; | ||
| if (masterFingerprint) { | ||
| let masterFingerprintHex = Number(masterFingerprint).toString(16); | ||
| if (masterFingerprintHex.length < 8) masterFingerprintHex = '0' + masterFingerprintHex; | ||
| masterFingerprintBuffer = Buffer.from(Buffer.from(masterFingerprintHex, 'hex')).reverse(); | ||
| } else { | ||
| masterFingerprintBuffer = Buffer.from([0x00, 0x00, 0x00, 0x00]); | ||
| } | ||
|
|
||
| let psbt = new bitcoin.Psbt(); | ||
| inputs.forEach(input => { | ||
| psbt = this._addPsbtInput(psbt, input, sequence, masterFingerprintBuffer); | ||
| }); | ||
| outputs.forEach(output => { | ||
| psbt.addOutput({ | ||
| address: output.address || changeAddress, | ||
| value: BigInt(output.value), | ||
| }); | ||
| }); | ||
|
|
||
| let tx: bitcoin.Transaction | undefined; | ||
| if (!skipSigning) { | ||
| inputs.forEach((input, idx) => { | ||
| const keyPair = ECPair.fromWIF(this._getWifForAddress(String(input.address))); | ||
| const tapInternalKey = psbt.data.inputs[idx].tapInternalKey as Uint8Array; | ||
| psbt.signTaprootInput(idx, keyPair.tweak(bitcoin.crypto.taggedHash('TapTweak', tapInternalKey))); | ||
| }); | ||
| psbt.finalizeAllInputs(); | ||
| tx = psbt.extractTransaction(); | ||
| } | ||
|
|
||
| const totalIn = inputs.reduce((sum, i) => sum + i.value, 0); | ||
| const totalOut = outputs.reduce((sum, o) => sum + o.value, 0); | ||
|
|
||
| return { | ||
| tx, | ||
| psbt, | ||
| inputs: inputs.map(i => ({ | ||
| txid: i.txid, | ||
| vout: i.vout, | ||
| address: i.address, | ||
| value: i.value, | ||
| })), | ||
| outputs: outputs.map(o => ({ | ||
| address: o.address || changeAddress, | ||
| value: o.value, | ||
| })), | ||
| fee: totalIn - totalOut, | ||
| changeAddresses: planned.changeAddresses, | ||
| }; | ||
| } |
Contributor
There was a problem hiding this comment.
This looks like it duplicates most of the PSBT build/sign logic from AbstractHDElectrumWallet.createTransaction
was there a reason it couldn't reuse it? or can we extract it?
I noticed the copy also skips tapBip32Derivation/tapInternalKey metadata the parent adds to change outputs, wonder why is that?
Contributor
Author
There was a problem hiding this comment.
can't really do that.. the split pipeline requires atomic interleaving of SP derivation with multi-output planning that the parent doesn't support.
tapBip32Derivation is used for hardware wallet export. which we don't currently support
chaitika
reviewed
Jul 22, 2026
chaitika
reviewed
Jul 22, 2026
chaitika
reviewed
Jul 22, 2026
chaitika
reviewed
Jul 22, 2026
chaitika
reviewed
Jul 22, 2026
notTanveer
force-pushed
the
feat/split-payments-v2
branch
5 times, most recently
from
July 23, 2026 12:14
fc5a315 to
371f93d
Compare
chaitika
reviewed
Jul 24, 2026
chaitika
reviewed
Jul 24, 2026
chaitika
reviewed
Jul 24, 2026
chaitika
reviewed
Jul 24, 2026
chaitika
reviewed
Jul 24, 2026
chaitika
reviewed
Jul 24, 2026
chaitika
reviewed
Jul 24, 2026
notTanveer
force-pushed
the
feat/split-payments-v2
branch
from
July 24, 2026 13:40
371f93d to
ceefb03
Compare
Contributor
|
ack ceefb03 |
notTanveer
force-pushed
the
feat/split-payments-v2
branch
from
July 31, 2026 12:24
ceefb03 to
2239f41
Compare
notTanveer
force-pushed
the
feat/split-payments-v2
branch
from
July 31, 2026 12:26
2239f41 to
ae2c2d2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds Split Payments for Silent Payment wallets to level up privacy.
Instead of sending everything in a single output, the wallet splits the payment into two outputs. That makes amount based heuristics way less effective and makes it much harder for chain surveillance to correlate the sender and recipient based on the payment amount.
The end result is better on-chain privacy without changing the user experience.
check out the test txn here.
Screenshots