ravensafe-cli 1.0.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/Docs.md +262 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/RavenSafe.js +4 -0
- package/assets/ravensafe-cli-brand.png +0 -0
- package/cli.js +4 -0
- package/package.json +55 -0
- package/src/RavenSafe.js +753 -0
- package/src/config/index.js +25 -0
- package/src/core/address.js +67 -0
- package/src/core/network.js +13 -0
- package/src/core/scan.js +218 -0
- package/src/core/session-cache.js +158 -0
- package/src/explorer/zelcore.js +526 -0
- package/src/interactive/index.js +972 -0
- package/src/interactive/ui.js +441 -0
- package/src/ledger/index.js +405 -0
- package/src/tx/builder.js +448 -0
- package/src/tx/signer.js +205 -0
- package/tools/probe-ledger.js +90 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const RVN_DONATION_ADDRESS = 'RYW4QozWJtmSipDAzXVJk2nyxRbY1fppbv';
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
branding: {
|
|
7
|
+
donations: {
|
|
8
|
+
rvn: {
|
|
9
|
+
address: RVN_DONATION_ADDRESS,
|
|
10
|
+
explorerUrl: `https://explorer.rvn.zelcore.io/address/${RVN_DONATION_ADDRESS}`,
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
ravencoin: {
|
|
15
|
+
explorerBaseUrl: 'https://explorer.rvn.zelcore.io/api',
|
|
16
|
+
feeRateSatPerByte: 1000,
|
|
17
|
+
dustSats: 546,
|
|
18
|
+
defaultChangeIndex: 0,
|
|
19
|
+
scan: {
|
|
20
|
+
balanceReceivingMaxIndex: 50,
|
|
21
|
+
balanceChangeMaxIndex: 20,
|
|
22
|
+
receiveMaxIndex: 100,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const bitcoin = require('bitcoinjs-lib');
|
|
4
|
+
const secp256k1 = require('tiny-secp256k1');
|
|
5
|
+
const { ravencoinMainnet } = require('./network');
|
|
6
|
+
|
|
7
|
+
function normalizePublicKey(publicKeyHex) {
|
|
8
|
+
if (typeof publicKeyHex !== 'string' || publicKeyHex.trim() === '') {
|
|
9
|
+
throw new Error('Expected a hex-encoded public key from Ledger');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const normalized = publicKeyHex.trim().toLowerCase();
|
|
13
|
+
if (!/^[0-9a-f]+$/.test(normalized) || normalized.length % 2 !== 0) {
|
|
14
|
+
throw new Error('Ledger returned a public key that is not valid hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const publicKey = Buffer.from(normalized, 'hex');
|
|
18
|
+
if (!secp256k1.isPoint(publicKey)) {
|
|
19
|
+
throw new Error('Ledger returned an invalid secp256k1 public key');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return publicKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compressPublicKey(publicKey) {
|
|
26
|
+
if (!Buffer.isBuffer(publicKey)) {
|
|
27
|
+
throw new Error('Expected a public key buffer');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!secp256k1.isPoint(publicKey)) {
|
|
31
|
+
throw new Error('Ledger returned an invalid secp256k1 public key');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (publicKey.length === 33 && (publicKey[0] === 0x02 || publicKey[0] === 0x03)) {
|
|
35
|
+
return publicKey;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (publicKey.length === 65 && publicKey[0] === 0x04) {
|
|
39
|
+
const x = publicKey.subarray(1, 33);
|
|
40
|
+
const y = publicKey.subarray(33, 65);
|
|
41
|
+
const prefix = (y[31] & 1) === 0 ? 0x02 : 0x03;
|
|
42
|
+
|
|
43
|
+
return Buffer.concat([Buffer.from([prefix]), x]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error('Ledger returned a public key with an unsupported encoding');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function deriveRvnP2pkhAddress(publicKeyHex) {
|
|
50
|
+
const publicKey = compressPublicKey(normalizePublicKey(publicKeyHex));
|
|
51
|
+
const payment = bitcoin.payments.p2pkh({
|
|
52
|
+
pubkey: publicKey,
|
|
53
|
+
network: ravencoinMainnet,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!payment.address) {
|
|
57
|
+
throw new Error('Could not derive a Ravencoin P2PKH address');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return payment.address;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
compressPublicKey,
|
|
65
|
+
deriveRvnP2pkhAddress,
|
|
66
|
+
normalizePublicKey,
|
|
67
|
+
};
|
package/src/core/scan.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { createExplorer } = require('../explorer/zelcore');
|
|
4
|
+
const {
|
|
5
|
+
listLedgerAddressRequests,
|
|
6
|
+
listLedgerAddresses,
|
|
7
|
+
} = require('../ledger');
|
|
8
|
+
|
|
9
|
+
const SATS_PER_RVN = 100000000n;
|
|
10
|
+
|
|
11
|
+
function formatRvn(sats) {
|
|
12
|
+
if (sats === null || sats === undefined) {
|
|
13
|
+
return 'n/a';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const negative = sats < 0n;
|
|
17
|
+
const absolute = negative ? -sats : sats;
|
|
18
|
+
const whole = absolute / SATS_PER_RVN;
|
|
19
|
+
const fraction = (absolute % SATS_PER_RVN).toString().padStart(8, '0');
|
|
20
|
+
const trimmedFraction = fraction.replace(/0+$/, '');
|
|
21
|
+
const value = trimmedFraction === '' ? whole.toString() : `${whole}.${trimmedFraction}`;
|
|
22
|
+
|
|
23
|
+
return `${negative ? '-' : ''}${value} RVN`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sumUtxos(utxos, predicate) {
|
|
27
|
+
return utxos
|
|
28
|
+
.filter(predicate)
|
|
29
|
+
.reduce((total, utxo) => total + utxo.valueSats, 0n);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function readAddressBalance(explorer, address) {
|
|
33
|
+
const utxos = await explorer.getUtxos(address);
|
|
34
|
+
let balance = null;
|
|
35
|
+
let balanceError = null;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
balance = await explorer.getAddressBalance(address);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
balanceError = error;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const confirmedFromUtxos = sumUtxos(utxos, utxo => utxo.confirmations > 0);
|
|
44
|
+
const unconfirmedFromUtxos = sumUtxos(utxos, utxo => utxo.confirmations <= 0);
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
confirmedSats: balance ? balance.confirmedSats : confirmedFromUtxos,
|
|
48
|
+
unconfirmedSats: balance && balance.unconfirmedSats !== null
|
|
49
|
+
? balance.unconfirmedSats
|
|
50
|
+
: unconfirmedFromUtxos,
|
|
51
|
+
utxos,
|
|
52
|
+
utxoCount: utxos.length,
|
|
53
|
+
txAppearances: balance ? balance.txAppearances : null,
|
|
54
|
+
unconfirmedTxAppearances: balance ? balance.unconfirmedTxAppearances : null,
|
|
55
|
+
balanceSource: balance ? 'address' : 'utxos',
|
|
56
|
+
balanceError,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function readAddressUsage(explorer, address) {
|
|
61
|
+
const balance = await explorer.getAddressBalance(address);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
confirmedSats: balance.confirmedSats,
|
|
65
|
+
unconfirmedSats: balance.unconfirmedSats,
|
|
66
|
+
utxos: [],
|
|
67
|
+
utxoCount: 0,
|
|
68
|
+
txAppearances: balance.txAppearances,
|
|
69
|
+
unconfirmedTxAppearances: balance.unconfirmedTxAppearances,
|
|
70
|
+
balanceSource: 'address',
|
|
71
|
+
balanceError: null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function buildScanErrorResult(error) {
|
|
76
|
+
return {
|
|
77
|
+
error,
|
|
78
|
+
expectedContext: null,
|
|
79
|
+
hidDetected: false,
|
|
80
|
+
hidDeviceCount: 0,
|
|
81
|
+
app: null,
|
|
82
|
+
warnings: [],
|
|
83
|
+
scanResults: [],
|
|
84
|
+
receivingConfirmedSats: 0n,
|
|
85
|
+
changeConfirmedSats: 0n,
|
|
86
|
+
grandTotalConfirmedSats: 0n,
|
|
87
|
+
totalUtxoCount: 0,
|
|
88
|
+
allMatch: false,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function resolveExplorer(options) {
|
|
93
|
+
let explorer;
|
|
94
|
+
try {
|
|
95
|
+
explorer = options.explorer || createExplorer();
|
|
96
|
+
} catch (error) {
|
|
97
|
+
return {
|
|
98
|
+
explorer: null,
|
|
99
|
+
error,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
explorer,
|
|
105
|
+
error: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function scanLedgerResultWithExplorer(ledgerResult, explorer) {
|
|
110
|
+
const result = {
|
|
111
|
+
expectedContext: ledgerResult.expectedContext,
|
|
112
|
+
hidDetected: ledgerResult.hidDetected,
|
|
113
|
+
hidDeviceCount: ledgerResult.hidDeviceCount,
|
|
114
|
+
app: ledgerResult.app,
|
|
115
|
+
warnings: ledgerResult.warnings,
|
|
116
|
+
scanResults: [],
|
|
117
|
+
receivingConfirmedSats: 0n,
|
|
118
|
+
changeConfirmedSats: 0n,
|
|
119
|
+
grandTotalConfirmedSats: 0n,
|
|
120
|
+
totalUtxoCount: 0,
|
|
121
|
+
allMatch: false,
|
|
122
|
+
error: ledgerResult.error,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (ledgerResult.error) {
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const addressItem of ledgerResult.addressResults) {
|
|
130
|
+
const scanItem = {
|
|
131
|
+
chain: addressItem.chain,
|
|
132
|
+
index: addressItem.index,
|
|
133
|
+
path: addressItem.path,
|
|
134
|
+
rvnAddress: addressItem.rvnAddress,
|
|
135
|
+
ledgerAddress: addressItem.ledgerAddress,
|
|
136
|
+
matchesLedger: addressItem.matchesLedger,
|
|
137
|
+
confirmedSats: null,
|
|
138
|
+
unconfirmedSats: null,
|
|
139
|
+
utxos: [],
|
|
140
|
+
utxoCount: 0,
|
|
141
|
+
txAppearances: null,
|
|
142
|
+
ok: false,
|
|
143
|
+
error: null,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
if (!addressItem.ok) {
|
|
147
|
+
scanItem.error = addressItem.error;
|
|
148
|
+
result.scanResults.push(scanItem);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!addressItem.matchesLedger) {
|
|
153
|
+
scanItem.error = new Error('Local RVN address does not match Ledger-returned address.');
|
|
154
|
+
result.scanResults.push(scanItem);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const balance = await readAddressBalance(explorer, addressItem.rvnAddress);
|
|
160
|
+
scanItem.confirmedSats = balance.confirmedSats;
|
|
161
|
+
scanItem.unconfirmedSats = balance.unconfirmedSats;
|
|
162
|
+
scanItem.utxos = balance.utxos;
|
|
163
|
+
scanItem.utxoCount = balance.utxoCount;
|
|
164
|
+
scanItem.txAppearances = balance.txAppearances;
|
|
165
|
+
scanItem.unconfirmedTxAppearances = balance.unconfirmedTxAppearances;
|
|
166
|
+
scanItem.balanceSource = balance.balanceSource;
|
|
167
|
+
scanItem.balanceError = balance.balanceError;
|
|
168
|
+
scanItem.ok = true;
|
|
169
|
+
} catch (error) {
|
|
170
|
+
scanItem.error = error;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result.scanResults.push(scanItem);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
result.receivingConfirmedSats = result.scanResults
|
|
177
|
+
.filter(item => item.ok && item.chain === 'receiving')
|
|
178
|
+
.reduce((total, item) => total + item.confirmedSats, 0n);
|
|
179
|
+
result.changeConfirmedSats = result.scanResults
|
|
180
|
+
.filter(item => item.ok && item.chain === 'change')
|
|
181
|
+
.reduce((total, item) => total + item.confirmedSats, 0n);
|
|
182
|
+
result.grandTotalConfirmedSats = result.receivingConfirmedSats + result.changeConfirmedSats;
|
|
183
|
+
result.totalUtxoCount = result.scanResults
|
|
184
|
+
.filter(item => item.ok)
|
|
185
|
+
.reduce((total, item) => total + item.utxoCount, 0);
|
|
186
|
+
result.allMatch = result.scanResults.length > 0 &&
|
|
187
|
+
result.scanResults.every(item => item.ok && item.matchesLedger);
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function scanLedgerAddressRequests(options = {}) {
|
|
193
|
+
const resolved = await resolveExplorer(options);
|
|
194
|
+
if (resolved.error) {
|
|
195
|
+
return buildScanErrorResult(resolved.error);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const ledgerResult = await listLedgerAddressRequests(options);
|
|
199
|
+
return scanLedgerResultWithExplorer(ledgerResult, resolved.explorer);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function scanLedgerAddresses(options = {}) {
|
|
203
|
+
const resolved = await resolveExplorer(options);
|
|
204
|
+
if (resolved.error) {
|
|
205
|
+
return buildScanErrorResult(resolved.error);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const ledgerResult = await listLedgerAddresses(options);
|
|
209
|
+
return scanLedgerResultWithExplorer(ledgerResult, resolved.explorer);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
formatRvn,
|
|
214
|
+
readAddressBalance,
|
|
215
|
+
readAddressUsage,
|
|
216
|
+
scanLedgerAddressRequests,
|
|
217
|
+
scanLedgerAddresses,
|
|
218
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function cacheKey(chain, index) {
|
|
4
|
+
return `${chain}:${index}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function cloneUtxos(utxos) {
|
|
8
|
+
if (!Array.isArray(utxos)) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return utxos.map(utxo => ({
|
|
13
|
+
...utxo,
|
|
14
|
+
}));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizeSats(value) {
|
|
18
|
+
return typeof value === 'bigint' ? value : 0n;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class SessionAddressCache {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.addresses = new Map();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get size() {
|
|
27
|
+
return this.addresses.size;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
hasAny() {
|
|
31
|
+
return this.addresses.size > 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
clear() {
|
|
35
|
+
this.addresses.clear();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
key(chain, index) {
|
|
39
|
+
return cacheKey(chain, index);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get(chain, index) {
|
|
43
|
+
return this.addresses.get(this.key(chain, index)) || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
set(item) {
|
|
47
|
+
const address = item.rvnAddress || item.address;
|
|
48
|
+
const utxos = cloneUtxos(item.utxos);
|
|
49
|
+
const entry = {
|
|
50
|
+
chain: item.chain,
|
|
51
|
+
index: item.index,
|
|
52
|
+
path: item.path,
|
|
53
|
+
address,
|
|
54
|
+
rvnAddress: address,
|
|
55
|
+
ledgerAddress: item.ledgerAddress || null,
|
|
56
|
+
matchesLedger: item.matchesLedger !== false,
|
|
57
|
+
confirmedSats: normalizeSats(item.confirmedSats),
|
|
58
|
+
unconfirmedSats: normalizeSats(item.unconfirmedSats),
|
|
59
|
+
utxos,
|
|
60
|
+
utxoCount: Number.isSafeInteger(item.utxoCount) ? item.utxoCount : utxos.length,
|
|
61
|
+
txAppearances: Number.isSafeInteger(item.txAppearances) ? item.txAppearances : null,
|
|
62
|
+
unconfirmedTxAppearances: Number.isSafeInteger(item.unconfirmedTxAppearances)
|
|
63
|
+
? item.unconfirmedTxAppearances
|
|
64
|
+
: null,
|
|
65
|
+
balanceSource: item.balanceSource || null,
|
|
66
|
+
balanceError: item.balanceError || null,
|
|
67
|
+
ok: item.ok !== false,
|
|
68
|
+
error: item.error || null,
|
|
69
|
+
refreshedAt: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
this.addresses.set(this.key(entry.chain, entry.index), entry);
|
|
73
|
+
return entry;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
upsertScanItem(item) {
|
|
77
|
+
if (!item || !item.ok || !item.matchesLedger || !item.rvnAddress) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return this.set(item);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
updateBalance(chain, index, balance) {
|
|
85
|
+
const existing = this.get(chain, index);
|
|
86
|
+
if (!existing) {
|
|
87
|
+
throw new Error(`Cannot refresh uncached address ${chain}:${index}.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return this.set({
|
|
91
|
+
...existing,
|
|
92
|
+
confirmedSats: balance.confirmedSats,
|
|
93
|
+
unconfirmedSats: balance.unconfirmedSats,
|
|
94
|
+
utxos: balance.utxos,
|
|
95
|
+
utxoCount: balance.utxoCount,
|
|
96
|
+
txAppearances: balance.txAppearances,
|
|
97
|
+
unconfirmedTxAppearances: balance.unconfirmedTxAppearances,
|
|
98
|
+
balanceSource: balance.balanceSource,
|
|
99
|
+
balanceError: balance.balanceError,
|
|
100
|
+
ok: true,
|
|
101
|
+
error: null,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
updateUsage(chain, index, balance) {
|
|
106
|
+
const existing = this.get(chain, index);
|
|
107
|
+
if (!existing) {
|
|
108
|
+
throw new Error(`Cannot refresh uncached address ${chain}:${index}.`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this.set({
|
|
112
|
+
...existing,
|
|
113
|
+
confirmedSats: balance.confirmedSats,
|
|
114
|
+
unconfirmedSats: balance.unconfirmedSats,
|
|
115
|
+
txAppearances: balance.txAppearances,
|
|
116
|
+
unconfirmedTxAppearances: balance.unconfirmedTxAppearances,
|
|
117
|
+
balanceSource: balance.balanceSource,
|
|
118
|
+
balanceError: balance.balanceError,
|
|
119
|
+
ok: true,
|
|
120
|
+
error: null,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
values() {
|
|
125
|
+
return [...this.addresses.values()];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toScanItem(entry) {
|
|
129
|
+
return {
|
|
130
|
+
chain: entry.chain,
|
|
131
|
+
index: entry.index,
|
|
132
|
+
path: entry.path,
|
|
133
|
+
rvnAddress: entry.address,
|
|
134
|
+
ledgerAddress: entry.ledgerAddress,
|
|
135
|
+
matchesLedger: entry.matchesLedger,
|
|
136
|
+
confirmedSats: entry.confirmedSats,
|
|
137
|
+
unconfirmedSats: entry.unconfirmedSats,
|
|
138
|
+
utxos: cloneUtxos(entry.utxos),
|
|
139
|
+
utxoCount: entry.utxoCount,
|
|
140
|
+
txAppearances: entry.txAppearances,
|
|
141
|
+
unconfirmedTxAppearances: entry.unconfirmedTxAppearances,
|
|
142
|
+
balanceSource: entry.balanceSource,
|
|
143
|
+
balanceError: entry.balanceError,
|
|
144
|
+
ok: entry.ok,
|
|
145
|
+
error: entry.error,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function createSessionCache() {
|
|
151
|
+
return new SessionAddressCache();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
SessionAddressCache,
|
|
156
|
+
cacheKey,
|
|
157
|
+
createSessionCache,
|
|
158
|
+
};
|