@btc-vision/transaction 1.6.1 → 1.6.5
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/_version.d.ts +1 -1
- package/browser/epoch/ChallengeSolution.d.ts +3 -3
- package/browser/epoch/validator/EpochValidator.d.ts +5 -6
- package/browser/generators/builders/P2WDAGenerator.d.ts +13 -0
- package/browser/index.js +1 -1
- package/browser/keypair/Address.d.ts +3 -2
- package/browser/keypair/AddressVerificator.d.ts +13 -1
- package/browser/keypair/Wallet.d.ts +3 -0
- package/browser/opnet.d.ts +4 -0
- package/browser/p2wda/P2WDADetector.d.ts +16 -0
- package/browser/transaction/TransactionFactory.d.ts +3 -1
- package/browser/transaction/builders/DeploymentTransaction.d.ts +3 -3
- package/browser/transaction/builders/InteractionTransactionP2WDA.d.ts +37 -0
- package/browser/transaction/builders/SharedInteractionTransaction.d.ts +4 -4
- package/browser/transaction/builders/TransactionBuilder.d.ts +3 -0
- package/browser/transaction/interfaces/ITransactionParameters.d.ts +1 -0
- package/browser/transaction/mineable/IP2WSHAddress.d.ts +4 -0
- package/browser/transaction/mineable/TimelockGenerator.d.ts +2 -5
- package/browser/transaction/shared/TweakedTransaction.d.ts +23 -0
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/epoch/ChallengeSolution.d.ts +3 -3
- package/build/epoch/ChallengeSolution.js +3 -3
- package/build/epoch/validator/EpochValidator.d.ts +5 -6
- package/build/epoch/validator/EpochValidator.js +11 -12
- package/build/generators/builders/P2WDAGenerator.d.ts +13 -0
- package/build/generators/builders/P2WDAGenerator.js +62 -0
- package/build/keypair/Address.d.ts +3 -2
- package/build/keypair/Address.js +28 -2
- package/build/keypair/AddressVerificator.d.ts +13 -1
- package/build/keypair/AddressVerificator.js +82 -1
- package/build/keypair/Wallet.d.ts +3 -0
- package/build/keypair/Wallet.js +4 -0
- package/build/opnet.d.ts +4 -0
- package/build/opnet.js +4 -0
- package/build/p2wda/P2WDADetector.d.ts +16 -0
- package/build/p2wda/P2WDADetector.js +97 -0
- package/build/transaction/TransactionFactory.d.ts +3 -1
- package/build/transaction/TransactionFactory.js +35 -4
- package/build/transaction/builders/DeploymentTransaction.d.ts +3 -3
- package/build/transaction/builders/DeploymentTransaction.js +1 -1
- package/build/transaction/builders/InteractionTransactionP2WDA.d.ts +37 -0
- package/build/transaction/builders/InteractionTransactionP2WDA.js +205 -0
- package/build/transaction/builders/SharedInteractionTransaction.d.ts +4 -4
- package/build/transaction/builders/SharedInteractionTransaction.js +3 -3
- package/build/transaction/builders/TransactionBuilder.d.ts +3 -0
- package/build/transaction/builders/TransactionBuilder.js +18 -3
- package/build/transaction/interfaces/ITransactionParameters.d.ts +1 -0
- package/build/transaction/mineable/IP2WSHAddress.d.ts +4 -0
- package/build/transaction/mineable/IP2WSHAddress.js +1 -0
- package/build/transaction/mineable/TimelockGenerator.d.ts +2 -5
- package/build/transaction/shared/TweakedTransaction.d.ts +23 -0
- package/build/transaction/shared/TweakedTransaction.js +154 -18
- package/doc/README.md +0 -0
- package/doc/addresses/P2OP.md +1 -0
- package/doc/addresses/P2WDA.md +240 -0
- package/package.json +2 -2
- package/src/_version.ts +1 -1
- package/src/epoch/ChallengeSolution.ts +4 -4
- package/src/epoch/validator/EpochValidator.ts +12 -16
- package/src/generators/builders/P2WDAGenerator.ts +174 -0
- package/src/keypair/Address.ts +58 -3
- package/src/keypair/AddressVerificator.ts +147 -2
- package/src/keypair/Wallet.ts +16 -0
- package/src/opnet.ts +4 -0
- package/src/p2wda/P2WDADetector.ts +218 -0
- package/src/transaction/TransactionFactory.ts +79 -5
- package/src/transaction/builders/DeploymentTransaction.ts +4 -3
- package/src/transaction/builders/InteractionTransactionP2WDA.ts +376 -0
- package/src/transaction/builders/SharedInteractionTransaction.ts +7 -6
- package/src/transaction/builders/TransactionBuilder.ts +30 -3
- package/src/transaction/interfaces/ITransactionParameters.ts +1 -0
- package/src/transaction/mineable/IP2WSHAddress.ts +4 -0
- package/src/transaction/mineable/TimelockGenerator.ts +2 -6
- package/src/transaction/shared/TweakedTransaction.ts +246 -23
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { Buffer } from 'buffer';
|
|
2
|
+
import { Psbt, PsbtInput, toXOnly } from '@btc-vision/bitcoin';
|
|
3
|
+
import { TransactionType } from '../enums/TransactionType.js';
|
|
4
|
+
import { IInteractionParameters } from '../interfaces/ITransactionParameters.js';
|
|
5
|
+
import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder, } from './TransactionBuilder.js';
|
|
6
|
+
import { MessageSigner } from '../../keypair/MessageSigner.js';
|
|
7
|
+
import { Compressor } from '../../bytecode/Compressor.js';
|
|
8
|
+
import { P2WDAGenerator } from '../../generators/builders/P2WDAGenerator.js';
|
|
9
|
+
import { Feature, Features } from '../../generators/Features.js';
|
|
10
|
+
import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
|
|
11
|
+
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
|
|
12
|
+
import { ChallengeSolution } from '../../epoch/ChallengeSolution.js';
|
|
13
|
+
import { ECPairInterface } from 'ecpair';
|
|
14
|
+
import { P2WDADetector } from '../../p2wda/P2WDADetector.js';
|
|
15
|
+
import { IP2WSHAddress } from '../mineable/IP2WSHAddress.js';
|
|
16
|
+
import { TimeLockGenerator } from '../mineable/TimelockGenerator.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* P2WDA Interaction Transaction
|
|
20
|
+
*
|
|
21
|
+
* This transaction type uses the exact same operation data as regular interactions
|
|
22
|
+
* (via CalldataGenerator), but embeds it in the witness field instead of a taproot script.
|
|
23
|
+
* This achieves 75% cost reduction through the witness discount.
|
|
24
|
+
*/
|
|
25
|
+
export class InteractionTransactionP2WDA extends TransactionBuilder<TransactionType.INTERACTION> {
|
|
26
|
+
private static readonly MAX_WITNESS_FIELDS = 10;
|
|
27
|
+
private static readonly MAX_BYTES_PER_WITNESS = 80;
|
|
28
|
+
|
|
29
|
+
public readonly type: TransactionType.INTERACTION = TransactionType.INTERACTION;
|
|
30
|
+
protected readonly epochChallenge: IP2WSHAddress;
|
|
31
|
+
/**
|
|
32
|
+
* Disable auto refund
|
|
33
|
+
* @protected
|
|
34
|
+
*/
|
|
35
|
+
protected readonly disableAutoRefund: boolean;
|
|
36
|
+
private readonly contractAddress: string;
|
|
37
|
+
private readonly contractSecret: Buffer;
|
|
38
|
+
private readonly calldata: Buffer;
|
|
39
|
+
private readonly challenge: ChallengeSolution;
|
|
40
|
+
private readonly randomBytes: Buffer;
|
|
41
|
+
private p2wdaGenerator: P2WDAGenerator;
|
|
42
|
+
private scriptSigner: ECPairInterface;
|
|
43
|
+
private p2wdaInputIndices: Set<number> = new Set();
|
|
44
|
+
/**
|
|
45
|
+
* The compiled operation data from CalldataGenerator
|
|
46
|
+
* This is exactly what would go in a taproot script, but we put it in witness instead
|
|
47
|
+
*/
|
|
48
|
+
private readonly compiledOperationData: Buffer | null = null;
|
|
49
|
+
|
|
50
|
+
public constructor(parameters: IInteractionParameters) {
|
|
51
|
+
super(parameters);
|
|
52
|
+
|
|
53
|
+
if (!parameters.to) {
|
|
54
|
+
throw new Error('Contract address (to) is required');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!parameters.contract) {
|
|
58
|
+
throw new Error('Contract secret is required');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!parameters.calldata) {
|
|
62
|
+
throw new Error('Calldata is required');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!parameters.challenge) {
|
|
66
|
+
throw new Error('Challenge solution is required');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
this.disableAutoRefund = parameters.disableAutoRefund || false;
|
|
70
|
+
this.contractAddress = parameters.to;
|
|
71
|
+
this.contractSecret = Buffer.from(parameters.contract.replace('0x', ''), 'hex');
|
|
72
|
+
this.calldata = Compressor.compress(parameters.calldata);
|
|
73
|
+
this.challenge = parameters.challenge;
|
|
74
|
+
this.randomBytes = parameters.randomBytes || BitcoinUtils.rndBytes();
|
|
75
|
+
|
|
76
|
+
// Create the script signer (same as SharedInteractionTransaction does)
|
|
77
|
+
this.scriptSigner = this.generateKeyPairFromSeed();
|
|
78
|
+
|
|
79
|
+
// Create the P2WDA generator instead of CalldataGenerator
|
|
80
|
+
// P2WDA needs a different data format optimized for witness embedding
|
|
81
|
+
this.p2wdaGenerator = new P2WDAGenerator(
|
|
82
|
+
Buffer.from(this.signer.publicKey),
|
|
83
|
+
this.scriptSignerXOnlyPubKey(),
|
|
84
|
+
this.network,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
// Validate contract secret
|
|
88
|
+
if (this.contractSecret.length !== 32) {
|
|
89
|
+
throw new Error('Invalid contract secret length. Expected 32 bytes.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.epochChallenge = TimeLockGenerator.generateTimeLockAddress(
|
|
93
|
+
this.challenge.publicKey.originalPublicKeyBuffer(),
|
|
94
|
+
this.network,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Validate P2WDA inputs
|
|
98
|
+
this.validateP2WDAInputs();
|
|
99
|
+
|
|
100
|
+
this.compiledOperationData = this.p2wdaGenerator.compile(
|
|
101
|
+
this.calldata,
|
|
102
|
+
this.contractSecret,
|
|
103
|
+
this.challenge,
|
|
104
|
+
this.priorityFee,
|
|
105
|
+
this.generateFeatures(parameters),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Validate size early
|
|
109
|
+
this.validateOperationDataSize();
|
|
110
|
+
|
|
111
|
+
this.internalInit();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get random bytes (for compatibility if needed elsewhere)
|
|
116
|
+
*/
|
|
117
|
+
public getRndBytes(): Buffer {
|
|
118
|
+
return this.randomBytes;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get the challenge (for compatibility if needed elsewhere)
|
|
123
|
+
*/
|
|
124
|
+
public getChallenge(): ChallengeSolution {
|
|
125
|
+
return this.challenge;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get contract secret (for compatibility if needed elsewhere)
|
|
130
|
+
*/
|
|
131
|
+
public getContractSecret(): Buffer {
|
|
132
|
+
return this.contractSecret;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the transaction
|
|
137
|
+
*/
|
|
138
|
+
protected async buildTransaction(): Promise<void> {
|
|
139
|
+
if (!this.regenerated) {
|
|
140
|
+
this.addInputsFromUTXO();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Add refund
|
|
144
|
+
await this.createMineableRewardOutputs();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
protected async createMineableRewardOutputs(): Promise<void> {
|
|
148
|
+
if (!this.to) throw new Error('To address is required');
|
|
149
|
+
|
|
150
|
+
const amountSpent: bigint = this.getTransactionOPNetFee();
|
|
151
|
+
|
|
152
|
+
let amountToCA: bigint;
|
|
153
|
+
if (amountSpent > MINIMUM_AMOUNT_REWARD + MINIMUM_AMOUNT_CA) {
|
|
154
|
+
amountToCA = MINIMUM_AMOUNT_CA;
|
|
155
|
+
} else {
|
|
156
|
+
amountToCA = amountSpent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ALWAYS THE FIRST INPUT.
|
|
160
|
+
this.addOutput({
|
|
161
|
+
value: Number(amountToCA),
|
|
162
|
+
address: this.to,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ALWAYS SECOND.
|
|
166
|
+
if (
|
|
167
|
+
amountToCA === MINIMUM_AMOUNT_CA &&
|
|
168
|
+
amountSpent - MINIMUM_AMOUNT_CA > MINIMUM_AMOUNT_REWARD
|
|
169
|
+
) {
|
|
170
|
+
this.addOutput({
|
|
171
|
+
value: Number(amountSpent - amountToCA),
|
|
172
|
+
address: this.epochChallenge.address,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const amount = this.addOptionalOutputsAndGetAmount();
|
|
177
|
+
if (!this.disableAutoRefund) {
|
|
178
|
+
await this.addRefundOutput(amountSpent + amount);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Sign inputs with P2WDA-specific handling
|
|
184
|
+
*/
|
|
185
|
+
protected override async signInputs(transaction: Psbt): Promise<void> {
|
|
186
|
+
// Sign all inputs
|
|
187
|
+
for (let i = 0; i < transaction.data.inputs.length; i++) {
|
|
188
|
+
await this.signInput(transaction, transaction.data.inputs[i], i, this.signer);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Finalize with appropriate finalizers
|
|
192
|
+
for (let i = 0; i < transaction.data.inputs.length; i++) {
|
|
193
|
+
if (this.p2wdaInputIndices.has(i)) {
|
|
194
|
+
if (i === 0) {
|
|
195
|
+
transaction.finalizeInput(i, this.finalizePrimaryP2WDA.bind(this));
|
|
196
|
+
} else {
|
|
197
|
+
transaction.finalizeInput(i, this.finalizeSecondaryP2WDA.bind(this));
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
transaction.finalizeInput(i, this.customFinalizerP2SH.bind(this));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.finalized = true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Generate features array (same as InteractionTransaction)
|
|
209
|
+
*/
|
|
210
|
+
private generateFeatures(parameters: IInteractionParameters): Feature<Features>[] {
|
|
211
|
+
const features: Feature<Features>[] = [];
|
|
212
|
+
|
|
213
|
+
if (parameters.loadedStorage) {
|
|
214
|
+
features.push({
|
|
215
|
+
opcode: Features.ACCESS_LIST,
|
|
216
|
+
data: parameters.loadedStorage,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const submission = parameters.challenge.getSubmission();
|
|
221
|
+
if (submission) {
|
|
222
|
+
features.push({
|
|
223
|
+
opcode: Features.EPOCH_SUBMISSION,
|
|
224
|
+
data: submission,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return features;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generate keypair from seed (same as SharedInteractionTransaction)
|
|
233
|
+
*/
|
|
234
|
+
private generateKeyPairFromSeed(): ECPairInterface {
|
|
235
|
+
return EcKeyPair.fromSeedKeyPair(this.randomBytes, this.network);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get script signer x-only pubkey (same as SharedInteractionTransaction)
|
|
240
|
+
*/
|
|
241
|
+
private scriptSignerXOnlyPubKey(): Buffer {
|
|
242
|
+
return toXOnly(Buffer.from(this.scriptSigner.publicKey));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate that input 0 is P2WDA
|
|
247
|
+
*/
|
|
248
|
+
private validateP2WDAInputs(): void {
|
|
249
|
+
if (this.utxos.length === 0 || !P2WDADetector.isP2WDAUTXO(this.utxos[0])) {
|
|
250
|
+
throw new Error('Input 0 must be a P2WDA UTXO');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Track all P2WDA inputs
|
|
254
|
+
for (let i = 0; i < this.utxos.length; i++) {
|
|
255
|
+
if (P2WDADetector.isP2WDAUTXO(this.utxos[i])) {
|
|
256
|
+
this.p2wdaInputIndices.add(i);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < this.optionalInputs.length; i++) {
|
|
261
|
+
const actualIndex = this.utxos.length + i;
|
|
262
|
+
if (P2WDADetector.isP2WDAUTXO(this.optionalInputs[i])) {
|
|
263
|
+
this.p2wdaInputIndices.add(actualIndex);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Validate the compiled operation data will fit in witness fields
|
|
270
|
+
*/
|
|
271
|
+
private validateOperationDataSize(): void {
|
|
272
|
+
if (!this.compiledOperationData) {
|
|
273
|
+
throw new Error('Operation data not compiled');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// The data that goes in witness: COMPRESS(signature + compiledOperationData)
|
|
277
|
+
// Signature is 64 bytes
|
|
278
|
+
const estimatedSize = this.compiledOperationData.length;
|
|
279
|
+
|
|
280
|
+
if (!P2WDAGenerator.validateWitnessSize(estimatedSize)) {
|
|
281
|
+
const signatureSize = 64;
|
|
282
|
+
const totalSize = estimatedSize + signatureSize;
|
|
283
|
+
const compressedEstimate = Math.ceil(totalSize * 0.7);
|
|
284
|
+
const requiredFields = Math.ceil(
|
|
285
|
+
compressedEstimate / InteractionTransactionP2WDA.MAX_BYTES_PER_WITNESS,
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
throw new Error(
|
|
289
|
+
`Please dont use P2WDA for this operation. Data too large. Raw size: ${estimatedSize} bytes, ` +
|
|
290
|
+
`estimated compressed: ${compressedEstimate} bytes, ` +
|
|
291
|
+
`needs ${requiredFields} witness fields, max is ${InteractionTransactionP2WDA.MAX_WITNESS_FIELDS}`,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Finalize primary P2WDA input with the operation data
|
|
298
|
+
* This is where we create the signature and compress everything
|
|
299
|
+
*/
|
|
300
|
+
private finalizePrimaryP2WDA(
|
|
301
|
+
inputIndex: number,
|
|
302
|
+
input: PsbtInput,
|
|
303
|
+
): {
|
|
304
|
+
finalScriptSig: Buffer | undefined;
|
|
305
|
+
finalScriptWitness: Buffer | undefined;
|
|
306
|
+
} {
|
|
307
|
+
if (!input.partialSig || input.partialSig.length === 0) {
|
|
308
|
+
throw new Error(`No signature for P2WDA input #${inputIndex}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!input.witnessScript) {
|
|
312
|
+
throw new Error(`No witness script for P2WDA input #${inputIndex}`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!this.compiledOperationData) {
|
|
316
|
+
throw new Error('Operation data not compiled');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const txSignature = input.partialSig[0].signature;
|
|
320
|
+
const messageToSign = Buffer.concat([txSignature, this.compiledOperationData]);
|
|
321
|
+
const signedMessage = MessageSigner.signMessage(
|
|
322
|
+
this.signer as ECPairInterface,
|
|
323
|
+
messageToSign,
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
const schnorrSignature = Buffer.from(signedMessage.signature);
|
|
327
|
+
|
|
328
|
+
// Combine and compress: COMPRESS(signature + compiledOperationData)
|
|
329
|
+
const fullData = Buffer.concat([schnorrSignature, this.compiledOperationData]);
|
|
330
|
+
const compressedData = Compressor.compress(fullData);
|
|
331
|
+
|
|
332
|
+
// Split into chunks
|
|
333
|
+
const chunks = this.splitIntoWitnessChunks(compressedData);
|
|
334
|
+
|
|
335
|
+
if (chunks.length > InteractionTransactionP2WDA.MAX_WITNESS_FIELDS) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Compressed data needs ${chunks.length} witness fields, max is ${InteractionTransactionP2WDA.MAX_WITNESS_FIELDS}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Build witness stack
|
|
342
|
+
const witnessStack: Buffer[] = [txSignature];
|
|
343
|
+
|
|
344
|
+
// Add exactly 10 data fields
|
|
345
|
+
// Bitcoin stack is reversed!
|
|
346
|
+
for (let i = 0; i < InteractionTransactionP2WDA.MAX_WITNESS_FIELDS; i++) {
|
|
347
|
+
witnessStack.push(i < chunks.length ? chunks[i] : Buffer.alloc(0));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
witnessStack.push(input.witnessScript);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
finalScriptSig: undefined,
|
|
354
|
+
finalScriptWitness: TransactionBuilder.witnessStackToScriptWitness(witnessStack),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Split data into 80-byte chunks
|
|
360
|
+
*/
|
|
361
|
+
private splitIntoWitnessChunks(data: Buffer): Buffer[] {
|
|
362
|
+
const chunks: Buffer[] = [];
|
|
363
|
+
let offset = 0;
|
|
364
|
+
|
|
365
|
+
while (offset < data.length) {
|
|
366
|
+
const size = Math.min(
|
|
367
|
+
InteractionTransactionP2WDA.MAX_BYTES_PER_WITNESS,
|
|
368
|
+
data.length - offset,
|
|
369
|
+
);
|
|
370
|
+
chunks.push(data.subarray(offset, offset + size));
|
|
371
|
+
offset += size;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return chunks;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { P2TRPayment, PaymentType, Psbt, PsbtInput, Signer, Taptree, toXOnly } from '@btc-vision/bitcoin';
|
|
1
|
+
import { P2TRPayment, PaymentType, Psbt, PsbtInput, Signer, Taptree, toXOnly, } from '@btc-vision/bitcoin';
|
|
2
2
|
import { ECPairInterface } from 'ecpair';
|
|
3
|
-
import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder } from './TransactionBuilder.js';
|
|
3
|
+
import { MINIMUM_AMOUNT_CA, MINIMUM_AMOUNT_REWARD, TransactionBuilder, } from './TransactionBuilder.js';
|
|
4
4
|
import { TransactionType } from '../enums/TransactionType.js';
|
|
5
5
|
import { CalldataGenerator } from '../../generators/builders/CalldataGenerator.js';
|
|
6
6
|
import { SharedInteractionParameters } from '../interfaces/ITransactionParameters.js';
|
|
@@ -8,8 +8,9 @@ import { Compressor } from '../../bytecode/Compressor.js';
|
|
|
8
8
|
import { EcKeyPair } from '../../keypair/EcKeyPair.js';
|
|
9
9
|
import { BitcoinUtils } from '../../utils/BitcoinUtils.js';
|
|
10
10
|
import { UnisatSigner } from '../browser/extensions/UnisatSigner.js';
|
|
11
|
-
import {
|
|
11
|
+
import { TimeLockGenerator } from '../mineable/TimelockGenerator.js';
|
|
12
12
|
import { ChallengeSolution } from '../../epoch/ChallengeSolution.js';
|
|
13
|
+
import { IP2WSHAddress } from '../mineable/IP2WSHAddress.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Shared interaction transaction
|
|
@@ -33,7 +34,7 @@ export abstract class SharedInteractionTransaction<
|
|
|
33
34
|
protected abstract readonly scriptTree: Taptree;
|
|
34
35
|
|
|
35
36
|
protected readonly challenge: ChallengeSolution;
|
|
36
|
-
protected readonly epochChallenge:
|
|
37
|
+
protected readonly epochChallenge: IP2WSHAddress;
|
|
37
38
|
|
|
38
39
|
protected calldataGenerator: CalldataGenerator;
|
|
39
40
|
|
|
@@ -111,7 +112,7 @@ export abstract class SharedInteractionTransaction<
|
|
|
111
112
|
/**
|
|
112
113
|
* Get the preimage
|
|
113
114
|
*/
|
|
114
|
-
public
|
|
115
|
+
public getChallenge(): ChallengeSolution {
|
|
115
116
|
return this.challenge;
|
|
116
117
|
}
|
|
117
118
|
|
|
@@ -340,7 +341,7 @@ export abstract class SharedInteractionTransaction<
|
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
|
|
344
|
+
protected async createMineableRewardOutputs(): Promise<void> {
|
|
344
345
|
if (!this.to) throw new Error('To address is required');
|
|
345
346
|
|
|
346
347
|
const amountSpent: bigint = this.getTransactionOPNetFee();
|
|
@@ -28,6 +28,7 @@ initEccLib(ecc);
|
|
|
28
28
|
|
|
29
29
|
export const MINIMUM_AMOUNT_REWARD: bigint = 540n;
|
|
30
30
|
export const MINIMUM_AMOUNT_CA: bigint = 297n;
|
|
31
|
+
export const ANCHOR_SCRIPT = Buffer.from('51024e73', 'hex');
|
|
31
32
|
|
|
32
33
|
/**
|
|
33
34
|
* Allows to build a transaction like you would on Ethereum.
|
|
@@ -148,6 +149,12 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
148
149
|
*/
|
|
149
150
|
protected isPubKeyDestination: boolean;
|
|
150
151
|
|
|
152
|
+
/**
|
|
153
|
+
* @description If the transaction need an anchor output
|
|
154
|
+
* @protected
|
|
155
|
+
*/
|
|
156
|
+
protected anchor: boolean;
|
|
157
|
+
|
|
151
158
|
protected note?: Buffer;
|
|
152
159
|
|
|
153
160
|
protected constructor(parameters: ITransactionParameters) {
|
|
@@ -174,6 +181,8 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
174
181
|
}
|
|
175
182
|
}
|
|
176
183
|
|
|
184
|
+
this.anchor = parameters.anchor ?? false;
|
|
185
|
+
|
|
177
186
|
this.isPubKeyDestination = this.to
|
|
178
187
|
? AddressVerificator.isValidPublicKey(this.to, this.network)
|
|
179
188
|
: false;
|
|
@@ -191,6 +200,7 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
191
200
|
|
|
192
201
|
this.transaction = new Psbt({
|
|
193
202
|
network: this.network,
|
|
203
|
+
version: this.txVersion,
|
|
194
204
|
});
|
|
195
205
|
}
|
|
196
206
|
|
|
@@ -247,6 +257,13 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
247
257
|
});
|
|
248
258
|
}
|
|
249
259
|
|
|
260
|
+
public addAnchor(): void {
|
|
261
|
+
this.addOutput({
|
|
262
|
+
value: 0,
|
|
263
|
+
script: ANCHOR_SCRIPT,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
250
267
|
public async getFundingTransactionParameters(): Promise<IFundingTransactionParameters> {
|
|
251
268
|
if (!this.estimatedFees) {
|
|
252
269
|
this.estimatedFees = await this.estimateTransactionFees();
|
|
@@ -400,8 +417,10 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
400
417
|
throw new Error('Output script is too short');
|
|
401
418
|
}
|
|
402
419
|
|
|
403
|
-
if (script.script[0] !== opcodes.OP_RETURN) {
|
|
404
|
-
throw new Error(
|
|
420
|
+
if (script.script[0] !== opcodes.OP_RETURN && !script.script.equals(ANCHOR_SCRIPT)) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
'Output script must start with OP_RETURN or be an ANCHOR when value is 0',
|
|
423
|
+
);
|
|
405
424
|
}
|
|
406
425
|
} else if (output.value < TransactionBuilder.MINIMUM_DUST) {
|
|
407
426
|
throw new Error(
|
|
@@ -462,7 +481,11 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
462
481
|
}
|
|
463
482
|
|
|
464
483
|
public async rebuildFromBase64(base64: string): Promise<Psbt> {
|
|
465
|
-
this.transaction = Psbt.fromBase64(base64, {
|
|
484
|
+
this.transaction = Psbt.fromBase64(base64, {
|
|
485
|
+
network: this.network,
|
|
486
|
+
version: this.txVersion,
|
|
487
|
+
});
|
|
488
|
+
|
|
466
489
|
this.signed = false;
|
|
467
490
|
|
|
468
491
|
this.sighashTypes = [Transaction.SIGHASH_ANYONECANPAY, Transaction.SIGHASH_ALL];
|
|
@@ -517,6 +540,10 @@ export abstract class TransactionBuilder<T extends TransactionType> extends Twea
|
|
|
517
540
|
this.addOPReturn(this.note);
|
|
518
541
|
}
|
|
519
542
|
|
|
543
|
+
if (this.anchor) {
|
|
544
|
+
this.addAnchor();
|
|
545
|
+
}
|
|
546
|
+
|
|
520
547
|
/** Add the refund output */
|
|
521
548
|
const sendBackAmount: bigint = this.totalInputAmount - amountSpent;
|
|
522
549
|
if (sendBackAmount >= TransactionBuilder.MINIMUM_DUST) {
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import bitcoin, { Network, networks, opcodes, script } from '@btc-vision/bitcoin';
|
|
2
|
-
|
|
3
|
-
export interface ITimeLockOutput {
|
|
4
|
-
address: string;
|
|
5
|
-
witnessScript: Buffer;
|
|
6
|
-
}
|
|
2
|
+
import { IP2WSHAddress } from './IP2WSHAddress.js';
|
|
7
3
|
|
|
8
4
|
export class TimeLockGenerator {
|
|
9
5
|
private static readonly CSV_BLOCKS = 75;
|
|
@@ -16,7 +12,7 @@ export class TimeLockGenerator {
|
|
|
16
12
|
publicKey: Buffer,
|
|
17
13
|
network: Network = networks.bitcoin,
|
|
18
14
|
csvBlocks: number = TimeLockGenerator.CSV_BLOCKS,
|
|
19
|
-
):
|
|
15
|
+
): IP2WSHAddress {
|
|
20
16
|
const witnessScript = script.compile([
|
|
21
17
|
script.number.encode(csvBlocks),
|
|
22
18
|
opcodes.OP_CHECKSEQUENCEVERIFY,
|