@reppo/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/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/api/platform.d.ts +39 -0
- package/dist/api/platform.d.ts.map +1 -0
- package/dist/api/platform.js +148 -0
- package/dist/api/platform.js.map +1 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +60 -0
- package/dist/bin.js.map +1 -0
- package/dist/chain/abis.d.ts +576 -0
- package/dist/chain/abis.d.ts.map +1 -0
- package/dist/chain/abis.js +66 -0
- package/dist/chain/abis.js.map +1 -0
- package/dist/chain/addresses.d.ts +27 -0
- package/dist/chain/addresses.d.ts.map +1 -0
- package/dist/chain/addresses.js +39 -0
- package/dist/chain/addresses.js.map +1 -0
- package/dist/chain/clients.d.ts +46 -0
- package/dist/chain/clients.d.ts.map +1 -0
- package/dist/chain/clients.js +48 -0
- package/dist/chain/clients.js.map +1 -0
- package/dist/chain/contracts.d.ts +28 -0
- package/dist/chain/contracts.d.ts.map +1 -0
- package/dist/chain/contracts.js +70 -0
- package/dist/chain/contracts.js.map +1 -0
- package/dist/chain/errors.d.ts +14 -0
- package/dist/chain/errors.d.ts.map +1 -0
- package/dist/chain/errors.js +72 -0
- package/dist/chain/errors.js.map +1 -0
- package/dist/commands/_base.d.ts +19 -0
- package/dist/commands/_base.d.ts.map +1 -0
- package/dist/commands/_base.js +40 -0
- package/dist/commands/_base.js.map +1 -0
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +93 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/claim-emissions.d.ts +11 -0
- package/dist/commands/claim-emissions.d.ts.map +1 -0
- package/dist/commands/claim-emissions.js +192 -0
- package/dist/commands/claim-emissions.js.map +1 -0
- package/dist/commands/extend-lock.d.ts +11 -0
- package/dist/commands/extend-lock.d.ts.map +1 -0
- package/dist/commands/extend-lock.js +196 -0
- package/dist/commands/extend-lock.js.map +1 -0
- package/dist/commands/grant-access.d.ts +11 -0
- package/dist/commands/grant-access.d.ts.map +1 -0
- package/dist/commands/grant-access.js +191 -0
- package/dist/commands/grant-access.js.map +1 -0
- package/dist/commands/lock.d.ts +11 -0
- package/dist/commands/lock.d.ts.map +1 -0
- package/dist/commands/lock.js +192 -0
- package/dist/commands/lock.js.map +1 -0
- package/dist/commands/mint-pod.d.ts +15 -0
- package/dist/commands/mint-pod.d.ts.map +1 -0
- package/dist/commands/mint-pod.js +286 -0
- package/dist/commands/mint-pod.js.map +1 -0
- package/dist/commands/query/balance.d.ts +9 -0
- package/dist/commands/query/balance.d.ts.map +1 -0
- package/dist/commands/query/balance.js +91 -0
- package/dist/commands/query/balance.js.map +1 -0
- package/dist/commands/query/datanet.d.ts +10 -0
- package/dist/commands/query/datanet.d.ts.map +1 -0
- package/dist/commands/query/datanet.js +128 -0
- package/dist/commands/query/datanet.js.map +1 -0
- package/dist/commands/query/emissions-due.d.ts +18 -0
- package/dist/commands/query/emissions-due.d.ts.map +1 -0
- package/dist/commands/query/emissions-due.js +170 -0
- package/dist/commands/query/emissions-due.js.map +1 -0
- package/dist/commands/query/pod.d.ts +8 -0
- package/dist/commands/query/pod.d.ts.map +1 -0
- package/dist/commands/query/pod.js +88 -0
- package/dist/commands/query/pod.js.map +1 -0
- package/dist/commands/query/voting-power.d.ts +9 -0
- package/dist/commands/query/voting-power.d.ts.map +1 -0
- package/dist/commands/query/voting-power.js +82 -0
- package/dist/commands/query/voting-power.js.map +1 -0
- package/dist/commands/register-agent.d.ts +9 -0
- package/dist/commands/register-agent.d.ts.map +1 -0
- package/dist/commands/register-agent.js +115 -0
- package/dist/commands/register-agent.js.map +1 -0
- package/dist/commands/unlock.d.ts +11 -0
- package/dist/commands/unlock.d.ts.map +1 -0
- package/dist/commands/unlock.js +181 -0
- package/dist/commands/unlock.js.map +1 -0
- package/dist/commands/vote.d.ts +13 -0
- package/dist/commands/vote.d.ts.map +1 -0
- package/dist/commands/vote.js +174 -0
- package/dist/commands/vote.js.map +1 -0
- package/dist/config/load.d.ts +19 -0
- package/dist/config/load.d.ts.map +1 -0
- package/dist/config/load.js +66 -0
- package/dist/config/load.js.map +1 -0
- package/dist/output/format.d.ts +43 -0
- package/dist/output/format.d.ts.map +1 -0
- package/dist/output/format.js +58 -0
- package/dist/output/format.js.map +1 -0
- package/dist/state/db.d.ts +47 -0
- package/dist/state/db.d.ts.map +1 -0
- package/dist/state/db.js +169 -0
- package/dist/state/db.js.map +1 -0
- package/dist/state/idempotency.d.ts +66 -0
- package/dist/state/idempotency.d.ts.map +1 -0
- package/dist/state/idempotency.js +173 -0
- package/dist/state/idempotency.js.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Network } from '../chain/addresses.js';
|
|
2
|
+
export interface Config {
|
|
3
|
+
network: Network;
|
|
4
|
+
rpcUrl: string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Reppo platform API base URL. Mainnet defaults to https://api.reppo.xyz.
|
|
7
|
+
* Testnet has no stable hosted endpoint yet — operators MUST set
|
|
8
|
+
* REPPO_API_URL when network=testnet (commands that don't talk to the
|
|
9
|
+
* platform API ignore this).
|
|
10
|
+
*/
|
|
11
|
+
apiUrl: string | undefined;
|
|
12
|
+
apiKey: string | undefined;
|
|
13
|
+
privateKey: `0x${string}` | undefined;
|
|
14
|
+
voterPrivateKey: `0x${string}` | undefined;
|
|
15
|
+
}
|
|
16
|
+
export declare function loadConfig(overrides?: {
|
|
17
|
+
network?: Network;
|
|
18
|
+
}): Config;
|
|
19
|
+
//# sourceMappingURL=load.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.d.ts","sourceRoot":"","sources":["../../src/config/load.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAGrD,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B;;;;;OAKG;IACH,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,UAAU,EAAE,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;IACtC,eAAe,EAAE,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;CAC5C;AAmCD,wBAAgB,UAAU,CAAC,SAAS,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAoCxE"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layered config:
|
|
3
|
+
* 1. CLI flags (handled by clipanion at the command level)
|
|
4
|
+
* 2. .reppo.json in cwd
|
|
5
|
+
* 3. ~/.reppo/config.json
|
|
6
|
+
* 4. Environment variables
|
|
7
|
+
* 5. Built-in defaults
|
|
8
|
+
*
|
|
9
|
+
* Each value records its source so --debug can print "REPPO_NETWORK=mainnet
|
|
10
|
+
* (from env)".
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { homedir } from 'node:os';
|
|
15
|
+
import { cliError } from '../output/format.js';
|
|
16
|
+
const DEFAULT_API_MAINNET = 'https://api.reppo.xyz';
|
|
17
|
+
function readJsonIfExists(path) {
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
return {};
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function normalizePk(raw) {
|
|
28
|
+
if (!raw)
|
|
29
|
+
return undefined;
|
|
30
|
+
const trimmed = raw.trim();
|
|
31
|
+
if (trimmed.startsWith('0x') && /^0x[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
32
|
+
return trimmed;
|
|
33
|
+
}
|
|
34
|
+
if (/^[0-9a-fA-F]{64}$/.test(trimmed)) {
|
|
35
|
+
return `0x${trimmed}`;
|
|
36
|
+
}
|
|
37
|
+
throw cliError('INVALID_PRIVATE_KEY', 'Private key must be a 32-byte hex string (0x-prefixed or bare 64 hex chars).', 'Check that REPPO_PRIVATE_KEY (or REPPO_VOTER_PRIVATE_KEY) is exactly 64 hex chars, optionally 0x-prefixed.');
|
|
38
|
+
}
|
|
39
|
+
export function loadConfig(overrides = {}) {
|
|
40
|
+
const cwd = readJsonIfExists(join(process.cwd(), '.reppo.json'));
|
|
41
|
+
const home = readJsonIfExists(join(homedir(), '.reppo', 'config.json'));
|
|
42
|
+
const networkRaw = overrides.network ??
|
|
43
|
+
process.env.REPPO_NETWORK ??
|
|
44
|
+
cwd.network ??
|
|
45
|
+
home.network ??
|
|
46
|
+
'mainnet';
|
|
47
|
+
if (networkRaw !== 'mainnet' && networkRaw !== 'testnet') {
|
|
48
|
+
throw cliError('INVALID_NETWORK', `Invalid network "${networkRaw}" — must be "mainnet" or "testnet".`, 'Set REPPO_NETWORK=mainnet (default) or REPPO_NETWORK=testnet, or pass --network <name>.');
|
|
49
|
+
}
|
|
50
|
+
const network = networkRaw;
|
|
51
|
+
const apiUrlRaw = process.env.REPPO_API_URL ??
|
|
52
|
+
cwd.apiUrl ??
|
|
53
|
+
home.apiUrl ??
|
|
54
|
+
(network === 'mainnet' ? DEFAULT_API_MAINNET : undefined);
|
|
55
|
+
// Strip trailing slash so callers can append `${apiUrl}/me/agents` cleanly.
|
|
56
|
+
const apiUrl = apiUrlRaw?.replace(/\/+$/, '');
|
|
57
|
+
return {
|
|
58
|
+
network,
|
|
59
|
+
rpcUrl: process.env.REPPO_RPC_URL ?? cwd.rpcUrl ?? home.rpcUrl,
|
|
60
|
+
apiUrl,
|
|
61
|
+
apiKey: process.env.REPPO_API_KEY,
|
|
62
|
+
privateKey: normalizePk(process.env.REPPO_PRIVATE_KEY),
|
|
63
|
+
voterPrivateKey: normalizePk(process.env.REPPO_VOTER_PRIVATE_KEY),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=load.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../../src/config/load.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAiB/C,MAAM,mBAAmB,GAAG,uBAAuB,CAAC;AAQpD,SAAS,gBAAgB,CAAC,IAAY;IACpC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACjC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,OAAO,CAAC,CAAc,CAAC;IAC9D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,GAAuB;IAC1C,IAAI,CAAC,GAAG;QAAE,OAAO,SAAS,CAAC;IAC3B,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,IAAI,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,qBAAqB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACpE,OAAO,OAAwB,CAAC;IAClC,CAAC;IACD,IAAI,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;QACtC,OAAO,KAAK,OAAO,EAAE,CAAC;IACxB,CAAC;IACD,MAAM,QAAQ,CACZ,qBAAqB,EACrB,8EAA8E,EAC9E,4GAA4G,CAC7G,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,YAAmC,EAAE;IAC9D,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC;IAExE,MAAM,UAAU,GACd,SAAS,CAAC,OAAO;QACjB,OAAO,CAAC,GAAG,CAAC,aAAa;QACzB,GAAG,CAAC,OAAO;QACX,IAAI,CAAC,OAAO;QACZ,SAAS,CAAC;IAEZ,IAAI,UAAU,KAAK,SAAS,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QACzD,MAAM,QAAQ,CACZ,iBAAiB,EACjB,oBAAoB,UAAU,qCAAqC,EACnE,yFAAyF,CAC1F,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAY,UAAU,CAAC;IAEpC,MAAM,SAAS,GACb,OAAO,CAAC,GAAG,CAAC,aAAa;QACzB,GAAG,CAAC,MAAM;QACV,IAAI,CAAC,MAAM;QACX,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC5D,4EAA4E;IAC5E,MAAM,MAAM,GAAG,SAAS,EAAE,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE9C,OAAO;QACL,OAAO;QACP,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM;QAC9D,MAAM;QACN,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,aAAa;QACjC,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACtD,eAAe,EAAE,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC;KAClE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for the CLI. Two modes:
|
|
3
|
+
* - human (default): printed via console.log, friendly multi-line
|
|
4
|
+
* - json: single JSON object on stdout per command
|
|
5
|
+
*
|
|
6
|
+
* Errors ALWAYS emit JSON on stderr regardless of mode, with a stable
|
|
7
|
+
* `code` field agents can match on. Human mode also prints a friendly
|
|
8
|
+
* line to stderr above the JSON for terminal users.
|
|
9
|
+
*/
|
|
10
|
+
export type OutputMode = 'human' | 'json';
|
|
11
|
+
export interface CliError {
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
hint?: string;
|
|
15
|
+
details?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface FormatContext {
|
|
18
|
+
mode: OutputMode;
|
|
19
|
+
}
|
|
20
|
+
export declare function setOutputMode(mode: OutputMode): void;
|
|
21
|
+
export declare function getOutputMode(): OutputMode;
|
|
22
|
+
/**
|
|
23
|
+
* Build a thrown error with the CLI's stable `code` field (and optional
|
|
24
|
+
* `hint`) attached. Replaces the repeated
|
|
25
|
+
* `Object.assign(new Error(msg), { code, hint })` pattern across commands.
|
|
26
|
+
*
|
|
27
|
+
* `hint` is only attached when supplied — under `exactOptionalPropertyTypes`,
|
|
28
|
+
* `{ hint: undefined }` and "no hint" are distinct, and `fail()` only renders
|
|
29
|
+
* the hint when the property is actually present.
|
|
30
|
+
*/
|
|
31
|
+
export declare function cliError(code: string, message: string, hint?: string): Error & {
|
|
32
|
+
code: string;
|
|
33
|
+
hint?: string;
|
|
34
|
+
};
|
|
35
|
+
/** Emit a successful command result. */
|
|
36
|
+
export declare function emit(payload: Record<string, unknown>, humanLines?: string[]): void;
|
|
37
|
+
/**
|
|
38
|
+
* Emit a structured error to stderr and exit non-zero. Always JSON on
|
|
39
|
+
* stderr — agents reading stderr always get parseable output, regardless
|
|
40
|
+
* of mode.
|
|
41
|
+
*/
|
|
42
|
+
export declare function fail(err: CliError, exitCode?: number): never;
|
|
43
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../src/output/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAE1C,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,UAAU,CAAC;CAClB;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAEpD;AAED,wBAAgB,aAAa,IAAI,UAAU,CAE1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAG9G;AAED,wCAAwC;AACxC,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAUlF;AAED;;;;GAIG;AACH,wBAAgB,IAAI,CAAC,GAAG,EAAE,QAAQ,EAAE,QAAQ,SAAI,GAAG,KAAK,CAOvD"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatting for the CLI. Two modes:
|
|
3
|
+
* - human (default): printed via console.log, friendly multi-line
|
|
4
|
+
* - json: single JSON object on stdout per command
|
|
5
|
+
*
|
|
6
|
+
* Errors ALWAYS emit JSON on stderr regardless of mode, with a stable
|
|
7
|
+
* `code` field agents can match on. Human mode also prints a friendly
|
|
8
|
+
* line to stderr above the JSON for terminal users.
|
|
9
|
+
*/
|
|
10
|
+
let activeMode = 'human';
|
|
11
|
+
export function setOutputMode(mode) {
|
|
12
|
+
activeMode = mode;
|
|
13
|
+
}
|
|
14
|
+
export function getOutputMode() {
|
|
15
|
+
return activeMode;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Build a thrown error with the CLI's stable `code` field (and optional
|
|
19
|
+
* `hint`) attached. Replaces the repeated
|
|
20
|
+
* `Object.assign(new Error(msg), { code, hint })` pattern across commands.
|
|
21
|
+
*
|
|
22
|
+
* `hint` is only attached when supplied — under `exactOptionalPropertyTypes`,
|
|
23
|
+
* `{ hint: undefined }` and "no hint" are distinct, and `fail()` only renders
|
|
24
|
+
* the hint when the property is actually present.
|
|
25
|
+
*/
|
|
26
|
+
export function cliError(code, message, hint) {
|
|
27
|
+
const err = new Error(message);
|
|
28
|
+
return Object.assign(err, hint !== undefined ? { code, hint } : { code });
|
|
29
|
+
}
|
|
30
|
+
/** Emit a successful command result. */
|
|
31
|
+
export function emit(payload, humanLines) {
|
|
32
|
+
if (activeMode === 'json') {
|
|
33
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (humanLines) {
|
|
37
|
+
for (const line of humanLines)
|
|
38
|
+
process.stdout.write(line + '\n');
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Emit a structured error to stderr and exit non-zero. Always JSON on
|
|
46
|
+
* stderr — agents reading stderr always get parseable output, regardless
|
|
47
|
+
* of mode.
|
|
48
|
+
*/
|
|
49
|
+
export function fail(err, exitCode = 1) {
|
|
50
|
+
if (activeMode === 'human') {
|
|
51
|
+
process.stderr.write(`✗ ${err.code}: ${err.message}\n`);
|
|
52
|
+
if (err.hint)
|
|
53
|
+
process.stderr.write(` hint: ${err.hint}\n`);
|
|
54
|
+
}
|
|
55
|
+
process.stderr.write(JSON.stringify({ error: err }) + '\n');
|
|
56
|
+
process.exit(exitCode);
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../../src/output/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAeH,IAAI,UAAU,GAAe,OAAO,CAAC;AAErC,MAAM,UAAU,aAAa,CAAC,IAAgB;IAC5C,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAe,EAAE,IAAa;IACnE,MAAM,GAAG,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IAC/B,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,IAAI,CAAC,OAAgC,EAAE,UAAqB;IAC1E,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC;QACrD,OAAO;IACT,CAAC;IACD,IAAI,UAAU,EAAE,CAAC;QACf,KAAK,MAAM,IAAI,IAAI,UAAU;YAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACnE,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IAChE,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,IAAI,CAAC,GAAa,EAAE,QAAQ,GAAG,CAAC;IAC9C,IAAI,UAAU,KAAK,OAAO,EAAE,CAAC;QAC3B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,OAAO,IAAI,CAAC,CAAC;QACxD,IAAI,GAAG,CAAC,IAAI;YAAE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,WAAW,GAAG,CAAC,IAAI,IAAI,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5D,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type IdempotencyStatus = 'pending' | 'submitted' | 'confirmed' | 'failed';
|
|
2
|
+
export interface IdempotencyEntry {
|
|
3
|
+
command: string;
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic SHA-256 of the canonicalized command args (e.g.
|
|
6
|
+
* `{podId, subnetId, like}` for vote). Stored on first write so the
|
|
7
|
+
* cache can detect a key being reused for a different intent within
|
|
8
|
+
* the same command — `IDEMPOTENCY_COMMAND_MISMATCH` only catches
|
|
9
|
+
* cross-command reuse, not (vote pod 39 like) vs (vote pod 41 dislike)
|
|
10
|
+
* under one key.
|
|
11
|
+
*/
|
|
12
|
+
argsFingerprint: string;
|
|
13
|
+
status: IdempotencyStatus;
|
|
14
|
+
result: Record<string, unknown>;
|
|
15
|
+
txHash: string | null;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
}
|
|
19
|
+
export interface SessionEntry {
|
|
20
|
+
/** Optional — only set for /api/v1/agents/* persistent tokens. */
|
|
21
|
+
agentId?: string;
|
|
22
|
+
accessToken: string;
|
|
23
|
+
walletAddress: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Optional — set for api.reppo.xyz wallet-auth sessions (24h tokens).
|
|
26
|
+
* Epoch ms. Callers should treat the session as expired when
|
|
27
|
+
* Date.now() >= expiresAt.
|
|
28
|
+
*/
|
|
29
|
+
expiresAt?: number;
|
|
30
|
+
createdAt: number;
|
|
31
|
+
}
|
|
32
|
+
export declare function getIdempotent(key: string): Promise<IdempotencyEntry | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Insert OR update an idempotency entry under the lock. Allowed
|
|
35
|
+
* transitions:
|
|
36
|
+
* - first write (no prior entry): always allowed
|
|
37
|
+
* - same command, same argsFingerprint, non-terminal status: allowed
|
|
38
|
+
* - different command for the same key: rejected (caller bug)
|
|
39
|
+
* - same command, different argsFingerprint: rejected (caller bug —
|
|
40
|
+
* reusing one key across different intents would silently return
|
|
41
|
+
* the wrong cached result)
|
|
42
|
+
* - confirmed → anything: rejected (final state)
|
|
43
|
+
*/
|
|
44
|
+
export declare function upsertIdempotent(key: string, entry: IdempotencyEntry): Promise<void>;
|
|
45
|
+
export declare function getSession(network: string, name: string): Promise<SessionEntry | null>;
|
|
46
|
+
export declare function saveSession(network: string, name: string, entry: SessionEntry): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/state/db.ts"],"names":[],"mappings":"AAuBA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEjF,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;OAOG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,kEAAkE;IAClE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAmGD,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAGjF;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuC1F;AAID,wBAAsB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAG5F;AAED,wBAAsB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAKnG"}
|
package/dist/state/db.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny JSON-file store at ~/.reppo/cli-state.json. Used for the
|
|
3
|
+
* idempotency cache and platform-API session tokens. Pure JS — no native
|
|
4
|
+
* deps. Concurrent CLI invocations are serialized via proper-lockfile.
|
|
5
|
+
*
|
|
6
|
+
* Schema:
|
|
7
|
+
* {
|
|
8
|
+
* idempotency: { [key]: { command, status, result, txHash, createdAt, updatedAt } },
|
|
9
|
+
* sessions: { [`${network}:${name}`]: { agentId, accessToken, walletAddress, createdAt } }
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Concurrency model: every read-modify-write goes through `withLockedState`
|
|
13
|
+
* which acquires a proper-lockfile lock on the state file path BEFORE the
|
|
14
|
+
* read. Concurrent invocations queue. The state file is touched (created
|
|
15
|
+
* empty) under the same lock to avoid the lockfile-on-nonexistent-file
|
|
16
|
+
* footgun.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, chmodSync, renameSync } from 'node:fs';
|
|
19
|
+
import { dirname } from 'node:path';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import lockfile from 'proper-lockfile';
|
|
22
|
+
import { cliError } from '../output/format.js';
|
|
23
|
+
function statePath() {
|
|
24
|
+
return process.env.REPPO_STATE_PATH ?? `${homedir()}/.reppo/cli-state.json`;
|
|
25
|
+
}
|
|
26
|
+
function emptyState() {
|
|
27
|
+
return { idempotency: {}, sessions: {} };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Ensure the state file exists with 0o600 perms. Idempotent. The
|
|
31
|
+
* proper-lockfile library refuses to lock a non-existent path, so we
|
|
32
|
+
* have to create it before requesting the lock.
|
|
33
|
+
*/
|
|
34
|
+
function ensureFile(path) {
|
|
35
|
+
// Owner-only on the parent dir too, not just the state file. mkdirSync
|
|
36
|
+
// without a mode would default to 0o777 & ~umask (typically 0o755) —
|
|
37
|
+
// which lets anyone on a shared system enumerate ~/.reppo/ and read
|
|
38
|
+
// any quarantine files we leave behind.
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
40
|
+
try {
|
|
41
|
+
chmodSync(dirname(path), 0o700);
|
|
42
|
+
}
|
|
43
|
+
catch { /* best-effort */ }
|
|
44
|
+
if (!existsSync(path)) {
|
|
45
|
+
writeFileSync(path, JSON.stringify(emptyState()), { mode: 0o600 });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Re-assert 0o600 on every access. `mode` in writeFileSync only takes
|
|
49
|
+
// effect on creation; if a user ever chmodded the file, this fixes it.
|
|
50
|
+
try {
|
|
51
|
+
chmodSync(path, 0o600);
|
|
52
|
+
}
|
|
53
|
+
catch { /* best-effort */ }
|
|
54
|
+
}
|
|
55
|
+
function readUnsafe(path) {
|
|
56
|
+
const raw = readFileSync(path, 'utf-8');
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(raw);
|
|
59
|
+
return {
|
|
60
|
+
idempotency: parsed.idempotency ?? {},
|
|
61
|
+
sessions: parsed.sessions ?? {},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// Corrupt state — quarantine the bad file and start fresh, but
|
|
66
|
+
// surface the corruption to the user. Silently dropping records
|
|
67
|
+
// would defeat the whole idempotency contract.
|
|
68
|
+
const quarantine = `${path}.corrupt-${Date.now()}`;
|
|
69
|
+
try {
|
|
70
|
+
renameSync(path, quarantine);
|
|
71
|
+
// rename preserves perms but be defensive — the source file's
|
|
72
|
+
// mode might have drifted before the corruption.
|
|
73
|
+
chmodSync(quarantine, 0o600);
|
|
74
|
+
}
|
|
75
|
+
catch { /* best-effort */ }
|
|
76
|
+
process.stderr.write(`[reppo-cli] WARNING: state file ${path} was corrupt and has been ` +
|
|
77
|
+
`moved to ${quarantine}. Idempotency cache reset. Original error: ${err.message}\n`);
|
|
78
|
+
return emptyState();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function writeUnsafe(path, s) {
|
|
82
|
+
writeFileSync(path, JSON.stringify(s, null, 2), { mode: 0o600 });
|
|
83
|
+
try {
|
|
84
|
+
chmodSync(path, 0o600);
|
|
85
|
+
}
|
|
86
|
+
catch { /* best-effort */ }
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Acquire the state-file lock, run `mutator(currentState)`, persist its
|
|
90
|
+
* return value, release the lock. The mutator runs inside the critical
|
|
91
|
+
* section so reads + decisions + writes are atomic.
|
|
92
|
+
*/
|
|
93
|
+
async function withLockedState(mutator) {
|
|
94
|
+
const path = statePath();
|
|
95
|
+
ensureFile(path);
|
|
96
|
+
const release = await lockfile.lock(path, {
|
|
97
|
+
retries: { retries: 10, minTimeout: 50, maxTimeout: 500, factor: 2 },
|
|
98
|
+
stale: 10_000,
|
|
99
|
+
});
|
|
100
|
+
try {
|
|
101
|
+
const current = readUnsafe(path);
|
|
102
|
+
const next = await mutator(current);
|
|
103
|
+
writeUnsafe(path, next.state);
|
|
104
|
+
return next.result;
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await release();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Read-only state access. Acquires the lock briefly. */
|
|
111
|
+
async function readLocked() {
|
|
112
|
+
return withLockedState((s) => ({ state: s, result: s }));
|
|
113
|
+
}
|
|
114
|
+
// ── Idempotency API ────────────────────────────────────────────────────
|
|
115
|
+
export async function getIdempotent(key) {
|
|
116
|
+
const s = await readLocked();
|
|
117
|
+
return s.idempotency[key] ?? null;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Insert OR update an idempotency entry under the lock. Allowed
|
|
121
|
+
* transitions:
|
|
122
|
+
* - first write (no prior entry): always allowed
|
|
123
|
+
* - same command, same argsFingerprint, non-terminal status: allowed
|
|
124
|
+
* - different command for the same key: rejected (caller bug)
|
|
125
|
+
* - same command, different argsFingerprint: rejected (caller bug —
|
|
126
|
+
* reusing one key across different intents would silently return
|
|
127
|
+
* the wrong cached result)
|
|
128
|
+
* - confirmed → anything: rejected (final state)
|
|
129
|
+
*/
|
|
130
|
+
export async function upsertIdempotent(key, entry) {
|
|
131
|
+
await withLockedState((s) => {
|
|
132
|
+
const prior = s.idempotency[key];
|
|
133
|
+
if (prior) {
|
|
134
|
+
if (prior.command !== entry.command) {
|
|
135
|
+
throw cliError('IDEMPOTENCY_COMMAND_MISMATCH', `idempotency key "${key}" was previously used by command "${prior.command}", ` +
|
|
136
|
+
`not "${entry.command}". Use a unique key per (command, intent) pair.`, 'Use a key like "<command>-<intent>-<id>" (e.g. "vote-pod-34-like") so commands cannot collide.');
|
|
137
|
+
}
|
|
138
|
+
if (prior.argsFingerprint !== entry.argsFingerprint) {
|
|
139
|
+
// Pre-v0.2 entries lack argsFingerprint — render a placeholder so
|
|
140
|
+
// a legacy entry produces a clean structured error here too,
|
|
141
|
+
// rather than crashing on `.slice()` of undefined. Normal command
|
|
142
|
+
// flow catches legacy entries earlier via IDEMPOTENCY_LEGACY_ENTRY,
|
|
143
|
+
// but `upsertIdempotent` is a public API and may be called directly.
|
|
144
|
+
const priorFp = prior.argsFingerprint ? prior.argsFingerprint.slice(0, 12) : '(legacy)';
|
|
145
|
+
throw cliError('IDEMPOTENCY_ARGS_MISMATCH', `idempotency key "${key}" was previously used with different args ` +
|
|
146
|
+
`(fingerprint ${priorFp}…) and is now being reused with new args ` +
|
|
147
|
+
`(fingerprint ${entry.argsFingerprint.slice(0, 12)}…).`, 'A given idempotency key represents a single intent. Use a fresh key when the args change (e.g. different --pod or --like value).');
|
|
148
|
+
}
|
|
149
|
+
if (prior.status === 'confirmed') {
|
|
150
|
+
throw cliError('IDEMPOTENCY_TERMINAL_STATE', `idempotency key "${key}" is already in terminal state "confirmed"; ` +
|
|
151
|
+
`refusing to overwrite. Use a new key for a new attempt.`, 'A confirmed key is final. Use a fresh --idempotency-key for a new attempt.');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
s.idempotency[key] = { ...entry, updatedAt: Date.now() };
|
|
155
|
+
return { state: s, result: undefined };
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// ── Session API ────────────────────────────────────────────────────────
|
|
159
|
+
export async function getSession(network, name) {
|
|
160
|
+
const s = await readLocked();
|
|
161
|
+
return s.sessions[`${network}:${name}`] ?? null;
|
|
162
|
+
}
|
|
163
|
+
export async function saveSession(network, name, entry) {
|
|
164
|
+
await withLockedState((s) => {
|
|
165
|
+
s.sessions[`${network}:${name}`] = entry;
|
|
166
|
+
return { state: s, result: undefined };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=db.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/state/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type IdempotencyEntry, type IdempotencyStatus } from './db.js';
|
|
2
|
+
export type { IdempotencyEntry, IdempotencyStatus };
|
|
3
|
+
/**
|
|
4
|
+
* Decision returned by `peekIdempotent` so write commands can decide what
|
|
5
|
+
* to do *before* spending gas:
|
|
6
|
+
* - 'proceed' — no cache hit (or pre-submit failure); do the work.
|
|
7
|
+
* - 'return-confirmed' — cache has a final result; emit it and skip.
|
|
8
|
+
* - 'return-submitted' — cache has a tx hash that hasn't confirmed yet;
|
|
9
|
+
* emit it and tell the caller to poll the explorer.
|
|
10
|
+
* Two terminal "do not retry" states (`pending`, `failed-after-broadcast`)
|
|
11
|
+
* throw inside the helper because they never make sense to handle inline.
|
|
12
|
+
*/
|
|
13
|
+
export type CacheDecision<R> = {
|
|
14
|
+
kind: 'proceed';
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'return-confirmed';
|
|
17
|
+
result: R;
|
|
18
|
+
txHash: string | null;
|
|
19
|
+
} | {
|
|
20
|
+
kind: 'return-submitted';
|
|
21
|
+
result: R;
|
|
22
|
+
txHash: string | null;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* SHA-256 of the canonicalized JSON of the args object. Object keys are
|
|
26
|
+
* sorted so {a:1,b:2} and {b:2,a:1} fingerprint identically; bigints
|
|
27
|
+
* are stringified so they survive JSON.stringify; nested arrays/objects
|
|
28
|
+
* recurse. The output is the only thing the cache compares — small
|
|
29
|
+
* representation choices below matter for matching across runs.
|
|
30
|
+
*/
|
|
31
|
+
export declare function fingerprintArgs(args: Record<string, unknown>): string;
|
|
32
|
+
export declare function getIdempotent(key: string, command: string, args: Record<string, unknown>): Promise<IdempotencyEntry | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Inspect the idempotency cache and decide whether the caller should
|
|
35
|
+
* proceed with a write. Lifts the cache-decision logic that previously
|
|
36
|
+
* lived inline in `vote.ts` so every write command shares the same
|
|
37
|
+
* policy:
|
|
38
|
+
* - dry-run: never consult or mutate the cache (sim is read-only;
|
|
39
|
+
* persisting a "submitted" record would let a sim poison the cache
|
|
40
|
+
* for the next real call).
|
|
41
|
+
* - confirmed: short-circuit with the cached result.
|
|
42
|
+
* - submitted: short-circuit with the cached tx hash; user polls.
|
|
43
|
+
* - pending: refuse — another invocation is mid-flight.
|
|
44
|
+
* - failed + txHash: refuse — re-broadcasting under the same key would
|
|
45
|
+
* pay gas for a second doomed attempt; force a fresh key.
|
|
46
|
+
* - failed + no txHash: pre-submit failure, safe to retry: 'proceed'.
|
|
47
|
+
*
|
|
48
|
+
* The result type `R` matches the shape the command stores via
|
|
49
|
+
* `markConfirmed` (e.g. `{ txHash, podId, ..., basescanUrl }` for vote).
|
|
50
|
+
* Cached results are typed as the caller's `R`; runtime shape is whatever
|
|
51
|
+
* the original `markConfirmed` recorded.
|
|
52
|
+
*/
|
|
53
|
+
export declare function peekIdempotent<R>(key: string | undefined, command: string, args: Record<string, unknown>, isDryRun: boolean): Promise<CacheDecision<R>>;
|
|
54
|
+
export declare function begin(key: string, command: string, args: Record<string, unknown>): Promise<void>;
|
|
55
|
+
export declare function markSubmitted(key: string, command: string, args: Record<string, unknown>, txHash: string): Promise<void>;
|
|
56
|
+
export declare function markConfirmed(key: string, command: string, args: Record<string, unknown>, result: Record<string, unknown>, txHash: string): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Mark an idempotency entry as failed. Pass `txHash` when the failure
|
|
59
|
+
* happened *after* the tx was broadcast (e.g. on-chain revert, receipt
|
|
60
|
+
* timeout) so the cached entry retains the hash for forensics + so the
|
|
61
|
+
* "same-key retry on failed" guard in callers can distinguish pre-submit
|
|
62
|
+
* failures (safe to retry) from post-submit failures (must not retry —
|
|
63
|
+
* the tx already executed and may have side-effects).
|
|
64
|
+
*/
|
|
65
|
+
export declare function markFailed(key: string, command: string, args: Record<string, unknown>, error: string, txHash?: string | null): Promise<void>;
|
|
66
|
+
//# sourceMappingURL=idempotency.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../../src/state/idempotency.ts"],"names":[],"mappings":"AA2BA,OAAO,EAGL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,SAAS,CAAC;AAGjB,YAAY,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,MAAM,aAAa,CAAC,CAAC,IACvB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GAC9D;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,MAAM,EAAE,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEnE;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAcrE;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CA8BlC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,cAAc,CAAC,CAAC,EACpC,GAAG,EAAE,MAAM,GAAG,SAAS,EACvB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,QAAQ,EAAE,OAAO,GAChB,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CA8B3B;AAED,wBAAsB,KAAK,CACzB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC5B,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,EACb,MAAM,GAAE,MAAM,GAAG,IAAW,GAC3B,OAAO,CAAC,IAAI,CAAC,CAWf"}
|