@bsv/sdk 1.7.6 → 1.8.0

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 (57) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/transaction/Beef.js +25 -2
  3. package/dist/cjs/src/transaction/Beef.js.map +1 -1
  4. package/dist/cjs/src/transaction/Transaction.js +36 -40
  5. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  6. package/dist/cjs/src/transaction/fee-models/LivePolicy.js +90 -0
  7. package/dist/cjs/src/transaction/fee-models/LivePolicy.js.map +1 -0
  8. package/dist/cjs/src/transaction/fee-models/index.js +3 -1
  9. package/dist/cjs/src/transaction/fee-models/index.js.map +1 -1
  10. package/dist/cjs/src/wallet/WalletClient.js +43 -52
  11. package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
  12. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +19 -0
  13. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  14. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  15. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  16. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  17. package/dist/esm/src/transaction/Beef.js +25 -2
  18. package/dist/esm/src/transaction/Beef.js.map +1 -1
  19. package/dist/esm/src/transaction/Transaction.js +36 -40
  20. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  21. package/dist/esm/src/transaction/fee-models/LivePolicy.js +85 -0
  22. package/dist/esm/src/transaction/fee-models/LivePolicy.js.map +1 -0
  23. package/dist/esm/src/transaction/fee-models/index.js +1 -0
  24. package/dist/esm/src/transaction/fee-models/index.js.map +1 -1
  25. package/dist/esm/src/wallet/WalletClient.js +43 -52
  26. package/dist/esm/src/wallet/WalletClient.js.map +1 -1
  27. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +19 -0
  28. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  29. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  30. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  31. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  32. package/dist/types/src/transaction/Beef.d.ts +4 -0
  33. package/dist/types/src/transaction/Beef.d.ts.map +1 -1
  34. package/dist/types/src/transaction/Transaction.d.ts +2 -2
  35. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  36. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts +41 -0
  37. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts.map +1 -0
  38. package/dist/types/src/transaction/fee-models/index.d.ts +1 -0
  39. package/dist/types/src/transaction/fee-models/index.d.ts.map +1 -1
  40. package/dist/types/src/wallet/WalletClient.d.ts +1 -1
  41. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  42. package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts.map +1 -1
  43. package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts.map +1 -1
  44. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  45. package/dist/umd/bundle.js +3 -3
  46. package/dist/umd/bundle.js.map +1 -1
  47. package/docs/reference/transaction.md +73 -55
  48. package/docs/reference/wallet.md +1 -1
  49. package/package.json +1 -1
  50. package/src/transaction/Beef.ts +28 -2
  51. package/src/transaction/Transaction.ts +43 -40
  52. package/src/transaction/fee-models/LivePolicy.ts +97 -0
  53. package/src/transaction/fee-models/__tests/LivePolicy.test.ts +148 -0
  54. package/src/transaction/fee-models/index.ts +1 -0
  55. package/src/wallet/WalletClient.ts +50 -51
  56. package/src/wallet/substrates/WalletWireProcessor.ts +21 -0
  57. package/src/wallet/substrates/WalletWireTransceiver.ts +22 -10
