@fluxpointstudios/orynq-sdk-quickstart 0.2.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Flux Point Studios
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # @fluxpointstudios/orynq-sdk-quickstart
2
+
3
+ Solo-developer quickstart for orynq. Get from `npm install` to a chain-anchored first trace in under 5 minutes — no signer URI to manage, no wallet to seed, no Cardano addresses to look up.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install --global @fluxpointstudios/orynq-sdk-quickstart
9
+ ```
10
+
11
+ ## CLI
12
+
13
+ ```bash
14
+ orynq init # Generate identity + faucet-drip MATRA. Idempotent.
15
+ orynq trace # Build + submit a chain-anchored first trace.
16
+ orynq whoami # Print the saved SS58 address.
17
+ orynq status # Show gateway + chain health.
18
+ orynq help # Show usage.
19
+ ```
20
+
21
+ ## Programmatic API
22
+
23
+ ```typescript
24
+ import { bootstrapAndTrace } from "@fluxpointstudios/orynq-sdk-quickstart";
25
+
26
+ const result = await bootstrapAndTrace({
27
+ // All defaults point at Materios preprod. Set any subset to override.
28
+ agentId: "my-agent",
29
+ summary: "anchored from my CI pipeline",
30
+ onProgress(step) {
31
+ // Optional live progress stream.
32
+ console.log(step.kind);
33
+ },
34
+ });
35
+
36
+ console.log("Trace URL:", result.urls.blobStatus);
37
+ console.log("Cert hash:", result.certHash);
38
+ ```
39
+
40
+ ### Primitives
41
+
42
+ If you don't want the all-in-one bootstrap, mix and match:
43
+
44
+ ```typescript
45
+ import {
46
+ loadOrCreateIdentity,
47
+ requestFaucet,
48
+ firstTraceBundle,
49
+ buildExplorerUrls,
50
+ DEFAULT_RPC_URL,
51
+ DEFAULT_GATEWAY_URL,
52
+ } from "@fluxpointstudios/orynq-sdk-quickstart";
53
+
54
+ const identity = await loadOrCreateIdentity();
55
+ await requestFaucet({ address: identity.address, gatewayBaseUrl: DEFAULT_GATEWAY_URL });
56
+ const bundle = await firstTraceBundle({ agentId: "my-agent", summary: "hi" });
57
+ // ...build your own MateriosProvider, submit, certify...
58
+ const urls = buildExplorerUrls({ contentHash, blockHash, gatewayBaseUrl: DEFAULT_GATEWAY_URL, rpcUrl: DEFAULT_RPC_URL });
59
+ ```
60
+
61
+ ## Env variable overrides
62
+
63
+ | Env var | Default | Purpose |
64
+ |---|---|---|
65
+ | `ORYNQ_CONFIG_PATH` | `~/.orynq/config.json` | Identity file location |
66
+ | `ORYNQ_RPC_URL` | `wss://materios.fluxpointstudios.com/rpc` | Substrate WS RPC URL |
67
+ | `ORYNQ_GATEWAY_URL` | `https://materios.fluxpointstudios.com/blobs` | Blob-gateway base URL |
68
+ | `ORYNQ_AGENT_ID` | `orynq-quickstart` | Agent ID stamped on the trace |
69
+ | `ORYNQ_SUMMARY` | auto-generated | Observation event content |
70
+ | `ORYNQ_SKIP_FAUCET` | `0` | Set to `1` to skip faucet drip |
71
+ | `ORYNQ_VERBOSE` | `0` | Set to `1` for extra info on `whoami` |
72
+
73
+ ## Trust model
74
+
75
+ `orynq init` generates a fresh sr25519 keypair and writes the mnemonic to `~/.orynq/config.json` with 0600 permissions (POSIX). On Materios preprod, that address can:
76
+
77
+ - Faucet-drip test MATRA (one-shot per address)
78
+ - Upload blob data to the public gateway via sig-only auth
79
+ - Submit `submitReceipt` extrinsics on chain
80
+
81
+ There is **no** automatic upgrade to a Materios-mainnet identity yet — the preprod identity is meant for "hello world" learning. To move a real workload to mainnet, generate a separate mainnet identity (recommended: hardware-wallet-backed) and point `ORYNQ_RPC_URL` / `ORYNQ_GATEWAY_URL` at the mainnet endpoints.
82
+
83
+ ## Sub-5-minute contract
84
+
85
+ The package's CI exercises a fresh-install simulation that asserts `bootstrapAndTrace()` completes in under 300 seconds (5 minutes) including faucet drip, MOTRA generation, on-chain submission, and committee certification on Materios preprod.
86
+
87
+ Measured end-to-end on Gemtek (4-core, 16 GB RAM, residential link, 2026-05-14):
88
+
89
+ - `npm install --global` — ~10 s
90
+ - `orynq trace` (cold start, fresh address, no funds) — 167.9 s
91
+ - Total — ~178 s = 2:58, well under the 5-minute bar
92
+
93
+ `orynq trace` is idempotent — rerunning prints the same identity and submits a new receipt without re-faucet (the per-address ledger reuses the existing balance).
94
+
95
+ ## License
96
+
97
+ MIT
package/bin/orynq.mjs ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `orynq` — solo-developer CLI for orynq-sdk-quickstart.
4
+ *
5
+ * Subcommands:
6
+ * orynq init Generate identity + faucet drip + verify chain reach.
7
+ * No on-chain submission. Idempotent: reruns just print
8
+ * the current state.
9
+ * orynq trace Submit a one-event "hello" trace from the saved
10
+ * identity. Prints the explorer URLs at the end.
11
+ * Combines `init` + first submission.
12
+ * orynq whoami Print the saved address. No network calls.
13
+ * orynq status GET ${gateway}/health and print the summary.
14
+ *
15
+ * Env overrides:
16
+ * ORYNQ_CONFIG_PATH Path to identity file (default: ~/.orynq/config.json).
17
+ * ORYNQ_RPC_URL Substrate WS RPC URL (default: preprod).
18
+ * ORYNQ_GATEWAY_URL Blob-gateway URL (default: preprod).
19
+ * ORYNQ_AGENT_ID AgentId for the trace bundle.
20
+ * ORYNQ_SUMMARY Override the default observation event content.
21
+ * ORYNQ_SKIP_FAUCET "1" to skip the faucet step (already-funded addrs).
22
+ *
23
+ * Exit codes:
24
+ * 0 success
25
+ * 1 user-facing error (printed to stderr, no stack trace)
26
+ * 2 internal/unexpected error (full stack trace)
27
+ */
28
+ import { argv, env, exit, stderr, stdout, version as nodeVersion } from "node:process";
29
+
30
+ // Lazy-resolve the package's own dist so the CLI works both from the
31
+ // monorepo dev tree and from a published tarball.
32
+ async function loadSdk() {
33
+ return import("../dist/index.js").catch(() => import("../src/index.ts"));
34
+ }
35
+
36
+ function ansi(s, code) {
37
+ if (!stdout.isTTY) return s;
38
+ return `[${code}m${s}`;
39
+ }
40
+ const bold = (s) => ansi(s, "1");
41
+ const dim = (s) => ansi(s, "2");
42
+ const green = (s) => ansi(s, "32");
43
+ const yellow = (s) => ansi(s, "33");
44
+ const cyan = (s) => ansi(s, "36");
45
+ const red = (s) => ansi(s, "31");
46
+
47
+ function printUsage() {
48
+ stdout.write(
49
+ [
50
+ `${bold("orynq")} — solo-dev CLI for orynq-sdk-quickstart`,
51
+ ``,
52
+ `Usage:`,
53
+ ` ${bold("orynq init")} Generate identity + faucet-drip MATRA.`,
54
+ ` Idempotent — safe to rerun.`,
55
+ ` ${bold("orynq trace")} Submit your first trace on Materios.`,
56
+ ` Combines init + on-chain submit + cert.`,
57
+ ` ${bold("orynq whoami")} Print the saved SS58 address.`,
58
+ ` ${bold("orynq status")} Show gateway + chain health.`,
59
+ ` ${bold("orynq help")} Show this message.`,
60
+ ``,
61
+ `Env overrides:`,
62
+ ` ORYNQ_CONFIG_PATH=<path> Where to save identity (default: ~/.orynq/config.json)`,
63
+ ` ORYNQ_RPC_URL=<wss-url> Substrate RPC (default: Materios preprod)`,
64
+ ` ORYNQ_GATEWAY_URL=<url> Blob-gateway base URL`,
65
+ ` ORYNQ_SKIP_FAUCET=1 Skip the faucet drip step`,
66
+ ``,
67
+ `Docs: https://github.com/Flux-Point-Studios/orynq-sdk#quickstart`,
68
+ ``,
69
+ ].join("\n"),
70
+ );
71
+ }
72
+
73
+ async function cmdInit() {
74
+ const sdk = await loadSdk();
75
+ const identity = await sdk.loadOrCreateIdentity({
76
+ configPath: env.ORYNQ_CONFIG_PATH,
77
+ });
78
+
79
+ stdout.write(
80
+ `${bold("Identity")} ${identity.freshlyGenerated ? green("created") : dim("(reused)")}\n`,
81
+ );
82
+ stdout.write(` address ${cyan(identity.address)}\n`);
83
+ stdout.write(` configPath ${identity.configPath}\n`);
84
+ stdout.write(` generatedAt ${identity.generatedAt}\n`);
85
+ for (const w of identity.warnings) {
86
+ stdout.write(` ${yellow("warning")} ${w}\n`);
87
+ }
88
+
89
+ if (env.ORYNQ_SKIP_FAUCET !== "1") {
90
+ const gateway = env.ORYNQ_GATEWAY_URL ?? sdk.DEFAULT_GATEWAY_URL;
91
+ stdout.write(`\n${bold("Faucet")} ${dim(`(${gateway})`)}\n`);
92
+ const result = await sdk.requestFaucet({
93
+ address: identity.address,
94
+ gatewayBaseUrl: gateway,
95
+ });
96
+ switch (result.kind) {
97
+ case "success":
98
+ stdout.write(` ${green("dripped")} ${result.amount} units\n`);
99
+ stdout.write(` txHash ${result.txHash}\n`);
100
+ stdout.write(` ${dim("MOTRA will generate over the next few blocks.")}\n`);
101
+ break;
102
+ case "already-funded":
103
+ stdout.write(
104
+ ` ${dim("(already funded — drip ledger says yes; will reuse existing balance)")}\n`,
105
+ );
106
+ break;
107
+ case "cooldown":
108
+ stdout.write(
109
+ ` ${yellow("cooldown")} retry in ~${Math.round((result.retryAfterMs ?? 0) / 1000)}s\n`,
110
+ );
111
+ break;
112
+ case "error":
113
+ stdout.write(` ${red("error")} ${result.message} (HTTP ${result.status})\n`);
114
+ return 1;
115
+ }
116
+ } else {
117
+ stdout.write(`\n${bold("Faucet")} ${dim("(skipped via ORYNQ_SKIP_FAUCET=1)")}\n`);
118
+ }
119
+
120
+ stdout.write(`\n${green("init complete")}. Next:\n`);
121
+ stdout.write(` ${bold("orynq trace")} submit your first trace\n`);
122
+ return 0;
123
+ }
124
+
125
+ async function cmdTrace() {
126
+ const sdk = await loadSdk();
127
+ const traceStart = Date.now();
128
+
129
+ const result = await sdk.bootstrapAndTrace({
130
+ configPath: env.ORYNQ_CONFIG_PATH,
131
+ rpcUrl: env.ORYNQ_RPC_URL,
132
+ gatewayBaseUrl: env.ORYNQ_GATEWAY_URL,
133
+ agentId: env.ORYNQ_AGENT_ID,
134
+ summary: env.ORYNQ_SUMMARY,
135
+ skipFaucet: env.ORYNQ_SKIP_FAUCET === "1",
136
+ onProgress(step) {
137
+ const ts = ((Date.now() - traceStart) / 1000).toFixed(1).padStart(5, " ");
138
+ const tag = `${dim(`[+${ts}s]`)}`;
139
+ switch (step.kind) {
140
+ case "identity-loaded":
141
+ stdout.write(
142
+ `${tag} identity ${step.identity.freshlyGenerated ? green("created") : dim("(reused)")} ${cyan(step.identity.address)}\n`,
143
+ );
144
+ break;
145
+ case "faucet-result":
146
+ if (step.result.kind === "success") {
147
+ stdout.write(`${tag} faucet ${green("dripped")} ${step.result.amount} units (tx ${step.result.txHash.slice(0, 12)}...)\n`);
148
+ } else if (step.result.kind === "already-funded") {
149
+ stdout.write(`${tag} faucet ${dim("already funded — reusing balance")}\n`);
150
+ }
151
+ break;
152
+ case "waiting-for-motra":
153
+ stdout.write(`${tag} waiting for MOTRA fee currency to generate (~10-30s)...\n`);
154
+ break;
155
+ case "motra-ready":
156
+ stdout.write(`${tag} MOTRA ready (${step.balance.toString()} units)\n`);
157
+ break;
158
+ case "trace-built":
159
+ stdout.write(
160
+ `${tag} trace built — runId ${dim(step.bundle.runId.slice(0, 8))} rootHash ${dim(step.bundle.rootHash.slice(0, 12))}\n`,
161
+ );
162
+ break;
163
+ case "receipt-submitted":
164
+ stdout.write(
165
+ `${tag} receipt submitted — receiptId ${dim(step.receiptId.slice(0, 14))} block ${dim(step.blockHash.slice(0, 14))}\n`,
166
+ );
167
+ break;
168
+ case "certified":
169
+ stdout.write(
170
+ `${tag} ${green("certified")} certHash ${dim(step.certHash.slice(0, 14))}\n`,
171
+ );
172
+ break;
173
+ case "explorer-ready":
174
+ // Handled below in the summary block — keep the streaming
175
+ // output uncluttered.
176
+ break;
177
+ }
178
+ },
179
+ });
180
+
181
+ // Summary block — this is what the dev came for.
182
+ stdout.write(`\n${green("First trace anchored on Materios")} ${dim(`(${(result.elapsedMs / 1000).toFixed(1)}s)`)}\n\n`);
183
+ stdout.write(`${bold("View your trace:")}\n`);
184
+ stdout.write(` blob status ${cyan(result.urls.blobStatus)}\n`);
185
+ stdout.write(` chain block ${cyan(result.urls.explorer)}\n`);
186
+ stdout.write(` chain info ${dim(result.urls.chainInfo)}\n`);
187
+ stdout.write(` health ${dim(result.urls.gatewayHealth)}\n`);
188
+ stdout.write(`\n${bold("Hashes")}\n`);
189
+ stdout.write(` receiptId ${result.receiptId}\n`);
190
+ stdout.write(` blockHash ${result.blockHash}\n`);
191
+ stdout.write(` rootHash ${result.bundle.rootHash}\n`);
192
+ stdout.write(` merkleRoot ${result.bundle.merkleRoot}\n`);
193
+ if (result.certHash) {
194
+ stdout.write(` certHash ${result.certHash}\n`);
195
+ }
196
+ return 0;
197
+ }
198
+
199
+ async function cmdWhoami() {
200
+ const sdk = await loadSdk();
201
+ // Use loadOrCreateIdentity, which creates on first run. If the dev
202
+ // explicitly wants to refuse auto-create, they can rm the file before
203
+ // calling whoami — but the spec is "frictionless first call", so we
204
+ // create.
205
+ const identity = await sdk.loadOrCreateIdentity({
206
+ configPath: env.ORYNQ_CONFIG_PATH,
207
+ });
208
+ stdout.write(`${identity.address}\n`);
209
+ if (env.ORYNQ_VERBOSE === "1") {
210
+ stdout.write(`${dim("config: " + identity.configPath)}\n`);
211
+ stdout.write(`${dim("generatedAt: " + identity.generatedAt)}\n`);
212
+ }
213
+ return 0;
214
+ }
215
+
216
+ async function cmdStatus() {
217
+ const sdk = await loadSdk();
218
+ const gateway = env.ORYNQ_GATEWAY_URL ?? sdk.DEFAULT_GATEWAY_URL;
219
+ const base = gateway.replace(/\/blobs\/?$/, "").replace(/\/$/, "");
220
+ // /status is the cluster-wide rollup (gateway + cert-daemon + anchor-worker).
221
+ // /health is gateway-only. Prefer /status so the dev sees finality + L1
222
+ // anchor health at a glance.
223
+ const url = `${base}/status`;
224
+ let res;
225
+ try {
226
+ res = await fetch(url);
227
+ } catch (e) {
228
+ stderr.write(`${red("error")} could not reach ${url}: ${e instanceof Error ? e.message : String(e)}\n`);
229
+ return 1;
230
+ }
231
+ const text = await res.text();
232
+ if (!res.ok) {
233
+ stderr.write(`${red("error")} HTTP ${res.status} from ${url}\n${text}\n`);
234
+ return 1;
235
+ }
236
+ let parsed;
237
+ try {
238
+ parsed = JSON.parse(text);
239
+ } catch {
240
+ stdout.write(text);
241
+ return 0;
242
+ }
243
+ stdout.write(`${bold("Materios")} ${cyan(base)}\n`);
244
+ stdout.write(` status ${parsed.overall ?? parsed.status ?? "unknown"}\n`);
245
+ if (parsed.components?.gateway) {
246
+ const g = parsed.components.gateway;
247
+ stdout.write(` uptime ${Math.round((g.uptime ?? 0) / 60)}m\n`);
248
+ stdout.write(` totalReceipts ${g.storage?.totalReceipts ?? "?"}\n`);
249
+ }
250
+ if (parsed.components?.certDaemonAlice) {
251
+ const c = parsed.components.certDaemonAlice;
252
+ stdout.write(` bestBlock ${c.bestBlock}\n`);
253
+ stdout.write(` finalityGap ${c.finalityGap}\n`);
254
+ }
255
+ if (parsed.components?.anchorWorker) {
256
+ const a = parsed.components.anchorWorker;
257
+ stdout.write(` anchorCount ${a.anchorCount}\n`);
258
+ stdout.write(` cardanoTxs last=${a.lastTxHash?.slice(0, 12) ?? "?"}...\n`);
259
+ }
260
+ return 0;
261
+ }
262
+
263
+ async function main() {
264
+ const cmd = argv[2] ?? "help";
265
+ try {
266
+ switch (cmd) {
267
+ case "init":
268
+ return await cmdInit();
269
+ case "trace":
270
+ return await cmdTrace();
271
+ case "whoami":
272
+ return await cmdWhoami();
273
+ case "status":
274
+ return await cmdStatus();
275
+ case "help":
276
+ case "--help":
277
+ case "-h":
278
+ printUsage();
279
+ return 0;
280
+ case "--version":
281
+ case "-v": {
282
+ const sdk = await loadSdk();
283
+ stdout.write(`orynq-sdk-quickstart ${sdk.VERSION} (node ${nodeVersion})\n`);
284
+ return 0;
285
+ }
286
+ default:
287
+ stderr.write(`${red("error")} unknown command: ${cmd}\n\n`);
288
+ printUsage();
289
+ return 1;
290
+ }
291
+ } catch (err) {
292
+ const msg = err instanceof Error ? err.message : String(err);
293
+ // User-facing errors carry a leading-lowercase, no-trailing-period
294
+ // convention. Internal errors print the full stack.
295
+ if (err instanceof Error && /^[a-z]/.test(msg) && !/^Error:/.test(msg)) {
296
+ stderr.write(`${red("error")} ${msg}\n`);
297
+ return 1;
298
+ }
299
+ stderr.write(`${red("internal error")}\n`);
300
+ if (err instanceof Error && err.stack) stderr.write(err.stack + "\n");
301
+ else stderr.write(String(err) + "\n");
302
+ return 2;
303
+ }
304
+ }
305
+
306
+ main().then((code) => exit(code ?? 0));