@clanker-chain/clanker-cli 2026.9.12 → 2026.9.13
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/bin/chain-identity.mjs +40 -62
- package/bin/clanker.mjs +50 -12
- package/lib/doctor.mjs +20 -7
- package/lib/fund.mjs +61 -24
- package/lib/login.mjs +200 -0
- package/lib/operator-key.mjs +2 -3
- package/lib/operator-signer.mjs +284 -0
- package/lib/pair.mjs +8 -20
- package/lib/privy-client.mjs +419 -0
- package/lib/privy-constants.mjs +20 -0
- package/lib/privy-hpke.mjs +101 -0
- package/lib/privy-session.mjs +151 -0
- package/lib/profile.mjs +1 -1
- package/lib/resolve.mjs +11 -6
- package/lib/setup.mjs +122 -16
- package/package.json +6 -1
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `clanker login` / `clanker logout` — Privy device grant for the operator vault.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getAddress } from "viem";
|
|
6
|
+
import { openUrl } from "./fund.mjs";
|
|
7
|
+
import {
|
|
8
|
+
loadOperator,
|
|
9
|
+
writeOperator,
|
|
10
|
+
clankerHome,
|
|
11
|
+
loadConfig,
|
|
12
|
+
} from "./profile.mjs";
|
|
13
|
+
import {
|
|
14
|
+
authenticateWallets,
|
|
15
|
+
pollDeviceToken,
|
|
16
|
+
requestDeviceAuthorization,
|
|
17
|
+
} from "./privy-client.mjs";
|
|
18
|
+
import {
|
|
19
|
+
clearPrivySession,
|
|
20
|
+
loadPrivySession,
|
|
21
|
+
savePrivySession,
|
|
22
|
+
} from "./privy-session.mjs";
|
|
23
|
+
import { resolvePrivyAppId } from "./privy-constants.mjs";
|
|
24
|
+
import { c, nextHint } from "./ui.mjs";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string[]} argv
|
|
28
|
+
*/
|
|
29
|
+
export function parseLoginFlags(argv) {
|
|
30
|
+
let noOpen = false;
|
|
31
|
+
let json = false;
|
|
32
|
+
let operator = null;
|
|
33
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
34
|
+
const a = argv[i];
|
|
35
|
+
if (a === "--no-open") noOpen = true;
|
|
36
|
+
else if (a === "--json") json = true;
|
|
37
|
+
else if (a === "--operator" && argv[i + 1]) operator = argv[++i];
|
|
38
|
+
}
|
|
39
|
+
return { noOpen, json, operator };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param {string[]} argv
|
|
44
|
+
* @param {{
|
|
45
|
+
* home?: string,
|
|
46
|
+
* env?: NodeJS.ProcessEnv,
|
|
47
|
+
* fetchImpl?: typeof fetch,
|
|
48
|
+
* openUrlImpl?: typeof openUrl,
|
|
49
|
+
* sleep?: (ms: number) => Promise<void>,
|
|
50
|
+
* now?: () => number,
|
|
51
|
+
* }} [opts]
|
|
52
|
+
*/
|
|
53
|
+
export async function runLogin(argv = [], opts = {}) {
|
|
54
|
+
const flags = parseLoginFlags(argv);
|
|
55
|
+
const env = opts.env ?? process.env;
|
|
56
|
+
const home = opts.home ?? clankerHome(env);
|
|
57
|
+
const openUrlImpl = opts.openUrlImpl ?? openUrl;
|
|
58
|
+
|
|
59
|
+
const device = await requestDeviceAuthorization({
|
|
60
|
+
appId: resolvePrivyAppId(env),
|
|
61
|
+
fetchImpl: opts.fetchImpl,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const url =
|
|
65
|
+
device.verificationUriComplete ||
|
|
66
|
+
`${device.verificationUri}?user_code=${encodeURIComponent(device.userCode)}`;
|
|
67
|
+
|
|
68
|
+
if (!flags.json) {
|
|
69
|
+
console.log(c.bold("clanker login"));
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log("Approve CLI access to your operator wallet (email vault).");
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(`Visit: ${url}`);
|
|
74
|
+
console.log(`Code: ${device.userCode}`);
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(c.dim("Waiting for browser approval…"));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!flags.noOpen) {
|
|
80
|
+
await openUrlImpl(url);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const tokens = await pollDeviceToken({
|
|
84
|
+
appId: device.appId,
|
|
85
|
+
deviceCode: device.deviceCode,
|
|
86
|
+
interval: device.interval,
|
|
87
|
+
expiresIn: device.expiresIn,
|
|
88
|
+
fetchImpl: opts.fetchImpl,
|
|
89
|
+
sleep: opts.sleep,
|
|
90
|
+
now: opts.now,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const auth = await authenticateWallets({
|
|
94
|
+
appId: device.appId,
|
|
95
|
+
accessToken: tokens.accessToken,
|
|
96
|
+
fetchImpl: opts.fetchImpl,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const session = {
|
|
100
|
+
appId: device.appId,
|
|
101
|
+
accessToken: tokens.accessToken,
|
|
102
|
+
refreshToken: tokens.refreshToken,
|
|
103
|
+
expiresAt: Date.now() + tokens.expiresIn * 1000,
|
|
104
|
+
walletId: auth.walletId,
|
|
105
|
+
address: auth.address,
|
|
106
|
+
createdAt: Date.now(),
|
|
107
|
+
};
|
|
108
|
+
const stored = savePrivySession(session, home);
|
|
109
|
+
|
|
110
|
+
const existing = loadOperator(home);
|
|
111
|
+
const label = flags.operator || existing?.label || null;
|
|
112
|
+
if (existing?.owner && getAddress(existing.owner) !== auth.address) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Logged-in address ${auth.address} does not match operator.json owner ${existing.owner}. ` +
|
|
115
|
+
`Use a matching login, or clanker setup --force with the new owner.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (label || existing) {
|
|
120
|
+
writeOperator(
|
|
121
|
+
{
|
|
122
|
+
label: label || existing.label,
|
|
123
|
+
owner: auth.address,
|
|
124
|
+
key: { type: "privy", value: auth.walletId },
|
|
125
|
+
},
|
|
126
|
+
home,
|
|
127
|
+
);
|
|
128
|
+
} else if (!loadConfig(home)) {
|
|
129
|
+
// Login alone is enough for session; setup still needed for network + label.
|
|
130
|
+
} else {
|
|
131
|
+
// Config exists but no operator yet — leave operator.json to setup / mint.
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const payload = {
|
|
135
|
+
ok: true,
|
|
136
|
+
address: auth.address,
|
|
137
|
+
walletId: auth.walletId,
|
|
138
|
+
storage: stored.storage,
|
|
139
|
+
label: label || existing?.label || null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
if (flags.json) {
|
|
143
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
144
|
+
} else {
|
|
145
|
+
console.log("");
|
|
146
|
+
console.log(c.green(`Logged in as ${auth.address}`));
|
|
147
|
+
console.log(c.dim(`wallet: ${auth.walletId}`));
|
|
148
|
+
console.log(c.dim(`session: ${stored.storage}`));
|
|
149
|
+
if (!label && !existing?.label) {
|
|
150
|
+
nextHint([
|
|
151
|
+
"clanker setup --preset sepolia --operator org.you --yes # if no profile yet",
|
|
152
|
+
"clanker fund",
|
|
153
|
+
"clanker whoami",
|
|
154
|
+
"clanker pair add <peer> --yes",
|
|
155
|
+
]);
|
|
156
|
+
} else {
|
|
157
|
+
nextHint([
|
|
158
|
+
"clanker fund",
|
|
159
|
+
"clanker whoami",
|
|
160
|
+
"clanker pair add <peer> --yes",
|
|
161
|
+
"clanker operator mint <label> --yes # if not minted yet",
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return { exitCode: 0, ...payload, session };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* @param {string[]} argv
|
|
170
|
+
* @param {{ home?: string, env?: NodeJS.ProcessEnv }} [opts]
|
|
171
|
+
*/
|
|
172
|
+
export async function runLogout(argv = [], opts = {}) {
|
|
173
|
+
const json = argv.includes("--json");
|
|
174
|
+
const home = opts.home ?? clankerHome(opts.env ?? process.env);
|
|
175
|
+
const had = Boolean(loadPrivySession(home));
|
|
176
|
+
clearPrivySession(home);
|
|
177
|
+
const operator = loadOperator(home);
|
|
178
|
+
if (operator?.key?.type === "privy") {
|
|
179
|
+
writeOperator(
|
|
180
|
+
{
|
|
181
|
+
label: operator.label,
|
|
182
|
+
owner: operator.owner,
|
|
183
|
+
key: null,
|
|
184
|
+
},
|
|
185
|
+
home,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (json) {
|
|
189
|
+
console.log(JSON.stringify({ ok: true, cleared: had }, null, 2));
|
|
190
|
+
} else {
|
|
191
|
+
console.log(c.bold("clanker logout"));
|
|
192
|
+
console.log(had ? c.green("Cleared Privy CLI session.") : c.dim("No session stored."));
|
|
193
|
+
console.log(
|
|
194
|
+
c.dim(
|
|
195
|
+
"Revoke grants anytime in the Privy dashboard / account settings if needed.",
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
return { exitCode: 0, cleared: had };
|
|
200
|
+
}
|
package/lib/operator-key.mjs
CHANGED
|
@@ -10,9 +10,8 @@ import { defaultOperatorKeyPath } from "./foundry.mjs";
|
|
|
10
10
|
|
|
11
11
|
export { defaultOperatorKeyPath };
|
|
12
12
|
|
|
13
|
-
/** Coinbase Developer Platform
|
|
14
|
-
export const BASE_SEPOLIA_FAUCET_URL =
|
|
15
|
-
"https://portal.cdp.coinbase.com/products/faucet";
|
|
13
|
+
/** Coinbase Developer Platform portal (Faucets live in-nav; deep link /products/faucet 404s). */
|
|
14
|
+
export const BASE_SEPOLIA_FAUCET_URL = "https://portal.cdp.coinbase.com/";
|
|
16
15
|
|
|
17
16
|
/** Backup Base Sepolia faucet (amounts not guaranteed). */
|
|
18
17
|
export const ALCHEMY_BASE_SEPOLIA_FAUCET_URL =
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pluggable operator signer: local hex key or Privy device-grant vault.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
createPublicClient,
|
|
7
|
+
createWalletClient,
|
|
8
|
+
defineChain,
|
|
9
|
+
encodeFunctionData,
|
|
10
|
+
getAddress,
|
|
11
|
+
http,
|
|
12
|
+
} from "viem";
|
|
13
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
14
|
+
import {
|
|
15
|
+
isAnvilDefaultKey,
|
|
16
|
+
OWNER_SIGNER_REQUIRED,
|
|
17
|
+
parseKeyFlags,
|
|
18
|
+
resolveOperatorKey,
|
|
19
|
+
} from "./resolve.mjs";
|
|
20
|
+
import {
|
|
21
|
+
ANVIL_DEFAULT_PRIVATE_KEY,
|
|
22
|
+
isLocalRpc,
|
|
23
|
+
loadOperator,
|
|
24
|
+
resolveNetwork,
|
|
25
|
+
} from "./profile.mjs";
|
|
26
|
+
import { loadPrivySession } from "./privy-session.mjs";
|
|
27
|
+
import { privySendTransaction, privySignMessage } from "./privy-client.mjs";
|
|
28
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
29
|
+
import { normalizePrivateKey } from "./resolve.mjs";
|
|
30
|
+
|
|
31
|
+
export { OWNER_SIGNER_REQUIRED };
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* address: `0x${string}`,
|
|
36
|
+
* source: string,
|
|
37
|
+
* keyPointer: { type: string, value: string }|null,
|
|
38
|
+
* network: object,
|
|
39
|
+
* kind: 'local'|'privy',
|
|
40
|
+
* key?: `0x${string}`,
|
|
41
|
+
* signMessage: (args: { message: string }) => Promise<`0x${string}`>,
|
|
42
|
+
* writeContract: (args: object) => Promise<`0x${string}`>,
|
|
43
|
+
* publicClient: import('viem').PublicClient,
|
|
44
|
+
* }} OperatorSigner
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
async function chainFromRpc(rpc) {
|
|
48
|
+
const publicClient = createPublicClient({ transport: http(rpc) });
|
|
49
|
+
const id = await publicClient.getChainId();
|
|
50
|
+
return defineChain({
|
|
51
|
+
id,
|
|
52
|
+
name: `clanker-chain-${id}`,
|
|
53
|
+
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
54
|
+
rpcUrls: { default: { http: [rpc] } },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {string} rpc
|
|
60
|
+
* @param {`0x${string}`} key
|
|
61
|
+
*/
|
|
62
|
+
async function localSignerFromKey(rpc, key, meta) {
|
|
63
|
+
const account = privateKeyToAccount(key);
|
|
64
|
+
const transport = http(rpc);
|
|
65
|
+
const chain = await chainFromRpc(rpc);
|
|
66
|
+
const publicClient = createPublicClient({ chain, transport });
|
|
67
|
+
const wallet = createWalletClient({ account, chain, transport });
|
|
68
|
+
return {
|
|
69
|
+
...meta,
|
|
70
|
+
kind: "local",
|
|
71
|
+
key,
|
|
72
|
+
address: account.address,
|
|
73
|
+
publicClient,
|
|
74
|
+
signMessage: async ({ message }) => account.signMessage({ message }),
|
|
75
|
+
writeContract: async (args) => wallet.writeContract(args),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @param {string} rpc
|
|
81
|
+
* @param {{ address: string, walletId: string, home?: string, fetchImpl?: typeof fetch }} privy
|
|
82
|
+
* @param {object} meta
|
|
83
|
+
*/
|
|
84
|
+
async function privySignerFromSession(rpc, privy, meta) {
|
|
85
|
+
const transport = http(rpc);
|
|
86
|
+
const chain = await chainFromRpc(rpc);
|
|
87
|
+
const publicClient = createPublicClient({ chain, transport });
|
|
88
|
+
const address = getAddress(privy.address);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
...meta,
|
|
92
|
+
kind: "privy",
|
|
93
|
+
address,
|
|
94
|
+
publicClient,
|
|
95
|
+
signMessage: async ({ message }) =>
|
|
96
|
+
privySignMessage(message, {
|
|
97
|
+
home: privy.home,
|
|
98
|
+
fetchImpl: privy.fetchImpl,
|
|
99
|
+
}),
|
|
100
|
+
writeContract: async (args) => {
|
|
101
|
+
const data = encodeFunctionData({
|
|
102
|
+
abi: args.abi,
|
|
103
|
+
functionName: args.functionName,
|
|
104
|
+
args: args.args,
|
|
105
|
+
});
|
|
106
|
+
const value =
|
|
107
|
+
args.value == null
|
|
108
|
+
? undefined
|
|
109
|
+
: typeof args.value === "bigint"
|
|
110
|
+
? `0x${args.value.toString(16)}`
|
|
111
|
+
: args.value;
|
|
112
|
+
const chainId = chain.id;
|
|
113
|
+
return privySendTransaction(
|
|
114
|
+
{
|
|
115
|
+
to: args.address,
|
|
116
|
+
data,
|
|
117
|
+
value,
|
|
118
|
+
chain_id: chainId,
|
|
119
|
+
},
|
|
120
|
+
{ home: privy.home, fetchImpl: privy.fetchImpl },
|
|
121
|
+
);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolve an operator signer (local key or Privy session).
|
|
128
|
+
*
|
|
129
|
+
* Precedence for local: --key > --key-file > OPERATOR_PRIVATE_KEY > profile keyFile/env.
|
|
130
|
+
* Else if profile key.type === privy or a Privy session exists → Privy vault.
|
|
131
|
+
* Else Anvil on local RPC only.
|
|
132
|
+
*
|
|
133
|
+
* @param {string[]} argv
|
|
134
|
+
* @param {{
|
|
135
|
+
* home?: string,
|
|
136
|
+
* env?: NodeJS.ProcessEnv,
|
|
137
|
+
* rpc?: string,
|
|
138
|
+
* requireRegistry?: boolean,
|
|
139
|
+
* fetchImpl?: typeof fetch,
|
|
140
|
+
* }} [opts]
|
|
141
|
+
* @returns {Promise<OperatorSigner>}
|
|
142
|
+
*/
|
|
143
|
+
export async function resolveOperatorSigner(argv = [], opts = {}) {
|
|
144
|
+
const env = opts.env ?? process.env;
|
|
145
|
+
const network = resolveNetwork(argv, { home: opts.home, env });
|
|
146
|
+
const rpc = opts.rpc ?? network.rpc;
|
|
147
|
+
const home = opts.home ?? network.home;
|
|
148
|
+
const operator = loadOperator(home);
|
|
149
|
+
const requireRegistry = opts.requireRegistry !== false;
|
|
150
|
+
const flags = parseKeyFlags(argv);
|
|
151
|
+
const local = isLocalRpc(rpc);
|
|
152
|
+
|
|
153
|
+
if (requireRegistry && (!network.registry || !/^0x[0-9a-fA-F]{40}$/.test(network.registry))) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
"REGISTRY_ADDRESS or --registry is required (0x + 40 hex). " +
|
|
156
|
+
"Run `clanker init --preset sepolia` or set registry after `clanker chain deploy`.",
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Explicit local key paths first.
|
|
161
|
+
let key = null;
|
|
162
|
+
let source = null;
|
|
163
|
+
let keyFilePath = null;
|
|
164
|
+
if (flags.key) {
|
|
165
|
+
key = normalizePrivateKey(flags.key);
|
|
166
|
+
source = "--key";
|
|
167
|
+
} else if (flags.keyFile || network.keyFile) {
|
|
168
|
+
const path = flags.keyFile || network.keyFile;
|
|
169
|
+
if (!existsSync(path)) throw new Error(`Key file not found: ${path}`);
|
|
170
|
+
key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
|
|
171
|
+
keyFilePath = path;
|
|
172
|
+
source = `--key-file ${path}`;
|
|
173
|
+
} else if (env.OPERATOR_PRIVATE_KEY) {
|
|
174
|
+
key = normalizePrivateKey(env.OPERATOR_PRIVATE_KEY);
|
|
175
|
+
source = "OPERATOR_PRIVATE_KEY";
|
|
176
|
+
} else if (operator?.key?.type === "keyFile" && operator.key.value) {
|
|
177
|
+
const path = operator.key.value;
|
|
178
|
+
if (!existsSync(path)) throw new Error(`Profile keyFile not found: ${path}`);
|
|
179
|
+
key = normalizePrivateKey(readFileSync(path, "utf8").split(/\r?\n/)[0]);
|
|
180
|
+
keyFilePath = path;
|
|
181
|
+
source = `profile keyFile ${path}`;
|
|
182
|
+
} else if (operator?.key?.type === "env" && operator.key.value && env[operator.key.value]) {
|
|
183
|
+
key = normalizePrivateKey(env[operator.key.value]);
|
|
184
|
+
source = `profile env ${operator.key.value}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (key) {
|
|
188
|
+
if (!local && isAnvilDefaultKey(key)) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
"Refusing Anvil account #0 key on a non-local RPC. " +
|
|
191
|
+
"Use clanker login, or a real OPERATOR_PRIVATE_KEY / --key-file.",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const keyPointer =
|
|
195
|
+
keyFilePath
|
|
196
|
+
? { type: "keyFile", value: keyFilePath }
|
|
197
|
+
: source === "OPERATOR_PRIVATE_KEY" || source?.startsWith("profile env")
|
|
198
|
+
? { type: "env", value: "OPERATOR_PRIVATE_KEY" }
|
|
199
|
+
: { type: "env", value: "OPERATOR_PRIVATE_KEY" };
|
|
200
|
+
const signer = await localSignerFromKey(rpc, key, {
|
|
201
|
+
source,
|
|
202
|
+
keyPointer,
|
|
203
|
+
network: { ...network, rpc },
|
|
204
|
+
});
|
|
205
|
+
if (
|
|
206
|
+
operator?.owner &&
|
|
207
|
+
getAddress(operator.owner) !== getAddress(signer.address)
|
|
208
|
+
) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`Signing address ${signer.address} does not match operator.json owner ${operator.owner}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return signer;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Privy vault.
|
|
217
|
+
const session = loadPrivySession(home);
|
|
218
|
+
const wantsPrivy =
|
|
219
|
+
operator?.key?.type === "privy" || Boolean(session?.accessToken);
|
|
220
|
+
|
|
221
|
+
if (wantsPrivy) {
|
|
222
|
+
if (!session?.accessToken || !session?.address || !session?.walletId) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
"Privy session missing — run clanker login (or use --key-file / OPERATOR_PRIVATE_KEY).",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (
|
|
228
|
+
operator?.key?.type === "privy" &&
|
|
229
|
+
operator.key.value &&
|
|
230
|
+
operator.key.value !== session.walletId
|
|
231
|
+
) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Privy wallet ${session.walletId} does not match operator.json ${operator.key.value} — run clanker login again`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
operator?.owner &&
|
|
238
|
+
getAddress(operator.owner) !== getAddress(session.address)
|
|
239
|
+
) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Privy address ${session.address} does not match operator.json owner ${operator.owner}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
return privySignerFromSession(
|
|
245
|
+
rpc,
|
|
246
|
+
{
|
|
247
|
+
address: session.address,
|
|
248
|
+
walletId: session.walletId,
|
|
249
|
+
home,
|
|
250
|
+
fetchImpl: opts.fetchImpl,
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
source: "privy session",
|
|
254
|
+
keyPointer: { type: "privy", value: session.walletId },
|
|
255
|
+
network: { ...network, rpc },
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (local) {
|
|
261
|
+
return localSignerFromKey(
|
|
262
|
+
rpc,
|
|
263
|
+
normalizePrivateKey(ANVIL_DEFAULT_PRIVATE_KEY),
|
|
264
|
+
{
|
|
265
|
+
source: "anvil-default (local RPC)",
|
|
266
|
+
keyPointer: { type: "env", value: "OPERATOR_PRIVATE_KEY" },
|
|
267
|
+
network: { ...network, rpc },
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
throw new Error(
|
|
273
|
+
"Owner actions need clanker login (email vault), or a signing key (--key-file / OPERATOR_PRIVATE_KEY). " +
|
|
274
|
+
"A read-only profile can still run whoami, bots, fund, and doctor.",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Back-compat: local hex only (used by forge deploy etc.).
|
|
280
|
+
* Prefer resolveOperatorSigner for pair/mint.
|
|
281
|
+
*/
|
|
282
|
+
export function resolveOperatorKeyLocal(argv, opts) {
|
|
283
|
+
return resolveOperatorKey(argv, opts);
|
|
284
|
+
}
|
package/lib/pair.mjs
CHANGED
|
@@ -6,10 +6,9 @@
|
|
|
6
6
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
8
|
import { join } from "node:path";
|
|
9
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
10
9
|
import { loadOperator } from "./profile.mjs";
|
|
11
10
|
import { openclawConfigPath } from "./openclaw-wire.mjs";
|
|
12
|
-
import {
|
|
11
|
+
import { resolveOperatorSigner } from "./operator-signer.mjs";
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* @param {string} baseUrl
|
|
@@ -159,11 +158,11 @@ export function resolveOpenclawHome(argv) {
|
|
|
159
158
|
* @param {string|null} peerLabel
|
|
160
159
|
*/
|
|
161
160
|
export async function runPairAction(argv, action, peerLabel) {
|
|
162
|
-
const
|
|
161
|
+
const signer = await resolveOperatorSigner(argv, { requireRegistry: true });
|
|
163
162
|
const operator =
|
|
164
|
-
loadOperator(
|
|
165
|
-
(
|
|
166
|
-
? { label:
|
|
163
|
+
loadOperator(signer.network.home) ??
|
|
164
|
+
(signer.network.operatorLabel
|
|
165
|
+
? { label: signer.network.operatorLabel }
|
|
167
166
|
: null);
|
|
168
167
|
const operatorId =
|
|
169
168
|
argv.includes("--operator") && argv[argv.indexOf("--operator") + 1]
|
|
@@ -175,15 +174,14 @@ export async function runPairAction(argv, action, peerLabel) {
|
|
|
175
174
|
);
|
|
176
175
|
}
|
|
177
176
|
|
|
178
|
-
const authUrl = resolvePairAuthUrl(argv,
|
|
179
|
-
const account = privateKeyToAccount(resolved.key);
|
|
177
|
+
const authUrl = resolvePairAuthUrl(argv, signer.network);
|
|
180
178
|
|
|
181
179
|
if (action === "list" || action === "status") {
|
|
182
180
|
const { nonce, message } = await fetchPairNonce(authUrl, {
|
|
183
181
|
operatorId,
|
|
184
182
|
action: "list",
|
|
185
183
|
});
|
|
186
|
-
const signature = await
|
|
184
|
+
const signature = await signer.signMessage({ message });
|
|
187
185
|
const listed = await getPairList(authUrl, {
|
|
188
186
|
operatorId,
|
|
189
187
|
nonce,
|
|
@@ -191,16 +189,6 @@ export async function runPairAction(argv, action, peerLabel) {
|
|
|
191
189
|
});
|
|
192
190
|
if (action === "status") {
|
|
193
191
|
if (!peerLabel) throw new Error("Usage: clanker pair status <operator-label>");
|
|
194
|
-
// Resolve peer by scanning allows — mutual flag is per peer_operator_id;
|
|
195
|
-
// we match by requesting add-style peer label via a second pair-nonce is heavy.
|
|
196
|
-
// Instead: check if any allow entry is mutual when peer label hashes match… we
|
|
197
|
-
// only have peer_operator_id hex. Re-fetch is unnecessary: call add-path status
|
|
198
|
-
// by computing whether peer is in allow list via a dedicated pair status using
|
|
199
|
-
// the peer label through POST-less approach — GET list doesn't include labels.
|
|
200
|
-
// Best effort: show mutual if we can add then remove? No.
|
|
201
|
-
// Call /pair-nonce + post with action that doesn't mutate? Use list + ask user.
|
|
202
|
-
// Simpler: after list, for status we POST nothing — check if peer's keccak is in list.
|
|
203
|
-
// We need peer id: keccak of peerLabel.
|
|
204
192
|
const { keccak256, toBytes } = await import("viem");
|
|
205
193
|
const peerId = keccak256(toBytes(peerLabel)).toLowerCase();
|
|
206
194
|
const hit = (listed.allows ?? []).find(
|
|
@@ -228,7 +216,7 @@ export async function runPairAction(argv, action, peerLabel) {
|
|
|
228
216
|
action,
|
|
229
217
|
peerLabel,
|
|
230
218
|
});
|
|
231
|
-
const signature = await
|
|
219
|
+
const signature = await signer.signMessage({ message });
|
|
232
220
|
const result = await postPair(authUrl, {
|
|
233
221
|
operatorId,
|
|
234
222
|
peerLabel,
|