web3-tools-mcp 2.0.2 → 3.1.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 +182 -1
- package/README.md +76 -47
- package/dist/anvil.d.ts +17 -19
- package/dist/anvil.d.ts.map +1 -1
- package/dist/anvil.js +124 -228
- package/dist/anvil.js.map +1 -1
- 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 +9 -1
- package/dist/chains.d.ts.map +1 -1
- package/dist/chains.js +11 -2
- 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.map +1 -1
- package/dist/context.d.ts +22 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +24 -0
- package/dist/context.js.map +1 -0
- package/dist/hosted.d.ts +12 -0
- package/dist/hosted.d.ts.map +1 -0
- package/dist/hosted.js +12 -0
- package/dist/hosted.js.map +1 -0
- package/dist/http-server.d.ts +13 -0
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +22 -7
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +20 -12
- 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/lib.d.ts +24 -0
- package/dist/lib.d.ts.map +1 -0
- package/dist/lib.js +23 -0
- package/dist/lib.js.map +1 -0
- package/dist/mcp-server.d.ts +4 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +43 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/oauth.d.ts +57 -8
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +133 -63
- package/dist/oauth.js.map +1 -1
- package/dist/preview.d.ts +7 -0
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +21 -7
- package/dist/preview.js.map +1 -1
- package/dist/signing-requests.d.ts +81 -0
- package/dist/signing-requests.d.ts.map +1 -0
- package/dist/signing-requests.js +138 -0
- package/dist/signing-requests.js.map +1 -0
- package/dist/tools/index.d.ts +27 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +50 -15
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/{advanced.d.ts → read/advanced.d.ts} +39 -30
- package/dist/tools/read/advanced.d.ts.map +1 -0
- package/dist/tools/{advanced.js → read/advanced.js} +53 -41
- package/dist/tools/read/advanced.js.map +1 -0
- package/dist/tools/{balance.d.ts → read/balance.d.ts} +8 -7
- package/dist/tools/read/balance.d.ts.map +1 -0
- package/dist/tools/{balance.js → read/balance.js} +18 -33
- package/dist/tools/read/balance.js.map +1 -0
- package/dist/tools/{contract-info.d.ts → read/contract-info.d.ts} +20 -15
- package/dist/tools/read/contract-info.d.ts.map +1 -0
- package/dist/tools/{contract-info.js → read/contract-info.js} +54 -123
- package/dist/tools/read/contract-info.js.map +1 -0
- package/dist/tools/{contract.d.ts → read/contract.d.ts} +14 -12
- package/dist/tools/read/contract.d.ts.map +1 -0
- package/dist/tools/{contract.js → read/contract.js} +3 -2
- package/dist/tools/read/contract.js.map +1 -0
- package/dist/tools/read/ens.d.ts +104 -0
- package/dist/tools/read/ens.d.ts.map +1 -0
- package/dist/tools/{ens.js → read/ens.js} +25 -33
- package/dist/tools/read/ens.js.map +1 -0
- package/dist/tools/read/gas.d.ts +171 -0
- package/dist/tools/read/gas.d.ts.map +1 -0
- package/dist/tools/read/gas.js +333 -0
- package/dist/tools/read/gas.js.map +1 -0
- package/dist/tools/read/logs.d.ts +101 -0
- package/dist/tools/read/logs.d.ts.map +1 -0
- package/dist/tools/read/logs.js +211 -0
- package/dist/tools/read/logs.js.map +1 -0
- package/dist/tools/{signatures.d.ts → read/signatures.d.ts} +8 -4
- package/dist/tools/read/signatures.d.ts.map +1 -0
- package/dist/tools/{signatures.js → read/signatures.js} +4 -3
- package/dist/tools/read/signatures.js.map +1 -0
- package/dist/tools/read/signing.d.ts +27 -0
- package/dist/tools/read/signing.d.ts.map +1 -0
- package/dist/tools/read/signing.js +81 -0
- package/dist/tools/read/signing.js.map +1 -0
- package/dist/tools/sign/errors.d.ts +9 -0
- package/dist/tools/sign/errors.d.ts.map +1 -0
- package/dist/tools/sign/errors.js +29 -0
- package/dist/tools/sign/errors.js.map +1 -0
- package/dist/tools/sign/transactions.d.ts +141 -0
- package/dist/tools/sign/transactions.d.ts.map +1 -0
- package/dist/tools/sign/transactions.js +361 -0
- package/dist/tools/sign/transactions.js.map +1 -0
- package/dist/tools/sign/wallet.d.ts +50 -0
- package/dist/tools/sign/wallet.d.ts.map +1 -0
- package/dist/tools/sign/wallet.js +180 -0
- package/dist/tools/sign/wallet.js.map +1 -0
- package/dist/utils.d.ts +23 -2
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +48 -3
- package/dist/utils.js.map +1 -1
- package/dist/wallet-client.d.ts +34 -4
- package/dist/wallet-client.d.ts.map +1 -1
- package/dist/wallet-client.js +106 -27
- package/dist/wallet-client.js.map +1 -1
- package/dist/walletconnect.d.ts +100 -13
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js +215 -50
- package/dist/walletconnect.js.map +1 -1
- package/package.json +12 -9
- package/src/anvil.ts +150 -271
- package/src/chain-meta.ts +16 -7
- package/src/chains.ts +14 -2
- package/src/clear-signing.ts +15 -9
- package/src/client.ts +0 -1
- package/src/context.ts +27 -0
- package/src/hosted.ts +11 -0
- package/src/http-server.ts +38 -8
- package/src/index.ts +20 -13
- package/src/kv-storage.ts +6 -3
- package/src/lib.ts +24 -0
- package/src/mcp-server.ts +47 -0
- package/src/oauth.ts +181 -69
- package/src/preview.ts +28 -7
- package/src/signing-requests.ts +198 -0
- package/src/tools/index.ts +78 -16
- package/src/tools/{advanced.ts → read/advanced.ts} +74 -43
- package/src/tools/{balance.ts → read/balance.ts} +20 -35
- package/src/tools/{contract-info.ts → read/contract-info.ts} +66 -146
- package/src/tools/{contract.ts → read/contract.ts} +4 -3
- package/src/tools/{ens.ts → read/ens.ts} +27 -34
- package/src/tools/read/gas.ts +443 -0
- package/src/tools/read/logs.ts +281 -0
- package/src/tools/{signatures.ts → read/signatures.ts} +5 -5
- package/src/tools/read/signing.ts +92 -0
- package/src/tools/sign/errors.ts +28 -0
- package/src/tools/sign/transactions.ts +479 -0
- package/src/tools/sign/wallet.ts +216 -0
- package/src/utils.ts +52 -3
- package/src/wallet-client.ts +107 -24
- package/src/walletconnect.ts +250 -48
- package/dist/tools/advanced.d.ts.map +0 -1
- package/dist/tools/advanced.js.map +0 -1
- package/dist/tools/balance.d.ts.map +0 -1
- package/dist/tools/balance.js.map +0 -1
- package/dist/tools/contract-info.d.ts.map +0 -1
- package/dist/tools/contract-info.js.map +0 -1
- package/dist/tools/contract.d.ts.map +0 -1
- package/dist/tools/contract.js.map +0 -1
- package/dist/tools/ens.d.ts +0 -119
- package/dist/tools/ens.d.ts.map +0 -1
- package/dist/tools/ens.js.map +0 -1
- package/dist/tools/gas.d.ts +0 -99
- package/dist/tools/gas.d.ts.map +0 -1
- package/dist/tools/gas.js +0 -181
- package/dist/tools/gas.js.map +0 -1
- package/dist/tools/logs.d.ts +0 -39
- package/dist/tools/logs.d.ts.map +0 -1
- package/dist/tools/logs.js +0 -230
- package/dist/tools/logs.js.map +0 -1
- package/dist/tools/signatures.d.ts.map +0 -1
- package/dist/tools/signatures.js.map +0 -1
- package/dist/tools/transactions.d.ts +0 -155
- package/dist/tools/transactions.d.ts.map +0 -1
- package/dist/tools/transactions.js +0 -365
- package/dist/tools/transactions.js.map +0 -1
- package/src/tools/gas.ts +0 -219
- package/src/tools/logs.ts +0 -293
- package/src/tools/transactions.ts +0 -456
package/src/utils.ts
CHANGED
|
@@ -7,13 +7,16 @@ export function createTool<T extends z.ZodType>(
|
|
|
7
7
|
title: string,
|
|
8
8
|
description: string,
|
|
9
9
|
schema: T,
|
|
10
|
-
handler: (args: z.infer<T
|
|
10
|
+
handler: (args: z.infer<T>, identity: string) => Promise<ToolResult>,
|
|
11
|
+
/** Whether this server can serve the tool at all; one it cannot is never offered. */
|
|
12
|
+
available?: () => boolean
|
|
11
13
|
) {
|
|
12
14
|
return {
|
|
13
15
|
title,
|
|
14
16
|
description,
|
|
15
17
|
schema,
|
|
16
|
-
handler
|
|
18
|
+
handler,
|
|
19
|
+
available
|
|
17
20
|
}
|
|
18
21
|
}
|
|
19
22
|
|
|
@@ -133,11 +136,13 @@ function findErrorInTrace(trace: unknown): { errorPath: SummarizedCall[]; errorM
|
|
|
133
136
|
}
|
|
134
137
|
|
|
135
138
|
/**
|
|
136
|
-
* 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
|
|
137
141
|
* else pass through: viem validates them, and guessing here would only mangle them.
|
|
138
142
|
*/
|
|
139
143
|
function coerce(value: unknown, type: string): unknown {
|
|
140
144
|
if (value === null || value === undefined) return null
|
|
145
|
+
if (Array.isArray(value)) return value.map((item) => coerce(item, type))
|
|
141
146
|
|
|
142
147
|
if (type === 'address' || type === 'string' || type.startsWith('bytes')) {
|
|
143
148
|
return String(value)
|
|
@@ -192,6 +197,50 @@ export function convertEventArgsToTypes(
|
|
|
192
197
|
)
|
|
193
198
|
}
|
|
194
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.infuraApiKey, 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
|
+
|
|
195
244
|
// Format response as tool result
|
|
196
245
|
export function formatResponse(data: unknown): ToolResult {
|
|
197
246
|
const serializable = convertBigIntToString(data)
|
package/src/wallet-client.ts
CHANGED
|
@@ -3,18 +3,30 @@ 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 { LOCAL_PORT_ATTEMPTS, type TransactionRequest, type TransactionResponse, WalletRelay } from 'web3-wallet-relay'
|
|
6
|
+
import { LOCAL_PORT_ATTEMPTS, roomToken, type TransactionRequest, type TransactionResponse, WalletRelay } from 'web3-wallet-relay'
|
|
7
7
|
import { WebSocket } from 'ws'
|
|
8
|
+
import { HOSTED } from './hosted.js'
|
|
8
9
|
|
|
9
10
|
const REQUEST_TIMEOUT = 300_000
|
|
10
11
|
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Past this point the page may have signed and broadcast without the answer reaching us, so
|
|
14
|
+
* a failure is not a rejection: retrying blindly could send the same thing twice.
|
|
15
|
+
*/
|
|
16
|
+
const OUTCOME_UNKNOWN =
|
|
17
|
+
'The outcome is unknown — it may still have been signed or broadcast. Check the wallet or a block explorer before retrying.'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* How long a client with nothing in flight keeps its socket. A hosted server makes one per
|
|
21
|
+
* person who ever signed, and without this each would hold a relay connection for the life
|
|
22
|
+
* of the process. The next call reconnects.
|
|
23
|
+
*/
|
|
24
|
+
const IDLE_TIMEOUT = 10 * 60_000
|
|
14
25
|
|
|
15
|
-
|
|
26
|
+
/** Always the signer token: this URL goes to a browser, which may not act as a requester. */
|
|
27
|
+
function pairingUrlFor(port: number, secret: string, identity: string): string {
|
|
16
28
|
const base = HOSTED ? (process.env.MCP_PUBLIC_URL ?? `http://localhost:${port}`) : `http://127.0.0.1:${port}`
|
|
17
|
-
return `${base.replace(/\/$/, '')}/#t=${
|
|
29
|
+
return `${base.replace(/\/$/, '')}/#t=${roomToken(secret, identity, 'signer')}`
|
|
18
30
|
}
|
|
19
31
|
const SIGNER_WAIT_TIMEOUT = 30_000
|
|
20
32
|
const CONNECT_ATTEMPTS = 8
|
|
@@ -85,13 +97,24 @@ export class WalletClient {
|
|
|
85
97
|
private pageUrl: string | undefined
|
|
86
98
|
private pageAgent: string | undefined
|
|
87
99
|
private lastOpenedAt = 0
|
|
88
|
-
|
|
100
|
+
private idleTimer: NodeJS.Timeout | null = null
|
|
101
|
+
/** Instance fields so tests can shorten the waits instead of sitting out the real ones. */
|
|
89
102
|
private signerWaitTimeout = SIGNER_WAIT_TIMEOUT
|
|
103
|
+
private idleTimeout = IDLE_TIMEOUT
|
|
104
|
+
private requestTimeout = REQUEST_TIMEOUT
|
|
90
105
|
|
|
91
106
|
private readonly remoteUrl = process.env.WALLET_SERVER_URL
|
|
92
107
|
private readonly remoteToken = process.env.WALLET_TOKEN
|
|
93
108
|
|
|
94
|
-
|
|
109
|
+
/**
|
|
110
|
+
* `identity` is the room this client works in. Everything below it — the socket's token,
|
|
111
|
+
* the pairing link it hands out — is signed for that room, so two identities on one relay
|
|
112
|
+
* cannot see each other's pages or transactions.
|
|
113
|
+
*/
|
|
114
|
+
constructor(
|
|
115
|
+
private identity: string,
|
|
116
|
+
private relayOptions: { port?: number; token?: string } = {}
|
|
117
|
+
) {}
|
|
95
118
|
|
|
96
119
|
get isRemote(): boolean {
|
|
97
120
|
return Boolean(this.remoteUrl)
|
|
@@ -124,8 +147,8 @@ export class WalletClient {
|
|
|
124
147
|
// minute to come back.
|
|
125
148
|
for (let attempt = 0; attempt < CONNECT_ATTEMPTS; attempt++) {
|
|
126
149
|
try {
|
|
127
|
-
await this.openSocket(url, this.remoteToken)
|
|
128
|
-
this.pairingUrl = `${this.remoteUrl}#t=${this.remoteToken}`
|
|
150
|
+
await this.openSocket(url, roomToken(this.remoteToken, this.identity, 'requester'))
|
|
151
|
+
this.pairingUrl = `${this.remoteUrl}#t=${roomToken(this.remoteToken, this.identity, 'signer')}`
|
|
129
152
|
return
|
|
130
153
|
} catch (error) {
|
|
131
154
|
lastError = error as Error
|
|
@@ -147,23 +170,24 @@ export class WalletClient {
|
|
|
147
170
|
* the fix would be for the higher port to hand over when a lower one answers.
|
|
148
171
|
*/
|
|
149
172
|
private async connectLocal(): Promise<void> {
|
|
150
|
-
const
|
|
173
|
+
const secret = this.relayOptions.token ?? localToken()
|
|
174
|
+
const requester = roomToken(secret, this.identity, 'requester')
|
|
151
175
|
const ports = this.relayOptions.port ? [this.relayOptions.port] : LOCAL_PORTS
|
|
152
176
|
|
|
153
177
|
for (const port of ports) {
|
|
154
178
|
try {
|
|
155
|
-
await this.openSocket(`ws://127.0.0.1:${port}`,
|
|
156
|
-
this.pairingUrl = pairingUrlFor(port,
|
|
179
|
+
await this.openSocket(`ws://127.0.0.1:${port}`, requester)
|
|
180
|
+
this.pairingUrl = pairingUrlFor(port, secret, this.identity)
|
|
157
181
|
return
|
|
158
182
|
} catch {
|
|
159
183
|
// Nothing on this port, or something that isn't our relay — keep looking.
|
|
160
184
|
}
|
|
161
185
|
}
|
|
162
186
|
|
|
163
|
-
this.relay = new WalletRelay({ port: ports[0], token })
|
|
187
|
+
this.relay = new WalletRelay({ port: ports[0], token: secret })
|
|
164
188
|
await this.relay.start()
|
|
165
|
-
await this.openSocket(`ws://127.0.0.1:${this.relay.getPort()}`,
|
|
166
|
-
this.pairingUrl = this.relay.
|
|
189
|
+
await this.openSocket(`ws://127.0.0.1:${this.relay.getPort()}`, requester)
|
|
190
|
+
this.pairingUrl = pairingUrlFor(this.relay.getPort(), secret, this.identity)
|
|
167
191
|
}
|
|
168
192
|
|
|
169
193
|
/**
|
|
@@ -198,6 +222,7 @@ export class WalletClient {
|
|
|
198
222
|
clearTimeout(timer)
|
|
199
223
|
ws.off('message', onHandshake)
|
|
200
224
|
this.ws = ws
|
|
225
|
+
this.rearmIdle()
|
|
201
226
|
resolve()
|
|
202
227
|
}
|
|
203
228
|
|
|
@@ -217,6 +242,10 @@ export class WalletClient {
|
|
|
217
242
|
this.signerAddress = undefined
|
|
218
243
|
this.pageUrl = undefined
|
|
219
244
|
this.pageAgent = undefined
|
|
245
|
+
for (const [id, pending] of this.pending) {
|
|
246
|
+
this.pending.delete(id)
|
|
247
|
+
pending.reject(new Error(`Lost the connection to the wallet relay before the wallet answered. ${OUTCOME_UNKNOWN}`))
|
|
248
|
+
}
|
|
220
249
|
})
|
|
221
250
|
})
|
|
222
251
|
}
|
|
@@ -249,11 +278,27 @@ export class WalletClient {
|
|
|
249
278
|
const pending = this.pending.get(message.id)
|
|
250
279
|
if (!pending) return
|
|
251
280
|
this.pending.delete(message.id)
|
|
281
|
+
this.rearmIdle()
|
|
252
282
|
|
|
253
283
|
if (message.success) pending.resolve(message.result)
|
|
254
284
|
else pending.reject(new Error(message.error || 'Transaction failed'))
|
|
255
285
|
}
|
|
256
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Drop the socket, and this client, once nothing has needed it for a while. The owner of
|
|
289
|
+
* a local relay is exempt: it is the one that has to stop that relay at shutdown.
|
|
290
|
+
*/
|
|
291
|
+
private rearmIdle() {
|
|
292
|
+
if (this.idleTimer) clearTimeout(this.idleTimer)
|
|
293
|
+
if (this.relay) return
|
|
294
|
+
this.idleTimer = setTimeout(() => {
|
|
295
|
+
if (this.pending.size > 0) return this.rearmIdle()
|
|
296
|
+
this.ws?.close()
|
|
297
|
+
if (clients.get(this.identity) === this) clients.delete(this.identity)
|
|
298
|
+
}, this.idleTimeout)
|
|
299
|
+
this.idleTimer.unref()
|
|
300
|
+
}
|
|
301
|
+
|
|
257
302
|
/** Wait for a wallet page to connect, opening the browser first when running locally. */
|
|
258
303
|
async waitForSigner(): Promise<void> {
|
|
259
304
|
await this.connect()
|
|
@@ -291,11 +336,17 @@ export class WalletClient {
|
|
|
291
336
|
|
|
292
337
|
return new Promise((resolve, reject) => {
|
|
293
338
|
this.pending.set(request.id, { resolve, reject })
|
|
294
|
-
|
|
339
|
+
this.rearmIdle()
|
|
340
|
+
ws.send(JSON.stringify({ ...request, expiresAt: Date.now() + this.requestTimeout }))
|
|
295
341
|
|
|
296
342
|
setTimeout(() => {
|
|
297
|
-
if (this.pending.delete(request.id))
|
|
298
|
-
|
|
343
|
+
if (this.pending.delete(request.id)) {
|
|
344
|
+
this.rearmIdle()
|
|
345
|
+
// So the page stops offering it; its own expiresAt covers a cancel that never lands.
|
|
346
|
+
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'cancel', id: request.id }))
|
|
347
|
+
reject(new Error(`Wallet request timed out. ${OUTCOME_UNKNOWN}`))
|
|
348
|
+
}
|
|
349
|
+
}, this.requestTimeout)
|
|
299
350
|
})
|
|
300
351
|
}
|
|
301
352
|
|
|
@@ -318,8 +369,11 @@ export class WalletClient {
|
|
|
318
369
|
*/
|
|
319
370
|
getUrl(): string {
|
|
320
371
|
if (this.pairingUrl) return this.pairingUrl
|
|
321
|
-
if (this.remoteUrl)
|
|
322
|
-
|
|
372
|
+
if (this.remoteUrl) {
|
|
373
|
+
const token = this.remoteToken ? roomToken(this.remoteToken, this.identity, 'signer') : ''
|
|
374
|
+
return `${this.remoteUrl}#t=${token}`
|
|
375
|
+
}
|
|
376
|
+
return pairingUrlFor(LOCAL_PORTS[0], this.relayOptions.token ?? localToken(), this.identity)
|
|
323
377
|
}
|
|
324
378
|
|
|
325
379
|
/** A relay we do not own, or a server with no desktop, has no browser for us to open. */
|
|
@@ -388,15 +442,44 @@ export class WalletClient {
|
|
|
388
442
|
}
|
|
389
443
|
|
|
390
444
|
async stop(): Promise<void> {
|
|
445
|
+
if (this.idleTimer) clearTimeout(this.idleTimer)
|
|
391
446
|
this.ws?.close()
|
|
392
447
|
await this.relay?.stop()
|
|
393
448
|
this.relay = null
|
|
394
449
|
}
|
|
395
450
|
}
|
|
396
451
|
|
|
397
|
-
|
|
452
|
+
const clients = new Map<string, WalletClient>()
|
|
453
|
+
|
|
454
|
+
let relayOptions: { port?: number; token?: string } = {}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Where the relay is, for every client this process makes.
|
|
458
|
+
*
|
|
459
|
+
* A property of the deployment rather than of any one person: a hosted server runs the
|
|
460
|
+
* relay on its own port with its own secret, and each identity's client has to reach that
|
|
461
|
+
* one. Set it before anybody signs — a client that cannot find a relay starts its own,
|
|
462
|
+
* which then has nobody in it.
|
|
463
|
+
*/
|
|
464
|
+
export function configureWalletRelay(options: { port?: number; token?: string }): void {
|
|
465
|
+
relayOptions = options
|
|
466
|
+
}
|
|
398
467
|
|
|
399
|
-
|
|
400
|
-
|
|
468
|
+
/**
|
|
469
|
+
* One client per identity, because everything it holds is that person's: the socket, the
|
|
470
|
+
* in-flight requests, and which page is open with which account. A single-user server has
|
|
471
|
+
* exactly one, which is every self-hosted run.
|
|
472
|
+
*/
|
|
473
|
+
export function getWalletClient(identity: string): WalletClient {
|
|
474
|
+
let client = clients.get(identity)
|
|
475
|
+
if (!client) {
|
|
476
|
+
client = new WalletClient(identity, relayOptions)
|
|
477
|
+
clients.set(identity, client)
|
|
478
|
+
}
|
|
401
479
|
return client
|
|
402
480
|
}
|
|
481
|
+
|
|
482
|
+
/** Every live client, for shutdown — callers outside a tool call have no identity of their own. */
|
|
483
|
+
export function allWalletClients(): WalletClient[] {
|
|
484
|
+
return [...clients.values()]
|
|
485
|
+
}
|