arkiv-cli 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,144 @@
1
+ /**
2
+ * `arkiv cost` - the pre-flight sizing check.
3
+ *
4
+ * Arkiv charges for storage at write time and the exact fee is whatever the
5
+ * create transaction consumes, so this command does not invent a price. What it
6
+ * does is the part people get wrong: work out how many bytes and blocks a write
7
+ * really needs, whether it fits the protocol limits, how many creates that takes,
8
+ * and what the chain is charging for gas right now.
9
+ */
10
+ import { statSync, readFileSync } from "node:fs";
11
+ import { explainError } from "../arkiv.js";
12
+ import { BLOCK_SECONDS, MAX_ATTRIBUTES, MAX_BYTES_LENGTH, MAX_ENTITY_BYTES, MAX_LIFETIME, } from "../limits.js";
13
+ import { bold, cyan, dim, formatDuration, green, grey, out, red, table, yellow } from "../ui.js";
14
+ /** Rough per-attribute framing: name, type byte, length prefix, index flag. */
15
+ const ATTRIBUTE_OVERHEAD = 40;
16
+ export function parseLifetime(input, fallbackDays) {
17
+ if (!input)
18
+ return fallbackDays;
19
+ const match = /^(\d+(?:\.\d+)?)([smhdwy])$/.exec(input.trim());
20
+ if (!match) {
21
+ throw new Error(`cannot read lifetime "${input}"; use a number with s, m, h, d, w or y (for example 30d)`);
22
+ }
23
+ const value = Number(match[1]);
24
+ const unit = match[2];
25
+ const days = {
26
+ s: value / 86_400,
27
+ m: value / 1_440,
28
+ h: value / 24,
29
+ d: value,
30
+ w: value * 7,
31
+ y: value * 365,
32
+ }[unit];
33
+ return days ?? value;
34
+ }
35
+ export async function runCost(ctx, options) {
36
+ let payloadBytes = options.bytes;
37
+ let fileNote = "";
38
+ if (options.file) {
39
+ try {
40
+ const info = statSync(options.file);
41
+ payloadBytes = info.size;
42
+ fileNote = options.file;
43
+ // Read a little of it so an unreadable file fails here rather than mid-write.
44
+ readFileSync(options.file, { flag: "r" });
45
+ }
46
+ catch (error) {
47
+ out(red(`cannot read ${options.file}: ${error.message}`));
48
+ process.exitCode = 1;
49
+ return;
50
+ }
51
+ }
52
+ const attributeBytes = options.attributes * ATTRIBUTE_OVERHEAD + options.attributes * options.averageAttributeBytes;
53
+ const perCreate = payloadBytes + attributeBytes;
54
+ const creates = Math.max(1, Math.ceil(perCreate / MAX_ENTITY_BYTES));
55
+ const largestCreate = Math.min(perCreate, MAX_ENTITY_BYTES);
56
+ const blocks = Math.round(options.lifetimeDays * 86_400 / BLOCK_SECONDS);
57
+ const fits = {
58
+ attributes: options.attributes <= MAX_ATTRIBUTES - 1,
59
+ bytes: payloadBytes <= MAX_BYTES_LENGTH,
60
+ lifetime: blocks <= MAX_LIFETIME,
61
+ };
62
+ let gasPrice = null;
63
+ let gasNote = "unavailable";
64
+ try {
65
+ gasPrice = await ctx.client().getGasPrice();
66
+ gasNote = `${(Number(gasPrice) / 1e9).toFixed(4)} gwei`;
67
+ }
68
+ catch (error) {
69
+ gasNote = explainError(error);
70
+ }
71
+ if (ctx.json) {
72
+ process.stdout.write(`${JSON.stringify({
73
+ file: fileNote || null,
74
+ payloadBytes,
75
+ attributeBytes,
76
+ bytesPerCreate: perCreate,
77
+ creates,
78
+ lifetimeDays: options.lifetimeDays,
79
+ blocks,
80
+ fits,
81
+ gasPriceWei: gasPrice ? gasPrice.toString() : null,
82
+ }, null, 2)}\n`);
83
+ return;
84
+ }
85
+ out(bold("storage plan"));
86
+ out(table(["ITEM", "VALUE", "LIMIT", "STATUS"], [
87
+ [
88
+ "attributes",
89
+ String(options.attributes),
90
+ String(MAX_ATTRIBUTES - 1),
91
+ fits.attributes ? green("fits") : red("over"),
92
+ ],
93
+ [
94
+ "payload",
95
+ formatBytes(payloadBytes),
96
+ formatBytes(MAX_BYTES_LENGTH),
97
+ fits.bytes ? green("fits") : yellow("needs chunking"),
98
+ ],
99
+ [
100
+ "bytes per entity",
101
+ formatBytes(perCreate),
102
+ formatBytes(MAX_ENTITY_BYTES),
103
+ perCreate <= MAX_ENTITY_BYTES ? green("fits") : yellow(`${creates} creates`),
104
+ ],
105
+ [
106
+ "lifetime",
107
+ `${formatDuration(options.lifetimeDays * 86_400)} (${blocks.toLocaleString("en-US")} blocks)`,
108
+ `${formatDuration(MAX_LIFETIME * BLOCK_SECONDS)}`,
109
+ fits.lifetime ? green("fits") : red("over"),
110
+ ],
111
+ ]));
112
+ out();
113
+ out(`${grey("largest create ")} ${formatBytes(largestCreate)}`);
114
+ out(`${grey("gas price ")} ${gasNote} ${dim("(live, mainnet-style read)")}`);
115
+ out(dim("the fee itself is what the create transaction burns; extension costs depend on remaining lifetime, " +
116
+ "so measure it with `arkiv create --dry-run` against your own access key"));
117
+ if (!fits.lifetime) {
118
+ out();
119
+ out(yellow(`shrink the lifetime to ${formatDuration(MAX_LIFETIME * BLOCK_SECONDS)} or less, or chain extensions`));
120
+ }
121
+ if (!fits.attributes) {
122
+ out();
123
+ out(yellow("drop an attribute or move related fields into one bytes payload"));
124
+ }
125
+ }
126
+ export function formatBytes(bytes) {
127
+ if (bytes < 1024)
128
+ return `${bytes} B`;
129
+ if (bytes < 1024 * 1024)
130
+ return `${(bytes / 1024).toFixed(1)} KiB`;
131
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`;
132
+ }
133
+ export function describeLimits() {
134
+ return table(["LIMIT", "VALUE", "NOTE"], [
135
+ ["attributes per entity", `${MAX_ATTRIBUTES} (${MAX_ATTRIBUTES - 1} + schema)`, "arkiv.schema counts as one"],
136
+ ["bytes per entity", formatBytes(MAX_ENTITY_BYTES), "sum of all bytes values"],
137
+ ["single bytes attribute", formatBytes(MAX_BYTES_LENGTH), "chunk larger payloads"],
138
+ ["entity lifetime", `${MAX_LIFETIME.toLocaleString("en-US")} blocks`, formatDuration(MAX_LIFETIME * BLOCK_SECONDS)],
139
+ ["block time", `${BLOCK_SECONDS}s`, "used for expiry maths"],
140
+ ]);
141
+ }
142
+ export function limitSummary() {
143
+ return `${cyan(String(MAX_ATTRIBUTES))} attributes, ${cyan(formatBytes(MAX_ENTITY_BYTES))} per entity, ${cyan(String(MAX_LIFETIME))} blocks lifetime`;
144
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * `arkiv create` - write one entity from the terminal.
3
+ *
4
+ * The SDK only writes through a typed schema, so this command builds one at
5
+ * runtime from the flags you pass, then refuses anything the schema or the
6
+ * protocol limits would reject. `--dry-run` prints the exact plan without
7
+ * signing anything.
8
+ */
9
+ import type { Context } from "../context.js";
10
+ export type ValueTypeName = "str" | "bytes" | "u64" | "i32" | "u256" | "bool" | "addr" | "key" | "dec";
11
+ export declare const VALUE_TYPES: ValueTypeName[];
12
+ export interface CreateOptions {
13
+ schema: string;
14
+ sets: string[];
15
+ indexed: string[];
16
+ expires?: string;
17
+ dryRun: boolean;
18
+ }
19
+ export interface ParsedAttribute {
20
+ name: string;
21
+ type: ValueTypeName;
22
+ raw: string;
23
+ value: unknown;
24
+ bytes: number;
25
+ }
26
+ export declare function parseAttribute(input: string): ParsedAttribute;
27
+ export declare function runCreate(ctx: Context, options: CreateOptions): Promise<void>;
@@ -0,0 +1,239 @@
1
+ /**
2
+ * `arkiv create` - write one entity from the terminal.
3
+ *
4
+ * The SDK only writes through a typed schema, so this command builds one at
5
+ * runtime from the flags you pass, then refuses anything the schema or the
6
+ * protocol limits would reject. `--dry-run` prints the exact plan without
7
+ * signing anything.
8
+ */
9
+ import { readFileSync } from "node:fs";
10
+ import { ExpirationTime } from "@arkiv-network/sdk";
11
+ import { addr, bool as boolValue, bytes as bytesValue, dec, i32, key as keyValue, str, u64, u256, } from "@arkiv-network/sdk";
12
+ import { addr as addrColumn, bool as boolColumn, bytes as bytesColumn, dec as decColumn, defineSchema, i32 as i32Column, key as keyColumn, str as strColumn, u64 as u64Column, u256 as u256Column, } from "@arkiv-network/sdk/schema";
13
+ import { explainError, makeWallet, takeNotice } from "../arkiv.js";
14
+ import { MAX_ATTRIBUTES, MAX_BYTES_LENGTH, MAX_LIFETIME, BLOCK_SECONDS } from "../limits.js";
15
+ import { formatBytes, parseLifetime } from "./cost.js";
16
+ import { bold, cyan, dim, green, grey, out, red, table } from "../ui.js";
17
+ export const VALUE_TYPES = ["str", "bytes", "u64", "i32", "u256", "bool", "addr", "key", "dec"];
18
+ const COLUMNS = {
19
+ str: strColumn,
20
+ bytes: bytesColumn,
21
+ u64: u64Column,
22
+ i32: i32Column,
23
+ u256: u256Column,
24
+ bool: boolColumn,
25
+ addr: addrColumn,
26
+ key: keyColumn,
27
+ dec: decColumn,
28
+ };
29
+ export function parseAttribute(input) {
30
+ const eq = input.indexOf("=");
31
+ if (eq < 1)
32
+ throw new Error(`expected name=type:value, got "${input}"`);
33
+ const name = input.slice(0, eq).trim();
34
+ const rest = input.slice(eq + 1);
35
+ const colon = rest.indexOf(":");
36
+ if (colon < 1)
37
+ throw new Error(`missing type for "${name}"; use ${name}=str:hello`);
38
+ const type = rest.slice(0, colon).trim();
39
+ const raw = rest.slice(colon + 1);
40
+ if (!VALUE_TYPES.includes(type)) {
41
+ throw new Error(`unknown value type "${type}" for ${name}; use one of ${VALUE_TYPES.join(", ")}`);
42
+ }
43
+ if (name.length === 0 || Buffer.byteLength(name, "utf8") > 32) {
44
+ throw new Error(`attribute name "${name}" is empty or over 32 bytes`);
45
+ }
46
+ if (name === "arkiv.schema") {
47
+ throw new Error("arkiv.schema is set by --schema, not by --set");
48
+ }
49
+ let value;
50
+ let bytes;
51
+ switch (type) {
52
+ case "str": {
53
+ const encoded = Buffer.byteLength(raw, "utf8");
54
+ if (encoded > 128)
55
+ throw new Error(`${name} is ${encoded} bytes of text; str caps at 128, use bytes:.path`);
56
+ value = str(raw);
57
+ bytes = encoded;
58
+ break;
59
+ }
60
+ case "bytes": {
61
+ const data = readPayload(raw);
62
+ if (data.byteLength > MAX_BYTES_LENGTH) {
63
+ throw new Error(`${name} is ${formatBytes(data.byteLength)}; one bytes attribute caps at ${formatBytes(MAX_BYTES_LENGTH)}`);
64
+ }
65
+ value = bytesValue(data);
66
+ bytes = data.byteLength;
67
+ break;
68
+ }
69
+ case "u64":
70
+ value = u64(BigInt(raw));
71
+ bytes = 8;
72
+ break;
73
+ case "i32":
74
+ value = i32(Number.parseInt(raw, 10));
75
+ bytes = 4;
76
+ break;
77
+ case "u256":
78
+ value = u256(BigInt(raw));
79
+ bytes = 32;
80
+ break;
81
+ case "bool":
82
+ if (!/^(true|false)$/.test(raw))
83
+ throw new Error(`${name} must be true or false`);
84
+ value = boolValue(raw === "true");
85
+ bytes = 1;
86
+ break;
87
+ case "addr":
88
+ if (!/^0x[0-9a-fA-F]{40}$/.test(raw))
89
+ throw new Error(`${name} is not a 20-byte address: ${raw}`);
90
+ value = addr(raw);
91
+ bytes = 20;
92
+ break;
93
+ case "key":
94
+ if (!/^0x[0-9a-fA-F]{64}$/.test(raw))
95
+ throw new Error(`${name} is not a 32-byte key: ${raw}`);
96
+ value = keyValue(raw);
97
+ bytes = 32;
98
+ break;
99
+ case "dec":
100
+ value = dec(raw);
101
+ bytes = 32;
102
+ break;
103
+ default:
104
+ throw new Error(`unsupported type ${type}`);
105
+ }
106
+ return { name, type, raw, value, bytes };
107
+ }
108
+ function readPayload(raw) {
109
+ const path = raw.startsWith("@") ? raw.slice(1) : raw.startsWith("./") || raw.startsWith("/") ? raw : null;
110
+ if (path) {
111
+ try {
112
+ return new Uint8Array(readFileSync(path));
113
+ }
114
+ catch (error) {
115
+ throw new Error(`cannot read ${path}: ${error.message}`);
116
+ }
117
+ }
118
+ if (/^0x[0-9a-fA-F]*$/.test(raw)) {
119
+ const hex = raw.slice(2);
120
+ if (hex.length % 2 !== 0)
121
+ throw new Error("hex bytes must have an even number of digits");
122
+ return new Uint8Array(Buffer.from(hex, "hex"));
123
+ }
124
+ throw new Error('bytes values take a file path (@./file or ./file) or 0x-prefixed hex');
125
+ }
126
+ export async function runCreate(ctx, options) {
127
+ if (!/^[A-Za-z0-9][A-Za-z0-9._/-]{0,31}$/.test(options.schema)) {
128
+ out(red(`schema "${options.schema}" is not a valid name`));
129
+ out(dim("1 to 32 bytes of letters, digits, dot, slash, dash or underscore, starting with a letter or digit"));
130
+ process.exitCode = 2;
131
+ return;
132
+ }
133
+ let attributes;
134
+ try {
135
+ attributes = options.sets.map(parseAttribute);
136
+ }
137
+ catch (error) {
138
+ out(red(error.message));
139
+ process.exitCode = 2;
140
+ return;
141
+ }
142
+ const seen = new Set();
143
+ for (const attribute of attributes) {
144
+ if (seen.has(attribute.name)) {
145
+ out(red(`attribute ${attribute.name} is set twice`));
146
+ process.exitCode = 2;
147
+ return;
148
+ }
149
+ seen.add(attribute.name);
150
+ }
151
+ if (attributes.length + 1 > MAX_ATTRIBUTES) {
152
+ out(red(`${attributes.length} attributes plus arkiv.schema exceeds the limit of ${MAX_ATTRIBUTES}`));
153
+ out(dim("the schema attribute counts as one of them"));
154
+ process.exitCode = 2;
155
+ return;
156
+ }
157
+ const indexed = new Set(options.indexed);
158
+ for (const name of indexed) {
159
+ if (!seen.has(name)) {
160
+ out(red(`--index ${name} does not match any --set attribute`));
161
+ process.exitCode = 2;
162
+ return;
163
+ }
164
+ }
165
+ let lifetimeDays;
166
+ try {
167
+ lifetimeDays = parseLifetime(options.expires, 30);
168
+ }
169
+ catch (error) {
170
+ out(red(error.message));
171
+ process.exitCode = 2;
172
+ return;
173
+ }
174
+ const blocks = Math.round((lifetimeDays * 86_400) / BLOCK_SECONDS);
175
+ if (blocks > MAX_LIFETIME) {
176
+ out(red(`lifetime ${lifetimeDays}d is past the ${MAX_LIFETIME.toLocaleString("en-US")} block maximum`));
177
+ process.exitCode = 2;
178
+ return;
179
+ }
180
+ const totalBytes = attributes.reduce((sum, attribute) => sum + attribute.bytes, 0);
181
+ const columns = {};
182
+ const values = {};
183
+ for (const attribute of attributes) {
184
+ const build = COLUMNS[attribute.type];
185
+ let column = build();
186
+ if (indexed.has(attribute.name))
187
+ column = column.isIndexed();
188
+ columns[attribute.name] = column;
189
+ values[attribute.name] = attribute.value;
190
+ }
191
+ if (ctx.json) {
192
+ process.stdout.write(`${JSON.stringify({
193
+ schema: options.schema,
194
+ attributes: attributes.map((a) => ({ name: a.name, type: a.type, bytes: a.bytes, indexed: indexed.has(a.name) })),
195
+ totalBytes,
196
+ lifetimeDays,
197
+ blocks,
198
+ dryRun: options.dryRun,
199
+ }, null, 2)}\n`);
200
+ if (options.dryRun)
201
+ return;
202
+ }
203
+ else {
204
+ out(bold("create plan"));
205
+ out(table(["FIELD", "TYPE", "BYTES", "INDEXED"], attributes.map((a) => [a.name, cyan(a.type), String(a.bytes), indexed.has(a.name) ? green("yes") : grey("no")])));
206
+ out();
207
+ out(`${grey("schema ")} ${options.schema}`);
208
+ out(`${grey("payload ")} ${formatBytes(totalBytes)} ${dim(`of ${formatBytes(131072)} per entity`)}`);
209
+ out(`${grey("lifetime ")} ${lifetimeDays}d ${dim(`(${blocks.toLocaleString("en-US")} blocks)`)}`);
210
+ }
211
+ if (options.dryRun) {
212
+ out(dim("dry run: nothing was signed or sent"));
213
+ return;
214
+ }
215
+ if (!ctx.privateKey) {
216
+ out(red("no private key found, so this cannot be signed"));
217
+ out(dim("add ARKIV_PRIVATE_KEY to .env, then run `arkiv doctor`"));
218
+ process.exitCode = 1;
219
+ return;
220
+ }
221
+ const schema = defineSchema(options.schema, columns);
222
+ try {
223
+ const { client, account } = makeWallet({ network: ctx.network, accessKey: ctx.accessKey, rpcUrl: ctx.rpcUrl }, ctx.privateKey);
224
+ out(dim(`signing as ${account.address}`));
225
+ const result = await client.createEntity(schema, values, { expiration: ExpirationTime.fromBlocks(blocks) });
226
+ const notice = takeNotice();
227
+ if (notice)
228
+ out(dim(notice));
229
+ out(green("written"));
230
+ out(`${grey("key ")} ${result.entityKey}`);
231
+ out(`${grey("tx ")} ${result.txHash}`);
232
+ out(`${grey("expires ")} block ${Number(result.expiresAt).toLocaleString("en-US")}`);
233
+ }
234
+ catch (error) {
235
+ out(red("write failed"));
236
+ out(dim(explainError(error)));
237
+ process.exitCode = 1;
238
+ }
239
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * `arkiv doctor` - the first thing a new developer should run.
3
+ *
4
+ * Every check answers one question: will a write to Rabdi work from this
5
+ * machine, right now? Failures carry the exact command or URL that fixes them.
6
+ */
7
+ import type { Context } from "../context.js";
8
+ type Status = "ok" | "warn" | "fail";
9
+ interface Check {
10
+ id: string;
11
+ status: Status;
12
+ detail: string;
13
+ fix?: string;
14
+ }
15
+ export interface DoctorOptions {
16
+ fixPerms?: boolean;
17
+ }
18
+ export declare function runDoctor(ctx: Context, options?: DoctorOptions): Promise<{
19
+ checks: Check[];
20
+ ok: boolean;
21
+ }>;
22
+ export declare function printDoctor(checks: Check[], json: boolean): void;
23
+ export {};
@@ -0,0 +1,190 @@
1
+ /**
2
+ * `arkiv doctor` - the first thing a new developer should run.
3
+ *
4
+ * Every check answers one question: will a write to Rabdi work from this
5
+ * machine, right now? Failures carry the exact command or URL that fixes them.
6
+ */
7
+ import { privateKeyToAccount } from "viem/accounts";
8
+ import { resolveChain, explainError, getTiming } from "../arkiv.js";
9
+ import { envIsGitIgnored, tightenPermissions, validateAccessKey, validatePrivateKey } from "../env.js";
10
+ import { bold, cyan, dim, green, grey, out, red, yellow } from "../ui.js";
11
+ const MIN_NODE = [22, 10];
12
+ const MARK = {
13
+ ok: green("OK "),
14
+ warn: yellow("WARN"),
15
+ fail: red("FAIL"),
16
+ };
17
+ function nodeCheck() {
18
+ const [major, minor] = process.versions.node.split(".").map(Number);
19
+ const ok = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
20
+ return {
21
+ id: "node",
22
+ status: ok ? "ok" : "fail",
23
+ detail: `v${process.versions.node} ${ok ? "" : `(need >= ${MIN_NODE.join(".")})`}`.trim(),
24
+ fix: ok ? undefined : "Install a current Node.js LTS from https://nodejs.org",
25
+ };
26
+ }
27
+ export async function runDoctor(ctx, options = {}) {
28
+ const checks = [nodeCheck()];
29
+ /* ---------------------------------------------------------------- env file */
30
+ if (!ctx.envFile.exists) {
31
+ checks.push({
32
+ id: "env",
33
+ status: "warn",
34
+ detail: `no .env at ${ctx.envFile.path}`,
35
+ fix: "Run `arkiv init` or create .env with ARKIV_ACCESS_KEY and ARKIV_PRIVATE_KEY",
36
+ });
37
+ }
38
+ else {
39
+ const mode = (ctx.envFile.mode ?? 0).toString(8).padStart(3, "0");
40
+ if (ctx.envFile.tooOpen) {
41
+ const fixed = options.fixPerms ? tightenPermissions(ctx.envFile.path) : false;
42
+ checks.push({
43
+ id: "env",
44
+ status: fixed ? "ok" : "warn",
45
+ detail: fixed ? `.env tightened to 600` : `.env is readable by other users (mode ${mode})`,
46
+ fix: fixed ? undefined : "chmod 600 .env (or re-run with --fix-perms)",
47
+ });
48
+ }
49
+ else {
50
+ checks.push({ id: "env", status: "ok", detail: `.env mode ${mode}` });
51
+ }
52
+ }
53
+ /* ---------------------------------------------------------------- access key */
54
+ const access = validateAccessKey(ctx.accessKey);
55
+ checks.push({
56
+ id: "access-key",
57
+ status: access.ok ? "ok" : "warn",
58
+ detail: access.ok
59
+ ? "ARKIV_ACCESS_KEY set"
60
+ : "not set - queries go to the shared, rate-limited endpoint",
61
+ fix: access.ok ? undefined : "Get a free key at https://hub.arkiv.network/access-keys",
62
+ });
63
+ /* ---------------------------------------------------------------- signer */
64
+ let address = null;
65
+ const key = validatePrivateKey(ctx.privateKey);
66
+ if (!ctx.privateKey) {
67
+ checks.push({
68
+ id: "signer",
69
+ status: "warn",
70
+ detail: "ARKIV_PRIVATE_KEY not set (read-only: query/schema/watch still work)",
71
+ fix: "Add ARKIV_PRIVATE_KEY to .env, then fund it from https://hub.arkiv.network/faucet",
72
+ });
73
+ }
74
+ else if (!key.ok) {
75
+ checks.push({ id: "signer", status: "fail", detail: key.reason });
76
+ }
77
+ else {
78
+ try {
79
+ address = privateKeyToAccount(key.normalized).address;
80
+ checks.push({ id: "signer", status: "ok", detail: address });
81
+ }
82
+ catch {
83
+ checks.push({ id: "signer", status: "fail", detail: "ARKIV_PRIVATE_KEY is not a valid secp256k1 key" });
84
+ }
85
+ }
86
+ /* ---------------------------------------------------------------- transport */
87
+ let timing = null;
88
+ let chainId = 0;
89
+ try {
90
+ const chain = resolveChain({ network: ctx.network, accessKey: ctx.accessKey, rpcUrl: ctx.rpcUrl });
91
+ chainId = chain.id;
92
+ const started = Date.now();
93
+ const client = ctx.client();
94
+ const remoteId = await client.getChainId();
95
+ const latency = Date.now() - started;
96
+ if (remoteId !== chain.id) {
97
+ checks.push({
98
+ id: "rpc",
99
+ status: "fail",
100
+ detail: `endpoint is chain ${remoteId}, expected ${chain.id} (${ctx.network})`,
101
+ fix: "Check --rpc / ARKIV_RPC_URL, or drop the override to use the default endpoint",
102
+ });
103
+ }
104
+ else {
105
+ checks.push({
106
+ id: "rpc",
107
+ status: "ok",
108
+ detail: `${chain.rpcUrls.default.http[0]} (${latency} ms)`,
109
+ });
110
+ }
111
+ timing = await getTiming(client);
112
+ if (!timing)
113
+ throw new Error("the node returned no block timing");
114
+ checks.push({
115
+ id: "chain",
116
+ status: "ok",
117
+ detail: `${ctx.network} id ${chainId}, block ${timing.currentBlock.toLocaleString("en-US")}, ${timing.blockDuration}s blocks`,
118
+ });
119
+ const skew = Math.abs(Date.now() / 1000 - timing.currentBlockTime);
120
+ checks.push({
121
+ id: "clock",
122
+ status: skew < 120 ? "ok" : "warn",
123
+ detail: skew < 120
124
+ ? `local clock within ${skew.toFixed(0)}s of chain time`
125
+ : `local clock differs from chain by ${(skew / 60).toFixed(1)} min`,
126
+ fix: skew < 120 ? undefined : "Enable automatic time sync on this machine",
127
+ });
128
+ if (address) {
129
+ const balance = await client.getBalance({ address });
130
+ const glm = Number(balance) / 1e18;
131
+ checks.push({
132
+ id: "balance",
133
+ status: glm <= 0 ? "fail" : glm < 0.05 ? "warn" : "ok",
134
+ detail: `${glm.toFixed(4)} tGLM ${symbol(ctx)}`,
135
+ fix: glm <= 0.05 ? "Top up at https://hub.arkiv.network/faucet" : undefined,
136
+ });
137
+ }
138
+ const qStart = Date.now();
139
+ const probe = await client
140
+ .select({ $key: true })
141
+ .where("EXISTS(arkiv.schema)")
142
+ .limit(1)
143
+ .fetch();
144
+ checks.push({
145
+ id: "query",
146
+ status: "ok",
147
+ detail: `smoke query ok in ${Date.now() - qStart} ms (${probe.entities.length} row)`,
148
+ });
149
+ }
150
+ catch (error) {
151
+ checks.push({ id: "rpc", status: "fail", detail: explainError(error) });
152
+ }
153
+ /* ---------------------------------------------------------------- hygiene */
154
+ const ignored = envIsGitIgnored(ctx.cwd);
155
+ checks.push({
156
+ id: "gitignore",
157
+ status: ignored === false ? "fail" : ignored ? "ok" : "warn",
158
+ detail: ignored === true
159
+ ? ".env is covered by .gitignore"
160
+ : ignored === false
161
+ ? ".env is NOT ignored by git - a commit here leaks your key"
162
+ : "no .gitignore in this directory",
163
+ fix: ignored === true ? undefined : "Add `.env` to .gitignore",
164
+ });
165
+ return {
166
+ checks,
167
+ ok: !checks.some((c) => c.status === "fail"),
168
+ };
169
+ }
170
+ function symbol(ctx) {
171
+ const chain = resolveChain({ network: ctx.network, accessKey: ctx.accessKey, rpcUrl: ctx.rpcUrl });
172
+ return chain.nativeCurrency?.symbol ?? "tGLM";
173
+ }
174
+ export function printDoctor(checks, json) {
175
+ if (json) {
176
+ process.stdout.write(`${JSON.stringify({ ok: !checks.some((c) => c.status === "fail"), checks }, null, 2)}\n`);
177
+ return;
178
+ }
179
+ const width = Math.max(...checks.map((c) => c.id.length));
180
+ for (const check of checks) {
181
+ out(`${MARK[check.status]} ${bold(check.id.padEnd(width))} ${check.detail}`);
182
+ if (check.fix)
183
+ out(` ${dim(" ".repeat(width))} ${grey("-> " + check.fix)}`);
184
+ }
185
+ const fail = checks.filter((c) => c.status === "fail").length;
186
+ const warn = checks.filter((c) => c.status === "warn").length;
187
+ const ok = checks.filter((c) => c.status === "ok").length;
188
+ out();
189
+ out(`${green(`${ok} ok`)}${warn ? `, ${yellow(`${warn} warn`)}` : ""}${fail ? `, ${red(`${fail} fail`)}` : ""} ${dim(`${cyan("rabdi")} - https://hub.arkiv.network for keys and faucet`)}`);
190
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `arkiv entity <key>` - read one entity and print everything the node returns.
3
+ * Attribute values are untrusted bytes, so every string goes through sanitize()
4
+ * before it reaches the terminal.
5
+ */
6
+ import type { Context } from "../context.js";
7
+ export declare function runEntity(ctx: Context, rawKey: string): Promise<void>;