@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/transaction/Beef.js +25 -2
- package/dist/cjs/src/transaction/Beef.js.map +1 -1
- package/dist/cjs/src/transaction/Transaction.js +36 -40
- package/dist/cjs/src/transaction/Transaction.js.map +1 -1
- package/dist/cjs/src/transaction/fee-models/LivePolicy.js +90 -0
- package/dist/cjs/src/transaction/fee-models/LivePolicy.js.map +1 -0
- package/dist/cjs/src/transaction/fee-models/index.js +3 -1
- package/dist/cjs/src/transaction/fee-models/index.js.map +1 -1
- package/dist/cjs/src/wallet/WalletClient.js +43 -52
- package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +19 -0
- package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
- package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +18 -1
- package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/transaction/Beef.js +25 -2
- package/dist/esm/src/transaction/Beef.js.map +1 -1
- package/dist/esm/src/transaction/Transaction.js +36 -40
- package/dist/esm/src/transaction/Transaction.js.map +1 -1
- package/dist/esm/src/transaction/fee-models/LivePolicy.js +85 -0
- package/dist/esm/src/transaction/fee-models/LivePolicy.js.map +1 -0
- package/dist/esm/src/transaction/fee-models/index.js +1 -0
- package/dist/esm/src/transaction/fee-models/index.js.map +1 -1
- package/dist/esm/src/wallet/WalletClient.js +43 -52
- package/dist/esm/src/wallet/WalletClient.js.map +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +19 -0
- package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
- package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +18 -1
- package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/transaction/Beef.d.ts +4 -0
- package/dist/types/src/transaction/Beef.d.ts.map +1 -1
- package/dist/types/src/transaction/Transaction.d.ts +2 -2
- package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
- package/dist/types/src/transaction/fee-models/LivePolicy.d.ts +41 -0
- package/dist/types/src/transaction/fee-models/LivePolicy.d.ts.map +1 -0
- package/dist/types/src/transaction/fee-models/index.d.ts +1 -0
- package/dist/types/src/transaction/fee-models/index.d.ts.map +1 -1
- package/dist/types/src/wallet/WalletClient.d.ts +1 -1
- package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts.map +1 -1
- package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +3 -3
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/transaction.md +73 -55
- package/docs/reference/wallet.md +1 -1
- package/package.json +1 -1
- package/src/transaction/Beef.ts +28 -2
- package/src/transaction/Transaction.ts +43 -40
- package/src/transaction/fee-models/LivePolicy.ts +97 -0
- package/src/transaction/fee-models/__tests/LivePolicy.test.ts +148 -0
- package/src/transaction/fee-models/index.ts +1 -0
- package/src/wallet/WalletClient.ts +50 -51
- package/src/wallet/substrates/WalletWireProcessor.ts +21 -0
- 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 =
|
|
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), [
|
|
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
|
|
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 =
|
|
1635
|
+
async fee(modelOrFee: FeeModel | number = LivePolicy.getInstance(), changeDistribution: "equal" | "random" = "equal"): Promise<void>
|
|
1618
1636
|
```
|
|
1619
|
-
See also: [FeeModel](./transaction.md#interface-feemodel), [
|
|
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(),
|
|
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)
|
package/docs/reference/wallet.md
CHANGED
|
@@ -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
package/src/transaction/Beef.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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
|
|
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 =
|
|
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(),
|
|
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
|
|
920
|
-
|
|
921
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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 =
|
|
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
|
+
})
|