@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/README.md
CHANGED
|
@@ -8,9 +8,11 @@ lives in NIP-34 Nostr events and the objects live on Arweave.
|
|
|
8
8
|
wallet, nothing configured.
|
|
9
9
|
- **Writes are paid** — pushing objects and publishing events spends from a
|
|
10
10
|
payment channel funded by your wallet. Writes are permanent and non-refundable.
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
- **One node URL is the whole network configuration** — `rig` embeds its own
|
|
12
|
+
payment client (`@toon-protocol/client` 2.x) built from your seed phrase and
|
|
13
|
+
pays the TOON connector you name with `rig entry <url>`. The node describes
|
|
14
|
+
itself on `GET /ilp`; there is nothing to discover (see
|
|
15
|
+
[Pointing at a node](#pointing-at-a-node)).
|
|
14
16
|
|
|
15
17
|
`rig` owns a handful of TOON verbs (`init`, `remote`, `clone`, `fetch`, `push`,
|
|
16
18
|
`issue`, `pr`, `comment`, `identity`, `fund`, `balance`, `channel`). **Every other
|
|
@@ -78,35 +80,42 @@ rig init # repo id defaults to the directory name
|
|
|
78
80
|
Not a git repo yet? `rig init` offers to `git init` (or pass `--git-init`). No
|
|
79
81
|
identity yet? It offers to generate one (or pass `--generate-identity`).
|
|
80
82
|
|
|
81
|
-
### 4. Point at a relay
|
|
83
|
+
### 4. Point at a node, and at its relay
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
`
|
|
85
|
-
|
|
85
|
+
Two addresses, one node. The **connector** is where paid writes go: it is an
|
|
86
|
+
`https://` URL, and its `GET /ilp` describes everything rig needs (the routes it
|
|
87
|
+
prices, the chains it settles on, the key packets are sealed to). The **relay**
|
|
88
|
+
is where free reads come from, configured as a **real git remote** — `origin` is
|
|
89
|
+
your default publish target, and it also tells `rig fund` (next step) which
|
|
90
|
+
faucet to use.
|
|
86
91
|
|
|
87
92
|
```sh
|
|
93
|
+
rig entry https://proxy.relay.devnet.toonprotocol.dev # the shared devnet relay node
|
|
88
94
|
rig remote add origin wss://relay-ws.devnet.toonprotocol.dev
|
|
89
95
|
rig remote list
|
|
90
96
|
```
|
|
91
97
|
|
|
98
|
+
`rig entry` shows what is in effect; `TOON_CONNECTOR` overrides it per shell.
|
|
99
|
+
Any node works the same way — its operator hands you these two URLs.
|
|
100
|
+
|
|
92
101
|
### 5. Fund your wallet
|
|
93
102
|
|
|
94
103
|
Pushing is paid, so your wallet needs a balance. On devnet, `rig fund` drips test
|
|
95
|
-
**USDC** (the settlement token) to
|
|
104
|
+
**USDC** (the settlement token) to both supported chains — it's free and needs no
|
|
96
105
|
faucet URL because it infers devnet from your `origin` remote. The drip assumes
|
|
97
106
|
your wallet already holds enough native gas (the EVM leg still best-effort tops
|
|
98
|
-
up Base Sepolia gas; hold a little SOL
|
|
107
|
+
up Base Sepolia gas; hold a little SOL for Solana).
|
|
99
108
|
|
|
100
109
|
```sh
|
|
101
|
-
rig fund # devnet USDC drip,
|
|
102
|
-
rig fund sol # or fund a single chain: evm | sol
|
|
110
|
+
rig fund # devnet USDC drip, both chains
|
|
111
|
+
rig fund sol # or fund a single chain: evm | sol
|
|
103
112
|
rig balance # confirm the funds landed
|
|
104
113
|
```
|
|
105
114
|
|
|
106
115
|
Prefer a browser? The same faucet has a web UI at
|
|
107
116
|
**<https://faucet.devnet.toonprotocol.dev>** — pick a chain, paste an address,
|
|
108
|
-
get USDC. (See [Devnet
|
|
109
|
-
|
|
117
|
+
get USDC. (See [Devnet](#devnet) for the API routes; contract addresses are
|
|
118
|
+
read from the node's `GET /ilp`, never configured.)
|
|
110
119
|
|
|
111
120
|
> On any non-devnet network there is no faucet: `rig fund` prints your wallet
|
|
112
121
|
> address(es) so you can fund them externally, then `rig push` draws from there.
|
|
@@ -195,7 +204,7 @@ rig pr status <event-id> applied
|
|
|
195
204
|
| `rig identity import` | free | write an existing phrase (read from stdin, never argv) to the keystore |
|
|
196
205
|
| `rig init` | free | one-shot repo setup: git repo + identity + `toon.*` config + repo-local git commit-author from your Nostr identity |
|
|
197
206
|
| `rig remote add/remove/list` | free | relays as REAL git remotes (`origin` = default publish target) |
|
|
198
|
-
| `rig fund [chain]` | free | devnet USDC drip (gas assumed) to the active identity's wallet; `chain` = evm \| sol \|
|
|
207
|
+
| `rig fund [chain]` | free | devnet USDC drip (gas assumed) to the active identity's wallet; `chain` = evm \| sol \| all; prints addresses to fund externally off-devnet |
|
|
199
208
|
| `rig balance` | free | the active wallet's multi-chain balances |
|
|
200
209
|
| `rig clone <relay-url> <owner>/<repo-id> [dir]` | free | bootstrap a repo from TOON: relay state + SHA-verified Arweave objects → a real git repo. Shadows `git clone` |
|
|
201
210
|
| `rig fetch [remote]` | free | download the missing object delta + update `refs/remotes/<remote>/*`. Shadows `git fetch` |
|
|
@@ -211,8 +220,8 @@ rig pr status <event-id> applied
|
|
|
211
220
|
| `rig name status <name>` | free | an ArNS name's registry record, ANT process, and current target txId |
|
|
212
221
|
| `rig name buy <name>` / `rig name set <name> <txId>` | **paid¹** | buy an ArNS name / point it at an Arweave txId. On the devnet, buy/set default to brokering through the deployed store DVM (`--direct` opts out). ¹Paid in mARIO on Solana via the ar.io registry — **not** ILP; needs the optional `@ar.io/sdk` dep |
|
|
213
222
|
| `rig channel list/open/close/settle` | free / **paid** | inspect or manage the payment channels paid commands hold (`rig channels` = `rig channel list`) |
|
|
214
|
-
| `rig chain [set <c>\|unset]` | free | pin which chain/USDC settles paid writes: evm \| sol
|
|
215
|
-
| `rig entry [
|
|
223
|
+
| `rig chain [set <c>\|unset]` | free | pin which chain/USDC settles paid writes: evm \| sol (default: the first chain the node settles on that your identity holds a key for) |
|
|
224
|
+
| `rig entry [<connector-url>\|clear]` | free | name the TOON connector paid writes go to (`--relay <wss-url>` records its free-read relay too); bare `rig entry` shows what is in effect |
|
|
216
225
|
| `rig help` / `rig --version` | free | usage / version |
|
|
217
226
|
| everything else | — | executed as `git <args...>` with rig's stdio and git's exit code |
|
|
218
227
|
|
|
@@ -326,107 +335,94 @@ the human-readable snapshot (verified live 2026-07-17), useful when auditing
|
|
|
326
335
|
same id on every cluster); the asset pubkey is the SDK's `processId`.
|
|
327
336
|
- Free devnet test loop (#381): ARIO faucet `https://faucet.services.ar-io.dev/` →
|
|
328
337
|
`rig name buy --network devnet` → `rig name set` → resolves at the
|
|
329
|
-
devnet-connected gateway `https://<name>.ar-io.dev/`.
|
|
338
|
+
devnet-connected gateway `https://<name>.ar-io.dev/`. ⚠️ That gateway is
|
|
339
|
+
ar.io's **testnet** and resolves devnet names ONLY; a **mainnet** name never
|
|
340
|
+
appears there (nor on `arweave.net`, which runs a forked ArNS). Check mainnet
|
|
341
|
+
names on a mainnet gateway, e.g. `https://<name>.permagate.io/` or
|
|
342
|
+
`https://<name>.ardrive.net/`.
|
|
330
343
|
- `--process-id <id>` overrides the arns registry program outright (wins over
|
|
331
344
|
`--network`) — for pointing at a fresh/staging registry deployment.
|
|
332
345
|
|
|
333
346
|
---
|
|
334
347
|
|
|
335
|
-
##
|
|
348
|
+
## Pointing at a node
|
|
349
|
+
|
|
350
|
+
A TOON connector is a paid reverse proxy in front of an ordinary app (a relay, a
|
|
351
|
+
store). It describes itself on **`GET /ilp`** (connector ADR 0050): its ILP
|
|
352
|
+
addresses, the routes it prices, the settlement chains it accepts and the key a
|
|
353
|
+
payload is sealed to. That one document is everything rig reads about the
|
|
354
|
+
network — there is no announce to discover (kind:10032 was removed by connector
|
|
355
|
+
ADR 0046) and no topology to negotiate.
|
|
356
|
+
|
|
357
|
+
```sh
|
|
358
|
+
curl -s https://proxy.relay.devnet.toonprotocol.dev/ilp | jq '{ilpAddresses, routes, settlements: [.settlements[].chain]}'
|
|
359
|
+
```
|
|
336
360
|
|
|
337
|
-
|
|
338
|
-
self-hosted chain infrastructure. The authoritative, machine-readable source for
|
|
339
|
-
everything below is the apex's **kind:10032 announce** on the relay; the
|
|
340
|
-
authoritative doc is
|
|
341
|
-
[toon-meta `docs/deployment.md`](https://github.com/toon-protocol/toon-meta/blob/main/docs/deployment.md).
|
|
361
|
+
What rig derives from it, and the knob that overrides each:
|
|
342
362
|
|
|
343
|
-
|
|
363
|
+
| fact | default | override (env / `~/.toon-client/config.json`) |
|
|
364
|
+
|---|---|---|
|
|
365
|
+
| the connector | — (required) | `TOON_CONNECTOR` / `connectorUrl` (`rig entry <url>` writes it) |
|
|
366
|
+
| where events go | the node's first own address it also prices | `TOON_CLIENT_PUBLISH_DESTINATION` / `publishDestination` |
|
|
367
|
+
| where git objects go | the node's first priced `*.store` route, else `*.ario` | `TOON_CLIENT_STORE_DESTINATION` / `storeDestination`; `rig name --via` per invocation |
|
|
368
|
+
| a store on another node | — | `storeConnectorUrl` (that node holds its own channel: uploads ride a second client and watermark) or `storeSealTo` (same channel, sealed to that node) |
|
|
369
|
+
| settlement chain | the first chain in `settlements[]` your identity holds a key for | `TOON_CLIENT_CHAIN` / `chain` — `evm` or `solana` (`evm:8453` is read by its family) |
|
|
370
|
+
| chain RPC | the client's devnet preset | `TOON_CLIENT_RPC_URL` / `rpcUrl` / `chainRpcUrls[<chain>]` — **set this for a mainnet node** |
|
|
371
|
+
| who pays | the phrase's key on that chain | `RIG_SOLANA_KEY_FILE` / `solanaKeyFile` (a `solana-keygen` JSON), `RIG_EVM_PRIVATE_KEY` / `evmPrivateKey` |
|
|
372
|
+
| claim watermark file | `<TOON_CLIENT_HOME>/channels.json` | `TOON_CLIENT_CHANNEL_STORE` / `channelStorePath` — one file per channel writer, or two processes replay each other's nonces |
|
|
373
|
+
| first-open collateral | the client default | `TOON_CLIENT_DEPOSIT` / `deposit` (base units) |
|
|
374
|
+
| carriage | `auto` (HTTP unless the node requires BTP) | `TOON_CLIENT_TRANSPORT` / `transport` |
|
|
375
|
+
|
|
376
|
+
**Prices are schedules.** A route publishes a base price and, when it meters by
|
|
377
|
+
size, a `pricePerKib` (connector ADR 0065). `rig push`'s fee table prices every
|
|
378
|
+
object by its sealed size with the same rule the client puts on the claim, so
|
|
379
|
+
the estimate equals what is paid.
|
|
380
|
+
|
|
381
|
+
**Who pays and who signs are independent.** The repo owner is the phrase's
|
|
382
|
+
Nostr key at `m/44'/1237'/0'/0/<account>` — unchanged from every earlier rig.
|
|
383
|
+
Its EVM wallet is that same key (the client's `keyDerivation: 'legacy'`, rig's
|
|
384
|
+
default, so channels an existing identity funded are still its own); set
|
|
385
|
+
`TOON_CLIENT_KEY_DERIVATION=standard` for a fresh identity you want to import
|
|
386
|
+
into MetaMask. The payer key may be a different key entirely: the connector
|
|
387
|
+
attributes payment from the claim, never from the event.
|
|
388
|
+
|
|
389
|
+
### Devnet
|
|
390
|
+
|
|
391
|
+
The shared devnet's nodes and the faucet (authoritative:
|
|
392
|
+
[toon-meta `docs/deployment.md`](https://github.com/toon-protocol/toon-meta/blob/main/docs/deployment.md);
|
|
393
|
+
the node URLs are also `@toon-protocol/client`'s `DEVNET` preset):
|
|
344
394
|
|
|
345
395
|
| What | URL |
|
|
346
396
|
|---|---|
|
|
347
397
|
| Faucet (web UI + API) | `https://faucet.devnet.toonprotocol.dev` |
|
|
348
|
-
| Relay (
|
|
349
|
-
|
|
|
350
|
-
| Store
|
|
398
|
+
| Relay node (paid writes: `rig entry`) | `https://proxy.relay.devnet.toonprotocol.dev` |
|
|
399
|
+
| Relay (free reads: `rig remote add origin`) | `wss://relay-ws.devnet.toonprotocol.dev` |
|
|
400
|
+
| Store route (`--via` for ArNS jobs) | an ILP **destination** the node prices, e.g. `g.toon.relay.store` |
|
|
401
|
+
|
|
402
|
+
`--via` names an ILP destination rather than an HTTP endpoint. The store sits
|
|
403
|
+
behind the connector's payment termination, so it has no public job endpoint to
|
|
404
|
+
POST to, and a reachable one would be an unpaid path to a paid handler. The
|
|
405
|
+
kind:5095 buy and kind:5096 gas-station jobs therefore travel as paid packets,
|
|
406
|
+
exactly as `rig push`'s git-object writes do.
|
|
351
407
|
|
|
352
|
-
|
|
408
|
+
Faucet routes (`POST`, body `{"address": "<wallet>"}`):
|
|
353
409
|
|
|
354
|
-
|
|
|
410
|
+
| path | drips |
|
|
355
411
|
|---|---|
|
|
356
412
|
| `/api/base-sepolia/request` | 1000 USDC (ungated on-chain mint) + best-effort Base Sepolia gas |
|
|
357
413
|
| `/api/solana/usdc-request` | 1000 USDC (treasury transfer, no SOL leg) |
|
|
358
|
-
| `/api/mina/usdc-request` | USDC (treasury self-mint, rate-limited token; no MINA leg) |
|
|
359
414
|
| `/api/solana/request` | 2 SOL + 1000 USDC (airdrop leg subject to the public devnet's per-IP quota) |
|
|
360
|
-
| `/api/mina/request` | 5 MINA + USDC (treasury self-mint, rate-limited token) |
|
|
361
|
-
|
|
362
|
-
Body: `{"address": "<wallet>"}`. `rig fund` hits the **USDC-only** routes
|
|
363
|
-
(`/api/base-sepolia/request`, `/api/solana/usdc-request`, `/api/mina/usdc-request`)
|
|
364
|
-
for the identity's derived wallets — it funds USDC and assumes gas is already
|
|
365
|
-
present. The combined native-plus-USDC routes above remain for manual use.
|
|
366
|
-
|
|
367
|
-
### Settlement contracts
|
|
368
|
-
|
|
369
|
-
Chain ids below are the **announced spellings** — use them verbatim with
|
|
370
|
-
`TOON_CLIENT_CHAIN=<id> rig push` to pin a settlement chain.
|
|
371
|
-
|
|
372
|
-
| Chain (`TOON_CLIENT_CHAIN`) | Payment-channel contract/program/zkApp | USDC token (6dp) | Explorer |
|
|
373
|
-
|---|---|---|---|
|
|
374
|
-
| `evm:84532` (Base Sepolia) | TokenNetwork `0x1E95493fEF46707E034b4a1945f25a8C76A1823D` (registry `0xcC9079adE929b168B54145f6d25262b64FAB9D5b`) | `0x49beE1Bca5d15Fb0963117923403F9498119a9Ce` | [base-sepolia.blockscout.com](https://base-sepolia.blockscout.com) |
|
|
375
|
-
| `solana:devnet` | program `2aEVJ8koKD8LTZrLRSGtAtU7LBt4e7QjjCgf1kzQ7Rip` | mint `xyc5J8MgKFiEN13PnfftdXxUzYH34FEvw1LCrFwN7in` | [explorer.solana.com/?cluster=devnet](https://explorer.solana.com/?cluster=devnet) |
|
|
376
|
-
| `mina:devnet` | PaymentChannel zkApp `B62qmgPhv2Xo6QVEtwjLja8UZJUtu8yapRFAR6gaoGtbM9zE5hG7Tkf` | token `B62qqN1Pu3kF2KGmqLA8EwpqfWrnFTVZJGDSDHQuQRoVt5BCFjhNz3d`<br>tokenId `9497120696276615621907376728658022802954262638363646162765282600447713419198` | [minascan.io/devnet](https://minascan.io/devnet/home) |
|
|
377
|
-
|
|
378
|
-
### Config notes (rig ≥ 2.10.2)
|
|
379
|
-
|
|
380
|
-
Keep `~/.toon-client/config.json` **minimal** — settlement parameters derive
|
|
381
|
-
from the announce. Three additions are load-bearing on today's devnet:
|
|
382
|
-
|
|
383
|
-
```jsonc
|
|
384
|
-
{
|
|
385
|
-
"feePerEvent": "1000", // = the announced route price
|
|
386
|
-
"chainRpcUrls": { // per-field overrides only
|
|
387
|
-
"evm:84532": "https://base-sepolia-rpc.publicnode.com",
|
|
388
|
-
"mina:devnet": "https://api.minascan.io/node/devnet/v1/graphql"
|
|
389
|
-
},
|
|
390
|
-
"minaChannel": { // Mina is not announce-derivable yet
|
|
391
|
-
"graphqlUrl": "https://api.minascan.io/node/devnet/v1/graphql",
|
|
392
|
-
"zkAppAddress": "B62qmgPhv2Xo6QVEtwjLja8UZJUtu8yapRFAR6gaoGtbM9zE5hG7Tkf",
|
|
393
|
-
"tokenId": "9497120696276615621907376728658022802954262638363646162765282600447713419198",
|
|
394
|
-
"networkId": "devnet"
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
```
|
|
398
415
|
|
|
399
|
-
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
With `--json`, stdout carries **exactly one JSON document** — everything
|
|
411
|
-
human-facing (identity reports, deprecation nudges, progress lines, even stray
|
|
412
|
-
`console.log` from dependencies) is routed to stderr, so `rig <command> --json | jq`
|
|
413
|
-
always parses. Errors emit one machine envelope (`{"error": "<code>", "detail": …}`)
|
|
414
|
-
on stdout with the human detail on stderr and a non-zero exit. `--json` is a
|
|
415
|
-
per-subcommand flag on the commands rig owns, **not** a global rig flag.
|
|
416
|
-
|
|
417
|
-
## Git passthrough
|
|
418
|
-
|
|
419
|
-
Any subcommand rig does not own is executed as `git <args...>` verbatim: the exact
|
|
420
|
-
argv tail is handed to system git with `stdio: 'inherit'` (interactive commands,
|
|
421
|
-
pagers, colors, and prompts work), and git's exit code becomes rig's exit code (a
|
|
422
|
-
child killed by a signal maps to the shell convention 128+N). rig-owned verbs always
|
|
423
|
-
take precedence — in particular `rig push` is the TOON transport and shadows
|
|
424
|
-
`git push`; plain-git pushes remain available by calling `git push` directly. If no
|
|
425
|
-
system git is installed, passthrough fails with a clear error (exit 127).
|
|
426
|
-
|
|
427
|
-
The passthrough is exempt from the `--json` contract: `rig status --json` runs
|
|
428
|
-
`git status --json` (git rejects the flag), and flags before the subcommand
|
|
429
|
-
(`rig --json status`) pass through to git untouched.
|
|
416
|
+
`rig fund` hits the two USDC-only routes for the identity's derived wallets —
|
|
417
|
+
it funds USDC and assumes gas is already present.
|
|
418
|
+
|
|
419
|
+
Settlement facts (contract and program addresses, token mints) are **read from
|
|
420
|
+
the node's `GET /ilp`, never configured** — the table that used to sit here went
|
|
421
|
+
stale the first time a contract moved. Base Sepolia's official RPC
|
|
422
|
+
(`sepolia.base.org`) is a load-balancer that serves stale reads; channel opens
|
|
423
|
+
want a single-backend RPC such as `base-sepolia-rpc.publicnode.com`.
|
|
424
|
+
|
|
425
|
+
---
|
|
430
426
|
|
|
431
427
|
## Identity
|
|
432
428
|
|
|
@@ -466,32 +462,19 @@ Every paid command reports which source is active and the derived pubkey
|
|
|
466
462
|
(`Identity: <pubkey> (from …)`, and an `identity` object in `--json` output) — the
|
|
467
463
|
phrase itself is never printed and never written to git config or any repo file.
|
|
468
464
|
|
|
469
|
-
##
|
|
470
|
-
|
|
471
|
-
Every standalone paid command
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
in `--json` as `"path": "daemon" | "standalone"`. Commands the daemon has no route
|
|
483
|
-
for — `rig fund`, `rig balance`, `rig channel open|close|settle` — always run
|
|
484
|
-
standalone (the on-chain channel mutations among them refuse while a same-identity
|
|
485
|
-
daemon runs, so as not to race its live claims: stop the daemon for those).
|
|
486
|
-
- **Standalone topology cache** — the resolved network topology (announce discovery,
|
|
487
|
-
payment-peer pick, settlement-chain selection) is cached under `TOON_CLIENT_HOME`
|
|
488
|
-
(`rig-topology-cache.json`), keyed by relay + identity + explicit config, for 15
|
|
489
|
-
minutes (`RIG_TOPOLOGY_TTL_MS` overrides; `0` disables). A cached topology that
|
|
490
|
-
fails to bootstrap is invalidated and re-resolved live. Money state (claim
|
|
491
|
-
watermarks, channel map) is never cached.
|
|
492
|
-
|
|
493
|
-
The `rig` bin also exits as soon as a command finishes and stdio is flushed, rather
|
|
494
|
-
than letting the embedded client's keep-alive socket hold the process open for ~30s.
|
|
465
|
+
## What a paid command does first
|
|
466
|
+
|
|
467
|
+
Every standalone paid command bootstraps the same way: resolve the identity,
|
|
468
|
+
read the connector's `GET /ilp`, open or adopt the payment channel with that
|
|
469
|
+
node, then sign one claim per packet. Adoption is free — a channel's id derives
|
|
470
|
+
from its participants, so a node you have paid before is found again without a
|
|
471
|
+
transaction. Money state (the claim watermark, the channel map) lives under
|
|
472
|
+
`TOON_CLIENT_HOME` and is never cached away.
|
|
473
|
+
|
|
474
|
+
Two rig processes on one identity race the claim nonce, so a per-identity lock
|
|
475
|
+
refuses the second (`RIG_STANDALONE=1` skips only the check for a same-identity
|
|
476
|
+
`toon-clientd`; the lock stays). The `rig` bin exits as soon as a command
|
|
477
|
+
finishes and stdio is flushed.
|
|
495
478
|
|
|
496
479
|
## Pushing
|
|
497
480
|
|
|
@@ -590,32 +573,9 @@ it — that stays behind the `Publisher` seam:
|
|
|
590
573
|
[toon-meta#262](https://github.com/toon-protocol/toon-meta/issues/262) "agents
|
|
591
574
|
earning"): `planFactoryJob`/`executeFactoryJob` mirror `planPush`/`executePush`'s
|
|
592
575
|
split, and `JobDeliveryPort` is the injected seam for the per-increment
|
|
593
|
-
encrypt/pay leg.
|
|
594
|
-
the
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
below.
|
|
598
|
-
|
|
599
|
-
### Factory job proof — one paid increment end to end (#56)
|
|
600
|
-
|
|
601
|
-
`scripts/factory-job-proof.ts` is the runnable proof that a provider can
|
|
602
|
-
deliver one factory-job increment to a buyer and get paid for it, entirely
|
|
603
|
-
off-chain, using the real `@toon-protocol/client` (no fakes): encrypt → upload
|
|
604
|
-
→ offer → pay → fulfil → decrypt, then assert the provider's `getClaimState()`
|
|
605
|
-
spendable balance rose with the on-chain deposit unchanged (no settlement).
|
|
606
|
-
|
|
607
|
-
```sh
|
|
608
|
-
cd packages/rig
|
|
609
|
-
pnpm factory-job-proof
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
By default this generates and faucet-funds two fresh devnet identities (a
|
|
613
|
-
provider and a buyer) against the public devnet (see "Devnet reference"
|
|
614
|
-
below) and tears them down at the end — safe to re-run any number of times.
|
|
615
|
-
Set `FACTORY_JOB_PROOF_PROVIDER_MNEMONIC` / `FACTORY_JOB_PROOF_BUYER_MNEMONIC`
|
|
616
|
-
to reuse already-funded identities instead, or `FACTORY_JOB_PROOF_SKIP_FUND=1`
|
|
617
|
-
to skip the faucet drip. See the script's own header comment for the full env
|
|
618
|
-
var list.
|
|
576
|
+
encrypt/pay leg. The concrete port and the buyer-side payment helpers that
|
|
577
|
+
rode the 0.x client's serve-side job API were removed with the move to
|
|
578
|
+
`@toon-protocol/client` 2.x; the protocol pieces stay so a port can be
|
|
579
|
+
written against the 2.x client.
|
|
619
580
|
|
|
620
|
-
Part of [epic #222](https://github.com/toon-protocol/toon-client/issues/222) and
|
|
621
581
|
[epic #246](https://github.com/toon-protocol/toon-client/issues/246).
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_CONTENT_TYPE,
|
|
3
|
+
contentTypeForPath
|
|
4
|
+
} from "./chunk-YZRR2UO6.js";
|
|
5
|
+
import {
|
|
6
|
+
MAX_OBJECT_SIZE,
|
|
7
|
+
uploadChargeFor
|
|
8
|
+
} from "./chunk-GVXBUL3C.js";
|
|
9
|
+
|
|
10
|
+
// src/standalone/connector-publisher.ts
|
|
11
|
+
import { finalizeEvent } from "nostr-tools/pure";
|
|
12
|
+
var ConnectorPublishError = class extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "ConnectorPublishError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var ARWEAVE_TX_ID_REGEX = /^[A-Za-z0-9_-]{43}$/;
|
|
19
|
+
function decodeUtf8(bytes) {
|
|
20
|
+
return new TextDecoder().decode(bytes);
|
|
21
|
+
}
|
|
22
|
+
function nowSeconds() {
|
|
23
|
+
return Math.floor(Date.now() / 1e3);
|
|
24
|
+
}
|
|
25
|
+
function describeRefusal(result) {
|
|
26
|
+
const by = result.refusedBy ? ` by the ${result.refusedBy}` : "";
|
|
27
|
+
return `${result.code}${by}: ${result.message}`;
|
|
28
|
+
}
|
|
29
|
+
function extractArweaveTxId(status, body) {
|
|
30
|
+
const text = decodeUtf8(body);
|
|
31
|
+
if (status < 200 || status >= 300) {
|
|
32
|
+
throw new ConnectorPublishError(
|
|
33
|
+
`the store answered HTTP ${String(status)}${text ? ` - ${text}` : ""}`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
let parsed;
|
|
37
|
+
try {
|
|
38
|
+
parsed = JSON.parse(text);
|
|
39
|
+
} catch {
|
|
40
|
+
if (ARWEAVE_TX_ID_REGEX.test(text.trim())) return text.trim();
|
|
41
|
+
throw new ConnectorPublishError(
|
|
42
|
+
`the store's answer was not valid JSON: ${JSON.stringify(text.slice(0, 200))}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const answer = parsed ?? {};
|
|
46
|
+
if (answer.accept === false) {
|
|
47
|
+
const reason = typeof answer.error === "string" ? `: ${answer.error}` : "";
|
|
48
|
+
throw new ConnectorPublishError(
|
|
49
|
+
`upload refused by the store (accept:false)${reason}`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
if (typeof answer.txId === "string" && ARWEAVE_TX_ID_REGEX.test(answer.txId)) {
|
|
53
|
+
return answer.txId;
|
|
54
|
+
}
|
|
55
|
+
const nested = answer.result;
|
|
56
|
+
if (nested && typeof nested.txId === "string" && ARWEAVE_TX_ID_REGEX.test(nested.txId)) {
|
|
57
|
+
return nested.txId;
|
|
58
|
+
}
|
|
59
|
+
if (typeof answer.data === "string" && answer.data.length > 0) {
|
|
60
|
+
const decoded = Buffer.from(answer.data, "base64").toString("utf8");
|
|
61
|
+
if (ARWEAVE_TX_ID_REGEX.test(decoded)) return decoded;
|
|
62
|
+
}
|
|
63
|
+
throw new ConnectorPublishError(
|
|
64
|
+
`the store's answer carried no Arweave tx id: ${JSON.stringify(text.slice(0, 200))}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
var ConnectorPublisher = class {
|
|
68
|
+
publish;
|
|
69
|
+
store;
|
|
70
|
+
nostrSecretKey;
|
|
71
|
+
configuredEventFee;
|
|
72
|
+
uploadTimeoutMs;
|
|
73
|
+
warn;
|
|
74
|
+
rates;
|
|
75
|
+
constructor(options) {
|
|
76
|
+
this.publish = options.publish;
|
|
77
|
+
this.store = options.store;
|
|
78
|
+
this.nostrSecretKey = options.nostrSecretKey;
|
|
79
|
+
this.configuredEventFee = options.eventFee ?? 0n;
|
|
80
|
+
this.uploadTimeoutMs = options.uploadTimeoutMs;
|
|
81
|
+
this.warn = options.warn ?? (() => void 0);
|
|
82
|
+
}
|
|
83
|
+
/** Where events go, and where objects go. */
|
|
84
|
+
get destinations() {
|
|
85
|
+
return { publish: this.publish.destination, store: this.store.destination };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The two routes' published terms, read once per publisher. A route the
|
|
89
|
+
* node does not price is a configuration error and says which knob to fix.
|
|
90
|
+
*/
|
|
91
|
+
getFeeRates() {
|
|
92
|
+
this.rates ??= this.readFeeRates();
|
|
93
|
+
return this.rates;
|
|
94
|
+
}
|
|
95
|
+
async readFeeRates() {
|
|
96
|
+
const [publishTerms, storeTerms] = await Promise.all([
|
|
97
|
+
this.publish.client.routePrice(this.publish.destination),
|
|
98
|
+
this.store.client.routePrice(this.store.destination)
|
|
99
|
+
]);
|
|
100
|
+
if (!publishTerms) {
|
|
101
|
+
throw new ConnectorPublishError(
|
|
102
|
+
`the connector prices no route for the publish destination ${this.publish.destination} (set publishDestination / TOON_CLIENT_PUBLISH_DESTINATION to one of its GET /ilp routes)`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
if (!storeTerms) {
|
|
106
|
+
throw new ConnectorPublishError(
|
|
107
|
+
`the connector prices no route for the store destination ${this.store.destination} (set storeDestination / TOON_CLIENT_STORE_DESTINATION to one of its GET /ilp routes)`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (publishTerms.pricePerKib !== void 0 && publishTerms.pricePerKib > 0n) {
|
|
111
|
+
this.warn(
|
|
112
|
+
`rig: the publish route ${this.publish.destination} meters by size (${String(publishTerms.pricePerKib)}/KiB); the per-event estimate shows its base price only`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const eventFee = this.configuredEventFee > publishTerms.price ? this.configuredEventFee : publishTerms.price;
|
|
116
|
+
return {
|
|
117
|
+
uploadFee: storeTerms.price,
|
|
118
|
+
...storeTerms.pricePerKib !== void 0 && storeTerms.pricePerKib > 0n ? { uploadPerKib: storeTerms.pricePerKib } : {},
|
|
119
|
+
eventFee
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
sign(template) {
|
|
123
|
+
return finalizeEvent(
|
|
124
|
+
{
|
|
125
|
+
kind: template.kind,
|
|
126
|
+
content: template.content,
|
|
127
|
+
tags: template.tags,
|
|
128
|
+
created_at: template.created_at
|
|
129
|
+
},
|
|
130
|
+
this.nostrSecretKey
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
/** One paid kind:5094 write; returns the tx id and what the claim spent. */
|
|
134
|
+
async storeWrite(tags, bodyBytes, what) {
|
|
135
|
+
const rates = await this.getFeeRates();
|
|
136
|
+
const estimate = uploadChargeFor(rates, bodyBytes);
|
|
137
|
+
const event = this.sign({
|
|
138
|
+
kind: 5094,
|
|
139
|
+
content: "",
|
|
140
|
+
created_at: nowSeconds(),
|
|
141
|
+
tags: [...tags, ["bid", estimate.toString(), "usdc"]]
|
|
142
|
+
});
|
|
143
|
+
const result = await this.store.client.send(
|
|
144
|
+
this.store.destination,
|
|
145
|
+
{ body: { event } },
|
|
146
|
+
{
|
|
147
|
+
...this.store.sealTo !== void 0 ? { sealTo: this.store.sealTo } : {},
|
|
148
|
+
...this.uploadTimeoutMs !== void 0 ? { timeoutMs: this.uploadTimeoutMs } : {}
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
if (!result.fulfilled) {
|
|
152
|
+
throw new ConnectorPublishError(
|
|
153
|
+
`${what} rejected \u2014 ${describeRefusal(result)}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
let txId;
|
|
157
|
+
try {
|
|
158
|
+
txId = extractArweaveTxId(result.status, result.body);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
throw new ConnectorPublishError(
|
|
161
|
+
`${what}: ${err instanceof Error ? err.message : String(err)}`
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return { txId, feePaid: result.claim?.amount ?? estimate };
|
|
165
|
+
}
|
|
166
|
+
async uploadGitObject(upload) {
|
|
167
|
+
if (upload.body.length > MAX_OBJECT_SIZE) {
|
|
168
|
+
throw new ConnectorPublishError(
|
|
169
|
+
`git object ${upload.sha} exceeds the ${String(MAX_OBJECT_SIZE)}-byte limit: ${String(upload.body.length)} bytes`
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const contentType = upload.type === "blob" ? contentTypeForPath(upload.path) : DEFAULT_CONTENT_TYPE;
|
|
173
|
+
return this.storeWrite(
|
|
174
|
+
[
|
|
175
|
+
["i", upload.body.toString("base64"), "blob"],
|
|
176
|
+
["output", contentType],
|
|
177
|
+
["Git-SHA", upload.sha],
|
|
178
|
+
["Git-Type", upload.type],
|
|
179
|
+
["Repo", upload.repoId]
|
|
180
|
+
],
|
|
181
|
+
upload.body.length,
|
|
182
|
+
`git-object upload (${upload.sha})`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
async uploadBlob(upload) {
|
|
186
|
+
if (upload.body.length > MAX_OBJECT_SIZE) {
|
|
187
|
+
throw new ConnectorPublishError(
|
|
188
|
+
`blob exceeds the ${String(MAX_OBJECT_SIZE)}-byte limit: ${String(upload.body.length)} bytes`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
return this.storeWrite(
|
|
192
|
+
[
|
|
193
|
+
["i", upload.body.toString("base64"), "blob"],
|
|
194
|
+
["output", upload.contentType],
|
|
195
|
+
...upload.repoId ? [["Repo", upload.repoId]] : []
|
|
196
|
+
],
|
|
197
|
+
upload.body.length,
|
|
198
|
+
"blob upload"
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* One NIP-90 job to the store node's DVM, over the paid path. A refusal is
|
|
203
|
+
* returned, not thrown: `accept: false` and a non-2xx status are the
|
|
204
|
+
* answer's content, and the payer was charged for the answer (ADR 0020).
|
|
205
|
+
*/
|
|
206
|
+
async submitStoreJob(request) {
|
|
207
|
+
const rates = await this.getFeeRates();
|
|
208
|
+
const fee = rates.uploadFee;
|
|
209
|
+
const event = this.sign({
|
|
210
|
+
kind: request.kind,
|
|
211
|
+
content: "",
|
|
212
|
+
created_at: nowSeconds(),
|
|
213
|
+
tags: [
|
|
214
|
+
...request.params.map(([key, value]) => ["param", key, value]),
|
|
215
|
+
["bid", fee.toString(), "usdc"]
|
|
216
|
+
]
|
|
217
|
+
});
|
|
218
|
+
const result = await this.store.client.send(
|
|
219
|
+
this.store.destination,
|
|
220
|
+
{ body: { event } },
|
|
221
|
+
this.store.sealTo !== void 0 ? { sealTo: this.store.sealTo } : {}
|
|
222
|
+
);
|
|
223
|
+
if (!result.fulfilled) {
|
|
224
|
+
throw new ConnectorPublishError(
|
|
225
|
+
`kind:${String(request.kind)} job rejected \u2014 ${describeRefusal(result)}`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const text = decodeUtf8(result.body);
|
|
229
|
+
let body;
|
|
230
|
+
try {
|
|
231
|
+
body = JSON.parse(text);
|
|
232
|
+
} catch {
|
|
233
|
+
throw new ConnectorPublishError(
|
|
234
|
+
`kind:${String(request.kind)} job answer was not valid JSON (HTTP ${String(result.status)}): ${JSON.stringify(text.slice(0, 200))}`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
status: result.status,
|
|
239
|
+
accept: body.accept === true,
|
|
240
|
+
...typeof body.code === "string" ? { code: body.code } : {},
|
|
241
|
+
...typeof body.message === "string" ? { message: body.message } : {},
|
|
242
|
+
...body.result !== null && typeof body.result === "object" ? { result: body.result } : {},
|
|
243
|
+
feePaid: result.claim?.amount ?? fee
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Sign the event with the owner's key and pay to publish it. `relayUrls`
|
|
248
|
+
* is the seam's plural surface: this publisher routes to one configured
|
|
249
|
+
* destination, so more than one relay is refused rather than half-done.
|
|
250
|
+
*/
|
|
251
|
+
async publishEvent(event, relayUrls) {
|
|
252
|
+
if (relayUrls.length > 1) {
|
|
253
|
+
throw new ConnectorPublishError(
|
|
254
|
+
`multi-relay publish is not supported (got ${String(relayUrls.length)} relays) \u2014 the connector publisher routes to a single publish destination`
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
const rates = await this.getFeeRates();
|
|
258
|
+
const signed = this.sign(event);
|
|
259
|
+
const result = await this.publish.client.send(
|
|
260
|
+
this.publish.destination,
|
|
261
|
+
{ body: { event: signed } },
|
|
262
|
+
this.publish.sealTo !== void 0 ? { sealTo: this.publish.sealTo } : {}
|
|
263
|
+
);
|
|
264
|
+
if (!result.fulfilled) {
|
|
265
|
+
throw new ConnectorPublishError(
|
|
266
|
+
`publish rejected (kind ${String(event.kind)}) \u2014 ${describeRefusal(result)}`
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (result.status < 200 || result.status >= 300) {
|
|
270
|
+
throw new ConnectorPublishError(
|
|
271
|
+
`publish (kind ${String(event.kind)}): the relay answered HTTP ${String(result.status)} - ${decodeUtf8(result.body)}`
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
let eventId = signed.id;
|
|
275
|
+
try {
|
|
276
|
+
const answer = JSON.parse(decodeUtf8(result.body));
|
|
277
|
+
if (typeof answer.eventId === "string" && answer.eventId)
|
|
278
|
+
eventId = answer.eventId;
|
|
279
|
+
} catch {
|
|
280
|
+
}
|
|
281
|
+
return { eventId, feePaid: result.claim?.amount ?? rates.eventFee };
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export {
|
|
286
|
+
ConnectorPublishError,
|
|
287
|
+
extractArweaveTxId,
|
|
288
|
+
ConnectorPublisher
|
|
289
|
+
};
|
|
290
|
+
//# sourceMappingURL=chunk-CDSFVZDV.js.map
|