@bsv/sdk 1.7.7 → 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/Transaction.js +4 -4
- 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/Transaction.js +4 -4
- 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/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/Transaction.ts +4 -4
- 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
|
@@ -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(),
|
|
@@ -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
|
+
})
|
|
@@ -58,6 +58,7 @@ export default class WalletClient implements WalletInterface {
|
|
|
58
58
|
| 'window.CWI'
|
|
59
59
|
| 'json-api'
|
|
60
60
|
| 'react-native'
|
|
61
|
+
| 'secure-json-api'
|
|
61
62
|
| WalletInterface = 'auto',
|
|
62
63
|
originator?: OriginatorDomainNameStringUnder250Bytes
|
|
63
64
|
) {
|
|
@@ -68,6 +69,7 @@ export default class WalletClient implements WalletInterface {
|
|
|
68
69
|
if (substrate === 'XDM') substrate = new XDMSubstrate()
|
|
69
70
|
if (substrate === 'json-api') substrate = new HTTPWalletJSON(originator)
|
|
70
71
|
if (substrate === 'react-native') substrate = new ReactNativeWebView(originator)
|
|
72
|
+
if (substrate === 'secure-json-api') substrate = new HTTPWalletJSON(originator, 'https://localhost:2121')
|
|
71
73
|
this.substrate = substrate
|
|
72
74
|
this.originator = originator
|
|
73
75
|
}
|
|
@@ -76,61 +78,58 @@ export default class WalletClient implements WalletInterface {
|
|
|
76
78
|
if (typeof this.substrate === 'object') {
|
|
77
79
|
return // substrate is already connected
|
|
78
80
|
}
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
let result
|
|
82
|
-
if (typeof timeout === 'number') {
|
|
83
|
-
result = await Promise.race([
|
|
84
|
-
sub.getVersion({}),
|
|
85
|
-
new Promise<never>((_resolve, reject) =>
|
|
86
|
-
setTimeout(() => reject(new Error('Timed out.')), timeout)
|
|
87
|
-
)
|
|
88
|
-
])
|
|
89
|
-
} else {
|
|
90
|
-
result = await sub.getVersion({})
|
|
91
|
-
}
|
|
92
|
-
if (typeof result !== 'object' || typeof result.version !== 'string') {
|
|
93
|
-
throw new Error('Failed to use substrate.')
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
try {
|
|
97
|
-
sub = new WindowCWISubstrate()
|
|
98
|
-
await checkSub()
|
|
99
|
-
this.substrate = sub
|
|
100
|
-
} catch (e) {
|
|
101
|
-
// XDM failed, try the next one...
|
|
81
|
+
|
|
82
|
+
const attemptSubstrate = async (factory: () => WalletInterface, timeout?: number): Promise<{ success: boolean, sub?: WalletInterface }> => {
|
|
102
83
|
try {
|
|
103
|
-
sub =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.substrate = sub
|
|
118
|
-
} catch (e) {
|
|
119
|
-
// HTTP JSON failed, attempt the next...
|
|
120
|
-
try {
|
|
121
|
-
sub = new ReactNativeWebView(this.originator)
|
|
122
|
-
await checkSub()
|
|
123
|
-
this.substrate = sub
|
|
124
|
-
} catch (e) {
|
|
125
|
-
// No comms. Tell the user to install a BSV wallet.
|
|
126
|
-
throw new Error(
|
|
127
|
-
'No wallet available over any communication substrate. Install a BSV wallet today!'
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
}
|
|
84
|
+
const sub = factory()
|
|
85
|
+
let result
|
|
86
|
+
if (typeof timeout === 'number') {
|
|
87
|
+
result = await Promise.race([
|
|
88
|
+
sub.getVersion({}),
|
|
89
|
+
new Promise<never>((_resolve, reject) =>
|
|
90
|
+
setTimeout(() => reject(new Error('Timed out.')), timeout)
|
|
91
|
+
)
|
|
92
|
+
])
|
|
93
|
+
} else {
|
|
94
|
+
result = await sub.getVersion({})
|
|
95
|
+
}
|
|
96
|
+
if (typeof result !== 'object' || typeof result.version !== 'string') {
|
|
97
|
+
return { success: false }
|
|
131
98
|
}
|
|
99
|
+
return { success: true, sub }
|
|
100
|
+
} catch {
|
|
101
|
+
return { success: false }
|
|
132
102
|
}
|
|
133
103
|
}
|
|
104
|
+
|
|
105
|
+
// Try fast substrates first
|
|
106
|
+
const fastAttempts = [
|
|
107
|
+
attemptSubstrate(() => new WindowCWISubstrate()),
|
|
108
|
+
attemptSubstrate(() => new HTTPWalletJSON(this.originator, 'https://localhost:2121')),
|
|
109
|
+
attemptSubstrate(() => new HTTPWalletJSON(this.originator)),
|
|
110
|
+
attemptSubstrate(() => new ReactNativeWebView(this.originator)),
|
|
111
|
+
attemptSubstrate(() => new WalletWireTransceiver(new HTTPWalletWire(this.originator)))
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const fastResults = await Promise.allSettled(fastAttempts)
|
|
115
|
+
const fastSuccessful = fastResults
|
|
116
|
+
.filter((r): r is PromiseFulfilledResult<{ success: boolean, sub?: WalletInterface }> => r.status === 'fulfilled' && r.value.success && r.value.sub !== undefined)
|
|
117
|
+
.map(r => r.value.sub as WalletInterface)
|
|
118
|
+
|
|
119
|
+
if (fastSuccessful.length > 0) {
|
|
120
|
+
this.substrate = fastSuccessful[0]
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Fall back to slower XDM substrate
|
|
125
|
+
const xdmResult = await attemptSubstrate(() => new XDMSubstrate(), MAX_XDM_RESPONSE_WAIT)
|
|
126
|
+
if (xdmResult.success && xdmResult.sub !== undefined) {
|
|
127
|
+
this.substrate = xdmResult.sub
|
|
128
|
+
} else {
|
|
129
|
+
throw new Error(
|
|
130
|
+
'No wallet available over any communication substrate. Install a BSV wallet today!'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
async createAction (args: CreateActionArgs): Promise<CreateActionResult> {
|
|
@@ -1782,6 +1782,27 @@ export default class WalletWireProcessor implements WalletWire {
|
|
|
1782
1782
|
// Write certificate binary length and data
|
|
1783
1783
|
resultWriter.writeVarIntNum(certBin.length)
|
|
1784
1784
|
resultWriter.write(certBin)
|
|
1785
|
+
|
|
1786
|
+
if (cert.keyring && Object.keys(cert.keyring).length > 0) {
|
|
1787
|
+
resultWriter.writeInt8(1) // Flag indicating keyring is present
|
|
1788
|
+
const keyringEntries = Object.entries(cert.keyring)
|
|
1789
|
+
resultWriter.writeVarIntNum(keyringEntries.length)
|
|
1790
|
+
for (const [fieldName, fieldValue] of keyringEntries) {
|
|
1791
|
+
const fieldNameBytes = Utils.toArray(fieldName, 'utf8')
|
|
1792
|
+
resultWriter.writeVarIntNum(fieldNameBytes.length)
|
|
1793
|
+
resultWriter.write(fieldNameBytes)
|
|
1794
|
+
|
|
1795
|
+
const fieldValueBytes = Utils.toArray(fieldValue, 'base64')
|
|
1796
|
+
resultWriter.writeVarIntNum(fieldValueBytes.length)
|
|
1797
|
+
resultWriter.write(fieldValueBytes)
|
|
1798
|
+
}
|
|
1799
|
+
} else {
|
|
1800
|
+
resultWriter.writeInt8(0) // Flag indicating no keyring
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
const verifierBytes = Utils.toArray(cert.verifier, 'hex')
|
|
1804
|
+
resultWriter.writeVarIntNum(verifierBytes.length)
|
|
1805
|
+
resultWriter.write(verifierBytes)
|
|
1785
1806
|
}
|
|
1786
1807
|
|
|
1787
1808
|
// Return the response
|