@zeam-labs/x402-mcp-bridge 2.0.5 → 2.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.
Files changed (4) hide show
  1. package/README.md +41 -1
  2. package/index.mjs +8 -7
  3. package/package.json +9 -3
  4. package/viem.mjs +263 -0
package/README.md CHANGED
@@ -98,6 +98,46 @@ A refused payment re-reads the terms and retries once, because quotes for
98
98
  non-stable assets move with the oracle. Anything still failing falls back to the
99
99
  old probe-then-pay path rather than dropping your call.
100
100
 
101
+ ## Point a chain client at it
102
+
103
+ An agent that already has a viem client does not want an MCP tool called `rpc`.
104
+ It wants its provider URL to be a wallet instead of an API key:
105
+
106
+ ```js
107
+ import { createPublicClient } from 'viem'
108
+ import { base } from 'viem/chains'
109
+ import { prism } from '@zeam-labs/x402-mcp-bridge/viem'
110
+
111
+ const client = createPublicClient({ chain: base, transport: prism({ key: process.env.X402_PRIVATE_KEY }) })
112
+ await client.getBlockNumber() // paid from the wallet, served by an archive node
113
+ ```
114
+
115
+ Nothing else in the agent changes. The transport speaks JSON-RPC to Prism's
116
+ `/rpc/base` (or `/rpc/eth` with `chain: 'eth'`) and x402 back. The first request
117
+ funds a channel and buys one block. After that it holds a line: the meter is on
118
+ while calls are flowing, off within a second of them stopping, and the line is
119
+ let go after ten idle seconds. Vouchers cost no gas; only the deposit does.
120
+
121
+ Options, all optional: `url` (the Prism host), `chain` (`base` | `eth`),
122
+ `network`, `stateDir`, `depositMultiplier`, `asset`, `salt`, `rpcUrl` (a node
123
+ of your own for the payment client's chain reads), `aheadMs` (bought time to
124
+ keep on the meter while calling, default 2000), `idleMs` (meter off after this
125
+ long with no call, default 1000), `dropAfterMs` (let the line go, default
126
+ 10000), `log`. Anything else is passed to viem's `http()`. The same `X402_*`
127
+ environment variables the bridge reads are the defaults, and the channel state
128
+ directory is shared, so a channel the bridge funded is the one the transport
129
+ uses.
130
+
131
+ The transport carries three extra methods: `state()` reports the address,
132
+ channel, whether a line is held, whether the meter is on and the milliseconds
133
+ left; `close()` switches the meter off and drops the line; `refund()` returns the
134
+ unspent collateral and the time bought and not burned to the wallet. Call
135
+ `close()` or `refund()` before your process exits, or the open socket keeps it
136
+ alive.
137
+
138
+ `test/viem.mjs` drives it against a live server with real money and checks each
139
+ of those claims.
140
+
101
141
  ## Holding a line
102
142
 
103
143
  A server may sell **time** rather than calls, with a cheaper path than paying
@@ -158,7 +198,7 @@ time and an overlapping tick is refused as `channel_busy`.
158
198
  | `X402_STATE_DIR` | `~/.x402-mcp-bridge/<host>/<address>` | channel state |
159
199
  | `X402_SALT` | scheme default | open a distinct channel. Any string; it is hashed to bytes32 |
160
200
  | `X402_MAX_SPEND` | `10000000` (=$10) | ceiling on what **this run** may spend, in micro-USD. `0` removes it — see below |
161
- | `X402_DEPOSIT_MULTIPLIER` | `400` | collateral a deposit locks in escrow, as a multiple of the seller's quote for the opening call. That opening quote carries the one-time open fee, so against zeamprism the default locks ~$0.70 of **refundable** collateral it leaves your wallet when you open the channel and returns on refund. Lower it if that is more than you want committed; the scheme refuses below 3x. |
201
+ | `X402_DEPOSIT_MULTIPLIER` | *scheme default* | how much **refundable** collateral to lock, as a multiple of the seller's quote for the opening call. Unset, the x402 scheme sizes it (minimum 3); raise it to top up less often, lower it to commit less. It leaves your wallet when you open the channel and comes back on refund it is not the price. |
162
202
 
163
203
  ## It stops spending when you stop watching
164
204
 
