@bsv/sdk 1.7.7 → 1.8.1
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/kvstore/GlobalKVStore.js +420 -0
- package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -0
- package/dist/cjs/src/kvstore/LocalKVStore.js +6 -6
- package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -1
- package/dist/cjs/src/kvstore/kvStoreInterpreter.js +74 -0
- package/dist/cjs/src/kvstore/kvStoreInterpreter.js.map +1 -0
- package/dist/cjs/src/kvstore/types.js +11 -0
- package/dist/cjs/src/kvstore/types.js.map +1 -0
- package/dist/cjs/src/overlay-tools/Historian.js +153 -0
- package/dist/cjs/src/overlay-tools/Historian.js.map +1 -0
- package/dist/cjs/src/script/templates/PushDrop.js +2 -2
- package/dist/cjs/src/script/templates/PushDrop.js.map +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/kvstore/GlobalKVStore.js +416 -0
- package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -0
- package/dist/esm/src/kvstore/LocalKVStore.js +6 -6
- package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -1
- package/dist/esm/src/kvstore/kvStoreInterpreter.js +47 -0
- package/dist/esm/src/kvstore/kvStoreInterpreter.js.map +1 -0
- package/dist/esm/src/kvstore/types.js +8 -0
- package/dist/esm/src/kvstore/types.js.map +1 -0
- package/dist/esm/src/overlay-tools/Historian.js +155 -0
- package/dist/esm/src/overlay-tools/Historian.js.map +1 -0
- package/dist/esm/src/script/templates/PushDrop.js +2 -2
- package/dist/esm/src/script/templates/PushDrop.js.map +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/kvstore/GlobalKVStore.d.ts +129 -0
- package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -0
- package/dist/types/src/kvstore/kvStoreInterpreter.d.ts +22 -0
- package/dist/types/src/kvstore/kvStoreInterpreter.d.ts.map +1 -0
- package/dist/types/src/kvstore/types.d.ts +106 -0
- package/dist/types/src/kvstore/types.d.ts.map +1 -0
- package/dist/types/src/overlay-tools/Historian.d.ts +92 -0
- package/dist/types/src/overlay-tools/Historian.d.ts.map +1 -0
- package/dist/types/src/script/templates/PushDrop.d.ts +6 -5
- package/dist/types/src/script/templates/PushDrop.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/script.md +7 -19
- package/docs/reference/transaction.md +75 -6
- package/docs/reference/wallet.md +1 -1
- package/package.json +1 -1
- package/src/kvstore/GlobalKVStore.ts +478 -0
- package/src/kvstore/LocalKVStore.ts +7 -7
- package/src/kvstore/__tests/GlobalKVStore.test.ts +965 -0
- package/src/kvstore/__tests/LocalKVStore.test.ts +72 -0
- package/src/kvstore/kvStoreInterpreter.ts +49 -0
- package/src/kvstore/types.ts +114 -0
- package/src/overlay-tools/Historian.ts +195 -0
- package/src/overlay-tools/__tests/Historian.test.ts +690 -0
- package/src/script/templates/PushDrop.ts +6 -5
- 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
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
Signature,
|
|
7
7
|
PublicKey
|
|
8
8
|
} from '../../primitives/index.js'
|
|
9
|
-
import { WalletInterface
|
|
9
|
+
import { WalletInterface } from '../../wallet/Wallet.interfaces.js'
|
|
10
10
|
import { Transaction } from '../../transaction/index.js'
|
|
11
|
+
import { WalletProtocol } from '../../wallet/Wallet.interfaces.js'
|
|
11
12
|
|
|
12
13
|
function verifyTruthy<T>(v: T | undefined): T {
|
|
13
14
|
if (v == null) throw new Error('must have value')
|
|
@@ -117,7 +118,7 @@ export default class PushDrop implements ScriptTemplate {
|
|
|
117
118
|
* Creates a PushDrop locking script with arbitrary data fields and a public key lock.
|
|
118
119
|
*
|
|
119
120
|
* @param {number[][]} fields - The token fields to include in the locking script.
|
|
120
|
-
* @param {
|
|
121
|
+
* @param {WalletProtocol} protocolID - The protocol ID to use.
|
|
121
122
|
* @param {string} keyID - The key ID to use.
|
|
122
123
|
* @param {string} counterparty - The counterparty involved in the transaction, "self" or "anyone".
|
|
123
124
|
* @param {boolean} [forSelf=false] - Flag indicating if the lock is for the creator (default no).
|
|
@@ -126,7 +127,7 @@ export default class PushDrop implements ScriptTemplate {
|
|
|
126
127
|
*/
|
|
127
128
|
async lock(
|
|
128
129
|
fields: number[][],
|
|
129
|
-
protocolID:
|
|
130
|
+
protocolID: WalletProtocol,
|
|
130
131
|
keyID: string,
|
|
131
132
|
counterparty: string,
|
|
132
133
|
forSelf = false,
|
|
@@ -177,7 +178,7 @@ export default class PushDrop implements ScriptTemplate {
|
|
|
177
178
|
/**
|
|
178
179
|
* Creates an unlocking script for spending a PushDrop token output.
|
|
179
180
|
*
|
|
180
|
-
* @param {
|
|
181
|
+
* @param {WalletProtocol} protocolID - The protocol ID to use.
|
|
181
182
|
* @param {string} keyID - The key ID to use.
|
|
182
183
|
* @param {string} counterparty - The counterparty involved in the transaction, "self" or "anyone".
|
|
183
184
|
* @param {string} [sourceTXID] - The TXID of the source transaction.
|
|
@@ -188,7 +189,7 @@ export default class PushDrop implements ScriptTemplate {
|
|
|
188
189
|
* @returns {Object} An object containing functions to sign the transaction and estimate the script length.
|
|
189
190
|
*/
|
|
190
191
|
unlock(
|
|
191
|
-
protocolID:
|
|
192
|
+
protocolID: WalletProtocol,
|
|
192
193
|
keyID: string,
|
|
193
194
|
counterparty: string,
|
|
194
195
|
signOutputs: 'all' | 'none' | 'single' = 'all',
|
|
@@ -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
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
BooleanDefaultTrue,
|
|
12
12
|
Byte,
|
|
13
13
|
CertificateFieldNameUnder50Bytes,
|
|
14
|
+
CertificateResult,
|
|
14
15
|
CreateActionArgs,
|
|
15
16
|
CreateActionResult,
|
|
16
17
|
DescriptionString5to50Bytes,
|
|
@@ -1672,22 +1673,33 @@ export default class WalletWireTransceiver implements WalletInterface {
|
|
|
1672
1673
|
)
|
|
1673
1674
|
const resultReader = new Utils.Reader(result)
|
|
1674
1675
|
const totalCertificates = resultReader.readVarIntNum()
|
|
1675
|
-
const certificates: Array<
|
|
1676
|
-
type: Base64String
|
|
1677
|
-
subject: PubKeyHex
|
|
1678
|
-
serialNumber: Base64String
|
|
1679
|
-
certifier: PubKeyHex
|
|
1680
|
-
revocationOutpoint: OutpointString
|
|
1681
|
-
signature: HexString
|
|
1682
|
-
fields: Record<CertificateFieldNameUnder50Bytes, Base64String>
|
|
1683
|
-
}> = []
|
|
1676
|
+
const certificates: Array<CertificateResult> = []
|
|
1684
1677
|
for (let i = 0; i < totalCertificates; i++) {
|
|
1685
1678
|
const certificateLength = resultReader.readVarIntNum()
|
|
1686
1679
|
const certificateBin = resultReader.read(certificateLength)
|
|
1687
1680
|
const cert = Certificate.fromBinary(certificateBin)
|
|
1681
|
+
const keyringForVerifier: Record<string, string> = {}
|
|
1682
|
+
if (resultReader.readInt8() === 1) {
|
|
1683
|
+
const numFields = resultReader.readVarIntNum()
|
|
1684
|
+
for (let i = 0; i < numFields; i++) {
|
|
1685
|
+
const fieldKeyLength = resultReader.readVarIntNum()
|
|
1686
|
+
const fieldKey = Utils.toUTF8(resultReader.read(fieldKeyLength))
|
|
1687
|
+
const fieldValueLength = resultReader.readVarIntNum()
|
|
1688
|
+
keyringForVerifier[fieldKey] = Utils.toBase64(
|
|
1689
|
+
resultReader.read(fieldValueLength)
|
|
1690
|
+
)
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
const verifierLength = resultReader.readVarIntNum()
|
|
1694
|
+
let verifier: string | undefined = undefined
|
|
1695
|
+
if (verifierLength > 0) {
|
|
1696
|
+
verifier = Utils.toUTF8(resultReader.read(verifierLength))
|
|
1697
|
+
}
|
|
1688
1698
|
certificates.push({
|
|
1689
1699
|
...cert,
|
|
1690
|
-
signature: cert.signature as string
|
|
1700
|
+
signature: cert.signature as string,
|
|
1701
|
+
keyring: keyringForVerifier,
|
|
1702
|
+
verifier,
|
|
1691
1703
|
})
|
|
1692
1704
|
}
|
|
1693
1705
|
return {
|