@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.
Files changed (50) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/transaction/Transaction.js +4 -4
  3. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  4. package/dist/cjs/src/transaction/fee-models/LivePolicy.js +90 -0
  5. package/dist/cjs/src/transaction/fee-models/LivePolicy.js.map +1 -0
  6. package/dist/cjs/src/transaction/fee-models/index.js +3 -1
  7. package/dist/cjs/src/transaction/fee-models/index.js.map +1 -1
  8. package/dist/cjs/src/wallet/WalletClient.js +43 -52
  9. package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
  10. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +19 -0
  11. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  12. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  13. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  14. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/src/transaction/Transaction.js +4 -4
  16. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  17. package/dist/esm/src/transaction/fee-models/LivePolicy.js +85 -0
  18. package/dist/esm/src/transaction/fee-models/LivePolicy.js.map +1 -0
  19. package/dist/esm/src/transaction/fee-models/index.js +1 -0
  20. package/dist/esm/src/transaction/fee-models/index.js.map +1 -1
  21. package/dist/esm/src/wallet/WalletClient.js +43 -52
  22. package/dist/esm/src/wallet/WalletClient.js.map +1 -1
  23. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +19 -0
  24. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  25. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  26. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  27. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  28. package/dist/types/src/transaction/Transaction.d.ts +2 -2
  29. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  30. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts +41 -0
  31. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts.map +1 -0
  32. package/dist/types/src/transaction/fee-models/index.d.ts +1 -0
  33. package/dist/types/src/transaction/fee-models/index.d.ts.map +1 -1
  34. package/dist/types/src/wallet/WalletClient.d.ts +1 -1
  35. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  36. package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts.map +1 -1
  37. package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts.map +1 -1
  38. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  39. package/dist/umd/bundle.js +3 -3
  40. package/dist/umd/bundle.js.map +1 -1
  41. package/docs/reference/transaction.md +73 -55
  42. package/docs/reference/wallet.md +1 -1
  43. package/package.json +1 -1
  44. package/src/transaction/Transaction.ts +4 -4
  45. package/src/transaction/fee-models/LivePolicy.ts +97 -0
  46. package/src/transaction/fee-models/__tests/LivePolicy.test.ts +148 -0
  47. package/src/transaction/fee-models/index.ts +1 -0
  48. package/src/wallet/WalletClient.ts +50 -51
  49. package/src/wallet/substrates/WalletWireProcessor.ts +21 -0
  50. 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.7",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -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(),
@@ -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'
@@ -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
- let sub: WalletInterface
80
- const checkSub = async (timeout?: number): Promise<void> => {
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 = new XDMSubstrate()
104
- await checkSub(MAX_XDM_RESPONSE_WAIT)
105
- this.substrate = sub
106
- } catch (e) {
107
- // HTTP wire failed, move on...
108
- try {
109
- sub = new WalletWireTransceiver(new HTTPWalletWire(this.originator))
110
- await checkSub()
111
- this.substrate = sub
112
- } catch (e) {
113
- // HTTP Wire failed, attempt the next...
114
- try {
115
- sub = new HTTPWalletJSON(this.originator)
116
- await checkSub()
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