package/index.mjs CHANGED
@@ -42,10 +42,9 @@ if (has('--help') || has('-h')) {
42
42
  '',
43
43
  'Env: X402_PRIVATE_KEY (required), X402_MCP_URL (X402_UPSTREAM also accepted),',
44
44
  ' X402_MAX_SPEND (0 = no cap; base units of the paid asset if the server publishes no price),',
45
- ' X402_DEPOSIT_MULTIPLIER (default 400; the deposit is this times the seller\'s',
46
- ' quote for the OPENING call, held as refundable collateral against',
47
- ' zeamprism ~$0.70, since the opening quote carries the one-time open',
48
- ' fee), X402_LINE=auto|on|off,',
45
+ ' X402_DEPOSIT_MULTIPLIER (refundable collateral to lock, as a multiple of',
46
+ ' the opening quote; unset uses the x402 scheme default, minimum 3),',
47
+ ' X402_LINE=auto|on|off,',
49
48
  ' X402_SALT.',
50
49
  ' auto: buy per-call minimum holds until calls arrive faster than the',
51
50
  ' server minimum hold, then hold a line while that lasts. A held line',
@@ -103,7 +102,9 @@ try {
103
102
  if (f) channelId = f.replace(/\.json$/, '')
104
103
  } catch {}
105
104
 
106
- const depositPolicy = { depositMultiplier: Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 400) }
105
+ const depositPolicy = process.env.X402_DEPOSIT_MULTIPLIER
106
+ ? { depositMultiplier: Number(process.env.X402_DEPOSIT_MULTIPLIER) }
107
+ : {}
107
108
 
108
109
  const MAX_SPEND = Number(process.env.X402_MAX_SPEND ?? 10_000_000)
109
110
  let capReached = false
@@ -264,7 +265,7 @@ const explainPermit2 = (out) => {
264
265
  if (!token || approvalToldFor === token) return true
265
266
  approvalToldFor = token
266
267
  const per = Number(chosenAccept?.amount ?? 0)
267
- const mult = Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 400)
268
+ const mult = Number(process.env.X402_DEPOSIT_MULTIPLIER ?? 5)
268
269
  const suggested = per > 0 ? BigInt(Math.ceil(per * mult * 4)) : 0n
269
270
  log(`${token} moves through Permit2 and your wallet has not approved it.`)
270
271
  log(` send once, from your wallet: approve(${PERMIT2}, ${suggested || '<amount>'}) on ${token}`)
@@ -421,7 +422,7 @@ const callOnLine = async (name, args) => {
421
422
  dropLine(reasonFrom(out) ?? 'refused by the server')
422
423
  if (attempt === 2) {
423
424
  log('the line was refused twice; paying for this call directly. If the reason above ' +
424
- 'is collateral, raise X402_DEPOSIT_MULTIPLIER — the minimum of 3 buys under a second of line.')
425
+ 'is collateral, raise X402_DEPOSIT_MULTIPLIER — the scheme minimum is 3.')
425
426
  }
426
427
  }
427
428
  return payFirst(name, args)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeam-labs/x402-mcp-bridge",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
4
4
  "description": "Put a wallet in front of any x402-paid MCP server, and hold a metered line on the ones that sell time. Stock MCP clients cannot construct x402 payments; this one does, and proxies your existing client through it.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "main": "index.mjs",
10
10
  "files": [
11
11
  "index.mjs",
12
+ "viem.mjs",
12
13
  "README.md"
13
14
  ],
14
15
  "engines": {
@@ -17,13 +18,18 @@
17
18
  "license": "MIT",
18
19
  "dependencies": {
19
20
  "@modelcontextprotocol/sdk": "1.30.0",
20
- "@x402/mcp": "2.22.0",
21
21
  "@x402/evm": "2.22.0",
22
+ "@x402/fetch": "2.22.0",
23
+ "@x402/mcp": "2.22.0",
22
24
  "viem": "2.55.16"
23
25
  },
24
26
  "repository": {
25
27
  "type": "git",
26
28
  "url": "https://github.com/zeam-labs/x402-mcp-bridge"
27
29
  },
28
- "homepage": "https://mcp.zeamprism.com/llms.txt"
30
+ "homepage": "https://mcp.zeamprism.com/llms.txt",
31
+ "exports": {
32
+ ".": "./index.mjs",
33
+ "./viem": "./viem.mjs"
34
+ }
29
35
  }
