aavegotchi-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/README.md +120 -0
- package/dist/args.js +97 -0
- package/dist/chains.js +74 -0
- package/dist/command-runner.js +184 -0
- package/dist/commands/batch.js +189 -0
- package/dist/commands/bootstrap.js +97 -0
- package/dist/commands/index.js +6 -0
- package/dist/commands/mapped.js +71 -0
- package/dist/commands/onchain.js +215 -0
- package/dist/commands/policy.js +75 -0
- package/dist/commands/profile.js +67 -0
- package/dist/commands/rpc.js +29 -0
- package/dist/commands/signer.js +51 -0
- package/dist/commands/stubs.js +32 -0
- package/dist/commands/tx.js +173 -0
- package/dist/config.js +198 -0
- package/dist/errors.js +31 -0
- package/dist/idempotency.js +21 -0
- package/dist/index.js +33 -0
- package/dist/journal.js +377 -0
- package/dist/keychain.js +198 -0
- package/dist/logger.js +20 -0
- package/dist/output.js +112 -0
- package/dist/policy.js +38 -0
- package/dist/rpc.js +40 -0
- package/dist/schemas.js +79 -0
- package/dist/signer.js +348 -0
- package/dist/tx-engine.js +285 -0
- package/dist/types.js +2 -0
- package/package.json +50 -0
package/dist/output.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outputSuccess = outputSuccess;
|
|
4
|
+
exports.outputError = outputError;
|
|
5
|
+
exports.outputHelp = outputHelp;
|
|
6
|
+
function stringifyWithBigInt(input) {
|
|
7
|
+
return JSON.stringify(input, (_, value) => {
|
|
8
|
+
if (typeof value === "bigint") {
|
|
9
|
+
return value.toString();
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}, 2);
|
|
13
|
+
}
|
|
14
|
+
function buildMeta(mode) {
|
|
15
|
+
return {
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
mode,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function outputSuccess(command, data, globals) {
|
|
21
|
+
if (globals.json) {
|
|
22
|
+
const envelope = {
|
|
23
|
+
schemaVersion: "1.0.0",
|
|
24
|
+
command,
|
|
25
|
+
status: "ok",
|
|
26
|
+
data,
|
|
27
|
+
meta: buildMeta(globals.mode),
|
|
28
|
+
};
|
|
29
|
+
console.log(stringifyWithBigInt(envelope));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
console.log(`[ok] ${command}`);
|
|
33
|
+
console.log(stringifyWithBigInt(data));
|
|
34
|
+
}
|
|
35
|
+
function outputError(command, error, globals) {
|
|
36
|
+
if (globals.json) {
|
|
37
|
+
const envelope = {
|
|
38
|
+
schemaVersion: "1.0.0",
|
|
39
|
+
command,
|
|
40
|
+
status: "error",
|
|
41
|
+
error: {
|
|
42
|
+
code: error.code,
|
|
43
|
+
message: error.message,
|
|
44
|
+
details: error.details,
|
|
45
|
+
},
|
|
46
|
+
meta: buildMeta(globals.mode),
|
|
47
|
+
};
|
|
48
|
+
console.error(stringifyWithBigInt(envelope));
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.error(`[error:${error.code}] ${error.message}`);
|
|
52
|
+
if (error.details) {
|
|
53
|
+
console.error(stringifyWithBigInt(error.details));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function outputHelp() {
|
|
57
|
+
console.log(`
|
|
58
|
+
Aavegotchi CLI (agent-first foundation)
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
ag <command> [options]
|
|
62
|
+
|
|
63
|
+
Core commands:
|
|
64
|
+
bootstrap Create/update and activate a profile with RPC/signer preflight
|
|
65
|
+
profile list|show|use|export Manage profiles
|
|
66
|
+
signer check Verify active profile signer backend and account readiness
|
|
67
|
+
signer keychain list|import|remove
|
|
68
|
+
policy list|show|upsert Manage transaction policies
|
|
69
|
+
rpc check Verify RPC connectivity + signer backend health
|
|
70
|
+
|
|
71
|
+
Tx commands:
|
|
72
|
+
tx send Send a raw EVM transaction with simulation + policy checks + journaling
|
|
73
|
+
tx status Read tx status by idempotency key/hash or list recent
|
|
74
|
+
tx resume Resume waiting for a previously submitted tx
|
|
75
|
+
tx watch Poll journal until tx is confirmed
|
|
76
|
+
|
|
77
|
+
Automation commands:
|
|
78
|
+
batch run --file plan.yaml Run a YAML execution plan (dependency-aware)
|
|
79
|
+
|
|
80
|
+
Power-user commands:
|
|
81
|
+
onchain call Call any ABI function from --abi-file
|
|
82
|
+
onchain send Send any ABI function as a transaction
|
|
83
|
+
|
|
84
|
+
Domain namespaces:
|
|
85
|
+
gotchi, portal, wearables, items, inventory, baazaar, lending, realm, alchemica, forge, token
|
|
86
|
+
(many write flows are mapped to onchain send aliases; unmatched commands return typed not-implemented)
|
|
87
|
+
|
|
88
|
+
Global flags:
|
|
89
|
+
--mode <agent|human> Agent mode implies --json --yes
|
|
90
|
+
--json, -j Emit JSON envelope output
|
|
91
|
+
--yes, -y Skip prompts (write commands assume explicit flags)
|
|
92
|
+
--profile NAME Select profile globally
|
|
93
|
+
|
|
94
|
+
Bootstrap flags:
|
|
95
|
+
--profile NAME Profile to create or update (required)
|
|
96
|
+
--chain base|base-sepolia|<id> Chain key or numeric chain id (default: base)
|
|
97
|
+
--rpc-url URL RPC endpoint (optional when chain preset exists)
|
|
98
|
+
--signer readonly|env:VAR|keychain:<id>|ledger[:path|address|bridgeEnv]|remote:<url|address|authEnv>
|
|
99
|
+
--signer-address 0x... Optional override for remote/ledger signer address
|
|
100
|
+
--signer-auth-env-var ENV_VAR Optional remote signer bearer token env var
|
|
101
|
+
--signer-bridge-env-var ENV_VAR Optional ledger bridge command env var name
|
|
102
|
+
--policy NAME Policy label (default: default)
|
|
103
|
+
--skip-signer-check Persist signer config without backend validation
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
ag bootstrap --mode agent --profile prod --chain base --signer env:AGCLI_PRIVATE_KEY --json
|
|
107
|
+
AGCLI_KEYCHAIN_PASSPHRASE=... AGCLI_PRIVATE_KEY=0x... ag signer keychain import --account-id bot --private-key-env AGCLI_PRIVATE_KEY --json
|
|
108
|
+
ag tx send --profile prod --to 0xabc... --value-wei 1000000000000000 --wait --json
|
|
109
|
+
ag lending create --profile prod --abi-file ./abis/GotchiLendingFacet.json --address 0xabc... --args-json '[...]' --json
|
|
110
|
+
ag batch run --file ./plan.yaml --json
|
|
111
|
+
`);
|
|
112
|
+
}
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enforcePolicy = enforcePolicy;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
function exceeds(limit, current) {
|
|
6
|
+
if (!limit || current === undefined) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return current > BigInt(limit);
|
|
10
|
+
}
|
|
11
|
+
function enforcePolicy(input) {
|
|
12
|
+
const violations = [];
|
|
13
|
+
if (input.policy.allowedTo && input.policy.allowedTo.length > 0) {
|
|
14
|
+
const normalized = input.to.toLowerCase();
|
|
15
|
+
const allowed = input.policy.allowedTo.map((value) => value.toLowerCase());
|
|
16
|
+
if (!allowed.includes(normalized)) {
|
|
17
|
+
violations.push(`to address '${input.to}' is not allowlisted by policy '${input.policy.name}'`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (exceeds(input.policy.maxValueWei, input.valueWei)) {
|
|
21
|
+
violations.push(`value exceeds maxValueWei (${input.policy.maxValueWei})`);
|
|
22
|
+
}
|
|
23
|
+
if (exceeds(input.policy.maxGasLimit, input.gasLimit)) {
|
|
24
|
+
violations.push(`gas limit exceeds maxGasLimit (${input.policy.maxGasLimit})`);
|
|
25
|
+
}
|
|
26
|
+
if (exceeds(input.policy.maxFeePerGasWei, input.maxFeePerGasWei)) {
|
|
27
|
+
violations.push(`max fee per gas exceeds maxFeePerGasWei (${input.policy.maxFeePerGasWei})`);
|
|
28
|
+
}
|
|
29
|
+
if (exceeds(input.policy.maxPriorityFeePerGasWei, input.maxPriorityFeePerGasWei)) {
|
|
30
|
+
violations.push(`max priority fee per gas exceeds maxPriorityFeePerGasWei (${input.policy.maxPriorityFeePerGasWei})`);
|
|
31
|
+
}
|
|
32
|
+
if (violations.length > 0) {
|
|
33
|
+
throw new errors_1.CliError("POLICY_VIOLATION", "Transaction blocked by policy checks.", 2, {
|
|
34
|
+
policy: input.policy.name,
|
|
35
|
+
violations,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRpcClient = createRpcClient;
|
|
4
|
+
exports.runRpcPreflight = runRpcPreflight;
|
|
5
|
+
const viem_1 = require("viem");
|
|
6
|
+
const chains_1 = require("./chains");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
function createRpcClient(chain, rpcUrl) {
|
|
9
|
+
const viemChain = (0, chains_1.toViemChain)(chain, rpcUrl);
|
|
10
|
+
return (0, viem_1.createPublicClient)({
|
|
11
|
+
chain: viemChain,
|
|
12
|
+
transport: (0, viem_1.http)(rpcUrl),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async function runRpcPreflight(chain, rpcUrl) {
|
|
16
|
+
const client = createRpcClient(chain, rpcUrl);
|
|
17
|
+
try {
|
|
18
|
+
const [chainId, blockNumber] = await Promise.all([client.getChainId(), client.getBlockNumber()]);
|
|
19
|
+
if (chain.chainId !== chainId) {
|
|
20
|
+
throw new errors_1.CliError("CHAIN_MISMATCH", "Connected chain does not match requested chain.", 2, {
|
|
21
|
+
expectedChainId: chain.chainId,
|
|
22
|
+
actualChainId: chainId,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
client,
|
|
27
|
+
chainId,
|
|
28
|
+
blockNumber: blockNumber.toString(),
|
|
29
|
+
chainName: client.chain?.name || chain.key,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error instanceof errors_1.CliError) {
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
throw new errors_1.CliError("RPC_UNREACHABLE", "Failed to connect to RPC endpoint.", 2, {
|
|
37
|
+
rpcUrl,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.batchPlanSchema = exports.batchStepSchema = exports.legacyCliConfigSchema = exports.cliConfigSchema = exports.profileSchema = exports.policySchema = exports.signerSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const addressSchema = zod_1.z
|
|
6
|
+
.string()
|
|
7
|
+
.regex(/^0x[a-fA-F0-9]{40}$/, "Expected an EVM address")
|
|
8
|
+
.transform((value) => value.toLowerCase());
|
|
9
|
+
exports.signerSchema = zod_1.z.discriminatedUnion("type", [
|
|
10
|
+
zod_1.z.object({ type: zod_1.z.literal("readonly") }),
|
|
11
|
+
zod_1.z.object({ type: zod_1.z.literal("env"), envVar: zod_1.z.string().regex(/^[A-Z_][A-Z0-9_]*$/) }),
|
|
12
|
+
zod_1.z.object({ type: zod_1.z.literal("keychain"), accountId: zod_1.z.string().min(1) }),
|
|
13
|
+
zod_1.z.object({
|
|
14
|
+
type: zod_1.z.literal("ledger"),
|
|
15
|
+
derivationPath: zod_1.z.string().optional(),
|
|
16
|
+
address: addressSchema.optional(),
|
|
17
|
+
bridgeCommandEnvVar: zod_1.z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional(),
|
|
18
|
+
}),
|
|
19
|
+
zod_1.z.object({
|
|
20
|
+
type: zod_1.z.literal("remote"),
|
|
21
|
+
url: zod_1.z.string().url(),
|
|
22
|
+
address: addressSchema.optional(),
|
|
23
|
+
authEnvVar: zod_1.z.string().regex(/^[A-Z_][A-Z0-9_]*$/).optional(),
|
|
24
|
+
}),
|
|
25
|
+
]);
|
|
26
|
+
exports.policySchema = zod_1.z.object({
|
|
27
|
+
name: zod_1.z.string().min(1),
|
|
28
|
+
maxValueWei: zod_1.z.string().regex(/^\d+$/).optional(),
|
|
29
|
+
maxGasLimit: zod_1.z.string().regex(/^\d+$/).optional(),
|
|
30
|
+
maxFeePerGasWei: zod_1.z.string().regex(/^\d+$/).optional(),
|
|
31
|
+
maxPriorityFeePerGasWei: zod_1.z.string().regex(/^\d+$/).optional(),
|
|
32
|
+
allowedTo: zod_1.z.array(addressSchema).optional(),
|
|
33
|
+
createdAt: zod_1.z.string().datetime(),
|
|
34
|
+
updatedAt: zod_1.z.string().datetime(),
|
|
35
|
+
});
|
|
36
|
+
exports.profileSchema = zod_1.z.object({
|
|
37
|
+
name: zod_1.z.string().min(1),
|
|
38
|
+
chain: zod_1.z.string().min(1),
|
|
39
|
+
chainId: zod_1.z.number().int().positive(),
|
|
40
|
+
rpcUrl: zod_1.z.string().url(),
|
|
41
|
+
signer: exports.signerSchema,
|
|
42
|
+
policy: zod_1.z.string().min(1),
|
|
43
|
+
createdAt: zod_1.z.string().datetime(),
|
|
44
|
+
updatedAt: zod_1.z.string().datetime(),
|
|
45
|
+
});
|
|
46
|
+
exports.cliConfigSchema = zod_1.z.object({
|
|
47
|
+
schemaVersion: zod_1.z.literal(2),
|
|
48
|
+
activeProfile: zod_1.z.string().optional(),
|
|
49
|
+
profiles: zod_1.z.record(exports.profileSchema),
|
|
50
|
+
policies: zod_1.z.record(exports.policySchema),
|
|
51
|
+
});
|
|
52
|
+
exports.legacyCliConfigSchema = zod_1.z.object({
|
|
53
|
+
schemaVersion: zod_1.z.literal(1),
|
|
54
|
+
activeProfile: zod_1.z.string().optional(),
|
|
55
|
+
profiles: zod_1.z.record(zod_1.z.object({
|
|
56
|
+
name: zod_1.z.string(),
|
|
57
|
+
chain: zod_1.z.string(),
|
|
58
|
+
chainId: zod_1.z.number(),
|
|
59
|
+
rpcUrl: zod_1.z.string(),
|
|
60
|
+
signer: zod_1.z.any(),
|
|
61
|
+
policy: zod_1.z.string(),
|
|
62
|
+
createdAt: zod_1.z.string(),
|
|
63
|
+
updatedAt: zod_1.z.string(),
|
|
64
|
+
})),
|
|
65
|
+
});
|
|
66
|
+
exports.batchStepSchema = zod_1.z.object({
|
|
67
|
+
id: zod_1.z.string().min(1),
|
|
68
|
+
command: zod_1.z.string().min(1),
|
|
69
|
+
dependsOn: zod_1.z.array(zod_1.z.string()).optional(),
|
|
70
|
+
args: zod_1.z.record(zod_1.z.union([zod_1.z.string(), zod_1.z.number(), zod_1.z.boolean()])).optional(),
|
|
71
|
+
continueOnError: zod_1.z.boolean().optional(),
|
|
72
|
+
});
|
|
73
|
+
exports.batchPlanSchema = zod_1.z.object({
|
|
74
|
+
version: zod_1.z.literal(1),
|
|
75
|
+
profile: zod_1.z.string().optional(),
|
|
76
|
+
mode: zod_1.z.enum(["agent", "human"]).optional(),
|
|
77
|
+
continueOnError: zod_1.z.boolean().optional(),
|
|
78
|
+
steps: zod_1.z.array(exports.batchStepSchema).min(1),
|
|
79
|
+
});
|
package/dist/signer.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseSigner = parseSigner;
|
|
4
|
+
exports.resolveSignerRuntime = resolveSignerRuntime;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const viem_1 = require("viem");
|
|
7
|
+
const accounts_1 = require("viem/accounts");
|
|
8
|
+
const errors_1 = require("./errors");
|
|
9
|
+
const keychain_1 = require("./keychain");
|
|
10
|
+
const REMOTE_SIGN_ADDRESS_PATH = "/address";
|
|
11
|
+
const REMOTE_SIGN_TX_PATH = "/sign-transaction";
|
|
12
|
+
function parsePrivateKey(value, hint) {
|
|
13
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(value)) {
|
|
14
|
+
throw new errors_1.CliError("INVALID_PRIVATE_KEY", `${hint} is not a valid private key.`, 2);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function ensureAddress(value, hint) {
|
|
19
|
+
if (!value || !/^0x[a-fA-F0-9]{40}$/.test(value)) {
|
|
20
|
+
throw new errors_1.CliError("INVALID_ARGUMENT", `${hint} must be a valid EVM address.`, 2, {
|
|
21
|
+
value,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
return value.toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
function parseTxHash(value) {
|
|
27
|
+
if (typeof value === "string" && /^0x[a-fA-F0-9]{64}$/.test(value)) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function parseRawTx(value) {
|
|
33
|
+
if (typeof value === "string" && /^0x[a-fA-F0-9]+$/.test(value)) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
function addDefaultPaths(url, pathSuffix) {
|
|
39
|
+
return `${url.replace(/\/+$/, "")}${pathSuffix}`;
|
|
40
|
+
}
|
|
41
|
+
async function fetchJson(url, init) {
|
|
42
|
+
let response;
|
|
43
|
+
try {
|
|
44
|
+
response = await fetch(url, init);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
throw new errors_1.CliError("REMOTE_SIGNER_UNREACHABLE", "Failed to connect to remote signer service.", 2, {
|
|
48
|
+
url,
|
|
49
|
+
message: error instanceof Error ? error.message : String(error),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
throw new errors_1.CliError("REMOTE_SIGNER_HTTP_ERROR", `Remote signer responded with HTTP ${response.status}.`, 2, {
|
|
54
|
+
url,
|
|
55
|
+
status: response.status,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return (await response.json());
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new errors_1.CliError("REMOTE_SIGNER_PROTOCOL_ERROR", "Remote signer did not return valid JSON.", 2, {
|
|
63
|
+
url,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function buildRemoteHeaders(signer) {
|
|
68
|
+
const headers = {
|
|
69
|
+
"content-type": "application/json",
|
|
70
|
+
};
|
|
71
|
+
if (signer.authEnvVar) {
|
|
72
|
+
const token = process.env[signer.authEnvVar];
|
|
73
|
+
if (!token) {
|
|
74
|
+
throw new errors_1.CliError("MISSING_SIGNER_SECRET", `Missing environment variable '${signer.authEnvVar}'.`, 2);
|
|
75
|
+
}
|
|
76
|
+
headers.authorization = `Bearer ${token}`;
|
|
77
|
+
}
|
|
78
|
+
return headers;
|
|
79
|
+
}
|
|
80
|
+
async function resolveRemoteAddress(signer, headers) {
|
|
81
|
+
if (signer.address) {
|
|
82
|
+
return ensureAddress(signer.address, "remote signer address");
|
|
83
|
+
}
|
|
84
|
+
const response = (await fetchJson(addDefaultPaths(signer.url, REMOTE_SIGN_ADDRESS_PATH), {
|
|
85
|
+
method: "GET",
|
|
86
|
+
headers,
|
|
87
|
+
}));
|
|
88
|
+
return ensureAddress(response.address, "remote signer address response");
|
|
89
|
+
}
|
|
90
|
+
function runLedgerBridge(bridgeCommand, payload) {
|
|
91
|
+
const result = (0, child_process_1.spawnSync)(bridgeCommand, {
|
|
92
|
+
shell: true,
|
|
93
|
+
encoding: "utf8",
|
|
94
|
+
input: JSON.stringify(payload),
|
|
95
|
+
});
|
|
96
|
+
if (result.error) {
|
|
97
|
+
throw new errors_1.CliError("LEDGER_BRIDGE_FAILED", "Ledger bridge execution failed.", 2, {
|
|
98
|
+
message: result.error.message,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (result.status !== 0) {
|
|
102
|
+
throw new errors_1.CliError("LEDGER_BRIDGE_FAILED", "Ledger bridge returned non-zero exit code.", 2, {
|
|
103
|
+
status: result.status,
|
|
104
|
+
stderr: result.stderr,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const stdout = result.stdout.trim();
|
|
108
|
+
if (!stdout) {
|
|
109
|
+
throw new errors_1.CliError("LEDGER_BRIDGE_FAILED", "Ledger bridge returned empty output.", 2);
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(stdout);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new errors_1.CliError("LEDGER_BRIDGE_FAILED", "Ledger bridge output was not valid JSON.", 2, {
|
|
116
|
+
stdout,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async function resolveBalanceSummary(signerType, publicClient, address) {
|
|
121
|
+
const [nonce, balance] = await Promise.all([
|
|
122
|
+
publicClient.getTransactionCount({ address, blockTag: "pending" }),
|
|
123
|
+
publicClient.getBalance({ address }),
|
|
124
|
+
]);
|
|
125
|
+
return {
|
|
126
|
+
signerType,
|
|
127
|
+
address,
|
|
128
|
+
nonce,
|
|
129
|
+
balanceWei: balance.toString(),
|
|
130
|
+
canSign: true,
|
|
131
|
+
backendStatus: "ready",
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseSignerRemoteSpec(value) {
|
|
135
|
+
const body = value.slice("remote:".length).trim();
|
|
136
|
+
const [urlPart, addressPart, authPart] = body.split("|").map((entry) => entry.trim());
|
|
137
|
+
if (!/^https?:\/\//i.test(urlPart || "")) {
|
|
138
|
+
throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid remote signer format '${value}'.`, 2);
|
|
139
|
+
}
|
|
140
|
+
const signer = {
|
|
141
|
+
type: "remote",
|
|
142
|
+
url: urlPart,
|
|
143
|
+
};
|
|
144
|
+
if (addressPart) {
|
|
145
|
+
signer.address = ensureAddress(addressPart, "remote signer address");
|
|
146
|
+
}
|
|
147
|
+
if (authPart) {
|
|
148
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(authPart)) {
|
|
149
|
+
throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid remote signer auth env var in '${value}'.`, 2);
|
|
150
|
+
}
|
|
151
|
+
signer.authEnvVar = authPart;
|
|
152
|
+
}
|
|
153
|
+
return signer;
|
|
154
|
+
}
|
|
155
|
+
function parseSignerLedgerSpec(value) {
|
|
156
|
+
if (value === "ledger") {
|
|
157
|
+
return { type: "ledger" };
|
|
158
|
+
}
|
|
159
|
+
const body = value.slice("ledger:".length);
|
|
160
|
+
const [derivationPath, addressPart, bridgeEnvPart] = body.split("|").map((entry) => entry.trim());
|
|
161
|
+
const signer = {
|
|
162
|
+
type: "ledger",
|
|
163
|
+
};
|
|
164
|
+
if (derivationPath) {
|
|
165
|
+
signer.derivationPath = derivationPath;
|
|
166
|
+
}
|
|
167
|
+
if (addressPart) {
|
|
168
|
+
signer.address = ensureAddress(addressPart, "ledger signer address");
|
|
169
|
+
}
|
|
170
|
+
if (bridgeEnvPart) {
|
|
171
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(bridgeEnvPart)) {
|
|
172
|
+
throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid ledger bridge env var in '${value}'.`, 2);
|
|
173
|
+
}
|
|
174
|
+
signer.bridgeCommandEnvVar = bridgeEnvPart;
|
|
175
|
+
}
|
|
176
|
+
return signer;
|
|
177
|
+
}
|
|
178
|
+
function parseSigner(value) {
|
|
179
|
+
if (!value || value === "readonly") {
|
|
180
|
+
return { type: "readonly" };
|
|
181
|
+
}
|
|
182
|
+
if (value.startsWith("env:")) {
|
|
183
|
+
const envVar = value.slice(4);
|
|
184
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(envVar)) {
|
|
185
|
+
throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid env signer format '${value}'.`, 2);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
type: "env",
|
|
189
|
+
envVar,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
if (value.startsWith("keychain:")) {
|
|
193
|
+
const accountId = value.slice("keychain:".length).trim();
|
|
194
|
+
if (!accountId) {
|
|
195
|
+
throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Invalid keychain signer format '${value}'.`, 2);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
type: "keychain",
|
|
199
|
+
accountId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
if (value === "ledger" || value.startsWith("ledger:")) {
|
|
203
|
+
return parseSignerLedgerSpec(value);
|
|
204
|
+
}
|
|
205
|
+
if (value.startsWith("remote:")) {
|
|
206
|
+
return parseSignerRemoteSpec(value);
|
|
207
|
+
}
|
|
208
|
+
throw new errors_1.CliError("INVALID_SIGNER_SPEC", `Unsupported signer '${value}'. Use readonly, env:<ENV_VAR>, keychain:<id>, ledger[:path|address|bridgeEnv], or remote:<url|address|authEnv>.`, 2);
|
|
209
|
+
}
|
|
210
|
+
async function resolveSignerRuntime(signer, publicClient, rpcUrl, chain, customHome) {
|
|
211
|
+
if (signer.type === "readonly") {
|
|
212
|
+
return {
|
|
213
|
+
summary: {
|
|
214
|
+
signerType: "readonly",
|
|
215
|
+
canSign: false,
|
|
216
|
+
backendStatus: "ready",
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
if (signer.type === "env") {
|
|
221
|
+
const privateKeyRaw = process.env[signer.envVar];
|
|
222
|
+
if (!privateKeyRaw) {
|
|
223
|
+
throw new errors_1.CliError("MISSING_SIGNER_SECRET", `Missing environment variable '${signer.envVar}'.`, 2);
|
|
224
|
+
}
|
|
225
|
+
const privateKey = parsePrivateKey(privateKeyRaw, `Environment variable '${signer.envVar}'`);
|
|
226
|
+
const account = (0, accounts_1.privateKeyToAccount)(privateKey);
|
|
227
|
+
const walletClient = (0, viem_1.createWalletClient)({
|
|
228
|
+
account,
|
|
229
|
+
chain,
|
|
230
|
+
transport: (0, viem_1.http)(rpcUrl),
|
|
231
|
+
});
|
|
232
|
+
const summary = await resolveBalanceSummary("env", publicClient, account.address);
|
|
233
|
+
return {
|
|
234
|
+
summary,
|
|
235
|
+
sendTransaction: async (request) => walletClient.sendTransaction({
|
|
236
|
+
account,
|
|
237
|
+
chain: request.chain,
|
|
238
|
+
to: request.to,
|
|
239
|
+
data: request.data,
|
|
240
|
+
value: request.value,
|
|
241
|
+
gas: request.gas,
|
|
242
|
+
nonce: request.nonce,
|
|
243
|
+
...(request.maxFeePerGas ? { maxFeePerGas: request.maxFeePerGas } : {}),
|
|
244
|
+
...(request.maxPriorityFeePerGas ? { maxPriorityFeePerGas: request.maxPriorityFeePerGas } : {}),
|
|
245
|
+
}),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (signer.type === "keychain") {
|
|
249
|
+
const resolved = (0, keychain_1.keychainResolvePrivateKey)(signer.accountId, customHome);
|
|
250
|
+
const account = (0, accounts_1.privateKeyToAccount)(resolved.privateKey);
|
|
251
|
+
const walletClient = (0, viem_1.createWalletClient)({
|
|
252
|
+
account,
|
|
253
|
+
chain,
|
|
254
|
+
transport: (0, viem_1.http)(rpcUrl),
|
|
255
|
+
});
|
|
256
|
+
const summary = await resolveBalanceSummary("keychain", publicClient, account.address);
|
|
257
|
+
return {
|
|
258
|
+
summary,
|
|
259
|
+
sendTransaction: async (request) => walletClient.sendTransaction({
|
|
260
|
+
account,
|
|
261
|
+
chain: request.chain,
|
|
262
|
+
to: request.to,
|
|
263
|
+
data: request.data,
|
|
264
|
+
value: request.value,
|
|
265
|
+
gas: request.gas,
|
|
266
|
+
nonce: request.nonce,
|
|
267
|
+
...(request.maxFeePerGas ? { maxFeePerGas: request.maxFeePerGas } : {}),
|
|
268
|
+
...(request.maxPriorityFeePerGas ? { maxPriorityFeePerGas: request.maxPriorityFeePerGas } : {}),
|
|
269
|
+
}),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (signer.type === "remote") {
|
|
273
|
+
const headers = buildRemoteHeaders(signer);
|
|
274
|
+
const address = await resolveRemoteAddress(signer, headers);
|
|
275
|
+
const summary = await resolveBalanceSummary("remote", publicClient, address);
|
|
276
|
+
return {
|
|
277
|
+
summary,
|
|
278
|
+
sendTransaction: async (request) => {
|
|
279
|
+
const payload = {
|
|
280
|
+
chainId: request.chain.id,
|
|
281
|
+
tx: {
|
|
282
|
+
to: request.to,
|
|
283
|
+
data: request.data,
|
|
284
|
+
valueWei: request.value?.toString() || "0",
|
|
285
|
+
gas: request.gas.toString(),
|
|
286
|
+
nonce: request.nonce,
|
|
287
|
+
maxFeePerGasWei: request.maxFeePerGas?.toString(),
|
|
288
|
+
maxPriorityFeePerGasWei: request.maxPriorityFeePerGas?.toString(),
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
const response = (await fetchJson(addDefaultPaths(signer.url, REMOTE_SIGN_TX_PATH), {
|
|
292
|
+
method: "POST",
|
|
293
|
+
headers,
|
|
294
|
+
body: JSON.stringify(payload),
|
|
295
|
+
}));
|
|
296
|
+
const txHash = parseTxHash(response.txHash);
|
|
297
|
+
if (txHash) {
|
|
298
|
+
return txHash;
|
|
299
|
+
}
|
|
300
|
+
const rawTransaction = parseRawTx(response.rawTransaction) || parseRawTx(response.signedTransaction);
|
|
301
|
+
if (rawTransaction) {
|
|
302
|
+
return publicClient.sendRawTransaction({
|
|
303
|
+
serializedTransaction: rawTransaction,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
throw new errors_1.CliError("REMOTE_SIGNER_PROTOCOL_ERROR", "Remote signer response missing txHash/rawTransaction.", 2);
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const bridgeEnvVar = signer.bridgeCommandEnvVar || "AGCLI_LEDGER_BRIDGE_CMD";
|
|
311
|
+
const bridgeCommand = process.env[bridgeEnvVar];
|
|
312
|
+
if (!bridgeCommand) {
|
|
313
|
+
throw new errors_1.CliError("SIGNER_BACKEND_UNAVAILABLE", `Set ${bridgeEnvVar} for ledger signer bridge command.`, 2);
|
|
314
|
+
}
|
|
315
|
+
const ledgerAddress = ensureAddress(signer.address || process.env.AGCLI_LEDGER_ADDRESS, "ledger signer address");
|
|
316
|
+
const summary = await resolveBalanceSummary("ledger", publicClient, ledgerAddress);
|
|
317
|
+
return {
|
|
318
|
+
summary,
|
|
319
|
+
sendTransaction: async (request) => {
|
|
320
|
+
const payload = {
|
|
321
|
+
chainId: request.chain.id,
|
|
322
|
+
derivationPath: signer.derivationPath,
|
|
323
|
+
from: ledgerAddress,
|
|
324
|
+
tx: {
|
|
325
|
+
to: request.to,
|
|
326
|
+
data: request.data,
|
|
327
|
+
valueWei: request.value?.toString() || "0",
|
|
328
|
+
gas: request.gas.toString(),
|
|
329
|
+
nonce: request.nonce,
|
|
330
|
+
maxFeePerGasWei: request.maxFeePerGas?.toString(),
|
|
331
|
+
maxPriorityFeePerGasWei: request.maxPriorityFeePerGas?.toString(),
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
const response = runLedgerBridge(bridgeCommand, payload);
|
|
335
|
+
const txHash = parseTxHash(response.txHash);
|
|
336
|
+
if (txHash) {
|
|
337
|
+
return txHash;
|
|
338
|
+
}
|
|
339
|
+
const rawTransaction = parseRawTx(response.rawTransaction) || parseRawTx(response.signedTransaction);
|
|
340
|
+
if (rawTransaction) {
|
|
341
|
+
return publicClient.sendRawTransaction({
|
|
342
|
+
serializedTransaction: rawTransaction,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
throw new errors_1.CliError("LEDGER_BRIDGE_FAILED", "Ledger bridge output missing txHash/rawTransaction.", 2);
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
}
|