@@ -456,9 +456,9 @@ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](
456
456
  | [BeefParty](#class-beefparty) |
457
457
  | [BeefTx](#class-beeftx) |
458
458
  | [FetchHttpClient](#class-fetchhttpclient) |
459
+ | [LivePolicy](#class-livepolicy) |
459
460
  | [MerklePath](#class-merklepath) |
460
461
  | [NodejsHttpClient](#class-nodejshttpclient) |
461
- | [SatoshisPerKilobyte](#class-satoshisperkilobyte) |
462
462
  | [Transaction](#class-transaction) |
463
463
  | [WhatsOnChain](#class-whatsonchain) |
464
464
 
@@ -1198,6 +1198,72 @@ See also: [Fetch](./transaction.md#type-fetch), [HttpClient](./transaction.md#in
1198
1198
 
1199
1199
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1200
1200
 
1201
+ ---
1202
+ ### Class: LivePolicy
1203
+
1204
+ Represents a live fee policy that fetches current rates from ARC GorillaPool.
1205
+
1206
+ ```ts
1207
+ export default class LivePolicy implements FeeModel {
1208
+ constructor(cacheValidityMs: number = 5 * 60 * 1000)
1209
+ static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy
1210
+ async computeFee(tx: Transaction): Promise<number>
1211
+ }
1212
+ ```
1213
+
1214
+ See also: [FeeModel](./transaction.md#interface-feemodel), [Transaction](./transaction.md#class-transaction)
1215
+
1216
+ #### Constructor
1217
+
1218
+ Constructs an instance of the live policy fee model.
1219
+
1220
+ ```ts
1221
+ constructor(cacheValidityMs: number = 5 * 60 * 1000)
1222
+ ```
1223
+
1224
+ Argument Details
1225
+
1226
+ + **cacheValidityMs**
1227
+ + How long to cache the fee rate in milliseconds (default: 5 minutes)
1228
+
1229
+ #### Method computeFee
1230
+
1231
+ Computes the fee for a given transaction using the current live rate.
1232
+
1233
+ ```ts
1234
+ async computeFee(tx: Transaction): Promise<number>
1235
+ ```
1236
+ See also: [Transaction](./transaction.md#class-transaction)
1237
+
1238
+ Returns
1239
+
1240
+ The fee in satoshis for the transaction.
1241
+
1242
+ Argument Details
1243
+
1244
+ + **tx**
1245
+ + The transaction for which a fee is to be computed.
1246
+
1247
+ #### Method getInstance
1248
+
1249
+ Gets the singleton instance of LivePolicy to ensure cache sharing across the application.
1250
+
1251
+ ```ts
1252
+ static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy
1253
+ ```
1254
+ See also: [LivePolicy](./transaction.md#class-livepolicy)
1255
+
1256
+ Returns
1257
+
1258
+ The singleton LivePolicy instance
1259
+
1260
+ Argument Details
1261
+
1262
+ + **cacheValidityMs**
1263
+ + How long to cache the fee rate in milliseconds (default: 5 minutes)
1264
+
1265
+ Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1266
+
1201
1267
  ---
1202
1268
  ### Class: MerklePath
1203
1269
 
@@ -1420,54 +1486,6 @@ See also: [HttpClient](./transaction.md#interface-httpclient), [HttpClientReques
1420
1486
 
1421
1487
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1422
1488
 
1423
- ---
1424
- ### Class: SatoshisPerKilobyte
1425
-
1426
- Represents the "satoshis per kilobyte" transaction fee model.
1427
-
1428
- ```ts
1429
- export default class SatoshisPerKilobyte implements FeeModel {
1430
- value: number;
1431
- constructor(value: number)
1432
- async computeFee(tx: Transaction): Promise<number>
1433
- }
1434
- ```
1435
-
1436
- See also: [FeeModel](./transaction.md#interface-feemodel), [Transaction](./transaction.md#class-transaction)
1437
-
1438
- #### Constructor
1439
-
1440
- Constructs an instance of the sat/kb fee model.
1441
-
1442
- ```ts
1443
- constructor(value: number)
1444
- ```
1445
-
1446
- Argument Details
1447
-
1448
- + **value**
1449
- + The number of satoshis per kilobyte to charge as a fee.
1450
-
1451
- #### Method computeFee
1452
-
1453
- Computes the fee for a given transaction.
1454
-
1455
- ```ts
1456
- async computeFee(tx: Transaction): Promise<number>
1457
- ```
1458
- See also: [Transaction](./transaction.md#class-transaction)
1459
-
1460
- Returns
1461
-
1462
- The fee in satoshis for the transaction, as a BigNumber.
1463
-
1464
- Argument Details
1465
-
1466
- + **tx**
1467
- + The transaction for which a fee is to be computed.
1468
-
1469
- Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
1470
-
1471
1489
  ---
1472
1490
  ### Class: Transaction
1473
1491
 
@@ -1520,7 +1538,7 @@ export default class Transaction {
1520
1538
  addOutput(output: TransactionOutput): void
1521
1539
  addP2PKHOutput(address: number[] | string, satoshis?: number): void
1522
1540
  updateMetadata(metadata: Record<string, any>): void
1523
- async fee(modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1541
+ async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1524
1542
  getFee(): number
1525
1543
  async sign(): Promise<void>
1526
1544
  async broadcast(broadcaster: Broadcaster = defaultBroadcaster()): Promise<BroadcastResponse | BroadcastFailure>
@@ -1540,7 +1558,7 @@ export default class Transaction {
1540
1558
  }
1541
1559
  ```
1542
1560
 
1543
- See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [Broadcaster](./transaction.md#interface-broadcaster), [ChainTracker](./transaction.md#interface-chaintracker), [FeeModel](./transaction.md#interface-feemodel), [MerklePath](./transaction.md#class-merklepath), [Reader](./primitives.md#class-reader), [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput), [defaultBroadcaster](./transaction.md#function-defaultbroadcaster), [defaultChainTracker](./transaction.md#function-defaultchaintracker), [sign](./compat.md#variable-sign), [toHex](./primitives.md#variable-tohex), [verify](./compat.md#variable-verify)
1561
+ See also: [BroadcastFailure](./transaction.md#interface-broadcastfailure), [BroadcastResponse](./transaction.md#interface-broadcastresponse), [Broadcaster](./transaction.md#interface-broadcaster), [ChainTracker](./transaction.md#interface-chaintracker), [FeeModel](./transaction.md#interface-feemodel), [LivePolicy](./transaction.md#class-livepolicy), [MerklePath](./transaction.md#class-merklepath), [Reader](./primitives.md#class-reader), [TransactionInput](./transaction.md#interface-transactioninput), [TransactionOutput](./transaction.md#interface-transactionoutput), [defaultBroadcaster](./transaction.md#function-defaultbroadcaster), [defaultChainTracker](./transaction.md#function-defaultchaintracker), [sign](./compat.md#variable-sign), [toHex](./primitives.md#variable-tohex), [verify](./compat.md#variable-verify)
1544
1562
 
1545
1563
  #### Method addInput
1546
1564
 
@@ -1610,13 +1628,13 @@ Argument Details
1610
1628
  #### Method fee
1611
1629
 
1612
1630
  Computes fees prior to signing.
1613
- If no fee model is provided, uses a SatoshisPerKilobyte fee model that pays 1 sat/kb.
1631
+ If no fee model is provided, uses a LivePolicy fee model that fetches current rates from ARC.
1614
1632
  If fee is a number, the transaction uses that value as fee.
1615
1633
 
1616
1634
  ```ts
1617
- async fee(modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1635
+ async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
1618
1636
  ```
1619
- See also: [FeeModel](./transaction.md#interface-feemodel), [SatoshisPerKilobyte](./transaction.md#class-satoshisperkilobyte)
1637
+ See also: [FeeModel](./transaction.md#interface-feemodel), [LivePolicy](./transaction.md#class-livepolicy)
1620
1638
 
1621
1639
  Argument Details
1622
1640
 
@@ -2037,7 +2055,7 @@ Argument Details
2037
2055
  Example
2038
2056
 
2039
2057
  ```ts
2040
- tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
2058
+ tx.verify(new WhatsOnChain(), LivePolicy.getInstance())
2041
2059
  ```
2042
2060
 
2043
2061
  Links: [API](#api), [Interfaces](#interfaces), [Classes](#classes), [Functions](#functions), [Types](#types), [Enums](#enums), [Variables](#variables)
@@ -2629,7 +2629,7 @@ The SDK is how applications communicate with wallets over a communications subst
2629
2629
  export default class WalletClient implements WalletInterface {
2630
2630
  public substrate: "auto" | WalletInterface;
2631
2631
  originator?: OriginatorDomainNameStringUnder250Bytes;
2632
- constructor(substrate: "auto" | "Cicada" | "XDM" | "window.CWI" | "json-api" | "react-native" | WalletInterface = "auto", originator?: OriginatorDomainNameStringUnder250Bytes)
2632
+ constructor(substrate: "auto" | "Cicada" | "XDM" | "window.CWI" | "json-api" | "react-native" | "secure-json-api" | WalletInterface = "auto", originator?: OriginatorDomainNameStringUnder250Bytes)
2633
2633
  async connectToSubstrate(): Promise<void>
2634
2634
  async createAction(args: CreateActionArgs): Promise<CreateActionResult>
2635
2635
  async signAction(args: SignActionArgs): Promise<SignActionResult>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "1.7.6",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -77,6 +77,7 @@ export class Beef {
77
77
  txs: BeefTx[] = []
78
78
  version: number = BEEF_V2
79
79
  atomicTxid: string | undefined = undefined
80
+ private txidIndex: Map<string, BeefTx> | undefined = undefined
80
81
 
81
82
  constructor (version: number = BEEF_V2) {
82
83
  this.version = version
@@ -87,7 +88,25 @@ export class Beef {
87
88
  * @returns `BeefTx` in `txs` with `txid`.
88
89
  */
89
90
  findTxid (txid: string): BeefTx | undefined {
90
- return this.txs.find((tx) => tx.txid === txid)
91
+ return this.ensureTxidIndex().get(txid)
92
+ }
93
+
94
+ private ensureTxidIndex (): Map<string, BeefTx> {
95
+ if (this.txidIndex == null) {
96
+ this.txidIndex = new Map<string, BeefTx>()
97
+ for (const tx of this.txs) {
98
+ this.txidIndex.set(tx.txid, tx)
99
+ }
100
+ }
101
+ return this.txidIndex
102
+ }
103
+
104
+ private deleteFromIndex (txid: string): void {
105
+ this.txidIndex?.delete(txid)
106
+ }
107
+
108
+ private addToIndex (tx: BeefTx): void {
109
+ this.txidIndex?.set(tx.txid, tx)
91
110
  }
92
111
 
93
112
  /**
@@ -107,6 +126,7 @@ export class Beef {
107
126
  if (btx.isTxidOnly) {
108
127
  return btx
109
128
  }
129
+ this.deleteFromIndex(txid)
110
130
  this.txs.splice(i, 1)
111
131
  btx = this.mergeTxidOnly(txid)
112
132
  return btx
@@ -254,6 +274,7 @@ export class Beef {
254
274
  const newTx: BeefTx = new BeefTx(rawTx, bumpIndex)
255
275
  this.removeExistingTxid(newTx.txid)
256
276
  this.txs.push(newTx)
277
+ this.addToIndex(newTx)
257
278
  this.tryToValidateBumpIndex(newTx)
258
279
  return newTx
259
280
  }
@@ -277,6 +298,7 @@ export class Beef {
277
298
  }
278
299
  const newTx = new BeefTx(tx, bumpIndex)
279
300
  this.txs.push(newTx)
301
+ this.addToIndex(newTx)
280
302
  this.tryToValidateBumpIndex(newTx)
281
303
  bumpIndex = newTx.bumpIndex
282
304
  if (bumpIndex === undefined) {
@@ -296,15 +318,17 @@ export class Beef {
296
318
  removeExistingTxid (txid: string): void {
297
319
  const existingTxIndex = this.txs.findIndex((t) => t.txid === txid)
298
320
  if (existingTxIndex >= 0) {
321
+ this.deleteFromIndex(txid)
299
322
  this.txs.splice(existingTxIndex, 1)
300
323
  }
301
324
  }
302
325
 
303
326
  mergeTxidOnly (txid: string): BeefTx {
304
- let tx = this.txs.find((t) => t.txid === txid)
327
+ let tx = this.findTxid(txid)
305
328
  if (tx == null) {
306
329
  tx = new BeefTx(txid)
307
330
  this.txs.push(tx)
331
+ this.addToIndex(tx)
308
332
  this.tryToValidateBumpIndex(tx)
309
333
  }
310
334
  return tx
@@ -743,6 +767,7 @@ export class Beef {
743
767
  c.version = this.version
744
768
  c.bumps = Array.from(this.bumps)
745
769
  c.txs = Array.from(this.txs)
770
+ c.txidIndex = undefined
746
771
  return c
747
772
  }
748
773
 
@@ -754,6 +779,7 @@ export class Beef {
754
779
  for (let i = 0; i < this.txs.length;) {
755
780
  const tx = this.txs[i]
756
781
  if (tx.isTxidOnly && knownTxids.includes(tx.txid)) {
782
+ this.deleteFromIndex(tx.txid)
757
783
  this.txs.splice(i, 1)
758
784
  } else {
759
785
  i++
@@ -6,7 +6,7 @@ import LockingScript from '../script/LockingScript.js'
6
6
  import { Reader, Writer, toHex, toArray } from '../primitives/utils.js'
7
7
  import { hash256 } from '../primitives/Hash.js'
8
8
  import FeeModel from './FeeModel.js'
9
- import SatoshisPerKilobyte from './fee-models/SatoshisPerKilobyte.js'
9
+ import LivePolicy from './fee-models/LivePolicy.js'
10
10
  import { Broadcaster, BroadcastResponse, BroadcastFailure } from './Broadcaster.js'
11
11
  import MerklePath from './MerklePath.js'
12
12
  import Spend from '../script/Spend.js'
@@ -411,7 +411,7 @@ export default class Transaction {
411
411
 
412
412
  /**
413
413
  * Computes fees prior to signing.
414
- * If no fee model is provided, uses a SatoshisPerKilobyte fee model that pays 1 sat/kb.
414
+ * If no fee model is provided, uses a LivePolicy fee model that fetches current rates from ARC.
415
415
  * If fee is a number, the transaction uses that value as fee.
416
416
  *
417
417
  * @param modelOrFee - The initialized fee model to use or fixed fee for the transaction
@@ -420,7 +420,7 @@ export default class Transaction {
420
420
  *
421
421
  */
422
422
  async fee (
423
- modelOrFee: FeeModel | number = new SatoshisPerKilobyte(1),
423
+ modelOrFee: FeeModel | number = LivePolicy.getInstance(),
424
424
  changeDistribution: 'equal' | 'random' = 'equal'
425
425
  ): Promise<void> {
426
426
  this.cachedHash = undefined
@@ -774,7 +774,7 @@ export default class Transaction {
774
774
  *
775
775
  * @returns Whether the transaction is valid according to the rules of SPV.
776
776
  *
777
- * @example tx.verify(new WhatsOnChain(), new SatoshisPerKilobyte(1))
777
+ * @example tx.verify(new WhatsOnChain(), LivePolicy.getInstance())
778
778
  */
779
779
  async verify (
780
780
  chainTracker: ChainTracker | 'scripts only' = defaultChainTracker(),
@@ -912,49 +912,49 @@ export default class Transaction {
912
912
  const writer = new Writer()
913
913
  writer.writeUInt32LE(BEEF_V1)
914
914
  const BUMPs: MerklePath[] = []
915
+ const bumpIndexByInstance = new Map<MerklePath, number>()
916
+ const bumpIndexByRoot = new Map<string, number>()
915
917
  const txs: Array<{ tx: Transaction, pathIndex?: number }> = []
918
+ const seenTxids = new Set<string>()
919
+
920
+ const getBumpIndex = (merklePath: MerklePath): number => {
921
+ const existingByInstance = bumpIndexByInstance.get(merklePath)
922
+ if (existingByInstance !== undefined) {
923
+ return existingByInstance
924
+ }
925
+
926
+ const key = `${merklePath.blockHeight}:${merklePath.computeRoot()}`
927
+ const existingByRoot = bumpIndexByRoot.get(key)
928
+ if (existingByRoot !== undefined) {
929
+ BUMPs[existingByRoot].combine(merklePath)
930
+ bumpIndexByInstance.set(merklePath, existingByRoot)
931
+ return existingByRoot
932
+ }
933
+
934
+ const newIndex = BUMPs.length
935
+ BUMPs.push(merklePath)
936
+ bumpIndexByInstance.set(merklePath, newIndex)
937
+ bumpIndexByRoot.set(key, newIndex)
938
+ return newIndex
939
+ }
916
940
 
917
941
  // Recursive function to add paths and input transactions for a TX
918
942
  const addPathsAndInputs = (tx: Transaction): void => {
919
- const obj: { tx: Transaction, pathIndex?: number } = { tx }
920
- const hasProof = typeof tx.merklePath === 'object'
921
- if (hasProof) {
922
- let added = false
923
- // If this proof is identical to another one previously added, we use that first. Otherwise, we try to merge it with proofs from the same block.
924
- for (let i = 0; i < BUMPs.length; i++) {
925
- if (BUMPs[i] === tx.merklePath) {
926
- // Literally the same
927
- obj.pathIndex = i
928
- added = true
929
- break
930
- }
931
- if (tx.merklePath !== null && tx.merklePath !== undefined && BUMPs[i].blockHeight === tx.merklePath.blockHeight) {
932
- // Probably the same...
933
- const rootA = BUMPs[i].computeRoot()
934
- const rootB = tx.merklePath.computeRoot()
935
- if (rootA === rootB) {
936
- // Definitely the same... combine them to save space
937
- BUMPs[i].combine(tx.merklePath)
938
- obj.pathIndex = i
939
- added = true
940
- break
941
- }
942
- }
943
- }
944
- // Finally, if the proof is not yet added, add a new path.
945
- if (!added) {
946
- obj.pathIndex = BUMPs.length
947
- if (tx.merklePath !== null && tx.merklePath !== undefined) {
948
- BUMPs.push(tx.merklePath)
949
- }
950
- }
943
+ const txid = tx.id('hex')
944
+ if (seenTxids.has(txid)) {
945
+ return
951
946
  }
952
- const duplicate = txs.some((x) => x.tx.id('hex') === tx.id('hex'))
953
- if (!duplicate) {
954
- txs.unshift(obj)
947
+
948
+ const obj: { tx: Transaction, pathIndex?: number } = { tx }
949
+ const merklePath = tx.merklePath
950
+ const hasProof = typeof merklePath === 'object'
951
+
952
+ if (hasProof && merklePath != null) {
953
+ obj.pathIndex = getBumpIndex(merklePath)
955
954
  }
955
+
956
956
  if (!hasProof) {
957
- for (let i = 0; i < tx.inputs.length; i++) {
957
+ for (let i = tx.inputs.length - 1; i >= 0; i--) {
958
958
  const input = tx.inputs[i]
959
959
  if (typeof input.sourceTransaction === 'object') {
960
960
  addPathsAndInputs(input.sourceTransaction)
@@ -963,6 +963,9 @@ export default class Transaction {
963
963
  }
964
964
  }
965
965
  }
966
+
967
+ seenTxids.add(txid)
968
+ txs.push(obj)
966
969
  }
967
970
 
968
971
  addPathsAndInputs(this)
@@ -0,0 +1,97 @@
1
+ import SatoshisPerKilobyte from './SatoshisPerKilobyte.js'
2
+ import Transaction from '../Transaction.js'
3
+
4
+ /**
5
+ * Represents a live fee policy that fetches current rates from ARC GorillaPool.
6
+ * Extends SatoshisPerKilobyte to reuse transaction size calculation logic.
7
+ */
8
+ export default class LivePolicy extends SatoshisPerKilobyte {
9
+ private static readonly ARC_POLICY_URL = 'https://arc.gorillapool.io/v1/policy'
10
+ private static instance: LivePolicy | null = null
11
+ private cachedRate: number | null = null
12
+ private cacheTimestamp: number = 0
13
+ private readonly cacheValidityMs: number
14
+
15
+ /**
16
+ * Constructs an instance of the live policy fee model.
17
+ *
18
+ * @param {number} cacheValidityMs - How long to cache the fee rate in milliseconds (default: 5 minutes)
19
+ */
20
+ constructor(cacheValidityMs: number = 5 * 60 * 1000) {
21
+ super(100) // Initialize with dummy value, will be overridden by fetchFeeRate
22
+ this.cacheValidityMs = cacheValidityMs
23
+ }
24
+
25
+ /**
26
+ * Gets the singleton instance of LivePolicy to ensure cache sharing across the application.
27
+ *
28
+ * @param {number} cacheValidityMs - How long to cache the fee rate in milliseconds (default: 5 minutes)
29
+ * @returns The singleton LivePolicy instance
30
+ */
31
+ static getInstance(cacheValidityMs: number = 5 * 60 * 1000): LivePolicy {
32
+ if (!LivePolicy.instance) {
33
+ LivePolicy.instance = new LivePolicy(cacheValidityMs)
34
+ }
35
+ return LivePolicy.instance
36
+ }
37
+
38
+ /**
39
+ * Fetches the current fee rate from ARC GorillaPool API.
40
+ *
41
+ * @returns The current satoshis per kilobyte rate
42
+ */
43
+ private async fetchFeeRate(): Promise<number> {
44
+ const now = Date.now()
45
+
46
+ // Return cached rate if still valid
47
+ if (this.cachedRate !== null && (now - this.cacheTimestamp) < this.cacheValidityMs) {
48
+ return this.cachedRate
49
+ }
50
+
51
+ try {
52
+ const response = await fetch(LivePolicy.ARC_POLICY_URL)
53
+ if (!response.ok) {
54
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
55
+ }
56
+
57
+ const response_data = await response.json()
58
+
59
+ if (!response_data.policy?.miningFee || typeof response_data.policy.miningFee.satoshis !== 'number' || typeof response_data.policy.miningFee.bytes !== 'number') {
60
+ throw new Error('Invalid policy response format')
61
+ }
62
+
63
+ // Convert to satoshis per kilobyte
64
+ const rate = (response_data.policy.miningFee.satoshis / response_data.policy.miningFee.bytes) * 1000
65
+
66
+ // Cache the result
67
+ this.cachedRate = rate
68
+ this.cacheTimestamp = now
69
+
70
+ return rate
71
+ } catch (error) {
72
+ // If we have a cached rate, use it as fallback
73
+ if (this.cachedRate !== null) {
74
+ console.warn('Failed to fetch live fee rate, using cached value:', error)
75
+ return this.cachedRate
76
+ }
77
+
78
+ // Otherwise, use a reasonable default (100 sat/kb)
79
+ console.warn('Failed to fetch live fee rate, using default 100 sat/kb:', error)
80
+ return 100
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Computes the fee for a given transaction using the current live rate.
86
+ * Overrides the parent method to use dynamic rate fetching.
87
+ *
88
+ * @param tx The transaction for which a fee is to be computed.
89
+ * @returns The fee in satoshis for the transaction.
90
+ */
91
+ async computeFee(tx: Transaction): Promise<number> {
92
+ const rate = await this.fetchFeeRate()
93
+ // Update the value property so parent's computeFee uses the live rate
94
+ this.value = rate
95
+ return super.computeFee(tx)
96
+ }
97
+ }
@@ -0,0 +1,148 @@
1
+ import LivePolicy from '../LivePolicy.js'
2
+
3
+ describe('LivePolicy', () => {
4
+ let consoleSpy: jest.SpyInstance
5
+
6
+ const createMockTransaction = () => ({
7
+ inputs: [],
8
+ outputs: []
9
+ } as any)
10
+
11
+ const createSuccessfulFetchMock = (satoshis: number, bytes: number = 1000) =>
12
+ jest.fn().mockResolvedValue({
13
+ ok: true,
14
+ json: async () => ({
15
+ policy: {
16
+ miningFee: { satoshis, bytes }
17
+ }
18
+ })
19
+ })
20
+
21
+ const createErrorFetchMock = (status = 500, statusText = 'Internal Server Error') =>
22
+ jest.fn().mockResolvedValue({
23
+ ok: false,
24
+ status,
25
+ statusText
26
+ })
27
+
28
+ const createInvalidResponseMock = () =>
29
+ jest.fn().mockResolvedValue({
30
+ ok: true,
31
+ json: async () => ({ invalid: 'response' })
32
+ })
33
+
34
+ const createNetworkErrorMock = () =>
35
+ jest.fn().mockRejectedValue(new Error('Network error'))
36
+
37
+ const expectDefaultFallback = (consoleSpy: jest.SpyInstance) => {
38
+ expect(consoleSpy).toHaveBeenCalledWith(
39
+ 'Failed to fetch live fee rate, using default 100 sat/kb:',
40
+ expect.any(Error)
41
+ )
42
+ }
43
+
44
+ const expectCachedFallback = (consoleSpy: jest.SpyInstance) => {
45
+ expect(consoleSpy).toHaveBeenCalledWith(
46
+ 'Failed to fetch live fee rate, using cached value:',
47
+ expect.any(Error)
48
+ )
49
+ }
50
+
51
+ beforeEach(() => {
52
+ ;(LivePolicy as any).instance = null
53
+ jest.clearAllMocks()
54
+ consoleSpy = jest.spyOn(console, 'warn').mockImplementation()
55
+ })
56
+
57
+ afterEach(() => {
58
+ consoleSpy.mockRestore()
59
+ })
60
+
61
+ it('should return the same instance when getInstance is called multiple times', () => {
62
+ const instance1 = LivePolicy.getInstance()
63
+ const instance2 = LivePolicy.getInstance()
64
+
65
+ expect(instance1).toBe(instance2)
66
+ expect(instance1).toBeInstanceOf(LivePolicy)
67
+ })
68
+
69
+ it('should share cache between singleton instances', async () => {
70
+ const instance1 = LivePolicy.getInstance()
71
+ const instance2 = LivePolicy.getInstance()
72
+
73
+ global.fetch = createSuccessfulFetchMock(5)
74
+ const mockTx = createMockTransaction()
75
+
76
+ const fee1 = await instance1.computeFee(mockTx)
77
+ const fee2 = await instance2.computeFee(mockTx)
78
+
79
+ expect(fee1).toBe(fee2)
80
+ expect(fee1).toBe(1) // 5 sat/kb rate, minimum tx size gets 1 sat
81
+ expect(global.fetch).toHaveBeenCalledTimes(1)
82
+ })
83
+
84
+ it('should allow different cache validity when creating singleton', () => {
85
+ const instance1 = LivePolicy.getInstance(10000)
86
+ const instance2 = LivePolicy.getInstance(20000)
87
+
88
+ expect(instance1).toBe(instance2)
89
+ expect((instance1 as any).cacheValidityMs).toBe(10000)
90
+ })
91
+
92
+ it('should create instance with custom cache validity', () => {
93
+ const instance = new LivePolicy(30000)
94
+ expect((instance as any).cacheValidityMs).toBe(30000)
95
+ })
96
+
97
+ it('should handle HTTP error responses', async () => {
98
+ const instance = LivePolicy.getInstance()
99
+ global.fetch = createErrorFetchMock()
100
+ const mockTx = createMockTransaction()
101
+
102
+ const fee = await instance.computeFee(mockTx)
103
+
104
+ expect(fee).toBe(1)
105
+ expectDefaultFallback(consoleSpy)
106
+ })
107
+
108
+ it('should handle invalid API response format', async () => {
109
+ const instance = LivePolicy.getInstance()
110
+ global.fetch = createInvalidResponseMock()
111
+ const mockTx = createMockTransaction()
112
+
113
+ const fee = await instance.computeFee(mockTx)
114
+
115
+ expect(fee).toBe(1)
116
+ expectDefaultFallback(consoleSpy)
117
+ })
118
+
119
+ it('should use cached value when API fails after successful fetch', async () => {
120
+ const instance = LivePolicy.getInstance()
121
+ const mockTx = createMockTransaction()
122
+
123
+ // First call - successful fetch
124
+ global.fetch = createSuccessfulFetchMock(10)
125
+ const fee1 = await instance.computeFee(mockTx)
126
+ expect(fee1).toBe(1)
127
+
128
+ // Expire cache and simulate API failure
129
+ ;(instance as any).cacheTimestamp = Date.now() - (6 * 60 * 1000)
130
+ global.fetch = createNetworkErrorMock()
131
+
132
+ const fee2 = await instance.computeFee(mockTx)
133
+
134
+ expect(fee2).toBe(1)
135
+ expectCachedFallback(consoleSpy)
136
+ })
137
+
138
+ it('should handle network errors with no cached value', async () => {
139
+ const instance = LivePolicy.getInstance()
140
+ global.fetch = createNetworkErrorMock()
141
+ const mockTx = createMockTransaction()
142
+
143
+ const fee = await instance.computeFee(mockTx)
144
+
145
+ expect(fee).toBe(1)
146
+ expectDefaultFallback(consoleSpy)
147
+ })
148
+ })
@@ -1 +1,2 @@
1
1
  export { default as SatoshisPerKilobyte } from './SatoshisPerKilobyte.js'
2
+ export { default as LivePolicy } from './LivePolicy.js'