package/viem.mjs ADDED
@@ -0,0 +1,263 @@
1
+ // A viem transport that pays ZEAM Prism from a wallet.
2
+ //
3
+ // import { prism } from '@zeam-labs/x402-mcp-bridge/viem'
4
+ // const client = createPublicClient({ chain: base, transport: prism({ key }) })
5
+ //
6
+ // The agent's chain client speaks JSON-RPC to Prism's /rpc/<chain> door and
7
+ // this transport speaks x402 back. The first request funds a channel and buys
8
+ // one block; after that it holds a line, keeps time on the meter while calls
9
+ // are flowing, switches the meter off when they stop, and lets the line go
10
+ // when nothing has called for a while. Nothing in the agent changes.
11
+
12
+ import { http, createPublicClient, http as plainHttp, fallback, keccak256, toHex } from 'viem'
13
+ import { privateKeyToAccount } from 'viem/accounts'
14
+ import * as chains from 'viem/chains'
15
+ import { x402Client, x402HTTPClient, wrapFetchWithPayment } from '@x402/fetch'
16
+ import { BatchSettlementEvmScheme } from '@x402/evm/batch-settlement/client'
17
+ import { FileClientChannelStorage } from '@x402/evm/batch-settlement/client/file-storage'
18
+ import { toClientEvmSigner } from '@x402/evm'
19
+ import { mkdirSync, readdirSync } from 'node:fs'
20
+ import { homedir } from 'node:os'
21
+ import { join } from 'node:path'
22
+
23
+ const LINE_HEADER = 'x-line'
24
+ const LINE_GONE = new Set(['unknown_line', 'line_closed'])
25
+
26
+ const queue = () => {
27
+ let tail = Promise.resolve()
28
+ return (fn) => { const run = tail.then(fn, fn); tail = run.then(() => {}, () => {}); return run }
29
+ }
30
+
31
+ const saltOf = (raw) => /^0x[0-9a-fA-F]{64}$/.test(String(raw).trim()) ? String(raw).trim() : keccak256(toHex(String(raw)))
32
+
33
+ function wallet({ key, url, network, stateDir, depositMultiplier, asset, salt, rpcUrl, log }) {
34
+ if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key)) {
35
+ throw new Error('prism(): key must be a 0x-prefixed 32-byte private key. It signs vouchers locally and is never sent anywhere.')
36
+ }
37
+ const account = privateKeyToAccount(key)
38
+ const chainId = Number(String(network).split(':')[1])
39
+ const payChain = Object.values(chains).find((c) => c?.id === chainId)
40
+ if (!payChain) throw new Error(`prism(): unknown network ${network}`)
41
+ const dir = stateDir ?? join(homedir(), '.x402-mcp-bridge', new URL(url).host, account.address.toLowerCase())
42
+ mkdirSync(dir, { recursive: true })
43
+ const readers = [...(rpcUrl ? [rpcUrl] : []), ...(payChain.rpcUrls?.default?.http ?? []), new URL('/verify', url).toString()]
44
+ const pub = createPublicClient({ chain: payChain, transport: fallback(readers.map((u) => plainHttp(u))) })
45
+
46
+ const want = String(asset ?? '').toLowerCase()
47
+ const selector = (_v, accepts) => {
48
+ if (want) {
49
+ const hit = accepts.find((a) => String(a.asset).toLowerCase() === want || String(a.extra?.name ?? '').toLowerCase() === want)
50
+ if (hit) return hit
51
+ }
52
+ return accepts[0]
53
+ }
54
+ const storage = new FileClientChannelStorage({ directory: dir })
55
+ const state = { channelId: null }
56
+ try {
57
+ const f = readdirSync(join(dir, 'client')).find((n) => n.endsWith('.json'))
58
+ if (f) state.channelId = f.replace(/\.json$/, '')
59
+ } catch { }
60
+ const watched = {
61
+ get: (k) => storage.get(k),
62
+ delete: (k) => { if (state.channelId === k) state.channelId = null; return storage.delete(k) },
63
+ set: (k, ctx) => { state.channelId = k; return storage.set(k, ctx) },
64
+ }
65
+ const payments = new x402Client(selector).register(network,
66
+ new BatchSettlementEvmScheme(toClientEvmSigner(account, pub), {
67
+ ...(depositMultiplier ? { depositPolicy: { depositMultiplier: Number(depositMultiplier) } } : {}),
68
+ storage: watched,
69
+ ...(salt ? { salt: saltOf(salt) } : {}),
70
+ }))
71
+ const httpClient = new x402HTTPClient(payments)
72
+ const paidFetch = wrapFetchWithPayment(fetch, payments)
73
+ const oneAtATime = queue()
74
+ log(`prism: paying as ${account.address}, state in ${dir}`)
75
+ return { account, state, payments, httpClient, paidFetch, oneAtATime, dir }
76
+ }
77
+
78
+ function line({ url, w, aheadMs, idleMs, dropAfterMs, log }) {
79
+ const ws = new URL('/pay', url); ws.protocol = ws.protocol === 'https:' ? 'wss:' : 'ws:'
80
+ const s = { socket: null, credential: null, remainingMs: 0, readAt: 0, metering: false,
81
+ opening: null, waiting: new Map(), offTimer: null, dropTimer: null, tickTerms: null, topping: null }
82
+ const send = (frame) => { try { s.socket?.send(JSON.stringify(frame)) } catch { } }
83
+ const remaining = () => Math.max(0, s.remainingMs - (s.metering ? Date.now() - s.readAt : 0))
84
+ const read = (ms, metering) => { s.remainingMs = Number(ms ?? 0); s.readAt = Date.now(); if (metering !== undefined) s.metering = Boolean(metering) }
85
+ const settle = (op, frame) => { const w = s.waiting.get(op); if (w) { s.waiting.delete(op); w(frame) } }
86
+ const ask = (op, frame) => new Promise((resolve) => {
87
+ if (!s.socket || s.socket.readyState !== 1) return resolve(null)
88
+ s.waiting.set(op, resolve); send(frame)
89
+ setTimeout(() => { if (s.waiting.get(op) === resolve) { s.waiting.delete(op); resolve(null) } }, 10_000)
90
+ })
91
+ const drop = (why) => {
92
+ if (s.offTimer) clearTimeout(s.offTimer); if (s.dropTimer) clearTimeout(s.dropTimer)
93
+ s.offTimer = s.dropTimer = null
94
+ for (const [, w] of s.waiting) w(null); s.waiting.clear()
95
+ try { s.socket?.close() } catch { }
96
+ if (s.credential) log(`prism: line closed (${why})`)
97
+ s.socket = null; s.credential = null; s.metering = false
98
+ }
99
+ const terms = async () => {
100
+ if (s.tickTerms) return s.tickTerms
101
+ try {
102
+ const j = await (await fetch(new URL('/.well-known/x402', url))).json()
103
+ const accepts = Array.isArray(j.tickAccepts) && j.tickAccepts.length ? j.tickAccepts : j.accepts
104
+ if (Array.isArray(accepts) && accepts.length) s.tickTerms = { x402Version: j.x402Version ?? 2, accepts }
105
+ } catch { }
106
+ return s.tickTerms
107
+ }
108
+ const open = () => {
109
+ if (s.credential) return Promise.resolve(s.credential)
110
+ if (s.opening) return s.opening
111
+ if (!w.state.channelId) return Promise.resolve(null)
112
+ s.opening = new Promise((resolve) => {
113
+ let socket
114
+ try { socket = new WebSocket(ws.toString()) } catch (e) { log(`prism: line: ${e.message}`); return resolve(null) }
115
+ const giveUp = setTimeout(() => { try { socket.close() } catch { } ; resolve(null) }, 10_000)
116
+ socket.onmessage = async (ev) => {
117
+ let m; try { m = JSON.parse(String(ev.data)) } catch { return }
118
+ if (m.op === 'challenge') {
119
+ try { socket.send(JSON.stringify({ op: 'prove', signature: await w.account.signMessage({ message: m.message }) })) }
120
+ catch (e) { clearTimeout(giveUp); log(`prism: cannot sign the open challenge: ${e.message}`); try { socket.close() } catch { } ; resolve(null) }
121
+ return
122
+ }
123
+ if (m.op === 'open_failed') { clearTimeout(giveUp); log(`prism: line refused: ${m.error ?? m.why}`); try { socket.close() } catch { } ; return resolve(null) }
124
+ if (m.op === 'opened') {
125
+ clearTimeout(giveUp)
126
+ s.socket = socket; s.credential = m.credential; read(m.msRemaining, m.metering)
127
+ log(`prism: line open, ${m.msRemaining}ms on the meter, collateral buys ${m.buysMs ?? '?'}ms`)
128
+ return resolve(s.credential)
129
+ }
130
+ if (m.op === 'meter') { read(m.msRemaining, m.metering); return }
131
+ if (m.op === 'on' || m.op === 'off') { read(m.msRemaining, m.op === 'on'); return settle(m.op, m) }
132
+ if (m.op === 'refunded' || m.op === 'refund_failed') return settle('refund', m)
133
+ if (m.op === 'closing') { log(`prism: line closing: ${m.why}`); return drop(m.why) }
134
+ if (m.op === 'error') log(`prism: /pay: ${m.error}`)
135
+ }
136
+ socket.onopen = () => socket.send(JSON.stringify({ op: 'open', channelId: w.state.channelId }))
137
+ socket.onclose = () => { clearTimeout(giveUp); drop('socket closed'); resolve(null) }
138
+ socket.onerror = () => { }
139
+ }).finally(() => { s.opening = null })
140
+ return s.opening
141
+ }
142
+ const on = async () => { if (s.metering || !s.credential) return; const r = await ask('on', { op: 'on' }); if (!r) s.metering = false }
143
+ const off = async () => { if (!s.metering || !s.credential) return; await ask('off', { op: 'off' }) }
144
+ const touch = () => {
145
+ if (s.offTimer) clearTimeout(s.offTimer); if (s.dropTimer) clearTimeout(s.dropTimer)
146
+ s.offTimer = setTimeout(() => { off().catch(() => {}) }, idleMs); s.offTimer.unref?.()
147
+ s.dropTimer = setTimeout(() => drop('idle'), dropAfterMs); s.dropTimer.unref?.()
148
+ }
149
+ // One block at a time, in order: a voucher signs a cumulative total.
150
+ const tickOnce = () => w.oneAtATime(async () => {
151
+ if (!s.credential) return false
152
+ const t = await terms()
153
+ let r
154
+ if (t) {
155
+ const payload = await w.payments.createPaymentPayload(t)
156
+ const headers = { ...w.httpClient.encodePaymentSignatureHeader(payload), [LINE_HEADER]: s.credential, 'content-type': 'application/json' }
157
+ r = await fetch(new URL('/v1/tick', url), { method: 'POST', headers, body: '{}' })
158
+ await w.httpClient.processPaymentResult(payload, (n) => r.headers.get(n), r.status).catch(() => {})
159
+ if (r.status === 402) { s.tickTerms = null; r = null }
160
+ }
161
+ if (!r) {
162
+ r = await w.paidFetch(new URL('/v1/tick', url), { method: 'POST', headers: { [LINE_HEADER]: s.credential, 'content-type': 'application/json' }, body: '{}' })
163
+ }
164
+ const j = await r.json().catch(() => null)
165
+ if (r.status !== 200 || j?.paid === false) { drop(`tick refused: ${j?.error ?? j?.why ?? r.status}`); return false }
166
+ read(j.msRemaining, s.metering)
167
+ return true
168
+ })
169
+ // Enough for the next call now; the rest in the background.
170
+ const ensure = async (minMs) => { while (s.credential && remaining() < minMs) { if (!(await tickOnce())) return false } ; return Boolean(s.credential) }
171
+ const topUp = () => {
172
+ if (s.topping) return s.topping
173
+ s.topping = (async () => { while (s.credential && remaining() < aheadMs) { if (!(await tickOnce())) break } })().finally(() => { s.topping = null })
174
+ return s.topping
175
+ }
176
+ const refund = async () => {
177
+ const channelId = w.state.channelId
178
+ if (!channelId) return { op: 'refund_failed', why: 'no channel' }
179
+ const issued = new Date().toISOString()
180
+ const signature = await w.account.signMessage({ message: `ZEAM Prism refund\nchannel: ${channelId.toLowerCase()}\nissued: ${issued}` })
181
+ return new Promise((resolve) => {
182
+ const socket = new WebSocket(ws.toString())
183
+ const timer = setTimeout(() => { try { socket.close() } catch { } ; resolve({ op: 'refund_failed', why: 'timeout' }) }, 90_000)
184
+ socket.onmessage = (ev) => {
185
+ let m; try { m = JSON.parse(String(ev.data)) } catch { return }
186
+ if (m.op === 'refunded' || m.op === 'refund_failed' || m.error) { clearTimeout(timer); try { socket.close() } catch { } ; resolve(m) }
187
+ }
188
+ socket.onopen = () => socket.send(JSON.stringify({ op: 'refund', channelId, issued, signature }))
189
+ socket.onerror = () => { clearTimeout(timer); resolve({ op: 'refund_failed', why: 'socket error' }) }
190
+ })
191
+ }
192
+ return { s, open, on, off, touch, ensure, topUp, drop, remaining, refund }
193
+ }
194
+
195
+ export function prism(opts = {}) {
196
+ const {
197
+ key = process.env.X402_PRIVATE_KEY, url = process.env.X402_MCP_URL?.replace(/\/mcp\/?$/, '') ?? 'https://mcp.zeamprism.com',
198
+ chain = 'base', network = process.env.X402_NETWORK ?? 'eip155:8453',
199
+ stateDir = process.env.X402_STATE_DIR, depositMultiplier = process.env.X402_DEPOSIT_MULTIPLIER,
200
+ asset = process.env.X402_ASSET, salt = process.env.X402_SALT, rpcUrl = process.env.X402_RPC_URL,
201
+ aheadMs = 2000, idleMs = 1000, dropAfterMs = 10_000, blockMs = 250,
202
+ log = (...a) => console.error(...a),
203
+ ...httpConfig
204
+ } = opts
205
+ const w = wallet({ key, url, network, stateDir, depositMultiplier, asset, salt, rpcUrl, log })
206
+ const l = line({ url, w, aheadMs, idleMs, dropAfterMs, log })
207
+ const door = new URL(`/rpc/${chain}`, url).toString()
208
+
209
+ const withLine = (req) => { const r = new Request(req); r.headers.set(LINE_HEADER, l.s.credential); return r }
210
+ const codeOf = async (r) => {
211
+ const j = await r.clone().json().catch(() => null)
212
+ const d = j?.error?.data ?? j
213
+ return d?.code ?? d?.error ?? null
214
+ }
215
+
216
+ const fetchFn = async (input, init) => {
217
+ const req = new Request(input, init)
218
+ for (let attempt = 0; attempt < 3; attempt++) {
219
+ if (!l.s.credential && w.state.channelId) await l.open()
220
+ if (l.s.credential) {
221
+ await l.on()
222
+ if (!(await l.ensure(blockMs + 50))) continue
223
+ l.topUp()
224
+ const r = await fetch(withLine(req.clone()))
225
+ l.touch()
226
+ if (r.status !== 402) return r
227
+ const code = await codeOf(r)
228
+ if (code === 'line_unpaid') { l.s.metering = false; l.s.remainingMs = 0; continue }
229
+ if (LINE_GONE.has(code)) { l.drop(code); continue }
230
+ return r
231
+ }
232
+ // No line yet: this call pays for its block, and funds the channel if
233
+ // there is none. One payment at a time on a channel.
234
+ const r = await w.oneAtATime(() => w.paidFetch(req.clone()))
235
+ if (r.status !== 402) return r
236
+ const code = await codeOf(r)
237
+ if (code === 'line_required' && w.state.channelId) { await l.open(); continue }
238
+ return r
239
+ }
240
+ throw new Error('prism: could not hold a line after three attempts')
241
+ }
242
+
243
+ const transport = http(door, { ...httpConfig, fetchFn })
244
+ transport.close = async () => { await l.off().catch(() => {}); l.drop('closed') }
245
+ // Stop buying, let the payment in flight settle, then ask. A refund while a
246
+ // voucher is still being verified is refused as a request still open.
247
+ transport.refund = async () => {
248
+ l.drop('refunding')
249
+ await w.oneAtATime(() => {})
250
+ let r
251
+ for (let i = 0; i < 6; i++) {
252
+ r = await l.refund()
253
+ if (r.op === 'refunded' || !/still open/.test(String(r.why ?? ''))) return r
254
+ await new Promise((res) => setTimeout(res, 1000))
255
+ }
256
+ return r
257
+ }
258
+ transport.state = () => ({ address: w.account.address, channelId: w.state.channelId, line: Boolean(l.s.credential),
259
+ metering: l.s.metering, msRemaining: l.remaining() })
260
+ return transport
261
+ }
262
+
263
+ export { LINE_HEADER }