run402 4.119.0 → 4.121.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/core-dist/wallet-auth.js +77 -38
- package/core-dist/wallet.js +21 -0
- package/lib/ci.mjs +1 -1
- package/lib/command-manifest.mjs +3 -0
- package/lib/config.mjs +8 -0
- package/lib/contacts.mjs +3 -3
- package/lib/contracts.mjs +126 -15
- package/lib/orgs.mjs +18 -8
- package/lib/session-proofs.mjs +8 -4
- package/lib/subscriptions.mjs +2 -2
- package/lib/wallets.mjs +41 -1
- package/lib/webhook-secret.mjs +1 -1
- package/package.json +2 -1
- package/sdk/core-dist/wallet-auth.js +77 -38
- package/sdk/core-dist/wallet.js +21 -0
- package/sdk/dist/credentials.d.ts +15 -1
- package/sdk/dist/credentials.d.ts.map +1 -1
- package/sdk/dist/errors.d.ts +14 -0
- package/sdk/dist/errors.d.ts.map +1 -1
- package/sdk/dist/errors.js +22 -0
- package/sdk/dist/errors.js.map +1 -1
- package/sdk/dist/index.d.ts +1 -1
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -1
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/contracts.d.ts +47 -0
- package/sdk/dist/namespaces/contracts.d.ts.map +1 -1
- package/sdk/dist/namespaces/contracts.js +36 -0
- package/sdk/dist/namespaces/contracts.js.map +1 -1
- package/sdk/dist/namespaces/org.d.ts.map +1 -1
- package/sdk/dist/namespaces/org.js +5 -3
- package/sdk/dist/namespaces/org.js.map +1 -1
- package/sdk/dist/namespaces/org.types.d.ts +5 -6
- package/sdk/dist/namespaces/org.types.d.ts.map +1 -1
- package/sdk/dist/node/_paid-stack.d.ts +18 -0
- package/sdk/dist/node/_paid-stack.d.ts.map +1 -1
- package/sdk/dist/node/_paid-stack.js +19 -0
- package/sdk/dist/node/_paid-stack.js.map +1 -1
- package/sdk/dist/node/ci.d.ts +1 -1
- package/sdk/dist/node/ci.d.ts.map +1 -1
- package/sdk/dist/node/ci.js +8 -6
- package/sdk/dist/node/ci.js.map +1 -1
- package/sdk/dist/node/credentials.d.ts.map +1 -1
- package/sdk/dist/node/credentials.js +5 -4
- package/sdk/dist/node/credentials.js.map +1 -1
- package/sdk/dist/node/index.d.ts +2 -1
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +2 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/node/org-adopt.d.ts +1 -1
- package/sdk/dist/node/org-adopt.d.ts.map +1 -1
- package/sdk/dist/node/org-adopt.js +9 -7
- package/sdk/dist/node/org-adopt.js.map +1 -1
- package/sdk/dist/node/paid-fetch.d.ts.map +1 -1
- package/sdk/dist/node/paid-fetch.js +6 -4
- package/sdk/dist/node/paid-fetch.js.map +1 -1
- package/sdk/dist/node/signers/index.d.ts +26 -0
- package/sdk/dist/node/signers/index.d.ts.map +1 -0
- package/sdk/dist/node/signers/index.js +35 -0
- package/sdk/dist/node/signers/index.js.map +1 -0
- package/sdk/dist/node/signers/kms.d.ts +54 -0
- package/sdk/dist/node/signers/kms.d.ts.map +1 -0
- package/sdk/dist/node/signers/kms.js +236 -0
- package/sdk/dist/node/signers/kms.js.map +1 -0
- package/sdk/dist/node/wallets-send.d.ts +62 -0
- package/sdk/dist/node/wallets-send.d.ts.map +1 -0
- package/sdk/dist/node/wallets-send.js +149 -0
- package/sdk/dist/node/wallets-send.js.map +1 -0
- package/sdk/dist/node/wallets.d.ts +33 -5
- package/sdk/dist/node/wallets.d.ts.map +1 -1
- package/sdk/dist/node/wallets.js +78 -12
- package/sdk/dist/node/wallets.js.map +1 -1
- package/sdk/dist/scoped.d.ts +4 -1
- package/sdk/dist/scoped.d.ts.map +1 -1
- package/sdk/dist/scoped.js +9 -0
- package/sdk/dist/scoped.js.map +1 -1
- package/sdk/dist/secret-gate.d.ts +1 -1
- package/sdk/dist/secret-gate.d.ts.map +1 -1
- package/sdk/dist/secret-gate.js +7 -2
- package/sdk/dist/secret-gate.js.map +1 -1
- package/vault-surface.json +1 -1
package/core-dist/wallet-auth.js
CHANGED
|
@@ -6,7 +6,7 @@ import { randomBytes } from "node:crypto";
|
|
|
6
6
|
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
7
7
|
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
8
8
|
import { bytesToHex } from "@noble/hashes/utils.js";
|
|
9
|
-
import { readWallet } from "./wallet.js";
|
|
9
|
+
import { isSignerWallet, readWallet } from "./wallet.js";
|
|
10
10
|
import { getApiBase } from "./config.js";
|
|
11
11
|
/**
|
|
12
12
|
* EIP-55 mixed-case checksum encoding.
|
|
@@ -57,6 +57,33 @@ function personalSign(privateKeyHex, address, message) {
|
|
|
57
57
|
const vHex = (recovery + 27).toString(16).padStart(2, "0");
|
|
58
58
|
return "0x" + r + s + vHex;
|
|
59
59
|
}
|
|
60
|
+
export function prepareSIWx(opts) {
|
|
61
|
+
const message = formatSIWEMessage(opts, opts.address);
|
|
62
|
+
return {
|
|
63
|
+
message,
|
|
64
|
+
finish(signature) {
|
|
65
|
+
const payload = {
|
|
66
|
+
domain: opts.domain,
|
|
67
|
+
address: toChecksumAddress(opts.address),
|
|
68
|
+
statement: opts.statement,
|
|
69
|
+
uri: opts.uri,
|
|
70
|
+
version: opts.version ?? "1",
|
|
71
|
+
chainId: payloadChainId(opts.chainId),
|
|
72
|
+
type: opts.type ?? "eip191",
|
|
73
|
+
nonce: opts.nonce,
|
|
74
|
+
issuedAt: opts.issuedAt,
|
|
75
|
+
expirationTime: opts.expirationTime,
|
|
76
|
+
signature,
|
|
77
|
+
};
|
|
78
|
+
if (opts.resources !== undefined) {
|
|
79
|
+
payload.resources = opts.resources;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
"SIGN-IN-WITH-X": Buffer.from(JSON.stringify(payload)).toString("base64"),
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
60
87
|
/**
|
|
61
88
|
* Format an EIP-4361 (SIWE) message. Must be byte-for-byte compatible
|
|
62
89
|
* with the `siwe` library's message format used server-side for verification.
|
|
@@ -85,29 +112,54 @@ export function formatSIWEMessage(opts, address) {
|
|
|
85
112
|
}
|
|
86
113
|
return lines.join("\n");
|
|
87
114
|
}
|
|
115
|
+
/** SIWX headers signed with a local private key (synchronous). */
|
|
88
116
|
export function buildSIWxAuthHeaders(opts) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const payload = {
|
|
92
|
-
domain: opts.domain,
|
|
93
|
-
address: toChecksumAddress(opts.wallet.address),
|
|
94
|
-
statement: opts.statement,
|
|
95
|
-
uri: opts.uri,
|
|
96
|
-
version: opts.version ?? "1",
|
|
97
|
-
chainId: payloadChainId(opts.chainId),
|
|
98
|
-
type: opts.type ?? "eip191",
|
|
99
|
-
nonce: opts.nonce,
|
|
100
|
-
issuedAt: opts.issuedAt,
|
|
101
|
-
expirationTime: opts.expirationTime,
|
|
102
|
-
signature,
|
|
103
|
-
};
|
|
104
|
-
if (opts.resources !== undefined) {
|
|
105
|
-
payload.resources = opts.resources;
|
|
117
|
+
if (!opts.wallet.privateKey) {
|
|
118
|
+
throw new SignerWalletError();
|
|
106
119
|
}
|
|
120
|
+
const prepared = prepareSIWx({ ...opts, address: opts.wallet.address });
|
|
121
|
+
return prepared.finish(personalSign(opts.wallet.privateKey, opts.wallet.address, prepared.message));
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Thrown where a synchronous signature is required but the wallet signs
|
|
125
|
+
* through a signer (`wallet.json` `signer`). Callers that can wait use
|
|
126
|
+
* `getWalletAuthHeadersAsync` with the SDK's signer instead.
|
|
127
|
+
*/
|
|
128
|
+
export class SignerWalletError extends Error {
|
|
129
|
+
code = "SIGNER_WALLET_ASYNC";
|
|
130
|
+
constructor() {
|
|
131
|
+
super("This wallet signs through a signer (wallet.json 'signer'), which this synchronous path cannot use.");
|
|
132
|
+
this.name = "SignerWalletError";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function walletSIWxOptions(path) {
|
|
136
|
+
const apiBase = getApiBase();
|
|
137
|
+
const url = new URL(apiBase);
|
|
138
|
+
const now = new Date();
|
|
107
139
|
return {
|
|
108
|
-
|
|
140
|
+
domain: url.hostname,
|
|
141
|
+
uri: `${apiBase}${path}`,
|
|
142
|
+
statement: "Sign in to Run402",
|
|
143
|
+
chainId: "eip155:84532",
|
|
144
|
+
nonce: randomBytes(16).toString("hex"),
|
|
145
|
+
issuedAt: now.toISOString(),
|
|
146
|
+
expirationTime: new Date(now.getTime() + 5 * 60 * 1000).toISOString(),
|
|
109
147
|
};
|
|
110
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* SIWX headers for any wallet: a key-backed wallet signs locally; a
|
|
151
|
+
* signer-backed one through `signerFor(wallet)`. Null when no wallet exists.
|
|
152
|
+
*/
|
|
153
|
+
export async function getWalletAuthHeadersAsync(path, walletPath, signerFor) {
|
|
154
|
+
const localWallet = readWallet(walletPath);
|
|
155
|
+
if (!localWallet || !localWallet.address)
|
|
156
|
+
return null;
|
|
157
|
+
if (!isSignerWallet(localWallet))
|
|
158
|
+
return getWalletAuthHeaders(path, walletPath);
|
|
159
|
+
const prepared = prepareSIWx({ ...walletSIWxOptions(path), address: localWallet.address });
|
|
160
|
+
const sign = await signerFor(localWallet);
|
|
161
|
+
return prepared.finish(await sign(prepared.message));
|
|
162
|
+
}
|
|
111
163
|
/**
|
|
112
164
|
* Get SIWX auth headers for the Run402 API.
|
|
113
165
|
* Returns null if no local wallet is configured.
|
|
@@ -121,26 +173,13 @@ export function getWalletAuthHeaders(path, walletPath) {
|
|
|
121
173
|
// this helper returns SIWxAuthHeaders | null. Re-throw so callers above
|
|
122
174
|
// the CLI's wrapper (e.g. SDK paid-fetch) can decide whether to swallow it.
|
|
123
175
|
const localWallet = readWallet(walletPath);
|
|
124
|
-
if (!localWallet || !localWallet.address
|
|
176
|
+
if (!localWallet || !localWallet.address)
|
|
125
177
|
return null;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const now = new Date();
|
|
132
|
-
const issuedAt = now.toISOString();
|
|
133
|
-
const expirationTime = new Date(now.getTime() + 5 * 60 * 1000).toISOString();
|
|
134
|
-
return buildSIWxAuthHeaders({
|
|
135
|
-
wallet: localWallet,
|
|
136
|
-
domain,
|
|
137
|
-
uri,
|
|
138
|
-
statement: "Sign in to Run402",
|
|
139
|
-
chainId: "eip155:84532",
|
|
140
|
-
nonce,
|
|
141
|
-
issuedAt,
|
|
142
|
-
expirationTime,
|
|
143
|
-
});
|
|
178
|
+
if (isSignerWallet(localWallet))
|
|
179
|
+
throw new SignerWalletError();
|
|
180
|
+
if (!localWallet.privateKey)
|
|
181
|
+
return null;
|
|
182
|
+
return buildSIWxAuthHeaders({ wallet: localWallet, ...walletSIWxOptions(path) });
|
|
144
183
|
}
|
|
145
184
|
function messageChainId(chainId) {
|
|
146
185
|
if (typeof chainId === "number")
|
package/core-dist/wallet.js
CHANGED
|
@@ -6,6 +6,12 @@ import { getWalletPath } from "./config.js";
|
|
|
6
6
|
const ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
|
|
7
7
|
// 0x-prefixed 64-hex secp256k1 private key (32 bytes).
|
|
8
8
|
const PRIVATE_KEY_RE = /^0x[a-fA-F0-9]{64}$/;
|
|
9
|
+
// arn:aws:kms:<region>:<account>:key/<id>
|
|
10
|
+
export const KMS_KEY_ARN_RE = /^arn:aws[a-z-]*:kms:([a-z0-9-]+):\d{12}:key\/[A-Za-z0-9-]+$/;
|
|
11
|
+
/** True when the wallet signs through a signer instead of a local key. */
|
|
12
|
+
export function isSignerWallet(wallet) {
|
|
13
|
+
return wallet.signer !== undefined;
|
|
14
|
+
}
|
|
9
15
|
/**
|
|
10
16
|
* Load the agent wallet from disk.
|
|
11
17
|
*
|
|
@@ -78,6 +84,21 @@ export function readWallet(path) {
|
|
|
78
84
|
throw new Error("wallet.json missing valid 'address' (expected 0x-prefixed 40-hex string). " +
|
|
79
85
|
"Back up the file and run 'run402 init' to recreate it.");
|
|
80
86
|
}
|
|
87
|
+
if (data.privateKey !== undefined && data.signer !== undefined) {
|
|
88
|
+
throw new Error(`${p} has both 'privateKey' and 'signer'; a wallet is backed by exactly one. ` +
|
|
89
|
+
"Remove the one that does not belong to this wallet.");
|
|
90
|
+
}
|
|
91
|
+
if (data.signer !== undefined) {
|
|
92
|
+
const signer = data.signer;
|
|
93
|
+
if (!signer || typeof signer !== "object" || signer.kind !== "aws_kms" ||
|
|
94
|
+
typeof signer.key_arn !== "string" || !KMS_KEY_ARN_RE.test(signer.key_arn) ||
|
|
95
|
+
typeof signer.region !== "string" || signer.region.length === 0 ||
|
|
96
|
+
(signer.aws_profile !== undefined && (typeof signer.aws_profile !== "string" || signer.aws_profile.length === 0))) {
|
|
97
|
+
throw new Error(`${p} has a malformed 'signer' (expected { kind: "aws_kms", key_arn, region, aws_profile? }). ` +
|
|
98
|
+
"Re-import the wallet with 'run402 wallets import <name> --kms <key-arn>'.");
|
|
99
|
+
}
|
|
100
|
+
return data;
|
|
101
|
+
}
|
|
81
102
|
if (typeof data.privateKey !== "string" || !PRIVATE_KEY_RE.test(data.privateKey)) {
|
|
82
103
|
throw new Error("wallet.json missing valid 'privateKey' (expected 0x-prefixed 64-hex string). " +
|
|
83
104
|
"Back up the file and run 'run402 init' to recreate it.");
|
package/lib/ci.mjs
CHANGED
|
@@ -325,7 +325,7 @@ async function linkGithub(args) {
|
|
|
325
325
|
|
|
326
326
|
let signedDelegation;
|
|
327
327
|
try {
|
|
328
|
-
signedDelegation = signCiDelegation(values, { apiBase: API });
|
|
328
|
+
signedDelegation = await signCiDelegation(values, { apiBase: API });
|
|
329
329
|
} catch (err) {
|
|
330
330
|
fail({
|
|
331
331
|
code: "NO_WALLET",
|
package/lib/command-manifest.mjs
CHANGED
|
@@ -466,6 +466,9 @@ export const COMMAND_MANIFEST = [
|
|
|
466
466
|
{ path: ["contracts", "set-alert"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1", "--threshold-wei", "1"] },
|
|
467
467
|
{ path: ["contracts", "call"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1", "--to", "0x4444444444444444444444444444444444444444", "--abi", "[]", "--fn", "noop", "--args", "[]"] },
|
|
468
468
|
{ path: ["contracts", "deploy"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1", "--bytecode", "0x00"] },
|
|
469
|
+
{ path: ["contracts", "transfers", "create"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1", "--to", "0x4444444444444444444444444444444444444444", "--amount", "1"] },
|
|
470
|
+
{ path: ["contracts", "transfers", "get"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1", "--transfer", "txf_gate1"] },
|
|
471
|
+
{ path: ["contracts", "transfers", "list"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1"] },
|
|
469
472
|
{ path: ["contracts", "read"], positionals: [], projectScoped: false, legacyPositionalProject: false, minimalArgs: ["--chain", "base-sepolia", "--to", "0x4444444444444444444444444444444444444444", "--abi", "[]", "--fn", "noop", "--args", "[]"] },
|
|
470
473
|
{ path: ["contracts", "status"], positionals: [p("call_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["ccall_gate1"] },
|
|
471
474
|
{ path: ["contracts", "drain"], positionals: [p("signer_id")], projectScoped: true, legacyPositionalProject: true, minimalArgs: ["cwlt_gate1", "--to", "0x4444444444444444444444444444444444444444", "--confirm"] },
|
package/lib/config.mjs
CHANGED
|
@@ -81,7 +81,15 @@ export function saveWallet(data) {
|
|
|
81
81
|
coreSaveWallet(data);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* The wallet's SIWX headers for `path`, or the NO_WALLET failure. Most
|
|
86
|
+
* callers use this only as a "a wallet is configured" check before handing
|
|
87
|
+
* the request to the SDK. A signer-backed wallet (KMS) signs asynchronously
|
|
88
|
+
* inside the SDK, so here it only proves the wallet exists and returns no
|
|
89
|
+
* headers; callers that need its signature use the SDK.
|
|
90
|
+
*/
|
|
84
91
|
export function walletAuthHeaders(path) {
|
|
92
|
+
if (readWallet()?.signer) return {};
|
|
85
93
|
const headers = coreGetWalletAuthHeaders(path);
|
|
86
94
|
if (!headers) {
|
|
87
95
|
fail({
|
package/lib/contacts.mjs
CHANGED
|
@@ -104,7 +104,7 @@ async function preferences(args) {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
try {
|
|
107
|
-
const data = await getSdk().admin.setNotificationPreferences(patch, sessionProofs("/agent/v1/notifications/preferences"));
|
|
107
|
+
const data = await getSdk().admin.setNotificationPreferences(patch, await sessionProofs("/agent/v1/notifications/preferences"));
|
|
108
108
|
console.log(JSON.stringify(data, null, 2));
|
|
109
109
|
} catch (err) {
|
|
110
110
|
reportSdkError(err);
|
|
@@ -185,7 +185,7 @@ async function channelsConnect(args) {
|
|
|
185
185
|
fail({ code: "BAD_USAGE", message: `Unexpected argument for notifications channels connect: ${positionals[1]}` });
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
const proofs = sessionProofs("/agent/v1/notifications/channels/telegram");
|
|
188
|
+
const proofs = await sessionProofs("/agent/v1/notifications/channels/telegram");
|
|
189
189
|
let pending;
|
|
190
190
|
try {
|
|
191
191
|
pending = await getSdk().admin.channels.connectTelegram(label ? { label } : {}, proofs);
|
|
@@ -234,7 +234,7 @@ async function channelsRevoke(args) {
|
|
|
234
234
|
});
|
|
235
235
|
walletAuthHeaders("/agent/v1/notifications/channels/telegram");
|
|
236
236
|
try {
|
|
237
|
-
console.log(JSON.stringify(await getSdk().admin.channels.revokeTelegram(bindingId, sessionProofs("/agent/v1/notifications/channels/telegram")), null, 2));
|
|
237
|
+
console.log(JSON.stringify(await getSdk().admin.channels.revokeTelegram(bindingId, await sessionProofs("/agent/v1/notifications/channels/telegram")), null, 2));
|
|
238
238
|
} catch (err) {
|
|
239
239
|
reportSdkError(err);
|
|
240
240
|
}
|
package/lib/contracts.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
failUnknownSubcommand,
|
|
12
12
|
} from "./argparse.mjs";
|
|
13
13
|
|
|
14
|
-
const HELP = `run402 contracts —
|
|
14
|
+
const HELP = `run402 contracts — signers, your app's wallet, for smart-contract calls
|
|
15
15
|
|
|
16
16
|
Pricing: $0.04/day per signer ($1.20/month) plus $0.000005 per contract call.
|
|
17
17
|
Signer creation requires $1.20 of allowance (30 days of rent).
|
|
@@ -22,19 +22,25 @@ Usage:
|
|
|
22
22
|
|
|
23
23
|
Subcommands:
|
|
24
24
|
provision-signer <project_id> --chain <base-mainnet|base-sepolia> [--recovery 0x...]
|
|
25
|
-
Provision a
|
|
25
|
+
Provision a signer ($0.04/day wallet rent, requires $1.20 prepay).
|
|
26
26
|
get-signer <project_id> <signer_id>
|
|
27
27
|
Get signer metadata + live native balance.
|
|
28
28
|
list-signers <project_id>
|
|
29
|
-
List all
|
|
29
|
+
List all signers for the project (includes deleted).
|
|
30
|
+
transfers create <signer_id> --to 0x... --amount <base units> [--asset usdc] [--chain <base-mainnet|base-sepolia>] [--idempotency-key <k>]
|
|
31
|
+
Send a token from the signer with no gas on it: the platform's facilitator settles the signed EIP-3009 authorization and pays the gas. One signing fee.
|
|
32
|
+
transfers get <signer_id> --transfer <transfer_id>
|
|
33
|
+
Read one transfer (status, tx hash, nonce, failure phase).
|
|
34
|
+
transfers list <signer_id>
|
|
35
|
+
The signer's transfers, newest first.
|
|
30
36
|
set-recovery <project_id> <signer_id> [--address 0x... | --clear]
|
|
31
37
|
Set/clear the optional recovery address.
|
|
32
38
|
set-alert <project_id> <signer_id> --threshold-wei <n>
|
|
33
39
|
Set the low-balance alert threshold (in wei).
|
|
34
40
|
call <project_id> <signer_id> --to 0x... --abi <json> --fn <name> --args <json> [--value-wei <n>] [--idempotency-key <k>]
|
|
35
|
-
Submit a contract write call (
|
|
41
|
+
Submit a contract write call (gas from the signer's balance, topped up at cost when short, + $0.000005 signing fee).
|
|
36
42
|
deploy <project_id> <signer_id> --bytecode 0x... [--chain <base-mainnet|base-sepolia>] [--value-wei <n>] [--idempotency-key <k>]
|
|
37
|
-
Deploy a contract (
|
|
43
|
+
Deploy a contract (gas from the signer's balance + $0.000005 signing fee). Returns deterministic CREATE address synchronously.
|
|
38
44
|
read --chain <chain> --to 0x... --abi <json> --fn <name> --args <json>
|
|
39
45
|
Read-only contract call (free).
|
|
40
46
|
status <project_id> <call_id>
|
|
@@ -42,7 +48,7 @@ Subcommands:
|
|
|
42
48
|
drain <project_id> <signer_id> --to 0x... --confirm
|
|
43
49
|
Drain native balance to a destination address. Works on suspended signers.
|
|
44
50
|
delete <project_id> <signer_id> --confirm
|
|
45
|
-
Schedule the
|
|
51
|
+
Schedule the signing key for deletion (refused if balance >= dust).
|
|
46
52
|
|
|
47
53
|
Examples:
|
|
48
54
|
run402 contracts provision-signer proj_abc --chain base-mainnet
|
|
@@ -50,7 +56,22 @@ Examples:
|
|
|
50
56
|
`;
|
|
51
57
|
|
|
52
58
|
const SUB_HELP = {
|
|
53
|
-
|
|
59
|
+
transfers: `run402 contracts transfers — Send a token from a signer with no gas on it
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
run402 contracts transfers create <signer_id> --to 0x... --amount <base units> [--asset usdc] [--chain <base-mainnet|base-sepolia>] [--idempotency-key <k>] [--project <project_id>]
|
|
63
|
+
run402 contracts transfers get <signer_id> --transfer <transfer_id> [--project <project_id>]
|
|
64
|
+
run402 contracts transfers list <signer_id> [--project <project_id>]
|
|
65
|
+
|
|
66
|
+
The signer is the payer of an x402 payment: Run402 signs the token's EIP-3009
|
|
67
|
+
authorization with the signer's key and its facilitator settles it and pays the
|
|
68
|
+
gas. One $0.000005 signing fee, no gas, no ETH on the signer. --amount is in the
|
|
69
|
+
asset's base units (USDC: 6 decimals; 100000 is $0.10). An idempotency key
|
|
70
|
+
makes a repeat return the transfer already made; a transfer that failed before
|
|
71
|
+
settlement re-executes under its key, one that died in settlement never does.
|
|
72
|
+
A "unknown" status means the settle outcome never came back: poll it with get.
|
|
73
|
+
`,
|
|
74
|
+
"provision-signer": `run402 contracts provision-signer — Provision a signer
|
|
54
75
|
|
|
55
76
|
Usage:
|
|
56
77
|
run402 contracts provision-signer --chain <chain> [--project <project_id>] [options]
|
|
@@ -91,11 +112,11 @@ Legacy (still supported):
|
|
|
91
112
|
run402 contracts call <project_id> <signer_id> --to 0x... --abi <json>
|
|
92
113
|
--fn <name> --args <json> [options]
|
|
93
114
|
`,
|
|
94
|
-
deploy: `run402 contracts deploy — Deploy a smart contract from a
|
|
115
|
+
deploy: `run402 contracts deploy — Deploy a smart contract from a signer
|
|
95
116
|
|
|
96
|
-
|
|
117
|
+
the platform's key custody-signs a contract-creation transaction (to: null + data: bytecode) and broadcasts.
|
|
97
118
|
Returns the deterministic CREATE address synchronously — known from (signer, nonce)
|
|
98
|
-
before the tx confirms. Cost: chain gas at-cost + $0.000005
|
|
119
|
+
before the tx confirms. Cost: chain gas at-cost + $0.000005 signing fee.
|
|
99
120
|
|
|
100
121
|
Usage:
|
|
101
122
|
run402 contracts deploy <signer_id> --bytecode 0x... [--project <project_id>] [options]
|
|
@@ -130,7 +151,7 @@ Usage:
|
|
|
130
151
|
Legacy (still supported):
|
|
131
152
|
run402 contracts drain <project_id> <signer_id> --to 0x... --confirm
|
|
132
153
|
`,
|
|
133
|
-
delete: `run402 contracts delete — Schedule the
|
|
154
|
+
delete: `run402 contracts delete — Schedule the signing key for deletion
|
|
134
155
|
|
|
135
156
|
Usage:
|
|
136
157
|
run402 contracts delete <signer_id> --confirm [--project <project_id>]
|
|
@@ -153,7 +174,7 @@ Arguments:
|
|
|
153
174
|
Examples:
|
|
154
175
|
run402 contracts get-signer prj_abc123 cwlt_abc123
|
|
155
176
|
`,
|
|
156
|
-
"list-signers": `run402 contracts list-signers — List all
|
|
177
|
+
"list-signers": `run402 contracts list-signers — List all signers for a project
|
|
157
178
|
|
|
158
179
|
Usage:
|
|
159
180
|
run402 contracts list-signers [--project <project_id>]
|
|
@@ -344,7 +365,7 @@ async function call(projectId, signerId, args) {
|
|
|
344
365
|
fail({
|
|
345
366
|
code: "BAD_USAGE",
|
|
346
367
|
message: "Required flags: --to, --abi, --fn, --args.",
|
|
347
|
-
hint: "Cost: chain gas + $0.000005
|
|
368
|
+
hint: "Cost: chain gas + $0.000005 signing fee.",
|
|
348
369
|
});
|
|
349
370
|
}
|
|
350
371
|
assertAllowedValue(chain, ["base-mainnet", "base-sepolia"], "--chain");
|
|
@@ -369,6 +390,89 @@ async function call(projectId, signerId, args) {
|
|
|
369
390
|
}
|
|
370
391
|
}
|
|
371
392
|
|
|
393
|
+
/**
|
|
394
|
+
* `run402 contracts transfers create|get|list` — a signer sends a token with
|
|
395
|
+
* no gas on it: the gateway signs the EIP-3009 authorization with the
|
|
396
|
+
* signer's key and the platform's facilitator settles it and pays the gas.
|
|
397
|
+
*/
|
|
398
|
+
async function transfers(projectId, args) {
|
|
399
|
+
const [transferAction, ...rest] = Array.isArray(args) ? args : [];
|
|
400
|
+
if (transferAction === "create") return transfersCreate(projectId, rest[0], rest.slice(1));
|
|
401
|
+
if (transferAction === "get") return transfersGet(projectId, rest[0], rest.slice(1));
|
|
402
|
+
if (transferAction === "list") return transfersList(projectId, rest[0], rest.slice(1));
|
|
403
|
+
return fail({
|
|
404
|
+
code: "BAD_USAGE",
|
|
405
|
+
message: `Unknown contracts transfers verb: ${transferAction ?? "(none)"}`,
|
|
406
|
+
hint: "Verbs: create <signer_id> --to 0x… --amount <base units> [--asset usdc] [--chain …] [--idempotency-key <k>]; get <signer_id> --transfer <transfer_id>; list <signer_id>.",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function transfersCreate(projectId, signerId, args) {
|
|
411
|
+
const parsedArgs = normalizeArgv(args);
|
|
412
|
+
const valueFlags = ["--to", "--amount", "--asset", "--chain", "--idempotency-key"];
|
|
413
|
+
assertKnownFlags(parsedArgs, [...valueFlags, "--help", "-h"], valueFlags);
|
|
414
|
+
const extra = positionalArgs(parsedArgs, valueFlags);
|
|
415
|
+
if (!signerId || !/^[a-z]+_[A-Za-z0-9]{8,}$/.test(signerId)) {
|
|
416
|
+
fail({ code: "BAD_USAGE", message: "Missing <signer_id>.", hint: "run402 contracts transfers create <signer_id> --to 0x… --amount <base units>" });
|
|
417
|
+
}
|
|
418
|
+
if (extra.length > 0) {
|
|
419
|
+
fail({ code: "BAD_USAGE", message: `Unexpected argument for contracts transfers create: ${extra[0]}` });
|
|
420
|
+
}
|
|
421
|
+
const to = flagValue(parsedArgs, "--to");
|
|
422
|
+
const amount = flagValue(parsedArgs, "--amount");
|
|
423
|
+
const asset = flagValue(parsedArgs, "--asset") || "usdc";
|
|
424
|
+
const chain = flagValue(parsedArgs, "--chain") || "base-mainnet";
|
|
425
|
+
const idempotency = flagValue(parsedArgs, "--idempotency-key");
|
|
426
|
+
if (!to || !amount) {
|
|
427
|
+
fail({
|
|
428
|
+
code: "BAD_USAGE",
|
|
429
|
+
message: "Required flags: --to, --amount.",
|
|
430
|
+
hint: "--amount is in the asset's base units as a decimal string (USDC: 6 decimals, so 100000 is $0.10). Cost: one $0.000005 signing fee, no gas.",
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
assertAllowedValue(chain, ["base-mainnet", "base-sepolia"], "--chain");
|
|
434
|
+
validateEvmAddress(to, "--to");
|
|
435
|
+
if (!/^[1-9][0-9]*$/.test(amount)) {
|
|
436
|
+
fail({ code: "BAD_USAGE", message: "--amount must be a positive integer in the asset's base units." });
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const data = await getSdk().contracts.transfer(projectId, {
|
|
440
|
+
signerId, chain, asset, to, amount, idempotencyKey: idempotency ?? undefined,
|
|
441
|
+
});
|
|
442
|
+
console.log(JSON.stringify(data, null, 2));
|
|
443
|
+
} catch (err) {
|
|
444
|
+
reportSdkError(err);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function transfersGet(projectId, signerId, args) {
|
|
449
|
+
const parsedArgs = normalizeArgv(args);
|
|
450
|
+
assertKnownFlags(parsedArgs, ["--transfer", "--help", "-h"], ["--transfer"]);
|
|
451
|
+
const extra = positionalArgs(parsedArgs, ["--transfer"]);
|
|
452
|
+
const transferId = flagValue(parsedArgs, "--transfer");
|
|
453
|
+
if (!signerId || !transferId || extra.length > 0) {
|
|
454
|
+
fail({ code: "BAD_USAGE", message: "Usage: run402 contracts transfers get <signer_id> --transfer <transfer_id>" });
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
console.log(JSON.stringify(await getSdk().contracts.transferStatus(projectId, signerId, transferId), null, 2));
|
|
458
|
+
} catch (err) {
|
|
459
|
+
reportSdkError(err);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function transfersList(projectId, signerId, args) {
|
|
464
|
+
const parsedArgs = normalizeArgv(args);
|
|
465
|
+
assertKnownFlags(parsedArgs, ["--help", "-h"], []);
|
|
466
|
+
if (!signerId) {
|
|
467
|
+
fail({ code: "BAD_USAGE", message: "Usage: run402 contracts transfers list <signer_id>" });
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
console.log(JSON.stringify(await getSdk().contracts.listTransfers(projectId, signerId), null, 2));
|
|
471
|
+
} catch (err) {
|
|
472
|
+
reportSdkError(err);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
372
476
|
async function deploy(projectId, signerId, args) {
|
|
373
477
|
const parsedArgs = normalizeArgv(args);
|
|
374
478
|
const valueFlags = ["--bytecode", "--value-wei", "--chain", "--idempotency-key"];
|
|
@@ -385,7 +489,7 @@ async function deploy(projectId, signerId, args) {
|
|
|
385
489
|
fail({
|
|
386
490
|
code: "BAD_USAGE",
|
|
387
491
|
message: "Required flag: --bytecode (0x-prefixed hex; full creation calldata = creation bytecode + ABI-encoded constructor args, concatenated client-side).",
|
|
388
|
-
hint: "Cost: chain gas + $0.000005
|
|
492
|
+
hint: "Cost: chain gas + $0.000005 signing fee. Returns deterministic CREATE address synchronously.",
|
|
389
493
|
});
|
|
390
494
|
}
|
|
391
495
|
assertAllowedValue(chain, ["base-mainnet", "base-sepolia"], "--chain");
|
|
@@ -469,7 +573,7 @@ async function drain(projectId, signerId, args) {
|
|
|
469
573
|
fail({
|
|
470
574
|
code: "BAD_USAGE",
|
|
471
575
|
message: "Required: --to 0x... and --confirm.",
|
|
472
|
-
hint: "Cost: chain gas + $0.000005
|
|
576
|
+
hint: "Cost: chain gas + $0.000005 signing fee.",
|
|
473
577
|
});
|
|
474
578
|
}
|
|
475
579
|
validateEvmAddress(to, "--to");
|
|
@@ -508,6 +612,13 @@ export async function run(sub, args) {
|
|
|
508
612
|
// unknown subcommands (and the anonymous `read`) never trip project
|
|
509
613
|
// resolution.
|
|
510
614
|
const select = () => resolveProjectSelector(normalizeArgv(Array.isArray(args) ? args : []));
|
|
615
|
+
// The transfers group dispatches with `if` (not `case`) so the sync test
|
|
616
|
+
// inventories its leaf actions through parseContractsTransferActions.
|
|
617
|
+
if (sub === "transfers") {
|
|
618
|
+
const { projectId, rest } = select();
|
|
619
|
+
await transfers(projectId, rest);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
511
622
|
switch (sub) {
|
|
512
623
|
case "provision-signer": { const { projectId, rest } = select(); await provisionSigner(projectId, rest); break; }
|
|
513
624
|
case "get-signer": { const { projectId, rest } = select(); await getSigner(projectId, rest[0], rest.slice(1)); break; }
|
package/lib/orgs.mjs
CHANGED
|
@@ -21,7 +21,7 @@ Usage:
|
|
|
21
21
|
run402 orgs list
|
|
22
22
|
run402 orgs get [<org_id>]
|
|
23
23
|
run402 orgs rename [<org_id>] --name <display_name> (or: --clear to remove the label)
|
|
24
|
-
run402 orgs payout-wallet [<org_id>] <wallet_address> (or: --clear
|
|
24
|
+
run402 orgs payout-wallet [<org_id>] <wallet_address> (or: --signer <signer_id>; --clear removes the explicit default)
|
|
25
25
|
run402 orgs lightning-receiver set [<org_id>] <nwc-uri | -> (receive-only NWC pairing; - reads it from stdin)
|
|
26
26
|
run402 orgs lightning-receiver status [<org_id>]
|
|
27
27
|
run402 orgs lightning-receiver delete [<org_id>]
|
|
@@ -161,6 +161,7 @@ call after a dropped response can never double-bill.
|
|
|
161
161
|
|
|
162
162
|
Usage:
|
|
163
163
|
run402 orgs payout-wallet [<org_id>] <wallet_address>
|
|
164
|
+
run402 orgs payout-wallet [<org_id>] --signer <signer_id>
|
|
164
165
|
run402 orgs payout-wallet [<org_id>] --clear
|
|
165
166
|
|
|
166
167
|
Legacy (still supported):
|
|
@@ -170,6 +171,11 @@ Admin/owner-only + step-up gated. The wallet must already be active and linked
|
|
|
170
171
|
to the same org. This wallet receives x402 settlement for function web routes
|
|
171
172
|
that declare pricing.pay_to = "org_default_payout". Pass --clear to remove the
|
|
172
173
|
explicit default; a single active org wallet may still resolve automatically.
|
|
174
|
+
Pass --signer <signer_id> to make one of the org's own signers (your app's
|
|
175
|
+
wallet, run402 contracts provision-signer) the payout address instead, so what
|
|
176
|
+
your priced routes earn is what your app pays out from. The signer must be
|
|
177
|
+
active and owned by a project of this org; while it is selected, that project
|
|
178
|
+
cannot be transferred out.
|
|
173
179
|
|
|
174
180
|
The JSON response includes recovery.status, active_wallet_count, and
|
|
175
181
|
next_actions for PAYOUT_WALLET_REQUIRED / PAYOUT_WALLET_AMBIGUOUS setup.
|
|
@@ -478,20 +484,24 @@ async function rename(args) {
|
|
|
478
484
|
|
|
479
485
|
async function payoutWallet(args) {
|
|
480
486
|
const a = normalizeArgv(args);
|
|
481
|
-
assertKnownFlags(a, ["--org", "--address", "--clear", "--help", "-h"], ["--org", "--address"]);
|
|
487
|
+
assertKnownFlags(a, ["--org", "--address", "--signer", "--clear", "--help", "-h"], ["--org", "--address", "--signer"]);
|
|
482
488
|
const clear = a.includes("--clear");
|
|
483
489
|
const walletFlag = flagValue(a, "--address");
|
|
484
|
-
const
|
|
485
|
-
|
|
490
|
+
const signerFlag = flagValue(a, "--signer");
|
|
491
|
+
if (signerFlag !== null && (clear || walletFlag !== null)) {
|
|
492
|
+
fail({ code: "BAD_USAGE", message: "--signer cannot be combined with --address or --clear." });
|
|
493
|
+
}
|
|
494
|
+
const single = clear || walletFlag !== null || signerFlag !== null;
|
|
495
|
+
const { orgId: org, rest: pos } = await takeOrgPositional(a, ["--org", "--address", "--signer"], { cmd: "orgs" });
|
|
486
496
|
requireRest(pos, {
|
|
487
497
|
min: single ? 0 : 1,
|
|
488
498
|
max: single ? 0 : 1,
|
|
489
|
-
command: "run402 orgs payout-wallet [<org_id>] --address <wallet_address>",
|
|
490
|
-
missing: "Missing <wallet_address> (use --address, or
|
|
499
|
+
command: "run402 orgs payout-wallet [<org_id>] --address <wallet_address> | --signer <signer_id> | --clear",
|
|
500
|
+
missing: "Missing <wallet_address> (use --address, --signer <signer_id>, or --clear).",
|
|
491
501
|
});
|
|
492
|
-
const
|
|
502
|
+
const input = signerFlag !== null ? { signerId: signerFlag } : { walletAddress: clear ? null : (walletFlag ?? pos[0]) };
|
|
493
503
|
try {
|
|
494
|
-
console.log(JSON.stringify(await getSdk().org(org).setPayoutWallet(
|
|
504
|
+
console.log(JSON.stringify(await getSdk().org(org).setPayoutWallet(input), null, 2));
|
|
495
505
|
} catch (err) {
|
|
496
506
|
reportSdkError(err);
|
|
497
507
|
}
|
package/lib/session-proofs.mjs
CHANGED
|
@@ -9,13 +9,17 @@
|
|
|
9
9
|
* signed in once is never asked to enroll a second passkey. With no live cached
|
|
10
10
|
* session this returns `undefined` and the verb uses wallet auth alone.
|
|
11
11
|
*/
|
|
12
|
-
import { walletAuthHeaders } from "./config.mjs";
|
|
12
|
+
import { walletAuthHeaders, walletFile } from "./config.mjs";
|
|
13
13
|
import { loadLiveControlPlaneSession } from "../core-dist/control-plane-session.js";
|
|
14
|
+
import { getWalletAuthHeadersAsync } from "../core-dist/wallet-auth.js";
|
|
15
|
+
import { personalMessageSignerFor } from "#sdk/node";
|
|
14
16
|
|
|
15
17
|
/** `{ siwx, token }` for `path`, or `undefined` when no live sign-in session is cached. */
|
|
16
|
-
export function sessionProofs(path) {
|
|
17
|
-
|
|
18
|
+
export async function sessionProofs(path) {
|
|
19
|
+
walletAuthHeaders(path); // NO_WALLET when none is configured
|
|
18
20
|
const live = loadLiveControlPlaneSession();
|
|
19
21
|
if (!live || !live.control_plane_session_token) return undefined;
|
|
20
|
-
|
|
22
|
+
// A key-backed wallet signs locally; a signer-backed one (KMS) through its signer.
|
|
23
|
+
const headers = await getWalletAuthHeadersAsync(path, walletFile(), (w) => personalMessageSignerFor(w));
|
|
24
|
+
return { siwx: headers["SIGN-IN-WITH-X"], token: live.control_plane_session_token };
|
|
21
25
|
}
|
package/lib/subscriptions.mjs
CHANGED
|
@@ -136,7 +136,7 @@ async function rulesAdd(args) {
|
|
|
136
136
|
|
|
137
137
|
walletAuthHeaders("/agent/v1/notifications/rules");
|
|
138
138
|
try {
|
|
139
|
-
console.log(JSON.stringify(await getSdk().admin.rules.create(input, sessionProofs("/agent/v1/notifications/rules")), null, 2));
|
|
139
|
+
console.log(JSON.stringify(await getSdk().admin.rules.create(input, await sessionProofs("/agent/v1/notifications/rules")), null, 2));
|
|
140
140
|
} catch (err) {
|
|
141
141
|
reportSdkError(err);
|
|
142
142
|
}
|
|
@@ -162,7 +162,7 @@ async function rulesRm(args) {
|
|
|
162
162
|
});
|
|
163
163
|
walletAuthHeaders("/agent/v1/notifications/rules");
|
|
164
164
|
try {
|
|
165
|
-
console.log(JSON.stringify(await getSdk().admin.rules.delete(ruleId, sessionProofs("/agent/v1/notifications/rules")), null, 2));
|
|
165
|
+
console.log(JSON.stringify(await getSdk().admin.rules.delete(ruleId, await sessionProofs("/agent/v1/notifications/rules")), null, 2));
|
|
166
166
|
} catch (err) {
|
|
167
167
|
reportSdkError(err);
|
|
168
168
|
}
|