web3-tools-mcp 3.0.0 → 3.2.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/CHANGELOG.md +115 -2
- package/README.md +96 -60
- package/dist/chain-meta.d.ts.map +1 -1
- package/dist/chain-meta.js +19 -8
- package/dist/chain-meta.js.map +1 -1
- package/dist/chains.d.ts +609 -65
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +89 -28
- package/dist/chains.js.map +1 -1
- package/dist/clear-signing.d.ts.map +1 -1
- package/dist/clear-signing.js +17 -6
- package/dist/clear-signing.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -4
- package/dist/client.js.map +1 -1
- package/dist/http-server.d.ts +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +12 -5
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +5 -6
- package/dist/index.js.map +1 -1
- package/dist/kv-storage.d.ts +2 -1
- package/dist/kv-storage.d.ts.map +1 -1
- package/dist/kv-storage.js +5 -2
- package/dist/kv-storage.js.map +1 -1
- package/dist/oauth.d.ts +18 -4
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +62 -26
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +7 -5
- package/dist/preview.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +16 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/read/advanced.d.ts +25 -20
- package/dist/tools/read/advanced.d.ts.map +1 -1
- package/dist/tools/read/advanced.js +45 -33
- package/dist/tools/read/advanced.js.map +1 -1
- package/dist/tools/read/balance.d.ts +6 -6
- package/dist/tools/read/balance.d.ts.map +1 -1
- package/dist/tools/read/balance.js +17 -32
- package/dist/tools/read/balance.js.map +1 -1
- package/dist/tools/read/contract-info.d.ts +14 -12
- package/dist/tools/read/contract-info.d.ts.map +1 -1
- package/dist/tools/read/contract-info.js +53 -122
- package/dist/tools/read/contract-info.js.map +1 -1
- package/dist/tools/read/contract.d.ts +10 -10
- package/dist/tools/read/contract.d.ts.map +1 -1
- package/dist/tools/read/contract.js +2 -1
- package/dist/tools/read/contract.js.map +1 -1
- package/dist/tools/read/defi.d.ts +86 -0
- package/dist/tools/read/defi.d.ts.map +1 -0
- package/dist/tools/read/defi.js +210 -0
- package/dist/tools/read/defi.js.map +1 -0
- package/dist/tools/read/ens.d.ts +20 -40
- package/dist/tools/read/ens.d.ts.map +1 -1
- package/dist/tools/read/ens.js +25 -33
- package/dist/tools/read/ens.js.map +1 -1
- package/dist/tools/read/gas.d.ts +16 -16
- package/dist/tools/read/gas.d.ts.map +1 -1
- package/dist/tools/read/gas.js +13 -48
- package/dist/tools/read/gas.js.map +1 -1
- package/dist/tools/read/logs.d.ts +65 -4
- package/dist/tools/read/logs.d.ts.map +1 -1
- package/dist/tools/read/logs.js +96 -115
- package/dist/tools/read/logs.js.map +1 -1
- package/dist/tools/read/signatures.d.ts.map +1 -1
- package/dist/tools/read/signatures.js +4 -3
- package/dist/tools/read/signatures.js.map +1 -1
- package/dist/tools/read/signing.d.ts.map +1 -1
- package/dist/tools/read/signing.js +8 -2
- package/dist/tools/read/signing.js.map +1 -1
- package/dist/tools/sign/transactions.d.ts +16 -16
- package/dist/tools/sign/transactions.d.ts.map +1 -1
- package/dist/tools/sign/transactions.js +54 -9
- package/dist/tools/sign/transactions.js.map +1 -1
- package/dist/tools/sign/wallet.d.ts +4 -4
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +43 -2
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +1 -0
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +18 -5
- package/dist/wallet-client.js.map +1 -1
- package/package.json +4 -7
- package/src/chain-meta.ts +16 -7
- package/src/chains.ts +101 -31
- package/src/clear-signing.ts +15 -9
- package/src/client.ts +2 -3
- package/src/http-server.ts +11 -6
- package/src/index.ts +5 -6
- package/src/kv-storage.ts +6 -3
- package/src/oauth.ts +93 -34
- package/src/preview.ts +7 -5
- package/src/tools/index.ts +14 -1
- package/src/tools/read/advanced.ts +63 -34
- package/src/tools/read/balance.ts +18 -33
- package/src/tools/read/contract-info.ts +64 -144
- package/src/tools/read/contract.ts +2 -1
- package/src/tools/read/defi.ts +287 -0
- package/src/tools/read/ens.ts +26 -33
- package/src/tools/read/gas.ts +13 -50
- package/src/tools/read/logs.ts +131 -143
- package/src/tools/read/signatures.ts +4 -4
- package/src/tools/read/signing.ts +8 -2
- package/src/tools/sign/transactions.ts +60 -9
- package/src/types.ts +0 -1
- package/src/utils.ts +47 -2
- package/src/wallet-client.ts +26 -5
|
@@ -52,12 +52,18 @@ export default {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
if (request.state === 'failed') {
|
|
55
|
+
// Only an explicit rejection proves nothing was signed. A timeout or a dropped
|
|
56
|
+
// connection can land after the wallet signed and broadcast.
|
|
57
|
+
const rejected = request.stage === 'approval' && /rejected|denied/i.test(request.error ?? '')
|
|
55
58
|
return formatResponse({
|
|
56
59
|
success: false,
|
|
57
|
-
status: '
|
|
60
|
+
status: rejected ? 'rejected' : 'outcome_unknown',
|
|
58
61
|
what: request.summary,
|
|
59
62
|
error: request.error,
|
|
60
|
-
|
|
63
|
+
...(request.txHash && { transactionHash: request.txHash }),
|
|
64
|
+
message: rejected
|
|
65
|
+
? 'The wallet rejected it. Nothing was signed.'
|
|
66
|
+
: 'It did not complete here, but it may still have been signed or broadcast. Do not retry until the wallet or a block explorer shows it did not go through.'
|
|
61
67
|
})
|
|
62
68
|
}
|
|
63
69
|
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto'
|
|
2
2
|
import { type AbiFunction, type Address, encodeFunctionData, isAddress, parseAbiItem, parseUnits, toHex } from 'viem'
|
|
3
3
|
import { z } from 'zod'
|
|
4
|
+
import { tokenMeta } from '../../chain-meta.js'
|
|
4
5
|
import { getClientManager, SUPPORTED_CHAINS } from '../../client.js'
|
|
5
6
|
import { log } from '../../log.js'
|
|
6
7
|
import { buildTxPreview, type RawTx, type TxPreview } from '../../preview.js'
|
|
7
8
|
import { type SigningProgress, type SigningRequest, signingCall } from '../../signing-requests.js'
|
|
8
9
|
import type { ChainName } from '../../types.js'
|
|
9
|
-
import { createTool, formatResponse } from '../../utils.js'
|
|
10
|
+
import { convertArgumentsToTypes, createTool, formatResponse } from '../../utils.js'
|
|
10
11
|
import { getWalletClient } from '../../wallet-client.js'
|
|
11
12
|
import { getPhoneSigner } from '../../walletconnect.js'
|
|
12
13
|
import { failure } from './errors.js'
|
|
@@ -74,6 +75,33 @@ async function requirePhoneSession(identity: string, account?: string) {
|
|
|
74
75
|
return { phone, session, from }
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
/** The phone's wallet reaches chains through its own RPC, which cannot see this machine's node. */
|
|
79
|
+
function requireReachable(chain: ChainName, signWith: SignWith) {
|
|
80
|
+
if (chain === 'localhost' && signWith === 'phone') {
|
|
81
|
+
throw new Error('A phone wallet cannot sign on localhost: it has no route to this machine\'s node. Use signWith "browser".')
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The token's own decimals. A caller-supplied figure is only a cross-check: trusting a wrong
|
|
87
|
+
* one moves orders of magnitude more or less than was asked for.
|
|
88
|
+
*/
|
|
89
|
+
async function tokenDecimals(chain: ChainName, token: Address, claimed?: number): Promise<number> {
|
|
90
|
+
const onChain = (await tokenMeta(chain, token).catch(() => undefined))?.decimals
|
|
91
|
+
if (onChain === undefined) {
|
|
92
|
+
if (claimed === undefined) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Could not read decimals() from ${token} on ${chain}. Check it is an ERC20 token on this chain, or pass "decimals" explicitly.`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
return claimed
|
|
98
|
+
}
|
|
99
|
+
if (claimed !== undefined && claimed !== onChain) {
|
|
100
|
+
throw new Error(`"decimals" was ${claimed}, but ${token} reports ${onChain} on ${chain}. Nothing was sent.`)
|
|
101
|
+
}
|
|
102
|
+
return onChain
|
|
103
|
+
}
|
|
104
|
+
|
|
77
105
|
/** Long enough for a congested chain, short enough that a stuck one is reported rather than hung on. */
|
|
78
106
|
const RECEIPT_TIMEOUT = 180_000
|
|
79
107
|
|
|
@@ -140,6 +168,7 @@ async function requestSignature(
|
|
|
140
168
|
progress: SigningProgress
|
|
141
169
|
) {
|
|
142
170
|
log('info', 'Transaction', `${chain} → ${tx.to}, signing with ${signWith}`)
|
|
171
|
+
requireReachable(chain, signWith)
|
|
143
172
|
if (signWith === 'phone') return signOnPhone(chain, tx, identity, account, progress)
|
|
144
173
|
|
|
145
174
|
const wallet = getWalletClient(identity)
|
|
@@ -212,6 +241,8 @@ function previewSummary(preview: TxPreview) {
|
|
|
212
241
|
return {
|
|
213
242
|
action: preview.decoded?.intent ?? preview.decoded?.functionName,
|
|
214
243
|
protocol: preview.decoded?.protocol ?? preview.toLabel,
|
|
244
|
+
// Labels come from the contracts themselves, so the address they belong to goes too.
|
|
245
|
+
to: preview.to,
|
|
215
246
|
details: preview.decoded?.fields.map((f) => `${f.name}: ${f.value}${f.warning ? ` (${f.warning})` : ''}`),
|
|
216
247
|
simulation: preview.simulation
|
|
217
248
|
}
|
|
@@ -273,7 +304,11 @@ export default {
|
|
|
273
304
|
tokenAddress: z.string().describe('ERC20 token contract address'),
|
|
274
305
|
to: z.string().describe('Recipient address'),
|
|
275
306
|
amount: z.string().describe('Amount in token units (e.g., "100" for 100 USDC)'),
|
|
276
|
-
decimals: z
|
|
307
|
+
decimals: z
|
|
308
|
+
.number()
|
|
309
|
+
.int()
|
|
310
|
+
.optional()
|
|
311
|
+
.describe('Token decimals. Read from the token when omitted; when given, must match what the token reports.'),
|
|
277
312
|
signWith: SignWithSchema,
|
|
278
313
|
account: AccountSchema
|
|
279
314
|
}),
|
|
@@ -281,10 +316,11 @@ export default {
|
|
|
281
316
|
try {
|
|
282
317
|
const tokenAddress = requireAddress('token address', args.tokenAddress)
|
|
283
318
|
const to = requireAddress('recipient address', args.to)
|
|
319
|
+
const decimals = await tokenDecimals(args.chain as ChainName, tokenAddress, args.decimals)
|
|
284
320
|
|
|
285
321
|
const data = encodeFunctionData({
|
|
286
322
|
abi: [parseAbiItem('function transfer(address to, uint256 amount)')],
|
|
287
|
-
args: [to, parseUnits(args.amount,
|
|
323
|
+
args: [to, parseUnits(args.amount, decimals)]
|
|
288
324
|
})
|
|
289
325
|
|
|
290
326
|
const outcome = await signingCall(
|
|
@@ -327,9 +363,19 @@ export default {
|
|
|
327
363
|
contractAddress: z.string().describe('Contract address'),
|
|
328
364
|
functionAbi: z.string().describe('Function ABI definition (e.g., "function transfer(address to, uint256 amount)")'),
|
|
329
365
|
args: z
|
|
330
|
-
.array(
|
|
366
|
+
.array(
|
|
367
|
+
z.union([
|
|
368
|
+
z.string(),
|
|
369
|
+
// JSON numbers past 2^53 arrive already rounded, so signing one would send a different value.
|
|
370
|
+
z.number().refine(Number.isSafeInteger, {
|
|
371
|
+
message:
|
|
372
|
+
'Numbers must be integers within ±2^53-1. Pass large or exact values as strings, e.g. "1000000000000000000".'
|
|
373
|
+
}),
|
|
374
|
+
z.boolean()
|
|
375
|
+
])
|
|
376
|
+
)
|
|
331
377
|
.optional()
|
|
332
|
-
.describe('Function arguments in order matching the ABI signature'),
|
|
378
|
+
.describe('Function arguments in order matching the ABI signature. Pass uint/int values as strings.'),
|
|
333
379
|
value: z.string().optional().describe('Optional native value to send with transaction (in ETH units, e.g., "0.1")'),
|
|
334
380
|
signWith: SignWithSchema,
|
|
335
381
|
account: AccountSchema
|
|
@@ -342,7 +388,7 @@ export default {
|
|
|
342
388
|
const data = encodeFunctionData({
|
|
343
389
|
abi: [abiItem],
|
|
344
390
|
functionName: abiItem.name,
|
|
345
|
-
args: (args.args || [])
|
|
391
|
+
args: convertArgumentsToTypes(args.args || [], abiItem.inputs)
|
|
346
392
|
})
|
|
347
393
|
const value = args.value ? `0x${parseUnits(args.value, 18).toString(16)}` : '0x0'
|
|
348
394
|
|
|
@@ -389,8 +435,11 @@ export default {
|
|
|
389
435
|
try {
|
|
390
436
|
const sign = async (progress: SigningProgress) => {
|
|
391
437
|
if (args.signWith === 'phone') {
|
|
392
|
-
const { phone, from } = await requirePhoneSession(identity, args.account)
|
|
393
|
-
|
|
438
|
+
const { phone, session, from } = await requirePhoneSession(identity, args.account)
|
|
439
|
+
// personal_sign is chain-agnostic, but the request still has to name a chain the session holds.
|
|
440
|
+
const chain = SUPPORTED_CHAINS.find((c) => session.chains.includes(getClientManager().getChainId(c)))
|
|
441
|
+
if (!chain) throw new Error(`The paired wallet approved no chain this server knows: ${session.chains.join(', ')}.`)
|
|
442
|
+
return phone.request(chain, 'personal_sign', [toHex(args.message), from], {
|
|
394
443
|
identity,
|
|
395
444
|
account: args.account,
|
|
396
445
|
onWaiting: progress.waiting
|
|
@@ -403,7 +452,9 @@ export default {
|
|
|
403
452
|
id: generateRequestId(),
|
|
404
453
|
type: 'sign_message',
|
|
405
454
|
chain: 'any',
|
|
406
|
-
|
|
455
|
+
// Hex on both signers: a wallet reads a raw "0x…" string as bytes, so the same text
|
|
456
|
+
// would otherwise sign differently depending on where it was approved.
|
|
457
|
+
data: { message: toHex(args.message) }
|
|
407
458
|
})
|
|
408
459
|
}
|
|
409
460
|
|
package/src/types.ts
CHANGED
package/src/utils.ts
CHANGED
|
@@ -136,11 +136,13 @@ function findErrorInTrace(trace: unknown): { errorPath: SummarizedCall[]; errorM
|
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/**
|
|
139
|
-
* Coerce a JSON value into what viem expects for an ABI type.
|
|
139
|
+
* Coerce a JSON value into what viem expects for an ABI type. A list is coerced element by
|
|
140
|
+
* element — an array argument, or an OR filter on an event argument. Tuples and anything
|
|
140
141
|
* else pass through: viem validates them, and guessing here would only mangle them.
|
|
141
142
|
*/
|
|
142
143
|
function coerce(value: unknown, type: string): unknown {
|
|
143
144
|
if (value === null || value === undefined) return null
|
|
145
|
+
if (Array.isArray(value)) return value.map((item) => coerce(item, type))
|
|
144
146
|
|
|
145
147
|
if (type === 'address' || type === 'string' || type.startsWith('bytes')) {
|
|
146
148
|
return String(value)
|
|
@@ -195,6 +197,50 @@ export function convertEventArgsToTypes(
|
|
|
195
197
|
)
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
/** One tool call is one quota unit, so an unbounded batch would be a way around the quota. */
|
|
201
|
+
export const MAX_BATCH = 25
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Why an RPC call failed, without the request URL — viem puts it in `message`, and with a
|
|
205
|
+
* provider key configured that URL carries the key.
|
|
206
|
+
*/
|
|
207
|
+
export function rpcReason(error: unknown): string {
|
|
208
|
+
const { details, shortMessage } = error as { details?: string; shortMessage?: string }
|
|
209
|
+
return details || shortMessage || (error instanceof Error ? error.message : String(error))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Every configured key blanked out of `text`: RPC URLs carry them, and errors quote the URL. */
|
|
213
|
+
export function redactSecrets(text: string, config: Config): string {
|
|
214
|
+
return [config.alchemyApiKey, config.etherscanApiKey, config.hypersyncApiKey].reduce<string>(
|
|
215
|
+
(out, secret) => (secret ? out.replaceAll(secret, '[redacted]') : out),
|
|
216
|
+
text
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** A Map whose entries expire after `ttlMs`, dropping the oldest once it holds `maxSize`. */
|
|
221
|
+
export class TtlCache<V> {
|
|
222
|
+
private entries = new Map<string, { value: V; expires: number }>()
|
|
223
|
+
|
|
224
|
+
constructor(
|
|
225
|
+
private maxSize: number,
|
|
226
|
+
private ttlMs: number
|
|
227
|
+
) {}
|
|
228
|
+
|
|
229
|
+
get(key: string): V | undefined {
|
|
230
|
+
const entry = this.entries.get(key)
|
|
231
|
+
if (!entry) return undefined
|
|
232
|
+
if (entry.expires > Date.now()) return entry.value
|
|
233
|
+
this.entries.delete(key)
|
|
234
|
+
return undefined
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
set(key: string, value: V): void {
|
|
238
|
+
this.entries.delete(key)
|
|
239
|
+
this.entries.set(key, { value, expires: Date.now() + this.ttlMs })
|
|
240
|
+
if (this.entries.size > this.maxSize) this.entries.delete(this.entries.keys().next().value as string)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
198
244
|
// Format response as tool result
|
|
199
245
|
export function formatResponse(data: unknown): ToolResult {
|
|
200
246
|
const serializable = convertBigIntToString(data)
|
|
@@ -231,7 +277,6 @@ export function parseCommandLineArgs(): Config & { showHelp?: boolean } {
|
|
|
231
277
|
// Parse environment variables and command line arguments
|
|
232
278
|
config.etherscanApiKey = process.env.ETHERSCAN_API_KEY || getArgValue('--etherscan-api-key')
|
|
233
279
|
config.alchemyApiKey = process.env.ALCHEMY_API_KEY || getArgValue('--alchemy-api-key')
|
|
234
|
-
config.infuraApiKey = process.env.INFURA_API_KEY || getArgValue('--infura-api-key')
|
|
235
280
|
config.hypersyncApiKey = process.env.HYPERSYNC_API_KEY || getArgValue('--hypersync-api-key')
|
|
236
281
|
config.walletConnectProjectId = process.env.WALLETCONNECT_PROJECT_ID || getArgValue('--walletconnect-project-id')
|
|
237
282
|
|
package/src/wallet-client.ts
CHANGED
|
@@ -3,12 +3,26 @@ import { randomBytes } from 'node:crypto'
|
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { homedir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
LOCAL_PORT_ATTEMPTS,
|
|
8
|
+
roomToken,
|
|
9
|
+
SIGN_PATH,
|
|
10
|
+
type TransactionRequest,
|
|
11
|
+
type TransactionResponse,
|
|
12
|
+
WalletRelay
|
|
13
|
+
} from 'web3-wallet-relay'
|
|
7
14
|
import { WebSocket } from 'ws'
|
|
8
15
|
import { HOSTED } from './hosted.js'
|
|
9
16
|
|
|
10
17
|
const REQUEST_TIMEOUT = 300_000
|
|
11
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Past this point the page may have signed and broadcast without the answer reaching us, so
|
|
21
|
+
* a failure is not a rejection: retrying blindly could send the same thing twice.
|
|
22
|
+
*/
|
|
23
|
+
const OUTCOME_UNKNOWN =
|
|
24
|
+
'The outcome is unknown — it may still have been signed or broadcast. Check the wallet or a block explorer before retrying.'
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* How long a client with nothing in flight keeps its socket. A hosted server makes one per
|
|
14
28
|
* person who ever signed, and without this each would hold a relay connection for the life
|
|
@@ -19,7 +33,7 @@ const IDLE_TIMEOUT = 10 * 60_000
|
|
|
19
33
|
/** Always the signer token: this URL goes to a browser, which may not act as a requester. */
|
|
20
34
|
function pairingUrlFor(port: number, secret: string, identity: string): string {
|
|
21
35
|
const base = HOSTED ? (process.env.MCP_PUBLIC_URL ?? `http://localhost:${port}`) : `http://127.0.0.1:${port}`
|
|
22
|
-
return `${base.replace(/\/$/, '')}
|
|
36
|
+
return `${base.replace(/\/$/, '')}${SIGN_PATH}#t=${roomToken(secret, identity, 'signer')}`
|
|
23
37
|
}
|
|
24
38
|
const SIGNER_WAIT_TIMEOUT = 30_000
|
|
25
39
|
const CONNECT_ATTEMPTS = 8
|
|
@@ -94,6 +108,7 @@ export class WalletClient {
|
|
|
94
108
|
/** Instance fields so tests can shorten the waits instead of sitting out the real ones. */
|
|
95
109
|
private signerWaitTimeout = SIGNER_WAIT_TIMEOUT
|
|
96
110
|
private idleTimeout = IDLE_TIMEOUT
|
|
111
|
+
private requestTimeout = REQUEST_TIMEOUT
|
|
97
112
|
|
|
98
113
|
private readonly remoteUrl = process.env.WALLET_SERVER_URL
|
|
99
114
|
private readonly remoteToken = process.env.WALLET_TOKEN
|
|
@@ -234,6 +249,10 @@ export class WalletClient {
|
|
|
234
249
|
this.signerAddress = undefined
|
|
235
250
|
this.pageUrl = undefined
|
|
236
251
|
this.pageAgent = undefined
|
|
252
|
+
for (const [id, pending] of this.pending) {
|
|
253
|
+
this.pending.delete(id)
|
|
254
|
+
pending.reject(new Error(`Lost the connection to the wallet relay before the wallet answered. ${OUTCOME_UNKNOWN}`))
|
|
255
|
+
}
|
|
237
256
|
})
|
|
238
257
|
})
|
|
239
258
|
}
|
|
@@ -325,14 +344,16 @@ export class WalletClient {
|
|
|
325
344
|
return new Promise((resolve, reject) => {
|
|
326
345
|
this.pending.set(request.id, { resolve, reject })
|
|
327
346
|
this.rearmIdle()
|
|
328
|
-
ws.send(JSON.stringify(request))
|
|
347
|
+
ws.send(JSON.stringify({ ...request, expiresAt: Date.now() + this.requestTimeout }))
|
|
329
348
|
|
|
330
349
|
setTimeout(() => {
|
|
331
350
|
if (this.pending.delete(request.id)) {
|
|
332
351
|
this.rearmIdle()
|
|
333
|
-
|
|
352
|
+
// So the page stops offering it; its own expiresAt covers a cancel that never lands.
|
|
353
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'cancel', id: request.id }))
|
|
354
|
+
reject(new Error(`Wallet request timed out. ${OUTCOME_UNKNOWN}`))
|
|
334
355
|
}
|
|
335
|
-
},
|
|
356
|
+
}, this.requestTimeout)
|
|
336
357
|
})
|
|
337
358
|
}
|
|
338
359
|
|