mintbound-cli 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.
package/dist/read.js ADDED
@@ -0,0 +1,131 @@
1
+ import { Contract, JsonRpcProvider } from "ethers";
2
+ import { ASC_ABI, ASC_ERRORS, CHAIN_INFO_ABI, CONTINUITY_ABI, ERC20_ABI, VAULT_ABI } from "./abi.js";
3
+ import { CHAIN_INFO_ADDRESS, RPC, creditcoin, sepolia } from "./config.js";
4
+ export function providers() {
5
+ return {
6
+ cc: new JsonRpcProvider(RPC.creditcoin, undefined, { staticNetwork: true }),
7
+ sep: new JsonRpcProvider(RPC.sepolia, undefined, { staticNetwork: true }),
8
+ };
9
+ }
10
+ /** Any read that is context rather than part of the invariant may fail softly. */
11
+ async function soft(p, fallback) {
12
+ try {
13
+ return await p;
14
+ }
15
+ catch {
16
+ return fallback;
17
+ }
18
+ }
19
+ export async function readLive() {
20
+ const cc = creditcoin();
21
+ const sep = sepolia();
22
+ const { cc: ccProvider, sep: sepProvider } = providers();
23
+ const ascAddr = cc.contracts.MintBoundASC;
24
+ const wrapped = cc.contracts.WrappedAsset;
25
+ const sourceAsset = String(cc.config?.sourceAsset ?? sep.contracts.TestUSD);
26
+ const vaultAddr = String(cc.config?.canonicalVault ?? sep.contracts.ReserveVault);
27
+ const sourceChainKey = Number(cc.config?.sourceChainKey ?? 1);
28
+ const asc = new Contract(ascAddr, [...ASC_ABI, ...ASC_ERRORS], ccProvider);
29
+ const vault = new Contract(vaultAddr, VAULT_ABI, sepProvider);
30
+ const asset = new Contract(sourceAsset, ERC20_ABI, sepProvider);
31
+ const wrapper = new Contract(wrapped, ERC20_ABI, ccProvider);
32
+ const info = new Contract(CHAIN_INFO_ADDRESS, CHAIN_INFO_ABI, ccProvider);
33
+ const r = await asc.solvencyReport(sourceAsset);
34
+ const maxStaleness = Number(await asc.maxStalenessBlocks());
35
+ const [symbol, decimals] = await Promise.all([
36
+ soft(wrapper.symbol(), "wmTUSD"),
37
+ soft(wrapper.decimals().then(Number), 18),
38
+ ]);
39
+ const [sourceHead, vaultBalance, vaultEncumbered, emergencyEnabled, delay] = await Promise.all([
40
+ soft(sepProvider.getBlockNumber(), 0),
41
+ soft(asset.balanceOf(vaultAddr), 0n),
42
+ soft(vault.encumbered(sourceAsset), 0n),
43
+ soft(vault.emergencyEnabled(), true),
44
+ soft(vault.WITHDRAWAL_DELAY().then(Number), 0),
45
+ ]);
46
+ // Continuity is an optional module; a deployment without it simply scores zero on
47
+ // that obligation rather than failing the whole read.
48
+ let coveredThrough = 0;
49
+ let anchorHeight = 0;
50
+ if (cc.contracts.SolvencyContinuity) {
51
+ const cont = new Contract(cc.contracts.SolvencyContinuity, CONTINUITY_ABI, ccProvider);
52
+ coveredThrough = await soft(cont.coveredThrough(sourceAsset).then(Number), 0);
53
+ anchorHeight = await soft(cont.anchorHeight(sourceAsset).then(Number), 0);
54
+ }
55
+ // remoteChainKeys is an unbounded public array getter; probe until it reverts.
56
+ let registeredChains = 0;
57
+ for (let i = 0; i < 32; i++) {
58
+ try {
59
+ await asc.remoteChainKeys(sourceAsset, i);
60
+ registeredChains++;
61
+ }
62
+ catch {
63
+ break;
64
+ }
65
+ }
66
+ // A registered chain counts as reporting when its proven supply contributes to the
67
+ // aggregate. If totalLiabilities reverts the aggregate cannot be formed at all, which
68
+ // is exactly the freeze condition, so report zero coverage rather than guessing.
69
+ const liabilities = await soft(asc.totalLiabilities(sourceAsset), -1n);
70
+ const reportingChains = liabilities >= 0n ? registeredChains : 0;
71
+ const verifiedReserve = BigInt(r.verifiedReserve);
72
+ const encumberedReserve = BigInt(r.encumberedReserve);
73
+ const haircutBps = Number(r.haircutBps);
74
+ const unencumbered = verifiedReserve > encumberedReserve ? verifiedReserve - encumberedReserve : 0n;
75
+ const latestAttested = Number(r.latestAttestedHeight);
76
+ return {
77
+ asc: ascAddr,
78
+ vault: vaultAddr,
79
+ sourceAsset,
80
+ wrapped,
81
+ sourceChainKey,
82
+ symbol,
83
+ decimals,
84
+ verifiedReserve,
85
+ encumberedReserve,
86
+ outstandingSupply: BigInt(r.outstandingSupply),
87
+ // Mirrors MintBoundASC._effectiveReserve exactly: announced exits come off BEFORE
88
+ // the haircut. Displaying it any other way would let the readout and the
89
+ // enforcement disagree.
90
+ discountedReserve: (unencumbered * BigInt(haircutBps)) / 10000n,
91
+ maxMintable: BigInt(r.maxMintable),
92
+ collateralRatioBps: Number(r.collateralRatioBps),
93
+ haircutBps,
94
+ epoch: Number(r.epoch),
95
+ attestedAtHeight: Number(r.attestedAtHeight),
96
+ latestAttestedHeight: latestAttested,
97
+ stalenessBlocks: Number(r.stalenessBlocks),
98
+ maxStalenessBlocks: maxStaleness,
99
+ trustedParties: Number(r.trustedParties),
100
+ fresh: Boolean(r.fresh),
101
+ solvent: Boolean(r.solvent),
102
+ mintFrozen: Boolean(r.mintFrozen),
103
+ sourceHead,
104
+ vaultBalance,
105
+ vaultEncumbered,
106
+ emergencyEnabled,
107
+ withdrawalDelayBlocks: delay,
108
+ coveredThrough,
109
+ anchorHeight,
110
+ registeredChains,
111
+ reportingChains,
112
+ };
113
+ }
114
+ export function toAssuranceInput(s) {
115
+ return {
116
+ trustedParties: s.trustedParties,
117
+ fresh: s.fresh,
118
+ stalenessBlocks: s.stalenessBlocks,
119
+ maxStalenessBlocks: s.maxStalenessBlocks,
120
+ withdrawalDelayBlocks: s.withdrawalDelayBlocks,
121
+ // Detection latency is the live gap between the source tip and what Creditcoin has
122
+ // attested. Measured, not assumed — it moves, and the margin has to hold against
123
+ // whatever it actually is right now.
124
+ detectionLatencyBlocks: Math.max(s.sourceHead - s.latestAttestedHeight, 0),
125
+ registeredChains: s.registeredChains,
126
+ reportingChains: s.reportingChains,
127
+ coveredThrough: s.coveredThrough,
128
+ anchorHeight: s.anchorHeight,
129
+ emergencyRenounced: !s.emergencyEnabled,
130
+ };
131
+ }
package/dist/render.js ADDED
@@ -0,0 +1,51 @@
1
+ const ESC = String.fromCharCode(27);
2
+ const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
3
+ const wrap = (code) => (s) => useColor ? `${ESC}[${code}m${s}${ESC}[0m` : s;
4
+ export const c = {
5
+ dim: wrap("2"),
6
+ bold: wrap("1"),
7
+ green: wrap("32"),
8
+ red: wrap("31"),
9
+ yellow: wrap("33"),
10
+ cyan: wrap("36"),
11
+ grey: wrap("90"),
12
+ };
13
+ export const TICK = "✓";
14
+ export const CROSS = "✗";
15
+ export function rule(width = 68) {
16
+ console.log(c.grey("─".repeat(width)));
17
+ }
18
+ export function heading(title) {
19
+ console.log("");
20
+ console.log(c.bold(title));
21
+ rule();
22
+ }
23
+ export function step(n, total, text) {
24
+ process.stdout.write(c.grey(`[${n}/${total}] `) + text);
25
+ }
26
+ export function stepOk(text) {
27
+ console.log(" " + c.green(TICK) + " " + c.dim(text));
28
+ }
29
+ export function stepFail(text) {
30
+ console.log(" " + c.red(CROSS) + " " + c.dim(text));
31
+ }
32
+ export function kv(key, value, pad = 22) {
33
+ console.log(` ${c.grey(key.padEnd(pad))}${value}`);
34
+ }
35
+ /** Format a base-10 fixed-point integer without pulling in a bignum formatter. */
36
+ export function units(v, decimals = 18, dp = 2) {
37
+ const neg = v < 0n;
38
+ const abs = neg ? -v : v;
39
+ const base = 10n ** BigInt(decimals);
40
+ const whole = abs / base;
41
+ const frac = abs % base;
42
+ const fracStr = frac.toString().padStart(decimals, "0").slice(0, dp).replace(/0+$/, "");
43
+ const group = whole.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
44
+ return `${neg ? "-" : ""}${group}${fracStr ? "." + fracStr : ""}`;
45
+ }
46
+ export function bar(fraction, width = 28) {
47
+ const clamped = Math.max(0, Math.min(1, Number.isFinite(fraction) ? fraction : 0));
48
+ const filled = Math.round(clamped * width);
49
+ const body = "█".repeat(filled) + c.grey("░".repeat(width - filled));
50
+ return clamped >= 1 ? c.green(body) : clamped >= 0.5 ? c.cyan(body) : c.yellow(body);
51
+ }
package/dist/revert.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Turn a reverted call into a sentence a human can read.
3
+ *
4
+ * ethers' `shortMessage` gives up on custom errors and says "unknown custom error" even
5
+ * when the ABI is right there and the payload decodes perfectly. That matters here: a
6
+ * revert IS the product, so reporting it as unknown makes a correct rejection look like
7
+ * a malfunction. Decode from the raw data first, and only fall back to string scraping.
8
+ */
9
+ export function describeRevert(iface, e) {
10
+ const data = e?.data ?? e?.info?.error?.data ?? e?.error?.data;
11
+ if (typeof data === "string" && data.length >= 10 && data !== "0x") {
12
+ try {
13
+ const parsed = iface.parseError(data);
14
+ if (parsed) {
15
+ // Solidity's require-string revert decodes as Error(string). Unwrap it —
16
+ // "Error(Merkle proof validation failed)" reads worse than the message itself.
17
+ if (parsed.name === "Error")
18
+ return String(parsed.args[0]);
19
+ if (parsed.name === "Panic")
20
+ return `Panic(0x${BigInt(parsed.args[0]).toString(16)})`;
21
+ const args = parsed.args.map((a) => String(a)).join(", ");
22
+ return args ? `${parsed.name}(${args})` : `${parsed.name}()`;
23
+ }
24
+ }
25
+ catch {
26
+ // Not one of ours — fall through to the string forms below.
27
+ }
28
+ }
29
+ const msg = String(e?.shortMessage ?? e?.reason ?? e?.message ?? e);
30
+ const quoted = msg.match(/reverted with[^:]*: ?"?([^"]+)"?/);
31
+ if (quoted?.[1])
32
+ return quoted[1].trim();
33
+ const named = msg.match(/[A-Z][A-Za-z]+\([^)]*\)/);
34
+ if (named?.[0])
35
+ return named[0];
36
+ return msg.slice(0, 110);
37
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "mintbound-cli",
3
+ "version": "0.1.0",
4
+ "description": "Verify MintBound's solvency evidence yourself, against live chains, with no keys and no funds.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "mintbound": "dist/index.js",
9
+ "mintbound-cli": "dist/index.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "dev": "tsx src/index.ts",
18
+ "typecheck": "tsc --noEmit -p tsconfig.json",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "@gluwa/usc-sdk": "0.18.0",
23
+ "ethers": "^6.17.0"
24
+ },
25
+ "devDependencies": {
26
+ "@types/node": "^26.1.0",
27
+ "tsx": "^4.22.4",
28
+ "typescript": "^5.9.0"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Lawalgiyath/MintGuard.git",
36
+ "directory": "packages/cli"
37
+ },
38
+ "keywords": [
39
+ "creditcoin",
40
+ "attestcoin",
41
+ "proof-of-reserve",
42
+ "solvency",
43
+ "cross-chain",
44
+ "rwa",
45
+ "wrapped-assets"
46
+ ],
47
+ "engines": {
48
+ "node": ">=20"
49
+ }
50
+ }