run402 1.28.0 → 1.29.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/cli.mjs +6 -0
- package/lib/contracts.mjs +262 -0
- package/package.json +1 -1
package/cli.mjs
CHANGED
|
@@ -40,6 +40,7 @@ Commands:
|
|
|
40
40
|
auth Manage project user authentication (magic link, passwords, settings)
|
|
41
41
|
sender-domain Manage custom email sender domain (register, status, remove)
|
|
42
42
|
billing Email billing accounts, Stripe tier checkout, email packs
|
|
43
|
+
contracts KMS contract wallets ($0.04/day rental + $0.000005/sign)
|
|
43
44
|
agent Manage agent identity (contact info)
|
|
44
45
|
|
|
45
46
|
Run 'run402 <command> --help' for detailed usage of each command.
|
|
@@ -177,6 +178,11 @@ switch (cmd) {
|
|
|
177
178
|
await run(sub, rest);
|
|
178
179
|
break;
|
|
179
180
|
}
|
|
181
|
+
case "contracts": {
|
|
182
|
+
const { run } = await import("./lib/contracts.mjs");
|
|
183
|
+
await run(sub, rest);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
180
186
|
default:
|
|
181
187
|
console.error(`Unknown command: ${cmd}\n`);
|
|
182
188
|
console.log(HELP);
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { findProject, API } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
const HELP = `run402 contracts — KMS-backed Ethereum wallets for smart-contract calls
|
|
4
|
+
|
|
5
|
+
Pricing: $0.04/day per wallet ($1.20/month) plus $0.000005 per contract call.
|
|
6
|
+
Wallet creation requires $1.20 in cash credit (30 days of rent).
|
|
7
|
+
Non-custodial: see https://run402.com/humans/terms.html#non-custodial-kms-wallets
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
run402 contracts <subcommand> [args...]
|
|
11
|
+
|
|
12
|
+
Subcommands:
|
|
13
|
+
provision-wallet <project_id> --chain <base-mainnet|base-sepolia> [--recovery 0x...]
|
|
14
|
+
Provision a KMS wallet ($0.04/day, requires $1.20 prepay).
|
|
15
|
+
get-wallet <project_id> <wallet_id>
|
|
16
|
+
Get wallet metadata + live native balance.
|
|
17
|
+
list-wallets <project_id>
|
|
18
|
+
List all KMS wallets for the project (includes deleted).
|
|
19
|
+
set-recovery <project_id> <wallet_id> [--address 0x... | --clear]
|
|
20
|
+
Set/clear the optional recovery address.
|
|
21
|
+
set-alert <project_id> <wallet_id> --threshold-wei <n>
|
|
22
|
+
Set the low-balance alert threshold (in wei).
|
|
23
|
+
call <project_id> <wallet_id> --to 0x... --abi <json> --fn <name> --args <json> [--value-wei <n>] [--idempotency-key <k>]
|
|
24
|
+
Submit a contract write call (chain gas + $0.000005 KMS sign fee).
|
|
25
|
+
read --chain <chain> --to 0x... --abi <json> --fn <name> --args <json>
|
|
26
|
+
Read-only contract call (free).
|
|
27
|
+
status <project_id> <call_id>
|
|
28
|
+
Get call status, gas used, gas cost USD-micros, receipt.
|
|
29
|
+
drain <project_id> <wallet_id> --to 0x... --confirm
|
|
30
|
+
Drain native balance to a destination address. Works on suspended wallets.
|
|
31
|
+
delete <project_id> <wallet_id> --confirm
|
|
32
|
+
Schedule the KMS key for deletion (refused if balance >= dust).
|
|
33
|
+
|
|
34
|
+
Examples:
|
|
35
|
+
run402 contracts provision-wallet proj_abc --chain base-mainnet
|
|
36
|
+
run402 contracts call proj_abc cwlt_xyz --to 0x1234... --abi '[{"type":"function","name":"ping","inputs":[],"outputs":[]}]' --fn ping --args '[]'
|
|
37
|
+
`;
|
|
38
|
+
|
|
39
|
+
function parseFlag(args, flag) {
|
|
40
|
+
for (let i = 0; i < args.length; i++) {
|
|
41
|
+
if (args[i] === flag && args[i + 1]) return args[i + 1];
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function hasFlag(args, flag) {
|
|
46
|
+
return args.includes(flag);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function provisionWallet(projectId, args) {
|
|
50
|
+
const p = findProject(projectId);
|
|
51
|
+
const chain = parseFlag(args, "--chain");
|
|
52
|
+
if (!chain) {
|
|
53
|
+
console.error(JSON.stringify({ status: "error", message: "Missing --chain (base-mainnet or base-sepolia)" }));
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const recovery = parseFlag(args, "--recovery");
|
|
57
|
+
// Soft default of one wallet — confirm if project already has one
|
|
58
|
+
try {
|
|
59
|
+
const listRes = await fetch(`${API}/contracts/v1/wallets`, {
|
|
60
|
+
headers: { Authorization: `Bearer ${p.service_key}` },
|
|
61
|
+
});
|
|
62
|
+
if (listRes.ok) {
|
|
63
|
+
const list = await listRes.json();
|
|
64
|
+
const active = (list.wallets || []).filter((w) => w.status === "active");
|
|
65
|
+
if (active.length >= 1 && !hasFlag(args, "--yes")) {
|
|
66
|
+
console.error(`This project already has ${active.length} active wallet(s). Adding another costs $0.04/day each ($1.20/month). Re-run with --yes to confirm.`);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch { /* best-effort */ }
|
|
71
|
+
const body = { chain };
|
|
72
|
+
if (recovery) body.recovery_address = recovery;
|
|
73
|
+
const res = await fetch(`${API}/contracts/v1/wallets`, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
});
|
|
78
|
+
const data = await res.json();
|
|
79
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
80
|
+
console.log(JSON.stringify(data, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function getWallet(projectId, walletId) {
|
|
84
|
+
const p = findProject(projectId);
|
|
85
|
+
const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}`, {
|
|
86
|
+
headers: { Authorization: `Bearer ${p.service_key}` },
|
|
87
|
+
});
|
|
88
|
+
const data = await res.json();
|
|
89
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
90
|
+
console.log(JSON.stringify(data, null, 2));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function listWallets(projectId) {
|
|
94
|
+
const p = findProject(projectId);
|
|
95
|
+
const res = await fetch(`${API}/contracts/v1/wallets`, {
|
|
96
|
+
headers: { Authorization: `Bearer ${p.service_key}` },
|
|
97
|
+
});
|
|
98
|
+
const data = await res.json();
|
|
99
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
100
|
+
console.log(JSON.stringify(data, null, 2));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function setRecovery(projectId, walletId, args) {
|
|
104
|
+
const p = findProject(projectId);
|
|
105
|
+
const clear = hasFlag(args, "--clear");
|
|
106
|
+
const address = parseFlag(args, "--address");
|
|
107
|
+
if (!clear && !address) {
|
|
108
|
+
console.error(JSON.stringify({ status: "error", message: "Provide --address 0x... or --clear" }));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
const body = { recovery_address: clear ? null : address };
|
|
112
|
+
const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}/recovery-address`, {
|
|
113
|
+
method: "POST",
|
|
114
|
+
headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
115
|
+
body: JSON.stringify(body),
|
|
116
|
+
});
|
|
117
|
+
const data = await res.json();
|
|
118
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
119
|
+
console.log(JSON.stringify(data, null, 2));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function setAlert(projectId, walletId, args) {
|
|
123
|
+
const p = findProject(projectId);
|
|
124
|
+
const threshold = parseFlag(args, "--threshold-wei");
|
|
125
|
+
if (!threshold) {
|
|
126
|
+
console.error(JSON.stringify({ status: "error", message: "Missing --threshold-wei <n>" }));
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}/alert`, {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ threshold_wei: threshold }),
|
|
133
|
+
});
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
136
|
+
console.log(JSON.stringify(data, null, 2));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function call(projectId, walletId, args) {
|
|
140
|
+
const p = findProject(projectId);
|
|
141
|
+
const to = parseFlag(args, "--to");
|
|
142
|
+
const abi = parseFlag(args, "--abi");
|
|
143
|
+
const fn = parseFlag(args, "--fn");
|
|
144
|
+
const argsJson = parseFlag(args, "--args");
|
|
145
|
+
const value = parseFlag(args, "--value-wei");
|
|
146
|
+
const chain = parseFlag(args, "--chain") || "base-mainnet";
|
|
147
|
+
const idempotency = parseFlag(args, "--idempotency-key");
|
|
148
|
+
if (!to || !abi || !fn || !argsJson) {
|
|
149
|
+
console.error(JSON.stringify({ status: "error", message: "Required flags: --to, --abi, --fn, --args. Cost: chain gas + $0.000005 KMS sign fee." }));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
const body = {
|
|
153
|
+
wallet_id: walletId,
|
|
154
|
+
chain,
|
|
155
|
+
contract_address: to,
|
|
156
|
+
abi_fragment: JSON.parse(abi),
|
|
157
|
+
function_name: fn,
|
|
158
|
+
args: JSON.parse(argsJson),
|
|
159
|
+
};
|
|
160
|
+
if (value) body.value = value;
|
|
161
|
+
const headers = { Authorization: `Bearer ${p.service_key}`, "Content-Type": "application/json" };
|
|
162
|
+
if (idempotency) headers["Idempotency-Key"] = idempotency;
|
|
163
|
+
const res = await fetch(`${API}/contracts/v1/call`, { method: "POST", headers, body: JSON.stringify(body) });
|
|
164
|
+
const data = await res.json();
|
|
165
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
166
|
+
console.log(JSON.stringify(data, null, 2));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function read(args) {
|
|
170
|
+
const chain = parseFlag(args, "--chain");
|
|
171
|
+
const to = parseFlag(args, "--to");
|
|
172
|
+
const abi = parseFlag(args, "--abi");
|
|
173
|
+
const fn = parseFlag(args, "--fn");
|
|
174
|
+
const argsJson = parseFlag(args, "--args");
|
|
175
|
+
if (!chain || !to || !abi || !fn || !argsJson) {
|
|
176
|
+
console.error(JSON.stringify({ status: "error", message: "Required flags: --chain, --to, --abi, --fn, --args" }));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
const res = await fetch(`${API}/contracts/v1/read`, {
|
|
180
|
+
method: "POST",
|
|
181
|
+
headers: { "Content-Type": "application/json" },
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
chain,
|
|
184
|
+
contract_address: to,
|
|
185
|
+
abi_fragment: JSON.parse(abi),
|
|
186
|
+
function_name: fn,
|
|
187
|
+
args: JSON.parse(argsJson),
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
190
|
+
const data = await res.json();
|
|
191
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
192
|
+
console.log(JSON.stringify(data, null, 2));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function status(projectId, callId) {
|
|
196
|
+
const p = findProject(projectId);
|
|
197
|
+
const res = await fetch(`${API}/contracts/v1/calls/${encodeURIComponent(callId)}`, {
|
|
198
|
+
headers: { Authorization: `Bearer ${p.service_key}` },
|
|
199
|
+
});
|
|
200
|
+
const data = await res.json();
|
|
201
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
202
|
+
console.log(JSON.stringify(data, null, 2));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function drain(projectId, walletId, args) {
|
|
206
|
+
const p = findProject(projectId);
|
|
207
|
+
const to = parseFlag(args, "--to");
|
|
208
|
+
if (!to || !hasFlag(args, "--confirm")) {
|
|
209
|
+
console.error(JSON.stringify({ status: "error", message: "Required: --to 0x... and --confirm. Cost: chain gas + $0.000005 KMS sign fee." }));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}/drain`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers: {
|
|
215
|
+
Authorization: `Bearer ${p.service_key}`,
|
|
216
|
+
"Content-Type": "application/json",
|
|
217
|
+
"X-Confirm-Drain": walletId,
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({ destination_address: to }),
|
|
220
|
+
});
|
|
221
|
+
const data = await res.json();
|
|
222
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
223
|
+
console.log(JSON.stringify(data, null, 2));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function deleteWallet(projectId, walletId, args) {
|
|
227
|
+
const p = findProject(projectId);
|
|
228
|
+
if (!hasFlag(args, "--confirm")) {
|
|
229
|
+
console.error(JSON.stringify({ status: "error", message: "Required: --confirm" }));
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
const res = await fetch(`${API}/contracts/v1/wallets/${encodeURIComponent(walletId)}`, {
|
|
233
|
+
method: "DELETE",
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${p.service_key}`,
|
|
236
|
+
"X-Confirm-Delete": walletId,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
const data = await res.json().catch(() => ({}));
|
|
240
|
+
if (!res.ok) { console.error(JSON.stringify({ status: "error", http: res.status, ...data })); process.exit(1); }
|
|
241
|
+
console.log(JSON.stringify(data, null, 2));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function run(sub, args) {
|
|
245
|
+
if (!sub || sub === "--help" || sub === "-h") { console.log(HELP); process.exit(0); }
|
|
246
|
+
switch (sub) {
|
|
247
|
+
case "provision-wallet": await provisionWallet(args[0], args.slice(1)); break;
|
|
248
|
+
case "get-wallet": await getWallet(args[0], args[1]); break;
|
|
249
|
+
case "list-wallets": await listWallets(args[0]); break;
|
|
250
|
+
case "set-recovery": await setRecovery(args[0], args[1], args.slice(2)); break;
|
|
251
|
+
case "set-alert": await setAlert(args[0], args[1], args.slice(2)); break;
|
|
252
|
+
case "call": await call(args[0], args[1], args.slice(2)); break;
|
|
253
|
+
case "read": await read(args); break;
|
|
254
|
+
case "status": await status(args[0], args[1]); break;
|
|
255
|
+
case "drain": await drain(args[0], args[1], args.slice(2)); break;
|
|
256
|
+
case "delete": await deleteWallet(args[0], args[1], args.slice(2)); break;
|
|
257
|
+
default:
|
|
258
|
+
console.error(`Unknown subcommand: ${sub}\n`);
|
|
259
|
+
console.log(HELP);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
package/package.json
CHANGED