@toon-protocol/rig 3.6.0 → 4.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/README.md +112 -152
- package/dist/chunk-CDSFVZDV.js +290 -0
- package/dist/chunk-CDSFVZDV.js.map +1 -0
- package/dist/{chunk-B5ISARMU.js → chunk-GVXBUL3C.js} +15 -2
- package/dist/chunk-GVXBUL3C.js.map +1 -0
- package/dist/chunk-LTFMPOHU.js +194 -0
- package/dist/chunk-LTFMPOHU.js.map +1 -0
- package/dist/{chunk-LCYCKWAT.js → chunk-YZRR2UO6.js} +86 -5
- package/dist/chunk-YZRR2UO6.js.map +1 -0
- package/dist/{chunk-HJDPM37T.js → chunk-ZZBA5VVL.js} +11 -7
- package/dist/{chunk-HJDPM37T.js.map → chunk-ZZBA5VVL.js.map} +1 -1
- package/dist/cli/rig.js +290 -523
- package/dist/cli/rig.js.map +1 -1
- package/dist/index.d.ts +3 -115
- package/dist/index.js +9 -121
- package/dist/index.js.map +1 -1
- package/dist/{publisher-BFcnZeJI.d.ts → publisher-CcL96ugq.d.ts} +58 -1
- package/dist/standalone/index.d.ts +241 -438
- package/dist/standalone/index.js +19 -11
- package/dist/standalone-mode-LEAC4A2L.js +390 -0
- package/dist/standalone-mode-LEAC4A2L.js.map +1 -0
- package/package.json +5 -4
- package/dist/chunk-B5ISARMU.js.map +0 -1
- package/dist/chunk-IZ6KLYQ6.js +0 -724
- package/dist/chunk-IZ6KLYQ6.js.map +0 -1
- package/dist/chunk-LCYCKWAT.js.map +0 -1
- package/dist/chunk-PWNZWQOH.js +0 -404
- package/dist/chunk-PWNZWQOH.js.map +0 -1
- package/dist/chunk-UZZ2RN7A.js +0 -469
- package/dist/chunk-UZZ2RN7A.js.map +0 -1
- package/dist/network-bootstrap-27FYE3N7.js +0 -40
- package/dist/network-bootstrap-27FYE3N7.js.map +0 -1
- package/dist/standalone-mode-5MNNJC2D.js +0 -928
- package/dist/standalone-mode-5MNNJC2D.js.map +0 -1
package/dist/chunk-IZ6KLYQ6.js
DELETED
|
@@ -1,724 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
DEFAULT_CONTENT_TYPE,
|
|
3
|
-
NonceLock,
|
|
4
|
-
checkDaemonIdentity,
|
|
5
|
-
contentTypeForPath,
|
|
6
|
-
recordKey
|
|
7
|
-
} from "./chunk-LCYCKWAT.js";
|
|
8
|
-
import {
|
|
9
|
-
MAX_OBJECT_SIZE
|
|
10
|
-
} from "./chunk-B5ISARMU.js";
|
|
11
|
-
|
|
12
|
-
// src/standalone/standalone-publisher.ts
|
|
13
|
-
import { ToonClient, extractArweaveTxId } from "@toon-protocol/client";
|
|
14
|
-
var StandalonePublishError = class extends Error {
|
|
15
|
-
constructor(message) {
|
|
16
|
-
super(message);
|
|
17
|
-
this.name = "StandalonePublishError";
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
function deriveRouteDestinations(anchor) {
|
|
21
|
-
const segs = anchor.split(".");
|
|
22
|
-
if (segs.at(-1) === "store" && segs.at(-2) === "relay") {
|
|
23
|
-
const base = segs.slice(0, -2).join(".");
|
|
24
|
-
return { publish: `${base}.relay`, store: `${base}.store` };
|
|
25
|
-
}
|
|
26
|
-
return { publish: anchor, store: anchor };
|
|
27
|
-
}
|
|
28
|
-
function channelBindingKey(peerId, chain, tokenNetwork) {
|
|
29
|
-
return `${peerId}|${chain}|${tokenNetwork}`;
|
|
30
|
-
}
|
|
31
|
-
function peerIdFromBindingKey(key) {
|
|
32
|
-
return key.split("|")[0] ?? key;
|
|
33
|
-
}
|
|
34
|
-
function channelInternals(client) {
|
|
35
|
-
const c = client;
|
|
36
|
-
const negotiations = c.peerNegotiations instanceof Map ? c.peerNegotiations : void 0;
|
|
37
|
-
const cm = c.channelManager && typeof c.channelManager.trackChannel === "function" && c.channelManager.peerChannels instanceof Map ? c.channelManager : void 0;
|
|
38
|
-
const onChain = c.onChainChannelClient && c.onChainChannelClient.channelContext instanceof Map ? c.onChainChannelClient : void 0;
|
|
39
|
-
return {
|
|
40
|
-
...negotiations ? { peerNegotiations: negotiations } : {},
|
|
41
|
-
...cm ? { channelManager: cm } : {},
|
|
42
|
-
...onChain ? { onChainChannelClient: onChain } : {}
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
var StandalonePublisher = class {
|
|
46
|
-
client;
|
|
47
|
-
ownsClient;
|
|
48
|
-
publishDestination;
|
|
49
|
-
storeDestination;
|
|
50
|
-
channelDestination;
|
|
51
|
-
eventFee;
|
|
52
|
-
routePrices;
|
|
53
|
-
daemonPort;
|
|
54
|
-
skipDaemonCheck;
|
|
55
|
-
lockDir;
|
|
56
|
-
fetchImpl;
|
|
57
|
-
channelMap;
|
|
58
|
-
/** ILP anchor the channel is keyed by in the map (peer/apex destination). */
|
|
59
|
-
channelAnchor;
|
|
60
|
-
negotiationFallbacks;
|
|
61
|
-
warn;
|
|
62
|
-
storeClientConfig;
|
|
63
|
-
injectedStoreClient;
|
|
64
|
-
/** Lazily built on the first store write — see {@link storeLeg}. */
|
|
65
|
-
storeClient;
|
|
66
|
-
storeChannelId;
|
|
67
|
-
storeLegPromise;
|
|
68
|
-
lock;
|
|
69
|
-
channelId;
|
|
70
|
-
readyPromise;
|
|
71
|
-
/** Guard + client start only (no channel) — see {@link startClientOnly}. */
|
|
72
|
-
clientReadyPromise;
|
|
73
|
-
/** True when the last start() RESUMED the recorded channel (#263). */
|
|
74
|
-
lastOpenResumed = false;
|
|
75
|
-
constructor(options) {
|
|
76
|
-
if (options.client && options.clientConfig) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
"StandalonePublisher: provide either `clientConfig` or `client`, not both"
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
if (options.client) {
|
|
82
|
-
this.client = options.client;
|
|
83
|
-
this.ownsClient = false;
|
|
84
|
-
} else if (options.clientConfig) {
|
|
85
|
-
this.client = new ToonClient(options.clientConfig);
|
|
86
|
-
this.ownsClient = true;
|
|
87
|
-
} else {
|
|
88
|
-
throw new Error(
|
|
89
|
-
"StandalonePublisher: one of `clientConfig` (mnemonic-based ToonClient config) or `client` is required"
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
const anchor = options.channelDestination ?? options.clientConfig?.destinationAddress;
|
|
93
|
-
const routes = anchor ? deriveRouteDestinations(anchor) : void 0;
|
|
94
|
-
this.publishDestination = options.publishDestination ?? routes?.publish;
|
|
95
|
-
this.storeDestination = options.storeDestination ?? routes?.store;
|
|
96
|
-
this.channelDestination = options.channelDestination;
|
|
97
|
-
this.eventFee = options.eventFee ?? 1n;
|
|
98
|
-
this.routePrices = options.routePrices;
|
|
99
|
-
this.daemonPort = options.daemonPort;
|
|
100
|
-
this.skipDaemonCheck = options.skipDaemonCheck ?? false;
|
|
101
|
-
this.lockDir = options.lockDir;
|
|
102
|
-
this.fetchImpl = options.fetchImpl;
|
|
103
|
-
this.channelMap = options.channelMap;
|
|
104
|
-
this.channelAnchor = anchor;
|
|
105
|
-
if (options.storeClient && options.storeClientConfig) {
|
|
106
|
-
throw new Error(
|
|
107
|
-
"StandalonePublisher: provide either `storeClientConfig` or `storeClient`, not both"
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
this.storeClientConfig = options.storeClientConfig;
|
|
111
|
-
this.injectedStoreClient = options.storeClient;
|
|
112
|
-
this.negotiationFallbacks = options.negotiationFallbacks;
|
|
113
|
-
this.warn = options.warn ?? ((line) => process.stderr.write(`${line}
|
|
114
|
-
`));
|
|
115
|
-
}
|
|
116
|
-
/** Hex Nostr pubkey of the embedded identity (available before start). */
|
|
117
|
-
getPublicKey() {
|
|
118
|
-
return this.client.getPublicKey();
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Run the nonce guard, start the embedded client, and open (or resume) the
|
|
122
|
-
* payment channel. Called lazily by the first paid operation; safe to call
|
|
123
|
-
* eagerly to fail fast. Idempotent.
|
|
124
|
-
*/
|
|
125
|
-
start() {
|
|
126
|
-
this.readyPromise ??= (async () => {
|
|
127
|
-
await this.startClientOnly();
|
|
128
|
-
try {
|
|
129
|
-
this.channelId = await this.openOrResumeChannel();
|
|
130
|
-
} catch (err) {
|
|
131
|
-
this.lock?.release();
|
|
132
|
-
this.lock = void 0;
|
|
133
|
-
this.clientReadyPromise = void 0;
|
|
134
|
-
throw err;
|
|
135
|
-
}
|
|
136
|
-
})().catch((err) => {
|
|
137
|
-
this.readyPromise = void 0;
|
|
138
|
-
throw err;
|
|
139
|
-
});
|
|
140
|
-
return this.readyPromise;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Run the nonce guard and start the embedded client WITHOUT touching the
|
|
144
|
-
* payment channel (#263): `channel close`/`settle` operate on a RECORDED
|
|
145
|
-
* channel and must never open a fresh one as a side effect of starting.
|
|
146
|
-
* Idempotent; `start()` layers the channel open/resume on top of this.
|
|
147
|
-
*/
|
|
148
|
-
startClientOnly() {
|
|
149
|
-
this.clientReadyPromise ??= this.doStartClient().catch((err) => {
|
|
150
|
-
this.clientReadyPromise = void 0;
|
|
151
|
-
throw err;
|
|
152
|
-
});
|
|
153
|
-
return this.clientReadyPromise;
|
|
154
|
-
}
|
|
155
|
-
async doStartClient() {
|
|
156
|
-
const pubkey = this.client.getPublicKey();
|
|
157
|
-
if (!this.skipDaemonCheck) {
|
|
158
|
-
await checkDaemonIdentity(pubkey, {
|
|
159
|
-
...this.daemonPort !== void 0 ? { port: this.daemonPort } : {},
|
|
160
|
-
...this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
this.lock = await NonceLock.acquire(pubkey, {
|
|
164
|
-
...this.lockDir !== void 0 ? { dir: this.lockDir } : {}
|
|
165
|
-
});
|
|
166
|
-
try {
|
|
167
|
-
if (this.client.isStarted?.() !== true) {
|
|
168
|
-
await this.client.start();
|
|
169
|
-
}
|
|
170
|
-
this.applyNegotiationFallbacks();
|
|
171
|
-
} catch (err) {
|
|
172
|
-
this.lock.release();
|
|
173
|
-
this.lock = void 0;
|
|
174
|
-
throw err;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* Back-fill negotiated peer metadata the announce did not carry
|
|
179
|
-
* (#264/#260 root cause 3): after the client's bootstrap negotiation, any
|
|
180
|
-
* peer negotiation missing `tokenNetwork`/`tokenAddress` gets the derived
|
|
181
|
-
* per-chain fallback for its negotiated chain, BEFORE the lazy channel
|
|
182
|
-
* open reads them. Announced values are never overridden.
|
|
183
|
-
*/
|
|
184
|
-
applyNegotiationFallbacks() {
|
|
185
|
-
const fallbacks = this.negotiationFallbacks;
|
|
186
|
-
if (!fallbacks) return;
|
|
187
|
-
const negotiations = channelInternals(this.client).peerNegotiations;
|
|
188
|
-
if (!negotiations) {
|
|
189
|
-
this.warn(
|
|
190
|
-
"rig: settlement fallbacks configured but the client does not expose negotiation internals \u2014 on-chain channel opening may fail if the peer announced no TokenNetwork"
|
|
191
|
-
);
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
for (const negotiation of negotiations.values()) {
|
|
195
|
-
if (!negotiation.tokenNetwork) {
|
|
196
|
-
const tokenNetwork = fallbacks.tokenNetworks?.[negotiation.chain];
|
|
197
|
-
if (tokenNetwork) negotiation.tokenNetwork = tokenNetwork;
|
|
198
|
-
}
|
|
199
|
-
if (!negotiation.tokenAddress) {
|
|
200
|
-
const tokenAddress = fallbacks.preferredTokens?.[negotiation.chain];
|
|
201
|
-
if (tokenAddress) negotiation.tokenAddress = tokenAddress;
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Open the payment channel — or, when the channel map has a record for
|
|
207
|
-
* (identity, anchor), RESUME the recorded on-chain channel (#262).
|
|
208
|
-
*
|
|
209
|
-
* Resume seeds the client's ChannelManager (`trackChannel` rehydrates the
|
|
210
|
-
* cumulative-claim watermark from channels.json; `peerChannels` makes the
|
|
211
|
-
* subsequent `openChannel` return the same id instead of opening on-chain).
|
|
212
|
-
* A fresh open is RECORDED so the next invocation resumes it. A corrupt map
|
|
213
|
-
* file throws BEFORE anything is opened — never a silent duplicate open.
|
|
214
|
-
*/
|
|
215
|
-
async openOrResumeChannel() {
|
|
216
|
-
return this.openOrResumeChannelOn(this.client, this.channelDestination, {
|
|
217
|
-
anchor: this.channelAnchor,
|
|
218
|
-
trackResumeState: true
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* {@link openOrResumeChannel} for an arbitrary client + destination, so
|
|
223
|
-
* the store leg gets the same resume-before-open guarantee (and the same
|
|
224
|
-
* corruption check) as the publish leg rather than a second, weaker path.
|
|
225
|
-
*
|
|
226
|
-
* `anchor` defaults to the destination: a leg other than the publish one
|
|
227
|
-
* is keyed in the channel map by the route it pays for.
|
|
228
|
-
* `trackResumeState` is publish-only — {@link lastOpenResumed} feeds the
|
|
229
|
-
* `resumed` field of this publisher's channel status, which describes the
|
|
230
|
-
* publish channel and would be wrong to overwrite from a store open.
|
|
231
|
-
*/
|
|
232
|
-
async openOrResumeChannelOn(client, destination, opts = {}) {
|
|
233
|
-
const map = this.channelMap;
|
|
234
|
-
const trackResumeState = opts.trackResumeState ?? false;
|
|
235
|
-
if (!map) {
|
|
236
|
-
return client.openChannel(destination);
|
|
237
|
-
}
|
|
238
|
-
const anchorFor = opts.anchor ?? destination;
|
|
239
|
-
if (!anchorFor) {
|
|
240
|
-
this.warn(
|
|
241
|
-
"rig: no channel anchor destination configured \u2014 the peer\u2192channel mapping cannot be persisted, so this run may open a fresh channel"
|
|
242
|
-
);
|
|
243
|
-
return client.openChannel(destination);
|
|
244
|
-
}
|
|
245
|
-
const anchor = anchorFor;
|
|
246
|
-
const identity = client.getPublicKey();
|
|
247
|
-
const candidates = map.listFor(identity, anchor);
|
|
248
|
-
const internals = channelInternals(client);
|
|
249
|
-
const resumed = await this.resumeRecordedChannel(
|
|
250
|
-
map,
|
|
251
|
-
candidates,
|
|
252
|
-
internals,
|
|
253
|
-
client
|
|
254
|
-
);
|
|
255
|
-
const channelId = await client.openChannel(destination);
|
|
256
|
-
if (resumed && channelId === resumed.channelId) {
|
|
257
|
-
if (trackResumeState) this.lastOpenResumed = true;
|
|
258
|
-
map.touch(recordKey(resumed));
|
|
259
|
-
} else {
|
|
260
|
-
if (trackResumeState) this.lastOpenResumed = false;
|
|
261
|
-
this.recordOpenedChannel(
|
|
262
|
-
map,
|
|
263
|
-
internals,
|
|
264
|
-
identity,
|
|
265
|
-
anchor,
|
|
266
|
-
channelId,
|
|
267
|
-
client
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
return channelId;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Try to resume one recorded channel: the first candidate whose peer is
|
|
274
|
-
* still negotiated on the SAME chain + tokenNetwork and whose watermark
|
|
275
|
-
* does not show it closed/settled. Returns the resumed record, if any.
|
|
276
|
-
*/
|
|
277
|
-
async resumeRecordedChannel(map, candidates, internals, client) {
|
|
278
|
-
if (candidates.length === 0) return void 0;
|
|
279
|
-
const cm = internals.channelManager;
|
|
280
|
-
if (!cm?.trackChannel || !cm.peerChannels) {
|
|
281
|
-
this.warn(
|
|
282
|
-
"rig: a recorded channel exists but the client does not expose channel internals to resume it \u2014 a fresh channel may be opened"
|
|
283
|
-
);
|
|
284
|
-
return void 0;
|
|
285
|
-
}
|
|
286
|
-
for (const record of candidates) {
|
|
287
|
-
const negotiation = internals.peerNegotiations?.get(record.peerId);
|
|
288
|
-
if (!negotiation || negotiation.chain !== record.chain || (negotiation.tokenNetwork ?? "") !== record.tokenNetwork) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
const watermark = map.readWatermark(record.channelId);
|
|
292
|
-
if (watermark?.closedAt !== void 0 || watermark?.settledAt !== void 0) {
|
|
293
|
-
continue;
|
|
294
|
-
}
|
|
295
|
-
if (!watermark) {
|
|
296
|
-
this.warn(
|
|
297
|
-
`rig: resuming channel ${record.channelId} with no local claim watermark (${map.watermarkPath}) \u2014 if this channel was claimed against before, the peer will reject the stale claims; remove its entry from ${map.mapPath} to open a fresh channel instead`
|
|
298
|
-
);
|
|
299
|
-
}
|
|
300
|
-
cm.trackChannel(record.channelId, record.context);
|
|
301
|
-
cm.peerChannels.set(
|
|
302
|
-
channelBindingKey(record.peerId, record.chain, record.tokenNetwork),
|
|
303
|
-
record.channelId
|
|
304
|
-
);
|
|
305
|
-
if (record.context.chainType === "evm" && record.depositTotal === void 0 && client.rehydrateChannelDeposit) {
|
|
306
|
-
try {
|
|
307
|
-
const deposit = await client.rehydrateChannelDeposit(
|
|
308
|
-
record.channelId,
|
|
309
|
-
{
|
|
310
|
-
chain: record.chain,
|
|
311
|
-
tokenNetworkAddress: record.context.tokenNetworkAddress
|
|
312
|
-
}
|
|
313
|
-
);
|
|
314
|
-
if (deposit !== void 0) {
|
|
315
|
-
map.touch(recordKey(record), {
|
|
316
|
-
depositTotal: deposit.toString()
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
} catch (err) {
|
|
320
|
-
this.warn(
|
|
321
|
-
`rig: deposit re-read for resumed channel ${record.channelId} failed: ${err instanceof Error ? err.message : String(err)}`
|
|
322
|
-
);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
return record;
|
|
326
|
-
}
|
|
327
|
-
return void 0;
|
|
328
|
-
}
|
|
329
|
-
/** Record a freshly opened channel (+ seed its claim watermark at 0/0). */
|
|
330
|
-
recordOpenedChannel(map, internals, identity, destination, channelId, client) {
|
|
331
|
-
let peerId;
|
|
332
|
-
for (const [key, channel] of internals.channelManager?.peerChannels ?? []) {
|
|
333
|
-
if (channel === channelId) {
|
|
334
|
-
peerId = peerIdFromBindingKey(key);
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
const negotiation = peerId !== void 0 ? internals.peerNegotiations?.get(peerId) : void 0;
|
|
339
|
-
if (peerId === void 0 || !negotiation) {
|
|
340
|
-
this.warn(
|
|
341
|
-
`rig: opened channel ${channelId} but could not record the peer\u2192channel mapping (client does not expose negotiation internals) \u2014 the NEXT invocation may open another channel`
|
|
342
|
-
);
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
let depositTotal;
|
|
346
|
-
try {
|
|
347
|
-
depositTotal = client.getChannelDepositTotal?.(channelId);
|
|
348
|
-
} catch {
|
|
349
|
-
}
|
|
350
|
-
map.record({
|
|
351
|
-
channelId,
|
|
352
|
-
peerId,
|
|
353
|
-
identity,
|
|
354
|
-
destination,
|
|
355
|
-
chain: negotiation.chain,
|
|
356
|
-
tokenNetwork: negotiation.tokenNetwork ?? "",
|
|
357
|
-
// Context mirrors the daemon's persistApexChannel shape — exactly what
|
|
358
|
-
// trackChannel needs on resume.
|
|
359
|
-
context: {
|
|
360
|
-
chainType: negotiation.chainType,
|
|
361
|
-
chainId: typeof negotiation.chainId === "number" ? negotiation.chainId : 0,
|
|
362
|
-
tokenNetworkAddress: negotiation.tokenNetwork ?? "",
|
|
363
|
-
...negotiation.tokenAddress ? { tokenAddress: negotiation.tokenAddress } : {},
|
|
364
|
-
recipient: negotiation.settlementAddress
|
|
365
|
-
},
|
|
366
|
-
...depositTotal !== void 0 && depositTotal > 0n ? { depositTotal: depositTotal.toString() } : {}
|
|
367
|
-
});
|
|
368
|
-
map.seedWatermark(channelId);
|
|
369
|
-
}
|
|
370
|
-
/** Release the identity lock and stop the embedded client (if we own it). */
|
|
371
|
-
async stop() {
|
|
372
|
-
this.lock?.release();
|
|
373
|
-
this.lock = void 0;
|
|
374
|
-
this.readyPromise = void 0;
|
|
375
|
-
this.clientReadyPromise = void 0;
|
|
376
|
-
this.channelId = void 0;
|
|
377
|
-
if (this.storeClient) {
|
|
378
|
-
const storeClient = this.storeClient;
|
|
379
|
-
this.storeClient = void 0;
|
|
380
|
-
this.storeLegPromise = void 0;
|
|
381
|
-
this.storeChannelId = void 0;
|
|
382
|
-
await storeClient.stop();
|
|
383
|
-
}
|
|
384
|
-
if (this.ownsClient && this.client.isStarted?.() !== false) {
|
|
385
|
-
await this.client.stop();
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// ── Money lifecycle (#263) ──────────────────────────────────────────────────
|
|
389
|
-
/**
|
|
390
|
-
* Explicit `rig channel open`: the SAME resume-or-open path the lazy paid
|
|
391
|
-
* writes use (guard → start → resume the recorded channel or open + record
|
|
392
|
-
* a fresh one), surfaced with a receipt — plus an optional extra collateral
|
|
393
|
-
* deposit on top of the open/resume.
|
|
394
|
-
*/
|
|
395
|
-
async openChannelExplicit(opts) {
|
|
396
|
-
await this.start();
|
|
397
|
-
const channelId = this.requireChannel();
|
|
398
|
-
const identity = this.client.getPublicKey();
|
|
399
|
-
const destination = this.channelAnchor ?? "";
|
|
400
|
-
const record = this.channelMap && this.channelAnchor ? this.channelMap.listFor(identity, this.channelAnchor).find((r) => r.channelId === channelId) : void 0;
|
|
401
|
-
const outcome = {
|
|
402
|
-
channelId,
|
|
403
|
-
resumed: this.lastOpenResumed,
|
|
404
|
-
destination,
|
|
405
|
-
...record ? { chain: record.chain, peerId: record.peerId } : {},
|
|
406
|
-
...record?.depositTotal !== void 0 ? { depositTotal: record.depositTotal } : {}
|
|
407
|
-
};
|
|
408
|
-
if (opts?.deposit !== void 0 && opts.deposit > 0n) {
|
|
409
|
-
if (!this.client.depositToChannel) {
|
|
410
|
-
throw new StandalonePublishError(
|
|
411
|
-
"this client build does not support channel deposits (depositToChannel is unavailable)"
|
|
412
|
-
);
|
|
413
|
-
}
|
|
414
|
-
const deposited = await this.client.depositToChannel(
|
|
415
|
-
channelId,
|
|
416
|
-
opts.deposit
|
|
417
|
-
);
|
|
418
|
-
outcome.depositAdded = opts.deposit.toString();
|
|
419
|
-
outcome.depositTotal = deposited.depositTotal;
|
|
420
|
-
if (deposited.txHash) outcome.depositTxHash = deposited.txHash;
|
|
421
|
-
if (record) {
|
|
422
|
-
this.channelMap?.touch(recordKey(record), {
|
|
423
|
-
depositTotal: deposited.depositTotal
|
|
424
|
-
});
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
return outcome;
|
|
428
|
-
}
|
|
429
|
-
/**
|
|
430
|
-
* Adopt a RECORDED channel into the running client so on-chain close/
|
|
431
|
-
* settle/deposit can act on it: `trackChannel` rehydrates the claim
|
|
432
|
-
* watermark + withdraw timers from channels.json, `peerChannels` binds the
|
|
433
|
-
* peer, and the on-chain client's context cache is re-seeded (it only
|
|
434
|
-
* learns context at open time — a resumed channel would otherwise throw
|
|
435
|
-
* "No on-chain context").
|
|
436
|
-
*/
|
|
437
|
-
adoptRecordedChannel(record) {
|
|
438
|
-
const internals = channelInternals(this.client);
|
|
439
|
-
const cm = internals.channelManager;
|
|
440
|
-
if (cm?.trackChannel && cm.peerChannels) {
|
|
441
|
-
cm.trackChannel(record.channelId, record.context);
|
|
442
|
-
cm.peerChannels.set(
|
|
443
|
-
channelBindingKey(record.peerId, record.chain, record.tokenNetwork),
|
|
444
|
-
record.channelId
|
|
445
|
-
);
|
|
446
|
-
}
|
|
447
|
-
const contextCache = internals.onChainChannelClient?.channelContext;
|
|
448
|
-
if (contextCache && !contextCache.has(record.channelId)) {
|
|
449
|
-
contextCache.set(record.channelId, {
|
|
450
|
-
chain: record.chain,
|
|
451
|
-
tokenNetworkAddress: record.context.tokenNetworkAddress,
|
|
452
|
-
...record.context.tokenAddress ? { tokenAddress: record.context.tokenAddress } : {}
|
|
453
|
-
});
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
/**
|
|
457
|
-
* Close a recorded channel: starts the on-chain settlement challenge
|
|
458
|
-
* window. The client persists `closedAt`/`settleableAt` into the claim
|
|
459
|
-
* watermark store, which is exactly where `rig channel list`/`balance`
|
|
460
|
-
* derive the closing/settleable/settled status from. Guard + client start,
|
|
461
|
-
* but NEVER a channel open ({@link startClientOnly}).
|
|
462
|
-
*/
|
|
463
|
-
async closeRecordedChannel(record) {
|
|
464
|
-
await this.startClientOnly();
|
|
465
|
-
if (!this.client.closeChannel) {
|
|
466
|
-
throw new StandalonePublishError(
|
|
467
|
-
"this client build does not support closing channels (closeChannel is unavailable)"
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
this.adoptRecordedChannel(record);
|
|
471
|
-
const result = await this.client.closeChannel(record.channelId);
|
|
472
|
-
this.channelMap?.touch(recordKey(record));
|
|
473
|
-
return result;
|
|
474
|
-
}
|
|
475
|
-
/**
|
|
476
|
-
* Settle a recorded channel after its challenge window — releases the
|
|
477
|
-
* remaining collateral. The client enforces the `now >= settleableAt` time
|
|
478
|
-
* guard BEFORE spending gas (a too-early call throws its retryable
|
|
479
|
-
* SettleTooEarlyError) and persists `settledAt` into the watermark store.
|
|
480
|
-
*/
|
|
481
|
-
async settleRecordedChannel(record) {
|
|
482
|
-
await this.startClientOnly();
|
|
483
|
-
if (!this.client.settleChannel) {
|
|
484
|
-
throw new StandalonePublishError(
|
|
485
|
-
"this client build does not support settling channels (settleChannel is unavailable)"
|
|
486
|
-
);
|
|
487
|
-
}
|
|
488
|
-
this.adoptRecordedChannel(record);
|
|
489
|
-
const result = await this.client.settleChannel(record.channelId);
|
|
490
|
-
this.channelMap?.touch(recordKey(record));
|
|
491
|
-
return result;
|
|
492
|
-
}
|
|
493
|
-
/**
|
|
494
|
-
* The full multi-chain wallet view (#299) for the embedded identity — native
|
|
495
|
-
* coin + configured tokens (USDC) per chain — a FREE read on the UNSTARTED
|
|
496
|
-
* client (no nonce guard, no uplink, no channel). The client derives the
|
|
497
|
-
* Solana/Mina addresses from the mnemonic on demand, so ALL configured chains
|
|
498
|
-
* appear even before a start. Best-effort per chain (an unreachable RPC yields
|
|
499
|
-
* an `unreadable` chain, not a failure). `fallback` supplies Solana/Mina RPC
|
|
500
|
-
* defaults (e.g. the network preset) for chains the config has no channel for.
|
|
501
|
-
*/
|
|
502
|
-
async readWalletChainBalances(fallback) {
|
|
503
|
-
if (!this.client.getWalletBalances) return [];
|
|
504
|
-
return await this.client.getWalletBalances(fallback);
|
|
505
|
-
}
|
|
506
|
-
// ── Publisher ─────────────────────────────────────────────────────────────
|
|
507
|
-
/**
|
|
508
|
-
* Fee rates for `planPush` estimation, both flat per packet (ADR 0020),
|
|
509
|
-
* with the announced route prices folded in so the ESTIMATE always equals
|
|
510
|
-
* the CLAIMS: `eventFee` is returned pre-floored at the publish route price,
|
|
511
|
-
* and `uploadFee` IS the store route price.
|
|
512
|
-
*/
|
|
513
|
-
getFeeRates() {
|
|
514
|
-
return Promise.resolve({
|
|
515
|
-
uploadFee: this.uploadFee(),
|
|
516
|
-
eventFee: this.effectiveEventFee()
|
|
517
|
-
});
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* The flat per-event claim: `eventFee`, floored at the publish route's
|
|
521
|
-
* announced price (the connector rejects a claim below it — F06).
|
|
522
|
-
*/
|
|
523
|
-
effectiveEventFee() {
|
|
524
|
-
const min = this.routePrices?.publish ?? 0n;
|
|
525
|
-
return this.eventFee > min ? this.eventFee : min;
|
|
526
|
-
}
|
|
527
|
-
/**
|
|
528
|
-
* The per-upload claim: the store route's flat price, whatever the object's
|
|
529
|
-
* size. Byte-proportional pricing has no successor (ADR 0020) — the route
|
|
530
|
-
* table is the price list — and the connector charges this figure whether
|
|
531
|
-
* the body is 100 bytes or 100 KB.
|
|
532
|
-
*/
|
|
533
|
-
uploadFee() {
|
|
534
|
-
return this.routePrices?.store ?? 0n;
|
|
535
|
-
}
|
|
536
|
-
/**
|
|
537
|
-
* Upload one git object as a kind:5094 store write (Git-SHA/Git-Type/Repo
|
|
538
|
-
* tagged — the proven seed-pipeline shape), signing one balance-proof claim
|
|
539
|
-
* for the store route's flat price
|
|
540
|
-
* ({@link StandalonePublisherOptions.routePrices}) — ADR 0020 prices a
|
|
541
|
-
* packet per handler, not per byte.
|
|
542
|
-
*
|
|
543
|
-
* #368: a blob's `Content-Type` is derived from its path extension (else
|
|
544
|
-
* octet-stream) and sent in the `output` tag, which the store forwards onto
|
|
545
|
-
* the Arweave upload's Content-Type — so a gateway serves `index.html` as
|
|
546
|
-
* `text/html`, not `application/octet-stream`. Non-blob objects (trees,
|
|
547
|
-
* commits, tags) are never served as files, so they stay octet-stream.
|
|
548
|
-
*/
|
|
549
|
-
async uploadGitObject(upload) {
|
|
550
|
-
if (upload.body.length > MAX_OBJECT_SIZE) {
|
|
551
|
-
throw new StandalonePublishError(
|
|
552
|
-
`git object ${upload.sha} exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`
|
|
553
|
-
);
|
|
554
|
-
}
|
|
555
|
-
await this.start();
|
|
556
|
-
const leg = await this.storeLeg();
|
|
557
|
-
const contentType = upload.type === "blob" ? contentTypeForPath(upload.path) : DEFAULT_CONTENT_TYPE;
|
|
558
|
-
const fee = this.uploadFee();
|
|
559
|
-
const event = leg.client.signEvent({
|
|
560
|
-
kind: 5094,
|
|
561
|
-
content: "",
|
|
562
|
-
created_at: nowSeconds(),
|
|
563
|
-
tags: [
|
|
564
|
-
["i", upload.body.toString("base64"), "blob"],
|
|
565
|
-
["bid", fee.toString(), "usdc"],
|
|
566
|
-
["output", contentType],
|
|
567
|
-
["Git-SHA", upload.sha],
|
|
568
|
-
["Git-Type", upload.type],
|
|
569
|
-
["Repo", upload.repoId]
|
|
570
|
-
]
|
|
571
|
-
});
|
|
572
|
-
const claim = await leg.client.signBalanceProof(leg.channelId, fee);
|
|
573
|
-
const result = await leg.client.publishEvent(event, {
|
|
574
|
-
...this.storeDestination ? { destination: this.storeDestination } : {},
|
|
575
|
-
claim,
|
|
576
|
-
ilpAmount: fee,
|
|
577
|
-
// The store backend serves POST /store (not the relay's /write).
|
|
578
|
-
proxyPath: "/store"
|
|
579
|
-
});
|
|
580
|
-
if (!result.success) {
|
|
581
|
-
throw new StandalonePublishError(
|
|
582
|
-
`git-object upload rejected (${upload.sha}): ${result.error ?? "store rejected the write"}`
|
|
583
|
-
);
|
|
584
|
-
}
|
|
585
|
-
if (!result.response) {
|
|
586
|
-
throw new StandalonePublishError(
|
|
587
|
-
`git-object upload FULFILL carried no sealed response (${upload.sha}); expected the Arweave tx ID`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
return { txId: extractArweaveTxId(result.response), feePaid: fee };
|
|
591
|
-
}
|
|
592
|
-
/**
|
|
593
|
-
* Upload one raw blob (no git envelope) with an explicit `Content-Type`
|
|
594
|
-
* (#368) — a kind:5094 store write carrying just the `i`/`bid`/`output`
|
|
595
|
-
* tags (and an optional `Repo` provenance tag), NOT the Git-SHA/Git-Type
|
|
596
|
-
* tags. Without those the store keeps the bytes verbatim instead of
|
|
597
|
-
* re-deriving a git envelope, which is exactly what the ar.io path manifest
|
|
598
|
-
* needs. One balance-proof claim for the store route's flat price.
|
|
599
|
-
*/
|
|
600
|
-
async uploadBlob(upload) {
|
|
601
|
-
if (upload.body.length > MAX_OBJECT_SIZE) {
|
|
602
|
-
throw new StandalonePublishError(
|
|
603
|
-
`blob exceeds the ${MAX_OBJECT_SIZE}-byte limit: ${upload.body.length} bytes`
|
|
604
|
-
);
|
|
605
|
-
}
|
|
606
|
-
await this.start();
|
|
607
|
-
const leg = await this.storeLeg();
|
|
608
|
-
const fee = this.uploadFee();
|
|
609
|
-
const event = leg.client.signEvent({
|
|
610
|
-
kind: 5094,
|
|
611
|
-
content: "",
|
|
612
|
-
created_at: nowSeconds(),
|
|
613
|
-
tags: [
|
|
614
|
-
["i", upload.body.toString("base64"), "blob"],
|
|
615
|
-
["bid", fee.toString(), "usdc"],
|
|
616
|
-
["output", upload.contentType],
|
|
617
|
-
...upload.repoId ? [["Repo", upload.repoId]] : []
|
|
618
|
-
]
|
|
619
|
-
});
|
|
620
|
-
const claim = await leg.client.signBalanceProof(leg.channelId, fee);
|
|
621
|
-
const result = await leg.client.publishEvent(event, {
|
|
622
|
-
...this.storeDestination ? { destination: this.storeDestination } : {},
|
|
623
|
-
claim,
|
|
624
|
-
ilpAmount: fee,
|
|
625
|
-
proxyPath: "/store"
|
|
626
|
-
});
|
|
627
|
-
if (!result.success) {
|
|
628
|
-
throw new StandalonePublishError(
|
|
629
|
-
`blob upload rejected: ${result.error ?? "store rejected the write"}`
|
|
630
|
-
);
|
|
631
|
-
}
|
|
632
|
-
if (!result.response) {
|
|
633
|
-
throw new StandalonePublishError(
|
|
634
|
-
"blob upload FULFILL carried no sealed response; expected the Arweave tx ID"
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
return { txId: extractArweaveTxId(result.response), feePaid: fee };
|
|
638
|
-
}
|
|
639
|
-
/**
|
|
640
|
-
* Sign the event with the embedded identity and pay-to-publish it through
|
|
641
|
-
* the relay write route, one claim for the flat per-event fee.
|
|
642
|
-
*
|
|
643
|
-
* `relayUrls` is the interface's plural forward-compat surface (parked
|
|
644
|
-
* #84): the standalone impl routes over ILP to its single configured
|
|
645
|
-
* publish destination, so more than one relay is refused rather than
|
|
646
|
-
* silently half-published.
|
|
647
|
-
*/
|
|
648
|
-
async publishEvent(event, relayUrls) {
|
|
649
|
-
if (relayUrls.length > 1) {
|
|
650
|
-
throw new StandalonePublishError(
|
|
651
|
-
`multi-relay publish is not supported yet (got ${relayUrls.length} relays) \u2014 the standalone publisher routes to a single relay destination (#84 parked)`
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
await this.start();
|
|
655
|
-
const channelId = this.requireChannel();
|
|
656
|
-
const signed = this.client.signEvent(event);
|
|
657
|
-
const fee = this.effectiveEventFee();
|
|
658
|
-
const claim = await this.client.signBalanceProof(channelId, fee);
|
|
659
|
-
const result = await this.client.publishEvent(signed, {
|
|
660
|
-
...this.publishDestination ? { destination: this.publishDestination } : {},
|
|
661
|
-
claim,
|
|
662
|
-
ilpAmount: fee
|
|
663
|
-
});
|
|
664
|
-
if (!result.success) {
|
|
665
|
-
throw new StandalonePublishError(
|
|
666
|
-
`publish rejected (kind ${event.kind}): ${result.error ?? "relay rejected the event"}`
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
return { eventId: result.eventId ?? signed.id, feePaid: fee };
|
|
670
|
-
}
|
|
671
|
-
requireChannel() {
|
|
672
|
-
if (!this.channelId) {
|
|
673
|
-
throw new StandalonePublishError(
|
|
674
|
-
"no payment channel open \u2014 start() did not complete"
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
return this.channelId;
|
|
678
|
-
}
|
|
679
|
-
/**
|
|
680
|
-
* The client + channel a STORE write must be paid on.
|
|
681
|
-
*
|
|
682
|
-
* With no `storeClientConfig` this is the publish leg unchanged — the
|
|
683
|
-
* single-node case, where the store terminates on the connector that
|
|
684
|
-
* already holds the channel. With one, the store node is a different
|
|
685
|
-
* connector holding a different channel, and paying it from the publish
|
|
686
|
-
* channel is precisely the `F01 - claim rejected: names a channel this
|
|
687
|
-
* connector has no record of` failure this exists to prevent.
|
|
688
|
-
*
|
|
689
|
-
* Built on FIRST STORE WRITE, not in `start()`: `rig push` with nothing to
|
|
690
|
-
* upload, and every read-only command, should not open a second on-chain
|
|
691
|
-
* channel (gas) for a leg they never use. Memoised on the promise so
|
|
692
|
-
* concurrent uploads share one open rather than racing two.
|
|
693
|
-
*/
|
|
694
|
-
async storeLeg() {
|
|
695
|
-
const injected = this.injectedStoreClient;
|
|
696
|
-
const config = this.storeClientConfig;
|
|
697
|
-
if (!config && !injected) {
|
|
698
|
-
return { client: this.client, channelId: this.requireChannel() };
|
|
699
|
-
}
|
|
700
|
-
this.storeLegPromise ??= (async () => {
|
|
701
|
-
const client = injected ?? new ToonClient(config);
|
|
702
|
-
this.storeClient = client;
|
|
703
|
-
await client.start();
|
|
704
|
-
const channelId = await this.openOrResumeChannelOn(
|
|
705
|
-
client,
|
|
706
|
-
this.storeDestination
|
|
707
|
-
);
|
|
708
|
-
this.storeChannelId = channelId;
|
|
709
|
-
return { client, channelId };
|
|
710
|
-
})();
|
|
711
|
-
return this.storeLegPromise;
|
|
712
|
-
}
|
|
713
|
-
};
|
|
714
|
-
function nowSeconds() {
|
|
715
|
-
return Math.floor(Date.now() / 1e3);
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
export {
|
|
719
|
-
extractArweaveTxId,
|
|
720
|
-
StandalonePublishError,
|
|
721
|
-
deriveRouteDestinations,
|
|
722
|
-
StandalonePublisher
|
|
723
|
-
};
|
|
724
|
-
//# sourceMappingURL=chunk-IZ6KLYQ6.js.map
|