@inversealtruism/cairn-cli 0.3.13 → 0.3.15
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/dist/cli.js +60 -15
- package/dist/lib/cairnx.js +12 -11
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,28 @@ import { cairnxGet, activeCairnxBase, defaultBases, buildTransferRecord, humanTo
|
|
|
11
11
|
import { randomBytes } from "node:crypto";
|
|
12
12
|
import { createInterface } from "node:readline";
|
|
13
13
|
import { c, banner, bannerAnimated, rule, badge, bar, csd as csdFmt, ok, warn, err, key as kdim, pad, spinner, sleep, isTty, anim, clearScreen, cursorHome, san } from "./lib/ui.js";
|
|
14
|
-
|
|
14
|
+
// CSD → base units via EXACT decimal-string parsing (Plan 57 B5a; Plan 56 A.4 finding 3). The
|
|
15
|
+
// old `Math.round(n * 1e8)` float path silently lost precision above ~9e7 CSD; this reuses the
|
|
16
|
+
// token path's exact converter (humanToBase, 8 dp), so both money paths speak the same input
|
|
17
|
+
// language (plain decimals; no exponent/hex forms). Unparseable or beyond-2^53 amounts return
|
|
18
|
+
// NaN, which every call site's isSafeInteger guard treats as invalid (fail-loud, never silently
|
|
19
|
+
// wrong; the three registry fee sites gained that guard in this same change, closing an old
|
|
20
|
+
// Math.max(MIN, NaN) fee-poison path that could reach the signer as a literal "NaN").
|
|
21
|
+
const CSD = (v) => {
|
|
22
|
+
try {
|
|
23
|
+
const b = humanToBase(String(v), 8);
|
|
24
|
+
return b <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(b) : NaN;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return NaN;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
// P8 (Plan 57 B8a review): an unparseable --fee falls back to the default/minimum (safe
|
|
31
|
+
// direction, fee only falls DOWN), but silently: say so, or a typo like `--fee 2e-2` looks
|
|
32
|
+
// accepted while a different fee is used.
|
|
33
|
+
const warnBadFee = (v, usedBase) => {
|
|
34
|
+
console.log(warn(`unparseable --fee ${String(v)}: using ${csdToCoins(usedBase)} CSD (pass a plain decimal like 0.25)`));
|
|
35
|
+
};
|
|
15
36
|
// ── max-fee sanity guard (UTXO-VALUE-1) ──────────────────────────────────────────────────────
|
|
16
37
|
// A CSD fee is implicit (Σin − Σout) and the chain enforces NO maximum, so a hostile proxy that
|
|
17
38
|
// UNDER-reports the picked UTXO's value would make `csd` compute too-small a change and the
|
|
@@ -25,7 +46,7 @@ const MAX_FEE_ABS = 100_000_000; // 1 CSD absolute floor — every honest fee is
|
|
|
25
46
|
const MAX_FEE_VALUE_FRACTION = 0.25; // …and ≤ 25% of the value moved
|
|
26
47
|
function feeCap(txValue, a) {
|
|
27
48
|
if (a.flags["max-fee"] !== undefined) {
|
|
28
|
-
const m = CSD(
|
|
49
|
+
const m = CSD(a.flags["max-fee"]);
|
|
29
50
|
if (Number.isSafeInteger(m) && m >= 0)
|
|
30
51
|
return m;
|
|
31
52
|
}
|
|
@@ -485,10 +506,10 @@ function gatherOutputs(a) {
|
|
|
485
506
|
const i = String(spec).lastIndexOf(":");
|
|
486
507
|
if (i < 0)
|
|
487
508
|
return `bad --output (want <addr>:<CSD>): ${spec}`;
|
|
488
|
-
outs.push({ to: String(spec).slice(0, i), value: CSD(
|
|
509
|
+
outs.push({ to: String(spec).slice(0, i), value: CSD(String(spec).slice(i + 1)) });
|
|
489
510
|
}
|
|
490
511
|
if (a.flags.to !== undefined || a.flags.amount !== undefined)
|
|
491
|
-
outs.push({ to: String(a.flags.to ?? ""), value: CSD(
|
|
512
|
+
outs.push({ to: String(a.flags.to ?? ""), value: CSD(a.flags.amount ?? 0) });
|
|
492
513
|
for (const o of outs) {
|
|
493
514
|
if (!/^0x[0-9a-fA-F]{40}$/.test(o.to))
|
|
494
515
|
return `bad recipient: ${o.to}`;
|
|
@@ -510,8 +531,10 @@ async function cmdSend(a) {
|
|
|
510
531
|
console.log(err("could not resolve your address — pass --address or run ") + c.cyan("cairn setup"));
|
|
511
532
|
return;
|
|
512
533
|
}
|
|
513
|
-
const
|
|
514
|
-
const fee = (Number.
|
|
534
|
+
const feeFlag = a.flags.fee !== undefined ? CSD(a.flags.fee) : 1_000_000;
|
|
535
|
+
const fee = (Number.isSafeInteger(feeFlag) && feeFlag >= 0) ? feeFlag : 1_000_000;
|
|
536
|
+
if (a.flags.fee !== undefined && fee !== feeFlag)
|
|
537
|
+
warnBadFee(a.flags.fee, fee);
|
|
515
538
|
const total = outs.reduce((s, o) => s + o.value, 0);
|
|
516
539
|
console.log(`${kdim("from")} ${c.cyan(addr)}`);
|
|
517
540
|
for (const o of outs)
|
|
@@ -587,8 +610,10 @@ async function cmdPropose(a) {
|
|
|
587
610
|
console.log(warn("usage: ") + c.cyan("cairn propose --domain csd:features --title <t> --body <b> [--link <url>] [--fee <CSD>] [--expires-days N]"));
|
|
588
611
|
return;
|
|
589
612
|
}
|
|
590
|
-
const
|
|
591
|
-
const fee = Math.max(MIN_FEE_PROPOSE, Number.
|
|
613
|
+
const feeFlag = a.flags.fee !== undefined ? CSD(a.flags.fee) : MIN_FEE_PROPOSE;
|
|
614
|
+
const fee = Math.max(MIN_FEE_PROPOSE, Number.isSafeInteger(feeFlag) ? feeFlag : MIN_FEE_PROPOSE);
|
|
615
|
+
if (a.flags.fee !== undefined && !Number.isSafeInteger(feeFlag))
|
|
616
|
+
warnBadFee(a.flags.fee, fee);
|
|
592
617
|
// operator-token path stays available for the instance operator
|
|
593
618
|
if (CAIRN_TOKEN && !(await csd.available())) {
|
|
594
619
|
const sp = spinner("posting via operator token");
|
|
@@ -654,8 +679,10 @@ async function cmdSupport(a) {
|
|
|
654
679
|
console.log(err("proposal id must be 0x…64-hex"));
|
|
655
680
|
return;
|
|
656
681
|
}
|
|
657
|
-
const
|
|
658
|
-
const fee = Math.max(MIN_FEE_ATTEST, Number.
|
|
682
|
+
const feeFlag = a.flags.fee !== undefined ? CSD(a.flags.fee) : MIN_FEE_ATTEST;
|
|
683
|
+
const fee = Math.max(MIN_FEE_ATTEST, Number.isSafeInteger(feeFlag) ? feeFlag : MIN_FEE_ATTEST);
|
|
684
|
+
if (a.flags.fee !== undefined && !Number.isSafeInteger(feeFlag))
|
|
685
|
+
warnBadFee(a.flags.fee, fee);
|
|
659
686
|
const score = Math.max(0, Math.min(100, parseInt(String(a.flags.score ?? 75)) || 0));
|
|
660
687
|
const confidence = Math.max(0, Math.min(100, parseInt(String(a.flags.confidence ?? 60)) || 0));
|
|
661
688
|
if (CAIRN_TOKEN && !(await csd.available())) {
|
|
@@ -967,7 +994,10 @@ async function cmdGateway(a) {
|
|
|
967
994
|
if (!p)
|
|
968
995
|
return;
|
|
969
996
|
const rec = buildGatewayRecord({ priv: p.priv, url, kind: a.flags.pin ? "pin" : "gateway", address: p.addr });
|
|
970
|
-
const
|
|
997
|
+
const feeFlag = a.flags.fee !== undefined ? CSD(a.flags.fee) : MIN_FEE_PROPOSE;
|
|
998
|
+
const fee = Math.max(MIN_FEE_PROPOSE, Number.isSafeInteger(feeFlag) ? feeFlag : MIN_FEE_PROPOSE);
|
|
999
|
+
if (a.flags.fee !== undefined && !Number.isSafeInteger(feeFlag))
|
|
1000
|
+
warnBadFee(a.flags.fee, fee); // NaN-guard: the old Math.max(MIN, NaN) poisoned the fee on a garbage --fee
|
|
971
1001
|
if (a.flags["dry-run"]) {
|
|
972
1002
|
console.log(`${kdim("domain")} ${c.cyan(rec.domain)}\n${kdim("url")} ${c.white(url)}\n${kdim("hash")} ${c.magenta(rec.payloadHash)}`);
|
|
973
1003
|
console.log(c.gray("\n[dry-run] not signed or submitted"));
|
|
@@ -991,7 +1021,10 @@ async function cmdPeer(a) {
|
|
|
991
1021
|
return;
|
|
992
1022
|
const caps = (a.multi.cap ?? (a.flags.cap ? [String(a.flags.cap)] : [])).filter(Boolean);
|
|
993
1023
|
const rec = buildPeerRecord({ priv: p.priv, peer_id: peerId, multiaddrs, caps: caps.length ? caps : undefined, address: p.addr });
|
|
994
|
-
const
|
|
1024
|
+
const feeFlag = a.flags.fee !== undefined ? CSD(a.flags.fee) : MIN_FEE_PROPOSE;
|
|
1025
|
+
const fee = Math.max(MIN_FEE_PROPOSE, Number.isSafeInteger(feeFlag) ? feeFlag : MIN_FEE_PROPOSE);
|
|
1026
|
+
if (a.flags.fee !== undefined && !Number.isSafeInteger(feeFlag))
|
|
1027
|
+
warnBadFee(a.flags.fee, fee); // NaN-guard: the old Math.max(MIN, NaN) poisoned the fee on a garbage --fee
|
|
995
1028
|
if (a.flags["dry-run"]) {
|
|
996
1029
|
console.log(`${kdim("domain")} ${c.cyan(rec.domain)}\n${kdim("peer")} ${c.white(peerId)}\n${kdim("hash")} ${c.magenta(rec.payloadHash)}`);
|
|
997
1030
|
console.log(c.gray("\n[dry-run] not signed or submitted"));
|
|
@@ -1014,7 +1047,10 @@ async function cmdIdentity(a) {
|
|
|
1014
1047
|
const p = await registryPrep(a);
|
|
1015
1048
|
if (!p)
|
|
1016
1049
|
return;
|
|
1017
|
-
const
|
|
1050
|
+
const feeFlag = a.flags.fee !== undefined ? CSD(a.flags.fee) : MIN_FEE_PROPOSE;
|
|
1051
|
+
const fee = Math.max(MIN_FEE_PROPOSE, Number.isSafeInteger(feeFlag) ? feeFlag : MIN_FEE_PROPOSE);
|
|
1052
|
+
if (a.flags.fee !== undefined && !Number.isSafeInteger(feeFlag))
|
|
1053
|
+
warnBadFee(a.flags.fee, fee); // NaN-guard: the old Math.max(MIN, NaN) poisoned the fee on a garbage --fee
|
|
1018
1054
|
if (a.flags.reveal) {
|
|
1019
1055
|
const salt = String(a.flags.salt ?? "");
|
|
1020
1056
|
if (!/^[0-9a-f]{16,}$/i.test(salt)) {
|
|
@@ -1090,6 +1126,13 @@ async function tokenDecimals() {
|
|
|
1090
1126
|
map[t.ticker] = t.decimals;
|
|
1091
1127
|
return map;
|
|
1092
1128
|
}
|
|
1129
|
+
// A name in `acct.nameDetails` with pending:true is a v2.5/v2.6 reservation (revealed, not yet finalized):
|
|
1130
|
+
// the resolver gives it no send address and no owner actions until nfinalize. Mark it so a reservation is
|
|
1131
|
+
// never shown as an owned name. Defensive — an older resolver that omits nameDetails yields an empty set.
|
|
1132
|
+
function pendingNames(acct) {
|
|
1133
|
+
return new Set((acct?.nameDetails ?? []).filter((d) => d?.pending).map((d) => String(d.name)));
|
|
1134
|
+
}
|
|
1135
|
+
const nameCell = (n, pend) => pend.has(n) ? c.cyan(san(n)) + c.gray(" (finalizing)") : c.green(san(n));
|
|
1093
1136
|
async function cmdTokens(a) {
|
|
1094
1137
|
const addr = await resolveCairnxAddr(a, a._[1]);
|
|
1095
1138
|
if (!addr)
|
|
@@ -1109,7 +1152,8 @@ async function cmdTokens(a) {
|
|
|
1109
1152
|
console.log(` ${c.cyan(pad(san(ticker), 14))} ${c.white(tokAmt(String(b.available ?? "0"), dec[ticker]))}${locked > 0n ? c.gray(` · ${tokAmt(String(b.locked), dec[ticker])} locked in open offers`) : ""}`);
|
|
1110
1153
|
}
|
|
1111
1154
|
const names = acct.names ?? [];
|
|
1112
|
-
|
|
1155
|
+
const pend = pendingNames(acct);
|
|
1156
|
+
console.log(`\n ${kdim(".csd names")} ${names.length ? names.map((n) => nameCell(n, pend)).join(c.gray(" · ")) : c.gray("none")}`);
|
|
1113
1157
|
}
|
|
1114
1158
|
async function cmdTokenInfo(a) {
|
|
1115
1159
|
const ticker = String(a._[1] ?? "").toUpperCase();
|
|
@@ -1263,8 +1307,9 @@ async function cmdNames(a) {
|
|
|
1263
1307
|
console.log(c.gray(" no names owned — claim one on " + (activeCairnxBase()?.includes("127.0.0.1") ? "the /trade marketplace" : "https://cairn-substrate.com/trade")));
|
|
1264
1308
|
return;
|
|
1265
1309
|
}
|
|
1310
|
+
const pend = pendingNames(acct);
|
|
1266
1311
|
for (const n of names)
|
|
1267
|
-
console.log(` ${
|
|
1312
|
+
console.log(` ${nameCell(n, pend)}${pend.has(n) ? c.gray(" · complete on /trade") : ""}`);
|
|
1268
1313
|
console.log(c.gray(`\n ${names.length} name${names.length === 1 ? "" : "s"} · cairn name <name> for detail`));
|
|
1269
1314
|
}
|
|
1270
1315
|
async function cmdName(a) {
|
package/dist/lib/cairnx.js
CHANGED
|
@@ -9,18 +9,22 @@
|
|
|
9
9
|
// the latent dual-canonicaliser seam).
|
|
10
10
|
// • exact human↔base-unit amount math as STRING/BigInt arithmetic — floats never touch
|
|
11
11
|
// token amounts (no "1.1 * 1e8 = 110000000.00000001" class of bug, no silent truncation).
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
import { DOMAIN, TICKER_RE, NAME_RE, ADDR_RE, MAX_AMOUNT,
|
|
12
|
+
import { MIN_FEE_PROPOSE } from "@inversealtruism/csd-codec";
|
|
13
|
+
// ★ Consensus shapes/constants AND the record builder are IMPORTED from cairnx-core, not
|
|
14
|
+
// hand-declared (shared-core de-dup, cairn docs/Plans/46 + 57 B5a): they validate a record BEFORE
|
|
15
|
+
// the CLI spends the anchor fee, so a drifted regex / limit would build a record the resolver
|
|
16
|
+
// no-ops (a lost fee). One source = the published convention.
|
|
17
|
+
import { DOMAIN, TICKER_RE, NAME_RE, ADDR_RE, MAX_AMOUNT, transfer } from "@inversealtruism/cairnx-core";
|
|
18
18
|
export const CAIRNX_DOMAIN = DOMAIN; // "cairnx:v1"
|
|
19
19
|
export const CAIRNX_ANCHOR_FEE = MIN_FEE_PROPOSE; // 0.25 CSD — the consensus min Propose fee that anchors a record
|
|
20
20
|
// re-export the §4 shapes the CLI's public surface exposed (now single-sourced from cairnx-core)
|
|
21
21
|
export { TICKER_RE, NAME_RE, MAX_AMOUNT };
|
|
22
22
|
export function buildTransferRecord(p) {
|
|
23
23
|
const to = String(p.to).toLowerCase();
|
|
24
|
+
// Friendly pre-checks first (CLI-grade error messages), then the CANONICAL builder does the
|
|
25
|
+
// authoritative work (Plan 57 B5a): cairnx-core transfer() round-trips through parseRecord, so
|
|
26
|
+
// what the CLI anchors is valid-by-construction against CONVENTION.md, not a hand-kept mirror.
|
|
27
|
+
// Output is byte-identical to the old hand-assembly (same record keys; canonicalJson sorts).
|
|
24
28
|
if (!TICKER_RE.test(p.ticker))
|
|
25
29
|
throw new Error(`bad ticker "${p.ticker}" — want 3–12 chars [A-Z0-9], starting with a letter`);
|
|
26
30
|
if (!ADDR_RE.test(to))
|
|
@@ -29,11 +33,8 @@ export function buildTransferRecord(p) {
|
|
|
29
33
|
throw new Error("amount must be > 0");
|
|
30
34
|
if (p.amount > MAX_AMOUNT)
|
|
31
35
|
throw new Error("amount exceeds the 96-bit token-amount limit");
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
if (Buffer.byteLength(uri, "utf8") > MAX_RECORD_BYTES)
|
|
35
|
-
throw new Error("record exceeds 512 bytes"); // unreachable for a transfer, kept as a guard
|
|
36
|
-
return { record, uri, payloadHash: sha256Hex(uri) };
|
|
36
|
+
const built = transfer({ ticker: p.ticker, to, amount: p.amount.toString() });
|
|
37
|
+
return { record: built.record, uri: built.uri, payloadHash: built.payloadHash };
|
|
37
38
|
}
|
|
38
39
|
// ── exact amount math (strings + BigInt only) ─────────────────────────────────────────────
|
|
39
40
|
// "1.5" with decimals 8 → 150000000n. Fails LOUDLY instead of truncating: "1.5" on a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inversealtruism/cairn-cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
4
4
|
"description": "CLI for Compute Substrate / Cairn — browse the board/wall/network, and send CSD, propose, attest, and place stones non-custodially by driving your own installed `csd` wallet (cairn never holds your key; works node-less via the Cairn proxy).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
"typescript": "^5.7.2"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@inversealtruism/cairnx-core": "0.1.
|
|
37
|
-
"@inversealtruism/csd-codec": "0.1.
|
|
38
|
-
"@inversealtruism/csd-registry": "0.1.
|
|
36
|
+
"@inversealtruism/cairnx-core": "0.1.31",
|
|
37
|
+
"@inversealtruism/csd-codec": "0.1.15",
|
|
38
|
+
"@inversealtruism/csd-registry": "0.1.15"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
41
|
"clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\"",
|