@toon-protocol/rig 3.5.3 → 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.
Files changed (34) hide show
  1. package/README.md +112 -152
  2. package/dist/chunk-CDSFVZDV.js +290 -0
  3. package/dist/chunk-CDSFVZDV.js.map +1 -0
  4. package/dist/{chunk-B5ISARMU.js → chunk-GVXBUL3C.js} +15 -2
  5. package/dist/chunk-GVXBUL3C.js.map +1 -0
  6. package/dist/chunk-LTFMPOHU.js +194 -0
  7. package/dist/chunk-LTFMPOHU.js.map +1 -0
  8. package/dist/{chunk-LCYCKWAT.js → chunk-YZRR2UO6.js} +86 -5
  9. package/dist/chunk-YZRR2UO6.js.map +1 -0
  10. package/dist/{chunk-HJDPM37T.js → chunk-ZZBA5VVL.js} +11 -7
  11. package/dist/{chunk-HJDPM37T.js.map → chunk-ZZBA5VVL.js.map} +1 -1
  12. package/dist/cli/rig.js +298 -529
  13. package/dist/cli/rig.js.map +1 -1
  14. package/dist/index.d.ts +3 -109
  15. package/dist/index.js +9 -108
  16. package/dist/index.js.map +1 -1
  17. package/dist/{publisher-BFcnZeJI.d.ts → publisher-CcL96ugq.d.ts} +58 -1
  18. package/dist/standalone/index.d.ts +241 -378
  19. package/dist/standalone/index.js +19 -11
  20. package/dist/standalone-mode-LEAC4A2L.js +390 -0
  21. package/dist/standalone-mode-LEAC4A2L.js.map +1 -0
  22. package/package.json +5 -4
  23. package/dist/chunk-4CAOEXGQ.js +0 -631
  24. package/dist/chunk-4CAOEXGQ.js.map +0 -1
  25. package/dist/chunk-B5ISARMU.js.map +0 -1
  26. package/dist/chunk-FFQXFV4W.js +0 -400
  27. package/dist/chunk-FFQXFV4W.js.map +0 -1
  28. package/dist/chunk-LCYCKWAT.js.map +0 -1
  29. package/dist/chunk-TTJCBGKW.js +0 -447
  30. package/dist/chunk-TTJCBGKW.js.map +0 -1
  31. package/dist/network-bootstrap-CTYIZMP6.js +0 -40
  32. package/dist/network-bootstrap-CTYIZMP6.js.map +0 -1
  33. package/dist/standalone-mode-LBWGU7NV.js +0 -724
  34. package/dist/standalone-mode-LBWGU7NV.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
- - **Standalone by default** — `rig` embeds its own payment client built from your
12
- seed phrase. No daemon is required (though a running `toon-clientd` makes paid
13
- commands faster — see [Daemon as accelerator](#daemon-as-accelerator)).
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
- Relays are configured as **real git remotes**. Add the shared devnet relay as
84
- `origin` — this is your default publish target, and it also tells `rig fund` (next
85
- step) which faucet to use.
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 every supported chain — it's free and needs no
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/MINA for the other chains).
107
+ up Base Sepolia gas; hold a little SOL for Solana).
99
108
 
100
109
  ```sh
101
- rig fund # devnet USDC drip, all chains (Mina can take ~75s)
102
- rig fund sol # or fund a single chain: evm | sol | mina
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 reference](#devnet-reference-public-chains) for the API
109
- routes and every deployed contract address.)
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 \| mina \| all; prints addresses to fund externally off-devnet |
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 \| mina |
215
- | `rig entry [apex\|sandbox\|url]` | free | choose the network entry node (payment ingress + relay); `sandbox` is the Mina-only multihop demo 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
- ## Devnet reference (public chains)
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
- Since 2026-07-19 the TOON devnet settles on **public networks** — there is no
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
- ### Endpoints
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 (free reads, `rig clone`/`fetch`) | `wss://relay-ws.devnet.toonprotocol.dev` |
349
- | Payment proxy (paid writes, BTP) | `wss://proxy.devnet.toonprotocol.dev:443` |
350
- | Store DVM (ArNS buyfor/gas-station jobs, `--via`) | `https://dvm.devnet.toonprotocol.dev` |
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
- ### Faucet routes
408
+ Faucet routes (`POST`, body `{"address": "<wallet>"}`):
353
409
 
354
- | `POST` path | Drips |
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
- - Do **not** set `supportedChains`/`tokenNetworks`/`preferredTokens` explicitly —
400
- explicit topology bypasses the announce's route prices and paid writes get
401
- rejected (F06).
402
- - After config changes, delete `~/.toon-client/rig-topology-cache.json` (cached
403
- topology can mask edits).
404
- - Base Sepolia's official RPC (`sepolia.base.org`) is a load-balancer that
405
- serves stale reads — channel opens need a single-backend RPC like
406
- `base-sepolia-rpc.publicnode.com` (already in the snippet above).
407
-
408
- ## Strict `--json` stdout (machine consumers)
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
- ## Daemon as accelerator
470
-
471
- Every standalone paid command pays a fixed bootstrap cost (relay discovery, peer
472
- negotiation, channel resume). Two things remove most of it:
473
-
474
- - **Automatic daemon delegation** — when a running `toon-clientd` on the loopback
475
- control port (`TOON_CLIENT_HTTP_PORT`, default 8787) holds the **same identity**,
476
- paid write commands (`push`, `issue`, `comment`, `pr create`, `pr status`)
477
- delegate to its `/git/*` routes instead of bootstrapping an embedded client. The
478
- daemon already owns the payment channel's cumulative-claim watermark, so one
479
- process signs all claims. The identity match is confirmed against `GET /status`
480
- **before** anything is sent; a daemon on a different identity, or none at all,
481
- runs standalone. The chosen path prints on stderr (`rig: paid path: …`) and lands
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. `factory-job-delivery-client.ts` (`ClientJobDeliveryPort`) is
594
- the concrete `JobDeliveryPort`, backed by `@toon-protocol/client`'s real
595
- hashlock helpers; `factory-job-pay.ts` is the buyer-side counterpart
596
- (`payIncrementOffer`/`decryptIncrementArtifact`). See "Factory job proof"
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