@zeam-labs/x402-mcp-bridge 2.0.6 → 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.
- package/README.md +40 -0
- package/package.json +9 -3
- 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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeam-labs/x402-mcp-bridge",
|
|
3
|
-
"version": "2.0
|
|
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 }
|