@clanker-chain/clanker-cli 2026.9.12-1 → 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 +48 -10
- package/lib/doctor.mjs +16 -8
- package/lib/login.mjs +200 -0
- 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 +3 -2
- package/lib/setup.mjs +34 -8
- package/package.json +6 -1
package/bin/chain-identity.mjs
CHANGED
|
@@ -3,41 +3,14 @@
|
|
|
3
3
|
* On-chain identity commands (ClankerIdentity contract).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
createPublicClient,
|
|
8
|
-
createWalletClient,
|
|
9
|
-
defineChain,
|
|
10
|
-
http,
|
|
11
|
-
} from "viem";
|
|
12
6
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
13
7
|
import { clankerIdentityAbi } from "../lib/clanker-identity-abi.mjs";
|
|
14
8
|
import { assertSafeBotLabel, writeBotKeyFiles } from "../lib/keys.mjs";
|
|
15
9
|
import { harnessSnippet, writeOperator } from "../lib/profile.mjs";
|
|
16
10
|
import { labelToId } from "../lib/identity-query.mjs";
|
|
11
|
+
import { resolveOperatorSigner } from "../lib/operator-signer.mjs";
|
|
17
12
|
import { resolveOperatorKey } from "../lib/resolve.mjs";
|
|
18
13
|
|
|
19
|
-
async function chainFromRpc(rpc) {
|
|
20
|
-
const publicClient = createPublicClient({ transport: http(rpc) });
|
|
21
|
-
const id = await publicClient.getChainId();
|
|
22
|
-
return defineChain({
|
|
23
|
-
id,
|
|
24
|
-
name: `clanker-chain-${id}`,
|
|
25
|
-
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
|
|
26
|
-
rpcUrls: { default: { http: [rpc] } },
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function walletFromKey(rpc, key) {
|
|
31
|
-
const account = privateKeyToAccount(key);
|
|
32
|
-
const transport = http(rpc);
|
|
33
|
-
const chain = await chainFromRpc(rpc);
|
|
34
|
-
return {
|
|
35
|
-
account,
|
|
36
|
-
public: createPublicClient({ chain, transport }),
|
|
37
|
-
wallet: createWalletClient({ account, chain, transport }),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
14
|
async function waitOk(pub, hash) {
|
|
42
15
|
const receipt = await pub.waitForTransactionReceipt({ hash });
|
|
43
16
|
if (receipt.status !== "success") {
|
|
@@ -48,6 +21,7 @@ async function waitOk(pub, hash) {
|
|
|
48
21
|
|
|
49
22
|
/**
|
|
50
23
|
* Shared resolve for mutating chain commands (Anvil-guarded).
|
|
24
|
+
* Prefer resolveOperatorSigner for Privy + local.
|
|
51
25
|
* @param {string[]} argv
|
|
52
26
|
*/
|
|
53
27
|
export function resolveForWrite(argv) {
|
|
@@ -55,15 +29,15 @@ export function resolveForWrite(argv) {
|
|
|
55
29
|
}
|
|
56
30
|
|
|
57
31
|
export async function chainMintOperator(label, argv, opts = {}) {
|
|
58
|
-
const
|
|
59
|
-
const
|
|
32
|
+
const signer = await resolveOperatorSigner(argv, opts);
|
|
33
|
+
const pub = signer.publicClient;
|
|
60
34
|
const operatorFee = await pub.readContract({
|
|
61
|
-
address: network.registry,
|
|
35
|
+
address: signer.network.registry,
|
|
62
36
|
abi: clankerIdentityAbi,
|
|
63
37
|
functionName: "operatorFee",
|
|
64
38
|
});
|
|
65
|
-
const hash = await
|
|
66
|
-
address: network.registry,
|
|
39
|
+
const hash = await signer.writeContract({
|
|
40
|
+
address: signer.network.registry,
|
|
67
41
|
abi: clankerIdentityAbi,
|
|
68
42
|
functionName: "registerOperator",
|
|
69
43
|
args: [label],
|
|
@@ -72,23 +46,27 @@ export async function chainMintOperator(label, argv, opts = {}) {
|
|
|
72
46
|
await waitOk(pub, hash);
|
|
73
47
|
|
|
74
48
|
writeOperator(
|
|
75
|
-
{
|
|
76
|
-
|
|
49
|
+
{
|
|
50
|
+
label,
|
|
51
|
+
owner: signer.address,
|
|
52
|
+
key: opts.keyPointer ?? signer.keyPointer,
|
|
53
|
+
},
|
|
54
|
+
opts.home ?? signer.network.home,
|
|
77
55
|
);
|
|
78
56
|
|
|
79
57
|
return {
|
|
80
58
|
ok: true,
|
|
81
59
|
label,
|
|
82
60
|
operator_id: labelToId(label),
|
|
83
|
-
owner: address,
|
|
61
|
+
owner: signer.address,
|
|
84
62
|
tx: hash,
|
|
85
63
|
};
|
|
86
64
|
}
|
|
87
65
|
|
|
88
66
|
export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
|
|
89
67
|
assertSafeBotLabel(botLabel);
|
|
90
|
-
const
|
|
91
|
-
const
|
|
68
|
+
const signer = await resolveOperatorSigner(argv, opts);
|
|
69
|
+
const pub = signer.publicClient;
|
|
92
70
|
let botPrivateKey;
|
|
93
71
|
for (let i = 0; i < argv.length; i += 1) {
|
|
94
72
|
const a = argv[i];
|
|
@@ -114,12 +92,12 @@ export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
|
|
|
114
92
|
);
|
|
115
93
|
|
|
116
94
|
const botFee = await pub.readContract({
|
|
117
|
-
address: network.registry,
|
|
95
|
+
address: signer.network.registry,
|
|
118
96
|
abi: clankerIdentityAbi,
|
|
119
97
|
functionName: "botFee",
|
|
120
98
|
});
|
|
121
|
-
const hash = await
|
|
122
|
-
address: network.registry,
|
|
99
|
+
const hash = await signer.writeContract({
|
|
100
|
+
address: signer.network.registry,
|
|
123
101
|
abi: clankerIdentityAbi,
|
|
124
102
|
functionName: "registerBot",
|
|
125
103
|
args: [operatorIdBytes, botLabel, botAccount.address],
|
|
@@ -130,7 +108,7 @@ export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
|
|
|
130
108
|
const channelsMqtt = harnessSnippet({
|
|
131
109
|
botId: botLabel,
|
|
132
110
|
operatorId: operatorLabel,
|
|
133
|
-
network,
|
|
111
|
+
network: signer.network,
|
|
134
112
|
});
|
|
135
113
|
|
|
136
114
|
return {
|
|
@@ -146,11 +124,11 @@ export async function chainMintBot(botLabel, operatorLabel, argv, opts = {}) {
|
|
|
146
124
|
}
|
|
147
125
|
|
|
148
126
|
export async function chainRotateBotKey(botLabel, newKey, argv, opts = {}) {
|
|
149
|
-
const
|
|
150
|
-
const
|
|
127
|
+
const signer = await resolveOperatorSigner(argv, opts);
|
|
128
|
+
const pub = signer.publicClient;
|
|
151
129
|
const botIdBytes = labelToId(botLabel);
|
|
152
|
-
const hash = await
|
|
153
|
-
address: network.registry,
|
|
130
|
+
const hash = await signer.writeContract({
|
|
131
|
+
address: signer.network.registry,
|
|
154
132
|
abi: clankerIdentityAbi,
|
|
155
133
|
functionName: "rotateBotKey",
|
|
156
134
|
args: [botIdBytes, newKey],
|
|
@@ -160,11 +138,11 @@ export async function chainRotateBotKey(botLabel, newKey, argv, opts = {}) {
|
|
|
160
138
|
}
|
|
161
139
|
|
|
162
140
|
export async function chainRevokeBot(botLabel, argv, opts = {}) {
|
|
163
|
-
const
|
|
164
|
-
const
|
|
141
|
+
const signer = await resolveOperatorSigner(argv, opts);
|
|
142
|
+
const pub = signer.publicClient;
|
|
165
143
|
const botIdBytes = labelToId(botLabel);
|
|
166
|
-
const hash = await
|
|
167
|
-
address: network.registry,
|
|
144
|
+
const hash = await signer.writeContract({
|
|
145
|
+
address: signer.network.registry,
|
|
168
146
|
abi: clankerIdentityAbi,
|
|
169
147
|
functionName: "revokeBot",
|
|
170
148
|
args: [botIdBytes],
|
|
@@ -174,11 +152,11 @@ export async function chainRevokeBot(botLabel, argv, opts = {}) {
|
|
|
174
152
|
}
|
|
175
153
|
|
|
176
154
|
export async function chainProposeOperatorTransfer(label, newOwner, argv, opts = {}) {
|
|
177
|
-
const
|
|
178
|
-
const
|
|
155
|
+
const signer = await resolveOperatorSigner(argv, opts);
|
|
156
|
+
const pub = signer.publicClient;
|
|
179
157
|
const id = labelToId(label);
|
|
180
|
-
const hash = await
|
|
181
|
-
address: network.registry,
|
|
158
|
+
const hash = await signer.writeContract({
|
|
159
|
+
address: signer.network.registry,
|
|
182
160
|
abi: clankerIdentityAbi,
|
|
183
161
|
functionName: "proposeOperatorTransfer",
|
|
184
162
|
args: [id, newOwner],
|
|
@@ -188,11 +166,11 @@ export async function chainProposeOperatorTransfer(label, newOwner, argv, opts =
|
|
|
188
166
|
}
|
|
189
167
|
|
|
190
168
|
export async function chainAcceptOperatorTransfer(label, argv, opts = {}) {
|
|
191
|
-
const
|
|
192
|
-
const
|
|
169
|
+
const signer = await resolveOperatorSigner(argv, opts);
|
|
170
|
+
const pub = signer.publicClient;
|
|
193
171
|
const id = labelToId(label);
|
|
194
|
-
const hash = await
|
|
195
|
-
address: network.registry,
|
|
172
|
+
const hash = await signer.writeContract({
|
|
173
|
+
address: signer.network.registry,
|
|
196
174
|
abi: clankerIdentityAbi,
|
|
197
175
|
functionName: "acceptOperatorTransfer",
|
|
198
176
|
args: [id],
|
|
@@ -201,12 +179,12 @@ export async function chainAcceptOperatorTransfer(label, argv, opts = {}) {
|
|
|
201
179
|
writeOperator(
|
|
202
180
|
{
|
|
203
181
|
label,
|
|
204
|
-
owner: address,
|
|
205
|
-
key: opts.keyPointer ?? keyPointer,
|
|
182
|
+
owner: signer.address,
|
|
183
|
+
key: opts.keyPointer ?? signer.keyPointer,
|
|
206
184
|
},
|
|
207
|
-
opts.home ?? network.home,
|
|
185
|
+
opts.home ?? signer.network.home,
|
|
208
186
|
);
|
|
209
|
-
return { ok: true, label, owner: address, tx: hash };
|
|
187
|
+
return { ok: true, label, owner: signer.address, tx: hash };
|
|
210
188
|
}
|
|
211
189
|
|
|
212
190
|
export { labelToId };
|
package/bin/clanker.mjs
CHANGED
|
@@ -24,9 +24,11 @@ import {
|
|
|
24
24
|
resolvePreferredOperator,
|
|
25
25
|
} from "../lib/identity-query.mjs";
|
|
26
26
|
import { resolveForRead, resolveOperatorKey, resolveReadIdentity } from "../lib/resolve.mjs";
|
|
27
|
+
import { resolveOperatorSigner } from "../lib/operator-signer.mjs";
|
|
27
28
|
import { runSetup } from "../lib/setup.mjs";
|
|
28
29
|
import { runDoctor } from "../lib/doctor.mjs";
|
|
29
30
|
import { runFund } from "../lib/fund.mjs";
|
|
31
|
+
import { runLogin, runLogout } from "../lib/login.mjs";
|
|
30
32
|
import {
|
|
31
33
|
assessMintBudget,
|
|
32
34
|
formatEthTrim,
|
|
@@ -171,6 +173,8 @@ function usage() {
|
|
|
171
173
|
|
|
172
174
|
Usage:
|
|
173
175
|
clanker setup [--preset sepolia|local] [--operator <label>] [--address 0x…] [--skip-key] [--bot-key path] [--key-file path] [--force]
|
|
176
|
+
clanker login [--no-open] [--operator <label>] [--json]
|
|
177
|
+
clanker logout [--json]
|
|
174
178
|
clanker fund [--address 0x…] [--no-open] [--timeout ms] [--json]
|
|
175
179
|
clanker doctor [--json]
|
|
176
180
|
clanker init --preset sepolia|local [--force]
|
|
@@ -197,7 +201,7 @@ Profile:
|
|
|
197
201
|
~/.clanker/operator.json label + owner + optional key pointer (never raw hex)
|
|
198
202
|
~/.clanker/keys/ bot keys (also written to ~/.openclaw/keys/)
|
|
199
203
|
|
|
200
|
-
Humans: \`clanker
|
|
204
|
+
Humans: \`clanker login\` (email vault) or \`clanker setup --generate-key\`, then \`fund\` / \`doctor\` / \`whoami\`.
|
|
201
205
|
Pairing (Policy): both operators run \`clanker pair add\` before DMs deliver on the hub.
|
|
202
206
|
Mutates print a plan and confirm unless --yes or --json.
|
|
203
207
|
whoami is fast by default; pass --with-bots to enrich child bots (or use \`clanker bots\`).
|
|
@@ -415,7 +419,8 @@ async function main() {
|
|
|
415
419
|
error: err.message,
|
|
416
420
|
because: "setup could not write a valid local profile",
|
|
417
421
|
try: [
|
|
418
|
-
"clanker
|
|
422
|
+
"clanker login",
|
|
423
|
+
"clanker setup --preset sepolia --operator org.you --yes --force",
|
|
419
424
|
"clanker fund",
|
|
420
425
|
"clanker doctor",
|
|
421
426
|
],
|
|
@@ -424,6 +429,36 @@ async function main() {
|
|
|
424
429
|
return;
|
|
425
430
|
}
|
|
426
431
|
|
|
432
|
+
if (cmd === "login") {
|
|
433
|
+
try {
|
|
434
|
+
const { exitCode } = await runLogin(rest);
|
|
435
|
+
process.exit(exitCode);
|
|
436
|
+
} catch (err) {
|
|
437
|
+
exitCliError({
|
|
438
|
+
error: err.message,
|
|
439
|
+
because: "login needs Privy CLI access enabled and /authorize approval",
|
|
440
|
+
try: [
|
|
441
|
+
"Open the printed URL and Approve",
|
|
442
|
+
"Privy dashboard: Enable CLI and agent access → Verification URI https://clanker-chain.com/authorize",
|
|
443
|
+
"clanker setup --generate-key # local key door instead",
|
|
444
|
+
],
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (cmd === "logout") {
|
|
450
|
+
try {
|
|
451
|
+
const { exitCode } = await runLogout(rest);
|
|
452
|
+
process.exit(exitCode);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
exitCliError({
|
|
455
|
+
error: err.message,
|
|
456
|
+
because: "logout could not clear the Privy session",
|
|
457
|
+
try: ["clanker logout"],
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
427
462
|
if (cmd === "fund") {
|
|
428
463
|
try {
|
|
429
464
|
const { exitCode } = await runFund(rest);
|
|
@@ -581,21 +616,21 @@ async function main() {
|
|
|
581
616
|
let planRows;
|
|
582
617
|
let preview;
|
|
583
618
|
try {
|
|
584
|
-
preview =
|
|
619
|
+
preview = await resolveOperatorSigner(flags);
|
|
585
620
|
planRows = [
|
|
586
621
|
["action", "registerOperator"],
|
|
587
622
|
["label", label],
|
|
588
623
|
["owner", preview.address],
|
|
589
624
|
["rpc", preview.network.rpc],
|
|
590
625
|
["registry", preview.network.registry ?? "(none)"],
|
|
591
|
-
["
|
|
626
|
+
["signer", preview.source],
|
|
592
627
|
];
|
|
593
628
|
} catch (err) {
|
|
594
629
|
exitCliError({
|
|
595
630
|
error: err.message,
|
|
596
|
-
because: "operator mint needs a
|
|
631
|
+
because: "operator mint needs a signer on this network",
|
|
597
632
|
try: [
|
|
598
|
-
"
|
|
633
|
+
"clanker login",
|
|
599
634
|
"clanker operator mint " + label + " --key-file ~/.clanker/op.key --yes",
|
|
600
635
|
"clanker fund",
|
|
601
636
|
"clanker doctor",
|
|
@@ -736,14 +771,15 @@ async function main() {
|
|
|
736
771
|
let preview = null;
|
|
737
772
|
if (!operatorLabel) {
|
|
738
773
|
try {
|
|
739
|
-
preview =
|
|
774
|
+
preview = await resolveOperatorSigner(flags);
|
|
740
775
|
const inferred = await resolveOperatorLabel(flags, preview.address, preview.network);
|
|
741
776
|
operatorLabel = inferred.label;
|
|
742
777
|
} catch (err) {
|
|
743
778
|
exitCliError({
|
|
744
779
|
error: err.message,
|
|
745
|
-
because: "bot mint needs an operator label or a resolvable
|
|
780
|
+
because: "bot mint needs an operator label or a resolvable signer",
|
|
746
781
|
try: [
|
|
782
|
+
"clanker login",
|
|
747
783
|
`clanker bot mint ${botLabel} <operator_label> --yes`,
|
|
748
784
|
"clanker setup",
|
|
749
785
|
],
|
|
@@ -752,12 +788,13 @@ async function main() {
|
|
|
752
788
|
}
|
|
753
789
|
if (!preview) {
|
|
754
790
|
try {
|
|
755
|
-
preview =
|
|
791
|
+
preview = await resolveOperatorSigner(flags);
|
|
756
792
|
} catch (err) {
|
|
757
793
|
exitCliError({
|
|
758
794
|
error: err.message,
|
|
759
|
-
because: "bot mint needs a
|
|
795
|
+
because: "bot mint needs a signer on this network",
|
|
760
796
|
try: [
|
|
797
|
+
"clanker login",
|
|
761
798
|
`clanker bot mint ${botLabel} ${operatorLabel} --key-file ~/.clanker/op.key --yes`,
|
|
762
799
|
"clanker fund",
|
|
763
800
|
],
|
|
@@ -770,6 +807,7 @@ async function main() {
|
|
|
770
807
|
["operator", operatorLabel],
|
|
771
808
|
["owner", preview.address],
|
|
772
809
|
["registry", preview.network.registry ?? "(none)"],
|
|
810
|
+
["signer", preview.source],
|
|
773
811
|
];
|
|
774
812
|
try {
|
|
775
813
|
if (preview.network.registry) {
|
package/lib/doctor.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from "./profile.mjs";
|
|
13
13
|
import { detectSetupHints, formatSetupDetectTable } from "./setup-detect.mjs";
|
|
14
14
|
import { publicClientFromRpc } from "./identity-query.mjs";
|
|
15
|
+
import { loadPrivySession } from "./privy-session.mjs";
|
|
15
16
|
import {
|
|
16
17
|
assessMintBudget,
|
|
17
18
|
budgetToJson,
|
|
@@ -96,17 +97,24 @@ export async function runDoctorChecks(opts = {}) {
|
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
|
|
100
|
+
const privySession = loadPrivySession(home);
|
|
101
|
+
const hasPrivy =
|
|
102
|
+
Boolean(operator?.key?.type === "privy" && operator.key.value) ||
|
|
103
|
+
Boolean(privySession?.accessToken && privySession?.walletId);
|
|
99
104
|
const hasKey =
|
|
100
105
|
Boolean(operator?.key?.type === "keyFile" && operator.key.value) ||
|
|
101
106
|
Boolean(operator?.key?.type === "env" && operator.key.value) ||
|
|
102
|
-
Boolean(env.OPERATOR_PRIVATE_KEY)
|
|
107
|
+
Boolean(env.OPERATOR_PRIVATE_KEY) ||
|
|
108
|
+
hasPrivy;
|
|
103
109
|
checks.push({
|
|
104
110
|
id: "signing",
|
|
105
111
|
ok: true,
|
|
106
112
|
level: hasKey ? "pass" : "warn",
|
|
107
|
-
message:
|
|
108
|
-
? "
|
|
109
|
-
:
|
|
113
|
+
message: hasPrivy
|
|
114
|
+
? "Privy session available (mint/pair/rotate via email vault)"
|
|
115
|
+
: hasKey
|
|
116
|
+
? "signing key pointer available (mint/pair/rotate OK)"
|
|
117
|
+
: "read-only profile — whoami/bots/fund OK; mint/pair/rotate need clanker login or a key pointer",
|
|
110
118
|
});
|
|
111
119
|
|
|
112
120
|
checks.push({
|
|
@@ -295,20 +303,20 @@ export async function runDoctor(argv = [], opts = {}) {
|
|
|
295
303
|
} else {
|
|
296
304
|
console.log(
|
|
297
305
|
c.dim(
|
|
298
|
-
"Mint/pair/rotate: need
|
|
306
|
+
"Mint/pair/rotate: need clanker login or a signing key (non-Anvil owner on public RPC)",
|
|
299
307
|
),
|
|
300
308
|
);
|
|
301
309
|
}
|
|
302
310
|
|
|
303
311
|
if (!report.readyWhoami) {
|
|
304
|
-
nextHint(["clanker setup"]);
|
|
312
|
+
nextHint(["clanker setup", "clanker login"]);
|
|
305
313
|
} else if (report.budget && !report.budget.funded && !report.budget.local) {
|
|
306
314
|
nextHint(["clanker fund", "clanker doctor"]);
|
|
307
315
|
} else if (!report.readyMint) {
|
|
308
316
|
nextHint([
|
|
317
|
+
"clanker login",
|
|
309
318
|
"clanker whoami",
|
|
310
|
-
"
|
|
311
|
-
"mint/pair/rotate need --key-file / OPERATOR_PRIVATE_KEY or stay on /join",
|
|
319
|
+
"mint/pair/rotate need login or --key-file",
|
|
312
320
|
]);
|
|
313
321
|
} else {
|
|
314
322
|
nextHint(["clanker whoami", "clanker bots"]);
|
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
|
+
}
|