@btc-vision/transaction 1.1.15 → 1.1.17

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.
Files changed (38) hide show
  1. package/browser/_version.d.ts +1 -1
  2. package/browser/index.js +1 -1
  3. package/browser/keypair/Address.d.ts +2 -0
  4. package/browser/opnet.d.ts +1 -0
  5. package/browser/signer/SignerUtils.d.ts +6 -0
  6. package/browser/transaction/shared/TweakedTransaction.d.ts +5 -6
  7. package/browser/utxo/interfaces/IUTXO.d.ts +1 -0
  8. package/build/_version.d.ts +1 -1
  9. package/build/_version.js +1 -1
  10. package/build/keypair/Address.d.ts +2 -0
  11. package/build/keypair/Address.js +9 -0
  12. package/build/opnet.d.ts +1 -0
  13. package/build/opnet.js +1 -0
  14. package/build/signer/SignerUtils.d.ts +6 -0
  15. package/build/signer/SignerUtils.js +56 -0
  16. package/build/transaction/browser/extensions/UnisatSigner.js +5 -32
  17. package/build/transaction/browser/extensions/XverseSigner.js +5 -48
  18. package/build/transaction/builders/FundingTransaction.js +6 -1
  19. package/build/transaction/builders/TransactionBuilder.js +3 -1
  20. package/build/transaction/shared/TweakedTransaction.d.ts +5 -6
  21. package/build/transaction/shared/TweakedTransaction.js +121 -91
  22. package/build/utils/BitcoinUtils.js +4 -4
  23. package/build/utxo/OPNetLimitedProvider.js +1 -0
  24. package/build/utxo/interfaces/IUTXO.d.ts +1 -0
  25. package/package.json +2 -5
  26. package/src/_version.ts +1 -1
  27. package/src/keypair/Address.ts +15 -0
  28. package/src/opnet.ts +2 -0
  29. package/src/signer/SignerUtils.ts +78 -0
  30. package/src/transaction/TransactionFactory.ts +0 -253
  31. package/src/transaction/browser/extensions/UnisatSigner.ts +4 -40
  32. package/src/transaction/browser/extensions/XverseSigner.ts +9 -68
  33. package/src/transaction/builders/FundingTransaction.ts +7 -2
  34. package/src/transaction/builders/TransactionBuilder.ts +3 -1
  35. package/src/transaction/shared/TweakedTransaction.ts +224 -77
  36. package/src/utils/BitcoinUtils.ts +4 -4
  37. package/src/utxo/OPNetLimitedProvider.ts +1 -0
  38. package/src/utxo/interfaces/IUTXO.ts +2 -0
@@ -2,9 +2,9 @@ import { Logger } from '@btc-vision/logger';
2
2
  import { address as bitAddress, crypto as bitCrypto, getFinalScripts, opcodes, payments, script, } from '@btc-vision/bitcoin';
3
3
  import { TweakedSigner } from '../../signer/TweakedSigner.js';
4
4
  import { toXOnly } from '@btc-vision/bitcoin/src/psbt/bip371.js';
5
- import { AddressTypes, AddressVerificator } from '../../keypair/AddressVerificator.js';
6
5
  import { varuint } from '@btc-vision/bitcoin/src/bufferutils.js';
7
- import * as bscript from '@btc-vision/bitcoin/src/script.js';
6
+ import { canSignNonTaprootInput, isTaprootInput, pubkeyInScript, } from '../../signer/SignerUtils.js';
7
+ import { isP2MS, isP2PK, isP2PKH, isP2SHScript, isP2TR, isP2WPKH, isP2WSHScript, } from '@btc-vision/bitcoin/src/psbt/psbtutils.js';
8
8
  export var TransactionSequence;
