@btc-vision/transaction 1.8.7-beta.0 → 1.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/btc-vision-bitcoin.js +364 -364
- package/browser/index.js +1022 -903
- package/browser/noble-curves.js +1183 -2156
- package/browser/src/transaction/builders/TransactionBuilder.d.ts +1 -0
- package/browser/src/transaction/offline/OfflineTransactionManager.d.ts +50 -0
- package/browser/src/transaction/offline/interfaces/ISerializableState.d.ts +10 -2
- package/browser/vendors.js +1395 -1395
- package/build/transaction/TransactionFactory.js +1 -1
- package/build/transaction/builders/FundingTransaction.js +17 -13
- package/build/transaction/builders/TransactionBuilder.d.ts +1 -0
- package/build/transaction/builders/TransactionBuilder.js +32 -7
- package/build/transaction/offline/OfflineTransactionManager.d.ts +50 -0
- package/build/transaction/offline/OfflineTransactionManager.js +142 -1
- package/build/transaction/offline/TransactionSerializer.js +9 -0
- package/build/transaction/offline/interfaces/ISerializableState.d.ts +10 -2
- package/build/transaction/offline/interfaces/ISerializableState.js +3 -2
- package/build/transaction/shared/TweakedTransaction.js +27 -5
- package/package.json +1 -1
- package/src/transaction/TransactionFactory.ts +1 -1
- package/src/transaction/builders/FundingTransaction.ts +21 -16
- package/src/transaction/builders/TransactionBuilder.ts +46 -10
- package/src/transaction/offline/OfflineTransactionManager.ts +191 -1
- package/src/transaction/offline/TransactionSerializer.ts +11 -0
- package/src/transaction/offline/interfaces/ISerializableState.ts +10 -2
- package/src/transaction/shared/TweakedTransaction.ts +33 -5
- package/test/add-refund-output.test.ts +96 -11
- package/test/csv-multisig-offline-edges.test.ts +293 -0
- package/test/csv-multisig-offline.test.ts +202 -0
- package/test/transaction-builders.test.ts +69 -3
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fromHex, Psbt, type PsbtInput, type Signer, toHex } from '@btc-vision/bitcoin';
|
|
1
|
+
import { fromHex, Psbt, type PsbtInput, type Signer, toHex, toXOnly } from '@btc-vision/bitcoin';
|
|
2
2
|
import { type UniversalSigner } from '@btc-vision/ecpair';
|
|
3
3
|
import { TransactionType } from '../enums/TransactionType.js';
|
|
4
4
|
import { TransactionBuilder } from '../builders/TransactionBuilder.js';
|
|
@@ -14,6 +14,21 @@ import type {
|
|
|
14
14
|
IInteractionParameters,
|
|
15
15
|
ITransactionParameters,
|
|
16
16
|
} from '../interfaces/ITransactionParameters.js';
|
|
17
|
+
import { CSVMultisigProvider } from '../mineable/CSVMultisigProvider.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Per-input CSV multisig signing status.
|
|
21
|
+
*/
|
|
22
|
+
export interface CSVMultisigInputStatus {
|
|
23
|
+
/** PSBT input index */
|
|
24
|
+
readonly inputIndex: number;
|
|
25
|
+
/** Threshold required to spend this input */
|
|
26
|
+
readonly required: number;
|
|
27
|
+
/** Number of distinct cosigner signatures collected so far */
|
|
28
|
+
readonly collected: number;
|
|
29
|
+
/** Hex-encoded x-only pubkeys of the cosigners that have signed */
|
|
30
|
+
readonly signers: string[];
|
|
31
|
+
}
|
|
17
32
|
|
|
18
33
|
/**
|
|
19
34
|
* Export options for offline transaction signing
|
|
@@ -626,4 +641,179 @@ export class OfflineTransactionManager {
|
|
|
626
641
|
|
|
627
642
|
return TransactionSerializer.toBase64(newState);
|
|
628
643
|
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Inspect collaborative signing progress on all CSV multisig inputs.
|
|
647
|
+
* Returns an empty array when no partial PSBT has been produced yet.
|
|
648
|
+
*/
|
|
649
|
+
public static csvMultisigGetStatus(serializedState: string): CSVMultisigInputStatus[] {
|
|
650
|
+
const state = TransactionSerializer.fromBase64(serializedState);
|
|
651
|
+
if (!state.partialPsbtBase64) return [];
|
|
652
|
+
|
|
653
|
+
const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName);
|
|
654
|
+
const psbt = Psbt.fromBase64(state.partialPsbtBase64, { network });
|
|
655
|
+
return this.computeCSVMultisigStatus(state, psbt, network);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Add the given signer's contribution to every CSV multisig input it can sign.
|
|
660
|
+
* No-op for inputs whose tapscript does not contain the signer's x-only pubkey.
|
|
661
|
+
*
|
|
662
|
+
* First call (no partial PSBT yet) builds the PSBT from the funding params
|
|
663
|
+
* using the provided signer as the builder's signer. Subsequent calls load
|
|
664
|
+
* the carried PSBT and add the signer's tapScriptSig where applicable.
|
|
665
|
+
*/
|
|
666
|
+
public static async addCSVMultisigSignature(
|
|
667
|
+
serializedState: string,
|
|
668
|
+
signer: Signer | UniversalSigner,
|
|
669
|
+
): Promise<{
|
|
670
|
+
state: string;
|
|
671
|
+
final: boolean;
|
|
672
|
+
perInput: CSVMultisigInputStatus[];
|
|
673
|
+
}> {
|
|
674
|
+
const state = TransactionSerializer.fromBase64(serializedState);
|
|
675
|
+
const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName);
|
|
676
|
+
|
|
677
|
+
let psbt: Psbt;
|
|
678
|
+
|
|
679
|
+
if (state.partialPsbtBase64) {
|
|
680
|
+
psbt = Psbt.fromBase64(state.partialPsbtBase64, { network });
|
|
681
|
+
this.addSignerToCSVInputs(psbt, signer, network);
|
|
682
|
+
} else {
|
|
683
|
+
const builder = this.importForSigning(serializedState, { signer });
|
|
684
|
+
psbt = await builder.signPSBT();
|
|
685
|
+
// Best-effort: ensure the signer's contribution is recorded on every
|
|
686
|
+
// CSV input where they're a cosigner (the builder already attempts
|
|
687
|
+
// this, but this guards against builds that bailed early).
|
|
688
|
+
this.addSignerToCSVInputs(psbt, signer, network);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const perInput = this.computeCSVMultisigStatus(state, psbt, network);
|
|
692
|
+
const final = perInput.length > 0 && perInput.every((s) => s.collected >= s.required);
|
|
693
|
+
|
|
694
|
+
const newState: ISerializableTransactionState = {
|
|
695
|
+
...state,
|
|
696
|
+
partialPsbtBase64: psbt.toBase64(),
|
|
697
|
+
};
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
state: TransactionSerializer.toBase64(newState),
|
|
701
|
+
final,
|
|
702
|
+
perInput,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Finalize every CSV multisig input on the partial PSBT and extract the
|
|
708
|
+
* raw transaction hex. Throws if any input is still below its threshold.
|
|
709
|
+
*/
|
|
710
|
+
public static csvMultisigFinalize(serializedState: string): string {
|
|
711
|
+
const state = TransactionSerializer.fromBase64(serializedState);
|
|
712
|
+
if (!state.partialPsbtBase64) {
|
|
713
|
+
throw new Error('No partial PSBT in state — call addCSVMultisigSignature first');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const network = TransactionReconstructor['nameToNetwork'](state.baseParams.networkName);
|
|
717
|
+
const psbt = Psbt.fromBase64(state.partialPsbtBase64, { network });
|
|
718
|
+
|
|
719
|
+
for (let i = 0; i < psbt.data.inputs.length; i++) {
|
|
720
|
+
const input = psbt.data.inputs[i] as PsbtInput;
|
|
721
|
+
if (!input.tapLeafScript || input.tapLeafScript.length === 0) continue;
|
|
722
|
+
|
|
723
|
+
const leaf = input.tapLeafScript[0];
|
|
724
|
+
if (!leaf) continue;
|
|
725
|
+
|
|
726
|
+
const addr = CSVMultisigProvider.deriveAddressFromTapscript(leaf.script, network);
|
|
727
|
+
if (!addr) continue;
|
|
728
|
+
|
|
729
|
+
CSVMultisigProvider.finalizePsbtInput(psbt, i, network, addr);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
return psbt.extractTransaction(true, true).toHex();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Sign every CSV multisig input whose tapscript contains the signer's
|
|
737
|
+
* x-only pubkey and does not yet carry a signature from that pubkey.
|
|
738
|
+
*/
|
|
739
|
+
private static addSignerToCSVInputs(
|
|
740
|
+
psbt: Psbt,
|
|
741
|
+
signer: Signer | UniversalSigner,
|
|
742
|
+
network: import('@btc-vision/bitcoin').Network,
|
|
743
|
+
): void {
|
|
744
|
+
const signerXOnly = toXOnly(signer.publicKey);
|
|
745
|
+
const signerXOnlyHex = toHex(signerXOnly);
|
|
746
|
+
|
|
747
|
+
for (let i = 0; i < psbt.data.inputs.length; i++) {
|
|
748
|
+
const input = psbt.data.inputs[i] as PsbtInput;
|
|
749
|
+
if (!input.tapLeafScript || input.tapLeafScript.length === 0) continue;
|
|
750
|
+
|
|
751
|
+
const leaf = input.tapLeafScript[0];
|
|
752
|
+
if (!leaf) continue;
|
|
753
|
+
|
|
754
|
+
const addr = CSVMultisigProvider.deriveAddressFromTapscript(leaf.script, network);
|
|
755
|
+
if (!addr) continue;
|
|
756
|
+
|
|
757
|
+
const isCosigner = addr.config.pubkeys.some(
|
|
758
|
+
(pk) => toHex(pk as Uint8Array) === signerXOnlyHex,
|
|
759
|
+
);
|
|
760
|
+
if (!isCosigner) continue;
|
|
761
|
+
|
|
762
|
+
const alreadySigned = (input.tapScriptSig ?? []).some(
|
|
763
|
+
(s) => toHex(s.pubkey) === signerXOnlyHex,
|
|
764
|
+
);
|
|
765
|
+
if (alreadySigned) continue;
|
|
766
|
+
|
|
767
|
+
psbt.signTaprootInput(i, signer);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Build per-input status for every CSV multisig input.
|
|
773
|
+
*
|
|
774
|
+
* Walks state.utxos so we can detect CSV multisig inputs even after the
|
|
775
|
+
* builder has already finalized them (which clears tapLeafScript). For
|
|
776
|
+
* partially-signed inputs the signers list reflects collected tapScriptSig
|
|
777
|
+
* entries; for finalized inputs collected is set to the threshold.
|
|
778
|
+
*/
|
|
779
|
+
private static computeCSVMultisigStatus(
|
|
780
|
+
state: ISerializableTransactionState,
|
|
781
|
+
psbt: Psbt,
|
|
782
|
+
network: import('@btc-vision/bitcoin').Network,
|
|
783
|
+
): CSVMultisigInputStatus[] {
|
|
784
|
+
const result: CSVMultisigInputStatus[] = [];
|
|
785
|
+
|
|
786
|
+
for (let i = 0; i < state.utxos.length; i++) {
|
|
787
|
+
const utxo = state.utxos[i];
|
|
788
|
+
if (!utxo?.witnessScript) continue;
|
|
789
|
+
|
|
790
|
+
const tapscript = fromHex(utxo.witnessScript);
|
|
791
|
+
const addr = CSVMultisigProvider.deriveAddressFromTapscript(tapscript, network);
|
|
792
|
+
if (!addr) continue;
|
|
793
|
+
|
|
794
|
+
const input = psbt.data.inputs[i] as PsbtInput | undefined;
|
|
795
|
+
if (!input) continue;
|
|
796
|
+
|
|
797
|
+
if (input.finalScriptWitness) {
|
|
798
|
+
// Already finalized — by construction it carried >= threshold sigs.
|
|
799
|
+
result.push({
|
|
800
|
+
inputIndex: i,
|
|
801
|
+
required: addr.config.threshold,
|
|
802
|
+
collected: addr.config.threshold,
|
|
803
|
+
signers: [],
|
|
804
|
+
});
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const tapSigs = input.tapScriptSig ?? [];
|
|
809
|
+
result.push({
|
|
810
|
+
inputIndex: i,
|
|
811
|
+
required: addr.config.threshold,
|
|
812
|
+
collected: tapSigs.length,
|
|
813
|
+
signers: tapSigs.map((s) => toHex(s.pubkey)),
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
return result;
|
|
818
|
+
}
|
|
629
819
|
}
|
|
@@ -67,6 +67,9 @@ export class TransactionSerializer {
|
|
|
67
67
|
// Write precomputed data
|
|
68
68
|
this.writePrecomputedData(writer, state.precomputedData);
|
|
69
69
|
|
|
70
|
+
// v2: partial PSBT (empty string when absent)
|
|
71
|
+
writer.writeStringWithLength(state.partialPsbtBase64 ?? '');
|
|
72
|
+
|
|
70
73
|
// Get buffer and calculate checksum
|
|
71
74
|
const dataBuffer = writer.getBuffer();
|
|
72
75
|
const checksum = this.calculateChecksum(dataBuffer);
|
|
@@ -128,6 +131,13 @@ export class TransactionSerializer {
|
|
|
128
131
|
// Read precomputed data
|
|
129
132
|
const precomputedData = this.readPrecomputedData(reader);
|
|
130
133
|
|
|
134
|
+
// v2+: partial PSBT (collaborative signing carryover)
|
|
135
|
+
let partialPsbtBase64: string | undefined;
|
|
136
|
+
if (header.formatVersion >= 2) {
|
|
137
|
+
const stored = reader.readStringWithLength();
|
|
138
|
+
partialPsbtBase64 = stored.length > 0 ? stored : undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
131
141
|
return {
|
|
132
142
|
header,
|
|
133
143
|
baseParams,
|
|
@@ -138,6 +148,7 @@ export class TransactionSerializer {
|
|
|
138
148
|
signerMappings,
|
|
139
149
|
typeSpecificData,
|
|
140
150
|
precomputedData,
|
|
151
|
+
...(partialPsbtBase64 !== undefined ? { partialPsbtBase64 } : {}),
|
|
141
152
|
};
|
|
142
153
|
}
|
|
143
154
|
|
|
@@ -3,9 +3,10 @@ import { ChainId } from '../../../network/ChainId.js';
|
|
|
3
3
|
import type { TypeSpecificData } from './ITypeSpecificData.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Format version for serialization compatibility
|
|
6
|
+
* Format version for serialization compatibility.
|
|
7
|
+
* v2 adds the optional `partialPsbtBase64` field on the top-level state.
|
|
7
8
|
*/
|
|
8
|
-
export const SERIALIZATION_FORMAT_VERSION =
|
|
9
|
+
export const SERIALIZATION_FORMAT_VERSION = 2;
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Magic byte for identifying serialized transaction state
|
|
@@ -138,4 +139,11 @@ export interface ISerializableTransactionState {
|
|
|
138
139
|
readonly typeSpecificData: TypeSpecificData;
|
|
139
140
|
/** Pre-computed data for deterministic rebuild */
|
|
140
141
|
readonly precomputedData: PrecomputedData;
|
|
142
|
+
/**
|
|
143
|
+
* Partial PSBT (base64) carrying signatures collected so far.
|
|
144
|
+
* Used by collaborative flows like CSV multisig where multiple cosigners
|
|
145
|
+
* accumulate signatures across hops before finalization.
|
|
146
|
+
* Only present once at least one signing hop has run.
|
|
147
|
+
*/
|
|
148
|
+
readonly partialPsbtBase64?: string;
|
|
141
149
|
}
|
|
@@ -769,11 +769,27 @@ export abstract class TweakedTransaction extends Logger implements Disposable {
|
|
|
769
769
|
}
|
|
770
770
|
}
|
|
771
771
|
|
|
772
|
+
let allFinalized = true;
|
|
772
773
|
for (let i = 0; i < transaction.data.inputs.length; i++) {
|
|
773
|
-
|
|
774
|
+
try {
|
|
775
|
+
transaction.finalizeInput(
|
|
776
|
+
i,
|
|
777
|
+
this.customFinalizerP2SH.bind(this) as FinalScriptsFunc,
|
|
778
|
+
);
|
|
779
|
+
} catch (e) {
|
|
780
|
+
// CSV multisig inputs may legitimately be below threshold during
|
|
781
|
+
// a collaborative signing hop. Leave them unfinalized so the
|
|
782
|
+
// partial PSBT can travel to the next cosigner.
|
|
783
|
+
if (this.csvMultisigInputs.has(i)) {
|
|
784
|
+
allFinalized = false;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
throw e;
|
|
789
|
+
}
|
|
774
790
|
}
|
|
775
791
|
|
|
776
|
-
this.finalized =
|
|
792
|
+
this.finalized = allFinalized;
|
|
777
793
|
}
|
|
778
794
|
|
|
779
795
|
/**
|
|
@@ -1509,12 +1525,24 @@ export abstract class TweakedTransaction extends Logger implements Disposable {
|
|
|
1509
1525
|
// then, we sign all the remaining inputs with the wallet signer.
|
|
1510
1526
|
await signer.multiSignPsbt([transaction]);
|
|
1511
1527
|
|
|
1512
|
-
|
|
1528
|
+
let allFinalized = true;
|
|
1513
1529
|
for (let i = 0; i < transaction.data.inputs.length; i++) {
|
|
1514
|
-
|
|
1530
|
+
try {
|
|
1531
|
+
transaction.finalizeInput(
|
|
1532
|
+
i,
|
|
1533
|
+
this.customFinalizerP2SH.bind(this) as FinalScriptsFunc,
|
|
1534
|
+
);
|
|
1535
|
+
} catch (e) {
|
|
1536
|
+
if (this.csvMultisigInputs.has(i)) {
|
|
1537
|
+
allFinalized = false;
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
throw e;
|
|
1542
|
+
}
|
|
1515
1543
|
}
|
|
1516
1544
|
|
|
1517
|
-
this.finalized =
|
|
1545
|
+
this.finalized = allFinalized;
|
|
1518
1546
|
}
|
|
1519
1547
|
|
|
1520
1548
|
protected isCSVScript(decompiled: (number | Uint8Array)[]): boolean {
|
|
@@ -52,6 +52,7 @@ describe('addRefundOutput , deterministic fee estimation', () => {
|
|
|
52
52
|
feeRate?: number;
|
|
53
53
|
feeUtxos?: UTXO[];
|
|
54
54
|
autoAdjustAmount?: boolean;
|
|
55
|
+
debugFees?: boolean;
|
|
55
56
|
}) {
|
|
56
57
|
const utxo = createTaprootUtxo(taprootAddress, opts.utxoValue);
|
|
57
58
|
return new FundingTransaction({
|
|
@@ -64,6 +65,7 @@ describe('addRefundOutput , deterministic fee estimation', () => {
|
|
|
64
65
|
priorityFee: 0n,
|
|
65
66
|
gasSatFee: 0n,
|
|
66
67
|
mldsaSigner: null,
|
|
68
|
+
debugFees: !!opts.debugFees,
|
|
67
69
|
...(opts.feeUtxos !== undefined && { feeUtxos: opts.feeUtxos }),
|
|
68
70
|
...(opts.autoAdjustAmount !== undefined && { autoAdjustAmount: opts.autoAdjustAmount }),
|
|
69
71
|
});
|
|
@@ -204,15 +206,38 @@ describe('addRefundOutput , deterministic fee estimation', () => {
|
|
|
204
206
|
});
|
|
205
207
|
});
|
|
206
208
|
|
|
209
|
+
// ================================================================
|
|
210
|
+
// Fee underpayment guard: leftover absorbed into fee must still meet feeRate
|
|
211
|
+
// ================================================================
|
|
212
|
+
describe('fee underpayment guard', () => {
|
|
213
|
+
it('should throw when leftover would underpay feeRate (200 sats at feeRate=10)', async () => {
|
|
214
|
+
const utxoValue = 100_000n;
|
|
215
|
+
const amount = utxoValue - 200n;
|
|
216
|
+
|
|
217
|
+
const tx = buildFunding({ utxoValue, amount, feeRate: 10 });
|
|
218
|
+
|
|
219
|
+
await expect(tx.signTransaction()).rejects.toThrow(/Insufficient funds for fee/);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should throw when leftover is a token amount (1 sat) far below feeRate', async () => {
|
|
223
|
+
const utxoValue = 100_000n;
|
|
224
|
+
const amount = utxoValue - 1n;
|
|
225
|
+
|
|
226
|
+
const tx = buildFunding({ utxoValue, amount, feeRate: 5 });
|
|
227
|
+
|
|
228
|
+
await expect(tx.signTransaction()).rejects.toThrow(/Insufficient funds for fee/);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
207
232
|
// ================================================================
|
|
208
233
|
// Tolerance: sendBack < 0 but totalInput > amountSpent
|
|
209
234
|
// ================================================================
|
|
210
|
-
describe('tolerance , effective fee is positive but less than estimated', () => {
|
|
211
|
-
it('should succeed when amount is close to totalInput
|
|
235
|
+
describe('tolerance , effective fee is positive but is a little bit less than estimated UTXO available', () => {
|
|
236
|
+
it('should succeed when amount+fees is close to totalInput', async () => {
|
|
212
237
|
const utxoValue = 100_000n;
|
|
213
238
|
// Leave 200 sats for fees , less than the estimated fee at high rate
|
|
214
239
|
// but totalInput > amountSpent so it's valid
|
|
215
|
-
const amount = utxoValue -
|
|
240
|
+
const amount = utxoValue - 1109n;
|
|
216
241
|
|
|
217
242
|
const tx = buildFunding({ utxoValue, amount, feeRate: 10 });
|
|
218
243
|
|
|
@@ -223,20 +248,80 @@ describe('addRefundOutput , deterministic fee estimation', () => {
|
|
|
223
248
|
|
|
224
249
|
// Verify fee is exactly the 200 sats leftover
|
|
225
250
|
const totalOut = signed.outs.reduce((sum, o) => sum + BigInt(o.value), 0n);
|
|
226
|
-
expect(utxoValue - totalOut).toBe(
|
|
251
|
+
expect(utxoValue - totalOut).toBe(1109n);
|
|
252
|
+
expect(tx.transactionFee).toBe(1109n);
|
|
227
253
|
});
|
|
228
254
|
|
|
229
|
-
|
|
255
|
+
// Const real fees = 555n, dust = 330n
|
|
256
|
+
const shouldSucceed: [
|
|
257
|
+
delta: bigint,
|
|
258
|
+
succeed: boolean,
|
|
259
|
+
overflow: bigint,
|
|
260
|
+
message: string,
|
|
261
|
+
][] = [
|
|
262
|
+
[0n, false, 0n, 'amount equal utxoValue'],
|
|
263
|
+
[1n, false, 0n, 'amount equal utxoValue less 1'],
|
|
264
|
+
|
|
265
|
+
// Check fees bondary
|
|
266
|
+
[553n, false, 0n, 'amount equal utxoValue less 553 (real fees - 2)'],
|
|
267
|
+
[554n, true, 0n, 'amount equal utxoValue less 554 (real fees - 1)'],
|
|
268
|
+
[555n, true, 0n, 'amount equal utxoValue less 555 (real fees)'],
|
|
269
|
+
[556n, true, 0n, 'amount equal utxoValue less 556 (real fees + 1)'],
|
|
270
|
+
|
|
271
|
+
// Check fees+dust bondaries
|
|
272
|
+
[555n + 329n, true, 0n, 'amount equal utxoValue less 884 (real fees + dust - 1)'],
|
|
273
|
+
[555n + 330n, true, 0n, 'amount equal utxoValue less 885 (real fees + dust)'],
|
|
274
|
+
[555n + 331n, true, 0n, 'amount equal utxoValue less 886 (real fees + dust + 1)'],
|
|
275
|
+
|
|
276
|
+
// If there is really a refund, a refund output section will be added
|
|
277
|
+
// to the tx, with a vbsize of 43 vbytes * 5 feeRate --> 215...
|
|
278
|
+
// Check the new fees+refund_section+dust boundaries
|
|
279
|
+
[
|
|
280
|
+
555n + 43n * 5n + 329n,
|
|
281
|
+
true,
|
|
282
|
+
0n,
|
|
283
|
+
'amount equal utxoValue less 1109 (real fees + refund + dust - 1)',
|
|
284
|
+
],
|
|
285
|
+
[
|
|
286
|
+
555n + 43n * 5n + 330n,
|
|
287
|
+
true,
|
|
288
|
+
330n,
|
|
289
|
+
'amount equal utxoValue less 1110 (real fees + refund + dust)',
|
|
290
|
+
],
|
|
291
|
+
[
|
|
292
|
+
555n + 43n * 5n + 331n,
|
|
293
|
+
true,
|
|
294
|
+
331n,
|
|
295
|
+
'amount equal utxoValue less 1111 (real fees + refund + dust + 1)',
|
|
296
|
+
],
|
|
297
|
+
];
|
|
298
|
+
it('should succeed with very small diff from calculated fees (1 sat)', async () => {
|
|
230
299
|
const utxoValue = 100_000n;
|
|
231
|
-
const amount = utxoValue - 1n;
|
|
232
300
|
|
|
233
|
-
|
|
301
|
+
let tx: FundingTransaction | undefined = undefined;
|
|
302
|
+
for (const [delta, shouldSuccess, overflow, message] of shouldSucceed) {
|
|
303
|
+
let succeed = false;
|
|
304
|
+
try {
|
|
305
|
+
const amount = utxoValue - delta;
|
|
234
306
|
|
|
235
|
-
|
|
236
|
-
expect(signed.ins.length).toBeGreaterThan(0);
|
|
307
|
+
tx = buildFunding({ utxoValue, amount, feeRate: 5, debugFees: false });
|
|
237
308
|
|
|
238
|
-
|
|
239
|
-
|
|
309
|
+
const signed = await tx.signTransaction();
|
|
310
|
+
expect(signed.ins.length).toBeGreaterThan(0);
|
|
311
|
+
|
|
312
|
+
const totalOut = signed.outs.reduce((sum, o) => sum + BigInt(o.value), 0n);
|
|
313
|
+
|
|
314
|
+
//console.log('DELTA', utxoValue, totalOut, delta, overflow, tx.estimatedFees, tx.overflowFees,);
|
|
315
|
+
expect(utxoValue - totalOut).toBe(delta - overflow);
|
|
316
|
+
succeed = true;
|
|
317
|
+
} catch (e) {
|
|
318
|
+
//console.log('E', e);
|
|
319
|
+
succeed = false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
expect(succeed, message).toBe(shouldSuccess);
|
|
323
|
+
expect(tx?.overflowFees, message).toBe(overflow);
|
|
324
|
+
}
|
|
240
325
|
});
|
|
241
326
|
});
|
|
242
327
|
|