9
9
  (function (TransactionSequence) {
10
10
  TransactionSequence[TransactionSequence["REPLACE_BY_FEE"] = 4294967293] = "REPLACE_BY_FEE";
@@ -154,36 +154,42 @@ export class TweakedTransaction extends Logger {
154
154
  getSignerKey() {
155
155
  return this.signer;
156
156
  }
157
- async signInput(transaction, input, i, signer, reverse = false) {
157
+ async signInput(transaction, input, i, signer, reverse = false, errored = false) {
158
158
  const publicKey = signer.publicKey;
159
- let isTaproot = this.isTaprootInput(input);
159
+ let isTaproot = isTaprootInput(input);
160
160
  if (reverse) {
161
161
  isTaproot = !isTaproot;
162
162
  }
163
163
  let signed = false;
164
+ let didError = false;
164
165
  if (isTaproot) {
165
166
  try {
166
167
  await this.attemptSignTaproot(transaction, input, i, signer, publicKey);
167
168
  signed = true;
168
169
  }
169
170
  catch (e) {
170
- this.error(`Failed to sign Taproot script path input ${i}: ${e}`);
171
+ this.error(`Failed to sign Taproot script path input ${i} (reverse: ${reverse}): ${e.message}`);
172
+ didError = true;
171
173
  }
172
174
  }
173
175
  else {
174
- if (!reverse ? this.canSignNonTaprootInput(input, publicKey) : true) {
176
+ if (!reverse ? canSignNonTaprootInput(input, publicKey) : true) {
175
177
  try {
176
178
  await this.signNonTaprootInput(signer, transaction, i);
177
179
  signed = true;
178
180
  }
179
181
  catch (e) {
180
- this.error(`Failed to sign non-Taproot input ${i}: ${e}`);
182
+ this.error(`Failed to sign non-Taproot input ${i}: ${e.stack}`);
183
+ didError = true;
181
184
  }
182
185
  }
183
186
  }
184
187
  if (!signed) {
188
+ if (didError && errored) {
189
+ throw new Error(`Failed to sign input ${i} with the provided signer.`);
190
+ }
185
191
  try {
186
- await this.signInput(transaction, input, i, signer, true);
192
+ await this.signInput(transaction, input, i, signer, true, didError);
187
193
  }
188
194
  catch {
189
195
  throw new Error(`Cannot sign input ${i} with the provided signer.`);
@@ -296,51 +302,127 @@ export class TweakedTransaction extends Logger {
296
302
  }
297
303
  return;
298
304
  }
305
+ generateP2SHP2PKHRedeemScript(inputAddr) {
306
+ const pubkey = Buffer.isBuffer(this.signer.publicKey)
307
+ ? this.signer.publicKey
308
+ : Buffer.from(this.signer.publicKey, 'hex');
309
+ const w = payments.p2wpkh({
310
+ pubkey: pubkey,
311
+ network: this.network,
312
+ });
313
+ const p = payments.p2sh({
314
+ redeem: w,
315
+ network: this.network,
316
+ });
317
+ const address = p.address;
318
+ const redeemScript = p.redeem?.output;
319
+ if (!redeemScript) {
320
+ throw new Error('Failed to generate P2SH-P2WPKH redeem script');
321
+ }
322
+ if (address === inputAddr && p.redeem && p.redeem.output && p.output) {
323
+ return {
324
+ redeemScript: p.redeem.output,
325
+ outputScript: p.output,
326
+ };
327
+ }
328
+ return;
329
+ }
299
330
  generatePsbtInputExtended(utxo, i) {
331
+ const script = Buffer.from(utxo.scriptPubKey.hex, 'hex');
300
332
  const input = {
301
333
  hash: utxo.transactionId,
302
334
  index: utxo.outputIndex,
303
335
  sequence: this.sequence,
304
336
  witnessUtxo: {
305
337
  value: Number(utxo.value),
306
- script: Buffer.from(utxo.scriptPubKey.hex, 'hex'),
338
+ script,
307
339
  },
308
340
  };
309
- if (utxo.scriptPubKey.address) {
310
- try {
311
- const addressType = AddressVerificator.detectAddressType(utxo.scriptPubKey.address, this.network);
312
- if (addressType === AddressTypes.P2SH_OR_P2SH_P2WPKH) {
313
- const redeemScript = this.generateP2SHRedeemScriptLegacy(utxo.scriptPubKey.address);
314
- if (!redeemScript) {
315
- throw new Error('Failed to generate redeem script');
316
- }
317
- input.redeemScript = redeemScript.outputScript;
318
- input.witnessScript = redeemScript.redeemScript;
341
+ if (isP2PKH(script)) {
342
+ if (utxo.nonWitnessUtxo) {
343
+ input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
344
+ ? utxo.nonWitnessUtxo
345
+ : Buffer.from(utxo.nonWitnessUtxo, 'hex');
346
+ }
347
+ else {
348
+ throw new Error('Missing nonWitnessUtxo for P2PKH UTXO');
349
+ }
350
+ }
351
+ else if (isP2WPKH(script)) {
352
+ }
353
+ else if (isP2WSHScript(script)) {
354
+ if (!utxo.witnessScript) {
355
+ throw new Error('Missing witnessScript for P2WSH UTXO');
356
+ }
357
+ input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
358
+ ? utxo.witnessScript
359
+ : Buffer.from(utxo.witnessScript, 'hex');
360
+ }
361
+ else if (isP2SHScript(script)) {
362
+ let redeemScriptBuf;
363
+ if (utxo.redeemScript) {
364
+ redeemScriptBuf = Buffer.isBuffer(utxo.redeemScript)
365
+ ? utxo.redeemScript
366
+ : Buffer.from(utxo.redeemScript, 'hex');
367
+ }
368
+ else {
369
+ if (!utxo.scriptPubKey.address) {
370
+ throw new Error('Missing redeemScript and no address to regenerate it for P2SH UTXO');
371
+ }
372
+ const legacyScripts = this.generateP2SHP2PKHRedeemScript(utxo.scriptPubKey.address);
373
+ if (!legacyScripts) {
374
+ throw new Error('Missing redeemScript for P2SH UTXO and unable to regenerate');
319
375
  }
376
+ redeemScriptBuf = legacyScripts.redeemScript;
320
377
  }
321
- catch (e) {
322
- this.error(`Failed to detect address type for ${utxo.scriptPubKey.address} - ${e}`);
378
+ input.redeemScript = redeemScriptBuf;
379
+ const payment = payments.p2sh({ redeem: { output: input.redeemScript } });
380
+ if (!payment.redeem) {
381
+ throw new Error('Failed to extract redeem script from P2SH UTXO');
382
+ }
383
+ const redeemOutput = payment.redeem.output;
384
+ if (!redeemOutput) {
385
+ throw new Error('Failed to extract redeem output from P2SH UTXO');
386
+ }
387
+ if (utxo.nonWitnessUtxo) {
388
+ input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
389
+ ? utxo.nonWitnessUtxo
390
+ : Buffer.from(utxo.nonWitnessUtxo, 'hex');
391
+ }
392
+ if (isP2WPKH(redeemOutput)) {
393
+ delete input.nonWitnessUtxo;
394
+ }
395
+ else if (isP2WSHScript(redeemOutput)) {
396
+ delete input.nonWitnessUtxo;
397
+ if (!input.witnessScript) {
398
+ throw new Error('Missing witnessScript for P2SH-P2WSH UTXO');
399
+ }
400
+ }
401
+ else {
402
+ delete input.witnessUtxo;
323
403
  }
324
404
  }
325
- if (utxo.nonWitnessUtxo) {
326
- input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
327
- ? utxo.nonWitnessUtxo
328
- : Buffer.from(utxo.nonWitnessUtxo, 'hex');
329
- }
330
- if (utxo.redeemScript) {
331
- input.redeemScript = Buffer.isBuffer(utxo.redeemScript)
332
- ? utxo.redeemScript
333
- : Buffer.from(utxo.redeemScript, 'hex');
334
- if (utxo.witnessScript) {
335
- input.witnessScript = Buffer.isBuffer(utxo.witnessScript)
336
- ? utxo.witnessScript
337
- : Buffer.from(utxo.witnessScript, 'hex');
405
+ else if (isP2TR(script)) {
406
+ if (this.sighashTypes) {
407
+ const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes);
408
+ if (inputSign)
409
+ input.sighashType = inputSign;
410
+ }
411
+ this.tweakSigner();
412
+ input.tapInternalKey = this.internalPubKeyToXOnly();
413
+ }
414
+ else if (isP2PK(script) || isP2MS(script)) {
415
+ if (utxo.nonWitnessUtxo) {
416
+ input.nonWitnessUtxo = Buffer.isBuffer(utxo.nonWitnessUtxo)
417
+ ? utxo.nonWitnessUtxo
418
+ : Buffer.from(utxo.nonWitnessUtxo, 'hex');
419
+ }
420
+ else {
421
+ throw new Error('Missing nonWitnessUtxo for P2PK or P2MS UTXO');
338
422
  }
339
423
  }
340
- if (this.sighashTypes) {
341
- const inputSign = TweakedTransaction.calculateSignHash(this.sighashTypes);
342
- if (inputSign)
343
- input.sighashType = inputSign;
424
+ else {
425
+ this.error(`Unknown or unsupported script type for output: ${utxo.scriptPubKey.hex}`);
344
426
  }
345
427
  if (this.tapLeafScript) {
346
428
  input.tapLeafScript = [this.tapLeafScript];
@@ -348,11 +430,6 @@ export class TweakedTransaction extends Logger {
348
430
  if (i === 0 && this.nonWitnessUtxo) {
349
431
  input.nonWitnessUtxo = this.nonWitnessUtxo;
350
432
  }
351
- if (utxo.scriptPubKey.address &&
352
- AddressVerificator.isValidP2TRAddress(utxo.scriptPubKey.address, this.network)) {
353
- this.tweakSigner();
354
- input.tapInternalKey = this.internalPubKeyToXOnly();
355
- }
356
433
  return input;
357
434
  }
358
435
  async signInputsWalletBased(transaction) {
@@ -389,60 +466,13 @@ export class TweakedTransaction extends Logger {
389
466
  isTaprootScriptSpend(input, publicKey) {
390
467
  if (input.tapLeafScript && input.tapLeafScript.length > 0) {
391
468
  for (const tapLeafScript of input.tapLeafScript) {
392
- if (this.pubkeyInScript(publicKey, tapLeafScript.script)) {
469
+ if (pubkeyInScript(publicKey, tapLeafScript.script)) {
393
470
  return true;
394
471
  }
395
472
  }
396
473
  }
397
474
  return false;
398
475
  }
399
- isTaprootInput(input) {
400
- if (input.tapInternalKey || input.tapKeySig || input.tapScriptSig || input.tapLeafScript) {
401
- return true;
402
- }
403
- if (input.witnessUtxo) {
404
- const script = input.witnessUtxo.script;
405
- return script.length === 34 && script[0] === opcodes.OP_1 && script[1] === 0x20;
406
- }
407
- return false;
408
- }
409
- canSignNonTaprootInput(input, publicKey) {
410
- const script = this.getInputRelevantScript(input);
411
- if (script) {
412
- return this.pubkeyInScript(publicKey, script);
413
- }
414
- return false;
415
- }
416
- getInputRelevantScript(input) {
417
- if (input.redeemScript) {
418
- return input.redeemScript;
419
- }
420
- if (input.witnessScript) {
421
- return input.witnessScript;
422
- }
423
- if (input.witnessUtxo) {
424
- return input.witnessUtxo.script;
425
- }
426
- if (input.nonWitnessUtxo) {
427
- return null;
428
- }
429
- return null;
430
- }
431
- pubkeyInScript(pubkey, script) {
432
- return this.pubkeyPositionInScript(pubkey, script) !== -1;
433
- }
434
- pubkeyPositionInScript(pubkey, script) {
435
- const pubkeyHash = bitCrypto.hash160(pubkey);
436
- const pubkeyXOnly = toXOnly(pubkey);
437
- const decompiled = bscript.decompile(script);
438
- if (decompiled === null)
439
- throw new Error('Unknown script error');
440
- return decompiled.findIndex((element) => {
441
- if (typeof element === 'number')
442
- return false;
443
- return (element.equals(pubkey) || element.equals(pubkeyHash) || element.equals(pubkeyXOnly));
444
- });
445
- }
446
476
  async signTaprootInput(signer, transaction, i, tapLeafHash) {
447
477
  if ('signTaprootInput' in signer) {
448
478
  try {
@@ -1,4 +1,4 @@
1
- import crypto, { createHash } from 'crypto';
1
+ import { createHash } from 'crypto';
2
2
  export class BitcoinUtils {
3
3
  static btcToSatoshi(btc) {
4
4
  return BigInt(btc * 100000000);
@@ -15,13 +15,13 @@ export class BitcoinUtils {
15
15
  window.crypto.getRandomValues(array);
16
16
  return Buffer.from(array);
17
17
  }
18
- else if (crypto && typeof crypto.getRandomValues === 'function') {
18
+ else if (globalThis.crypto && typeof globalThis.crypto.getRandomValues === 'function') {
19
19
  const array = new Uint8Array(length);
20
- crypto.getRandomValues(array);
20
+ globalThis.crypto.getRandomValues(array);
21
21
  return Buffer.from(array);
22
22
  }
23
23
  else {
24
- console.log(`No secure random number generator available. Please upgrade your environment.`, globalThis.window.crypto, crypto);
24
+ console.log(`No secure random number generator available. Please upgrade your environment.`, globalThis.window.crypto, globalThis.crypto);
25
25
  throw new Error('No secure random number generator available. Please upgrade your environment.');
26
26
  }
27
27
  }
@@ -58,6 +58,7 @@ export class OPNetLimitedProvider {
58
58
  outputIndex: utxo.outputIndex,
59
59
  value: utxoValue,
60
60
  scriptPubKey: utxo.scriptPubKey,
61
+ nonWitnessUtxo: Buffer.from(utxo.raw, 'base64'),
61
62
  });
62
63
  if (currentAmount > amountRequested) {
63
64
  break;
@@ -27,4 +27,5 @@ export interface RawUTXOResponse {
27
27
  readonly outputIndex: number;
28
28
  readonly value: string;
29
29
  readonly scriptPubKey: ScriptPubKey;
30
+ readonly raw: string;
30
31
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@btc-vision/transaction",
3
3
  "type": "module",
4
- "version": "1.1.15",
4
+ "version": "1.1.17",
5
5
  "author": "BlobMaster41",
6
6
  "description": "OPNet transaction library allows you to create and sign transactions for the OPNet network.",
7
7
  "engines": {
@@ -63,9 +63,6 @@
63
63
  "browserBuild": "webpack --mode production",
64
64
  "docs": "typedoc --out docs --exclude 'src/tests/*.ts' --tsconfig tsconfig.json --readme README.md --name OPNet --plugin typedoc-material-theme --themeColor '#cb9820' --exclude src/tests/test.ts --exclude src/index.ts src"
65
65
  },
66
- "peerDependencies": {
67
- "@btc-vision/bitcoin": "^6.3.0"
68
- },
69
66
  "devDependencies": {
70
67
  "@babel/core": "^7.26.0",
71
68
  "@babel/plugin-proposal-class-properties": "^7.18.6",
@@ -94,7 +91,7 @@
94
91
  "dependencies": {
95
92
  "@babel/plugin-proposal-object-rest-spread": "^7.20.7",
96
93
  "@bitcoinerlab/secp256k1": "^1.1.1",
97
- "@btc-vision/bitcoin": "^6.3.0",
94
+ "@btc-vision/bitcoin": "^6.3.1",
98
95
  "@btc-vision/bsi-bitcoin-rpc": "^1.0.29",
99
96
  "@btc-vision/logger": "^1.0.6",
100
97
  "@eslint/js": "^9.14.0",
package/src/_version.ts CHANGED
@@ -1 +1 @@
1
- export const version = '1.1.14';
1
+ export const version = '1.1.17';
@@ -104,6 +104,14 @@ export class Address extends Uint8Array {
104
104
  return Buffer.from(this);
105
105
  }
106
106
 
107
+ public originalPublicKeyBuffer(): Buffer {
108
+ if (!this.#originalPublicKey) {
109
+ throw new Error('Public key not set');
110
+ }
111
+
112
+ return Buffer.from(this.#originalPublicKey);
113
+ }
114
+
107
115
  public equals(a: Address): boolean {
108
116
  const b: Address = this as Address;
109
117
 
@@ -200,6 +208,13 @@ export class Address extends Uint8Array {
200
208
  return AddressVerificator.isValidPublicKey(Buffer.from(this).toString('hex'), network);
201
209
  }
202
210
 
211
+ /**
212
+ * Get the public key as address
213
+ */
214
+ public p2pk(): string {
215
+ return this.toHex();
216
+ }
217
+
203
218
  /**
204
219
  * Get the address in p2wpkh format
205
220
  * @param {Network} network The network
package/src/opnet.ts CHANGED
@@ -47,6 +47,7 @@ export * from './transaction/builders/TransactionBuilder.js';
47
47
 
48
48
  /** Utils */
49
49
  export * from './utils/BitcoinUtils.js';
50
+ export * from './utils/lengths.js';
50
51
 
51
52
  /** UTXO */
52
53
  export * from './utxo/interfaces/IUTXO.js';
@@ -89,3 +90,4 @@ export * from './transaction/browser/types/Xverse.js';
89
90
 
90
91
  export * from './metadata/tokens.js';
91
92
  export * from './transaction/browser/Web3Provider.js';
93
+
@@ -0,0 +1,78 @@
1
+ // Helper functions
2
+ import { crypto as bitCrypto, PsbtInput } from '@btc-vision/bitcoin';
3
+ import { isP2TR } from '@btc-vision/bitcoin/src/psbt/psbtutils.js';
4
+ import { toXOnly } from '@btc-vision/bitcoin/src/psbt/bip371.js';
5
+ import * as bscript from '@btc-vision/bitcoin/src/script.js';
6
+
7
+ export function isTaprootInput(input: PsbtInput): boolean {
8
+ return (
9
+ input &&
10
+ !!(
11
+ input.tapInternalKey ||
12
+ input.tapMerkleRoot ||
13
+ (input.tapLeafScript && input.tapLeafScript.length) ||
14
+ (input.tapBip32Derivation && input.tapBip32Derivation.length) ||
15
+ (input.witnessUtxo && isP2TR(input.witnessUtxo.script))
16
+ )
17
+ );
18
+ }
19
+
20
+ export function getInputRelevantScript(input: PsbtInput): Buffer | null {
21
+ if (input.redeemScript) {
22
+ return input.redeemScript;
23
+ }
24
+ if (input.witnessScript) {
25
+ return input.witnessScript;
26
+ }
27
+ if (input.witnessUtxo) {
28
+ return input.witnessUtxo.script;
29
+ }
30
+ if (input.nonWitnessUtxo) {
31
+ // Parse the full transaction from nonWitnessUtxo
32
+ /*const tx = Transaction.fromBuffer(input.nonWitnessUtxo);
33
+ // Retrieve the output referenced by the input index
34
+ const out = tx.outs[input.index];
35
+ if (!out) {
36
+ throw new Error(`No output at index ${input.index} in nonWitnessUtxo`);
37
+ }
38
+ return out.script;*/
39
+ }
40
+ return null;
41
+ }
42
+
43
+ export function canSignNonTaprootInput(input: PsbtInput, publicKey: Buffer): boolean {
44
+ if (
45
+ (input.nonWitnessUtxo &&
46
+ !input.redeemScript &&
47
+ !input.witnessScript &&
48
+ !input.witnessUtxo) ||
49
+ input.redeemScript
50
+ ) {
51
+ return true;
52
+ }
53
+
54
+ const script = getInputRelevantScript(input);
55
+ if (script) {
56
+ return pubkeyInScript(publicKey, script);
57
+ }
58
+ return false;
59
+ }
60
+
61
+ export function pubkeyPositionInScript(pubkey: Buffer, script: Buffer): number {
62
+ const pubkeyHash = bitCrypto.hash160(pubkey);
63
+ const pubkeyXOnly = toXOnly(pubkey);
64
+
65
+ const decompiled = bscript.decompile(script);
66
+ if (decompiled === null) throw new Error('Unknown script error');
67
+
68
+ const a = decompiled.findIndex((element) => {
69
+ if (typeof element === 'number') return false;
70
+ return element.equals(pubkey) || element.equals(pubkeyHash) || element.equals(pubkeyXOnly);
71
+ });
72
+
73
+ return a;
74
+ }
75
+
76
+ export function pubkeyInScript(pubkey: Buffer, script: Buffer): boolean {
77
+ return pubkeyPositionInScript(pubkey, script) !== -1;
78
+ }