@chainstream-io/cli 0.0.5 → 0.0.10
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/dist/index.js +301 -305
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -10,14 +10,12 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/lib/constants.ts
|
|
13
|
-
var CHAINSTREAM_API_URL, CHAINSTREAM_AUTH_URL,
|
|
13
|
+
var CHAINSTREAM_API_URL, CHAINSTREAM_AUTH_URL, SOLANA_RPC_URL, BASE_RPC_URL, BASE_CHAIN_ID;
|
|
14
14
|
var init_constants = __esm({
|
|
15
15
|
"src/lib/constants.ts"() {
|
|
16
16
|
"use strict";
|
|
17
17
|
CHAINSTREAM_API_URL = process.env.CHAINSTREAM_API_URL ?? "https://api.chainstream.io";
|
|
18
|
-
CHAINSTREAM_AUTH_URL = process.env.CHAINSTREAM_AUTH_URL ?? "https://
|
|
19
|
-
TURNKEY_AUTH_PROXY_CONFIG_ID = process.env.TURNKEY_AUTH_PROXY_CONFIG_ID ?? "7550819d-2607-4910-a3d9-8e6e3ff870f9";
|
|
20
|
-
TURNKEY_SUB_ORG_PREFIX = "chainstream-personal";
|
|
18
|
+
CHAINSTREAM_AUTH_URL = process.env.CHAINSTREAM_AUTH_URL ?? "https://api.chainstream.io";
|
|
21
19
|
SOLANA_RPC_URL = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";
|
|
22
20
|
BASE_RPC_URL = process.env.BASE_RPC_URL ?? "https://mainnet.base.org";
|
|
23
21
|
BASE_CHAIN_ID = Number(process.env.BASE_CHAIN_ID ?? "8453");
|
|
@@ -29,6 +27,8 @@ var config_exports = {};
|
|
|
29
27
|
__export(config_exports, {
|
|
30
28
|
getConfigDir: () => getConfigDir,
|
|
31
29
|
getWalletMode: () => getWalletMode,
|
|
30
|
+
isEvmChain: () => isEvmChain,
|
|
31
|
+
isSolanaChain: () => isSolanaChain,
|
|
32
32
|
loadConfig: () => loadConfig,
|
|
33
33
|
saveConfig: () => saveConfig,
|
|
34
34
|
updateConfig: () => updateConfig
|
|
@@ -36,6 +36,12 @@ __export(config_exports, {
|
|
|
36
36
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
37
37
|
import { homedir } from "os";
|
|
38
38
|
import { join } from "path";
|
|
39
|
+
function isEvmChain(chain) {
|
|
40
|
+
return chain === "base";
|
|
41
|
+
}
|
|
42
|
+
function isSolanaChain(chain) {
|
|
43
|
+
return chain === "sol";
|
|
44
|
+
}
|
|
39
45
|
function getConfigDir() {
|
|
40
46
|
if (!existsSync(CONFIG_DIR)) {
|
|
41
47
|
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
@@ -78,6 +84,16 @@ var init_config = __esm({
|
|
|
78
84
|
}
|
|
79
85
|
});
|
|
80
86
|
|
|
87
|
+
// src/wallet/types.ts
|
|
88
|
+
function toSdkChain(chain) {
|
|
89
|
+
return chain === "sol" ? "solana" : "evm";
|
|
90
|
+
}
|
|
91
|
+
var init_types = __esm({
|
|
92
|
+
"src/wallet/types.ts"() {
|
|
93
|
+
"use strict";
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
81
97
|
// src/wallet/raw-wallet.ts
|
|
82
98
|
import { privateKeyToAccount } from "viem/accounts";
|
|
83
99
|
import { Keypair, VersionedTransaction } from "@solana/web3.js";
|
|
@@ -109,14 +125,18 @@ var RawKeyWallet;
|
|
|
109
125
|
var init_raw_wallet = __esm({
|
|
110
126
|
"src/wallet/raw-wallet.ts"() {
|
|
111
127
|
"use strict";
|
|
128
|
+
init_types();
|
|
129
|
+
init_config();
|
|
112
130
|
RawKeyWallet = class {
|
|
113
131
|
chain;
|
|
132
|
+
paymentChain;
|
|
114
133
|
address;
|
|
115
134
|
privateKey;
|
|
116
135
|
constructor(key, chain) {
|
|
117
|
-
this.
|
|
136
|
+
this.paymentChain = chain;
|
|
137
|
+
this.chain = toSdkChain(chain);
|
|
118
138
|
this.privateKey = key;
|
|
119
|
-
if (
|
|
139
|
+
if (isEvmChain(this.paymentChain)) {
|
|
120
140
|
const hex = key.startsWith("0x") ? key : `0x${key}`;
|
|
121
141
|
const account = privateKeyToAccount(hex);
|
|
122
142
|
this.address = account.address;
|
|
@@ -127,7 +147,7 @@ var init_raw_wallet = __esm({
|
|
|
127
147
|
}
|
|
128
148
|
}
|
|
129
149
|
async signMessage(message) {
|
|
130
|
-
if (this.
|
|
150
|
+
if (isEvmChain(this.paymentChain)) {
|
|
131
151
|
const hex = this.privateKey.startsWith("0x") ? this.privateKey : `0x${this.privateKey}`;
|
|
132
152
|
const account = privateKeyToAccount(hex);
|
|
133
153
|
return account.signMessage({ message });
|
|
@@ -142,7 +162,7 @@ var init_raw_wallet = __esm({
|
|
|
142
162
|
}
|
|
143
163
|
async signTransaction(serializedTx) {
|
|
144
164
|
const txBytes = Buffer.from(serializedTx, "base64");
|
|
145
|
-
if (this.
|
|
165
|
+
if (!isEvmChain(this.paymentChain)) {
|
|
146
166
|
const tx = VersionedTransaction.deserialize(new Uint8Array(txBytes));
|
|
147
167
|
const decoded = bs58Decode(this.privateKey);
|
|
148
168
|
const keypair = Keypair.fromSecretKey(decoded);
|
|
@@ -162,20 +182,12 @@ var init_raw_wallet = __esm({
|
|
|
162
182
|
var turnkey_exports = {};
|
|
163
183
|
__export(turnkey_exports, {
|
|
164
184
|
TURNKEY_API_BASE: () => TURNKEY_API_BASE,
|
|
165
|
-
TURNKEY_CONFIG_ID: () => TURNKEY_CONFIG_ID,
|
|
166
|
-
checkAccount: () => checkAccount,
|
|
167
|
-
completeLogin: () => completeLogin,
|
|
168
185
|
createApiStamp: () => createApiStamp,
|
|
169
186
|
generateP256KeyPair: () => generateP256KeyPair,
|
|
170
187
|
getEvmAddress: () => getEvmAddress,
|
|
171
188
|
getSolanaAddress: () => getSolanaAddress,
|
|
172
189
|
listWalletAccounts: () => listWalletAccounts,
|
|
173
|
-
otpInit: () => otpInit,
|
|
174
|
-
otpLogin: () => otpLogin,
|
|
175
|
-
otpVerify: () => otpVerify,
|
|
176
190
|
refreshTurnkeySession: () => refreshTurnkeySession,
|
|
177
|
-
signRawP256: () => signRawP256,
|
|
178
|
-
signup: () => signup,
|
|
179
191
|
turnkeyRequest: () => turnkeyRequest,
|
|
180
192
|
updateTurnkeyUserEmail: () => updateTurnkeyUserEmail
|
|
181
193
|
});
|
|
@@ -199,160 +211,12 @@ function generateP256KeyPair() {
|
|
|
199
211
|
privateKeyDer: Buffer.from(privateKey).toString("base64")
|
|
200
212
|
};
|
|
201
213
|
}
|
|
202
|
-
function signRawP256(message, privateKeyDerBase64) {
|
|
203
|
-
const signer = createSign("SHA256");
|
|
204
|
-
signer.update(message);
|
|
205
|
-
signer.end();
|
|
206
|
-
const derSig = signer.sign({
|
|
207
|
-
key: Buffer.from(privateKeyDerBase64, "base64"),
|
|
208
|
-
format: "der",
|
|
209
|
-
type: "pkcs8"
|
|
210
|
-
});
|
|
211
|
-
return derToRawSignature(derSig);
|
|
212
|
-
}
|
|
213
|
-
function derToRawSignature(der) {
|
|
214
|
-
let offset = 0;
|
|
215
|
-
if (der[offset++] !== 48) throw new Error("Invalid DER signature");
|
|
216
|
-
offset++;
|
|
217
|
-
if (der[offset++] !== 2) throw new Error("Invalid DER signature");
|
|
218
|
-
const rLen = der[offset++];
|
|
219
|
-
const rBytes = der.subarray(offset, offset + rLen);
|
|
220
|
-
offset += rLen;
|
|
221
|
-
if (der[offset++] !== 2) throw new Error("Invalid DER signature");
|
|
222
|
-
const sLen = der[offset++];
|
|
223
|
-
const sBytes = der.subarray(offset, offset + sLen);
|
|
224
|
-
const r = padOrTrimTo32(rBytes);
|
|
225
|
-
const s = padOrTrimTo32(sBytes);
|
|
226
|
-
return Buffer.concat([r, s]).toString("hex");
|
|
227
|
-
}
|
|
228
|
-
function padOrTrimTo32(buf) {
|
|
229
|
-
if (buf.length === 32) return buf;
|
|
230
|
-
if (buf.length === 33 && buf[0] === 0) return buf.subarray(1);
|
|
231
|
-
if (buf.length < 32) {
|
|
232
|
-
const padded = Buffer.alloc(32);
|
|
233
|
-
buf.copy(padded, 32 - buf.length);
|
|
234
|
-
return padded;
|
|
235
|
-
}
|
|
236
|
-
return buf.subarray(buf.length - 32);
|
|
237
|
-
}
|
|
238
214
|
function decodeJwtPayload(jwt) {
|
|
239
215
|
const parts = jwt.split(".");
|
|
240
216
|
if (parts.length !== 3) throw new Error("Invalid JWT");
|
|
241
217
|
const payloadB64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
242
218
|
return JSON.parse(Buffer.from(payloadB64, "base64").toString("utf-8"));
|
|
243
219
|
}
|
|
244
|
-
async function proxyRequest(path, body, configId) {
|
|
245
|
-
if (!configId) throw new Error("TURNKEY_AUTH_PROXY_CONFIG_ID not configured.");
|
|
246
|
-
const res = await fetch(`${AUTH_PROXY_BASE}${path}`, {
|
|
247
|
-
method: "POST",
|
|
248
|
-
headers: {
|
|
249
|
-
"Content-Type": "application/json",
|
|
250
|
-
"X-Auth-Proxy-Config-ID": configId
|
|
251
|
-
},
|
|
252
|
-
body: JSON.stringify(body)
|
|
253
|
-
});
|
|
254
|
-
if (!res.ok) {
|
|
255
|
-
const text = await res.text().catch(() => "");
|
|
256
|
-
let detail = text;
|
|
257
|
-
try {
|
|
258
|
-
detail = JSON.parse(text).message ?? text;
|
|
259
|
-
} catch {
|
|
260
|
-
}
|
|
261
|
-
throw new Error(`Turnkey auth proxy error (${res.status}): ${detail}`);
|
|
262
|
-
}
|
|
263
|
-
return res.json();
|
|
264
|
-
}
|
|
265
|
-
async function otpInit(email, configId) {
|
|
266
|
-
return proxyRequest("/v1/otp_init", { otpType: "OTP_TYPE_EMAIL", contact: email }, configId);
|
|
267
|
-
}
|
|
268
|
-
async function otpVerify(otpId, otpCode, publicKeyHex, configId) {
|
|
269
|
-
return proxyRequest("/v1/otp_verify", { otpId, otpCode, publicKey: publicKeyHex }, configId);
|
|
270
|
-
}
|
|
271
|
-
async function checkAccount(email, configId, verificationToken) {
|
|
272
|
-
try {
|
|
273
|
-
const result = await proxyRequest(
|
|
274
|
-
"/v1/account",
|
|
275
|
-
{ filterType: "EMAIL", filterValue: email, ...verificationToken && { verificationToken } },
|
|
276
|
-
configId
|
|
277
|
-
);
|
|
278
|
-
return { exists: !!result.organizationId, organizationId: result.organizationId };
|
|
279
|
-
} catch {
|
|
280
|
-
return { exists: false };
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async function signup(email, configId, verificationToken, publicKeyHex, privateKeyDer) {
|
|
284
|
-
const decoded = decodeJwtPayload(verificationToken);
|
|
285
|
-
const verificationPublicKey = decoded.public_key ?? publicKeyHex;
|
|
286
|
-
const signupBody = {
|
|
287
|
-
userName: `${TURNKEY_SUB_ORG_PREFIX}-${email.split("@")[0]}`,
|
|
288
|
-
organizationName: `${TURNKEY_SUB_ORG_PREFIX}-${email}`,
|
|
289
|
-
userEmail: email,
|
|
290
|
-
verificationToken,
|
|
291
|
-
apiKeys: [],
|
|
292
|
-
authenticators: [],
|
|
293
|
-
oauthProviders: [],
|
|
294
|
-
wallet: {
|
|
295
|
-
walletName: "ChainStream Wallet",
|
|
296
|
-
accounts: [
|
|
297
|
-
{ curve: "CURVE_SECP256K1", pathFormat: "PATH_FORMAT_BIP32", path: "m/44'/60'/0'/0/0", addressFormat: "ADDRESS_FORMAT_ETHEREUM" },
|
|
298
|
-
{ curve: "CURVE_ED25519", pathFormat: "PATH_FORMAT_BIP32", path: "m/44'/501'/0'/0'", addressFormat: "ADDRESS_FORMAT_SOLANA" }
|
|
299
|
-
]
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
const signatureMessage = JSON.stringify({
|
|
303
|
-
signup: { email, apiKeys: signupBody.apiKeys, authenticators: signupBody.authenticators, oauthProviders: signupBody.oauthProviders },
|
|
304
|
-
tokenId: decoded.id,
|
|
305
|
-
type: "USAGE_TYPE_SIGNUP"
|
|
306
|
-
});
|
|
307
|
-
const signature = signRawP256(signatureMessage, privateKeyDer);
|
|
308
|
-
return proxyRequest("/v1/signup", {
|
|
309
|
-
...signupBody,
|
|
310
|
-
clientSignature: {
|
|
311
|
-
message: signatureMessage,
|
|
312
|
-
publicKey: verificationPublicKey,
|
|
313
|
-
scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
|
|
314
|
-
signature
|
|
315
|
-
}
|
|
316
|
-
}, configId);
|
|
317
|
-
}
|
|
318
|
-
async function otpLogin(verificationToken, publicKeyHex, privateKeyDerBase64, configId) {
|
|
319
|
-
const decoded = decodeJwtPayload(verificationToken);
|
|
320
|
-
const tokenUsageMessage = JSON.stringify({
|
|
321
|
-
login: { publicKey: publicKeyHex },
|
|
322
|
-
tokenId: decoded.id,
|
|
323
|
-
type: "USAGE_TYPE_LOGIN"
|
|
324
|
-
});
|
|
325
|
-
const signature = signRawP256(tokenUsageMessage, privateKeyDerBase64);
|
|
326
|
-
return proxyRequest("/v1/otp_login", {
|
|
327
|
-
verificationToken,
|
|
328
|
-
publicKey: publicKeyHex,
|
|
329
|
-
clientSignature: {
|
|
330
|
-
message: tokenUsageMessage,
|
|
331
|
-
publicKey: decoded.public_key ?? publicKeyHex,
|
|
332
|
-
scheme: "CLIENT_SIGNATURE_SCHEME_API_P256",
|
|
333
|
-
signature
|
|
334
|
-
}
|
|
335
|
-
}, configId);
|
|
336
|
-
}
|
|
337
|
-
async function completeLogin(otpId, otpCode, configId, email) {
|
|
338
|
-
const keyPair = generateP256KeyPair();
|
|
339
|
-
const { verificationToken } = await otpVerify(otpId, otpCode, keyPair.publicKeyHex, configId);
|
|
340
|
-
const account = await checkAccount(email, configId, verificationToken);
|
|
341
|
-
let isNewUser = false;
|
|
342
|
-
if (!account.exists) {
|
|
343
|
-
await signup(email, configId, verificationToken, keyPair.publicKeyHex, keyPair.privateKeyDer);
|
|
344
|
-
isNewUser = true;
|
|
345
|
-
}
|
|
346
|
-
const { session } = await otpLogin(verificationToken, keyPair.publicKeyHex, keyPair.privateKeyDer, configId);
|
|
347
|
-
const sessionPayload = decodeJwtPayload(session);
|
|
348
|
-
return {
|
|
349
|
-
session,
|
|
350
|
-
keyPair,
|
|
351
|
-
organizationId: sessionPayload.organization_id ?? "",
|
|
352
|
-
sessionExpiry: sessionPayload.exp,
|
|
353
|
-
isNewUser
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
220
|
function createApiStamp(body, publicKeyHex, privateKeyDerBase64) {
|
|
357
221
|
const signer = createSign("SHA256");
|
|
358
222
|
signer.update(body);
|
|
@@ -454,14 +318,11 @@ async function getSolanaAddress(creds) {
|
|
|
454
318
|
if (!sol) throw new Error("No Solana wallet found in Turnkey.");
|
|
455
319
|
return sol.address;
|
|
456
320
|
}
|
|
457
|
-
var
|
|
321
|
+
var TURNKEY_API_BASE;
|
|
458
322
|
var init_turnkey = __esm({
|
|
459
323
|
"src/lib/turnkey.ts"() {
|
|
460
324
|
"use strict";
|
|
461
|
-
init_constants();
|
|
462
|
-
AUTH_PROXY_BASE = "https://authproxy.turnkey.com";
|
|
463
325
|
TURNKEY_API_BASE = "https://api.turnkey.com";
|
|
464
|
-
TURNKEY_CONFIG_ID = TURNKEY_AUTH_PROXY_CONFIG_ID;
|
|
465
326
|
}
|
|
466
327
|
});
|
|
467
328
|
|
|
@@ -470,14 +331,18 @@ var TurnkeyWallet;
|
|
|
470
331
|
var init_turnkey_wallet = __esm({
|
|
471
332
|
"src/wallet/turnkey-wallet.ts"() {
|
|
472
333
|
"use strict";
|
|
334
|
+
init_types();
|
|
335
|
+
init_config();
|
|
473
336
|
init_turnkey();
|
|
474
337
|
TurnkeyWallet = class _TurnkeyWallet {
|
|
475
338
|
chain;
|
|
339
|
+
paymentChain;
|
|
476
340
|
address;
|
|
477
341
|
creds;
|
|
478
342
|
constructor(creds, chain, address) {
|
|
479
343
|
this.creds = creds;
|
|
480
|
-
this.
|
|
344
|
+
this.paymentChain = chain;
|
|
345
|
+
this.chain = toSdkChain(chain);
|
|
481
346
|
this.address = address;
|
|
482
347
|
}
|
|
483
348
|
static async create(creds) {
|
|
@@ -486,14 +351,14 @@ var init_turnkey_wallet = __esm({
|
|
|
486
351
|
getSolanaAddress(creds)
|
|
487
352
|
]);
|
|
488
353
|
return {
|
|
489
|
-
|
|
490
|
-
|
|
354
|
+
base: new _TurnkeyWallet(creds, "base", evmAddr),
|
|
355
|
+
sol: new _TurnkeyWallet(creds, "sol", solAddr)
|
|
491
356
|
};
|
|
492
357
|
}
|
|
493
358
|
async signMessage(message) {
|
|
494
359
|
let payloadHex;
|
|
495
360
|
let hashFunction;
|
|
496
|
-
if (this.
|
|
361
|
+
if (isEvmChain(this.paymentChain)) {
|
|
497
362
|
const prefix = `Ethereum Signed Message:
|
|
498
363
|
${message.length}`;
|
|
499
364
|
const prefixed = Buffer.concat([Buffer.from(prefix, "utf-8"), Buffer.from(message, "utf-8")]);
|
|
@@ -520,7 +385,7 @@ ${message.length}`;
|
|
|
520
385
|
);
|
|
521
386
|
const sigResult = result.activity?.result?.signRawPayloadResult;
|
|
522
387
|
if (!sigResult) throw new Error("Turnkey signMessage: no signature in response");
|
|
523
|
-
if (this.
|
|
388
|
+
if (isEvmChain(this.paymentChain)) {
|
|
524
389
|
const v = sigResult.v === "00" ? "1b" : "1c";
|
|
525
390
|
return `0x${sigResult.r}${sigResult.s}${v}`;
|
|
526
391
|
}
|
|
@@ -529,7 +394,7 @@ ${message.length}`;
|
|
|
529
394
|
async signTransaction(serializedTx) {
|
|
530
395
|
const txBytes = Buffer.from(serializedTx, "base64");
|
|
531
396
|
const unsignedHex = txBytes.toString("hex");
|
|
532
|
-
const txType = this.
|
|
397
|
+
const txType = isEvmChain(this.paymentChain) ? "TRANSACTION_TYPE_ETHEREUM" : "TRANSACTION_TYPE_SOLANA";
|
|
533
398
|
const result = await turnkeyRequest(
|
|
534
399
|
"/public/v1/submit/sign_transaction",
|
|
535
400
|
{
|
|
@@ -563,13 +428,15 @@ function createWallet(config, mode) {
|
|
|
563
428
|
return new RawKeyWallet(config.rawWallet.key, config.rawWallet.chain);
|
|
564
429
|
}
|
|
565
430
|
if (mode === "turnkey" && config.turnkey) {
|
|
566
|
-
const
|
|
431
|
+
const chain = config.walletChain ?? "base";
|
|
432
|
+
const address = isEvmChain(chain) ? config.turnkey.evmAddress : config.turnkey.solanaAddress;
|
|
567
433
|
if (!address) {
|
|
568
434
|
throw new Error(
|
|
569
|
-
|
|
435
|
+
`Turnkey ${chain} address not found in config.
|
|
436
|
+
Please re-login to resolve addresses: chainstream login`
|
|
570
437
|
);
|
|
571
438
|
}
|
|
572
|
-
return new TurnkeyWallet(config.turnkey,
|
|
439
|
+
return new TurnkeyWallet(config.turnkey, chain, address);
|
|
573
440
|
}
|
|
574
441
|
throw new Error(
|
|
575
442
|
"No wallet configured. Run:\n chainstream login # Create Turnkey wallet\n chainstream wallet set-raw # Use raw private key (dev)"
|
|
@@ -578,17 +445,18 @@ function createWallet(config, mode) {
|
|
|
578
445
|
async function createWalletWithAddresses(config, mode) {
|
|
579
446
|
if (mode === "raw" && config.rawWallet) {
|
|
580
447
|
const w = new RawKeyWallet(config.rawWallet.key, config.rawWallet.chain);
|
|
581
|
-
return config.rawWallet.chain
|
|
448
|
+
return isEvmChain(config.rawWallet.chain) ? { base: w } : { sol: w };
|
|
582
449
|
}
|
|
583
450
|
if (mode === "turnkey" && config.turnkey) {
|
|
584
451
|
const wallets = await TurnkeyWallet.create(config.turnkey);
|
|
585
|
-
return {
|
|
452
|
+
return { base: wallets.base, sol: wallets.sol };
|
|
586
453
|
}
|
|
587
454
|
throw new Error("No wallet configured.");
|
|
588
455
|
}
|
|
589
456
|
var init_wallet = __esm({
|
|
590
457
|
"src/wallet/index.ts"() {
|
|
591
458
|
"use strict";
|
|
459
|
+
init_config();
|
|
592
460
|
init_raw_wallet();
|
|
593
461
|
init_turnkey_wallet();
|
|
594
462
|
}
|
|
@@ -597,15 +465,18 @@ var init_wallet = __esm({
|
|
|
597
465
|
// src/lib/x402.ts
|
|
598
466
|
var x402_exports = {};
|
|
599
467
|
__export(x402_exports, {
|
|
600
|
-
autoPurchaseOnDemand: () => autoPurchaseOnDemand,
|
|
601
468
|
getPricing: () => getPricing,
|
|
602
|
-
getX402Fetch: () => getX402Fetch
|
|
469
|
+
getX402Fetch: () => getX402Fetch,
|
|
470
|
+
selectPlanInteractive: () => selectPlanInteractive
|
|
603
471
|
});
|
|
604
472
|
import { x402Client } from "@x402/core/client";
|
|
605
473
|
import { wrapFetchWithPayment } from "@x402/fetch";
|
|
606
474
|
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
475
|
+
import { ExactSvmScheme } from "@x402/svm/exact/client";
|
|
607
476
|
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
608
477
|
import { hashTypedData } from "viem";
|
|
478
|
+
import { createKeyPairSignerFromBytes } from "@solana/kit";
|
|
479
|
+
import * as readline from "readline/promises";
|
|
609
480
|
function createTurnkeyPaymentAccount(creds) {
|
|
610
481
|
const address = creds.evmAddress;
|
|
611
482
|
return {
|
|
@@ -637,55 +508,136 @@ function createTurnkeyPaymentAccount(creds) {
|
|
|
637
508
|
}
|
|
638
509
|
};
|
|
639
510
|
}
|
|
640
|
-
function
|
|
511
|
+
async function turnkeySolanaSign(creds, payload) {
|
|
512
|
+
const payloadHex = Buffer.from(payload).toString("hex");
|
|
513
|
+
const result = await turnkeyRequest(
|
|
514
|
+
"/public/v1/submit/sign_raw_payload",
|
|
515
|
+
{
|
|
516
|
+
type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
|
|
517
|
+
timestampMs: Date.now().toString(),
|
|
518
|
+
organizationId: creds.organizationId,
|
|
519
|
+
parameters: {
|
|
520
|
+
signWith: creds.solanaAddress,
|
|
521
|
+
payload: payloadHex,
|
|
522
|
+
encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
|
|
523
|
+
hashFunction: "HASH_FUNCTION_NOT_APPLICABLE"
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
creds
|
|
527
|
+
);
|
|
528
|
+
const sig = result.activity?.result?.signRawPayloadResult;
|
|
529
|
+
if (!sig) throw new Error("Turnkey Solana sign: no signature");
|
|
530
|
+
return new Uint8Array(Buffer.from(sig.r + sig.s, "hex"));
|
|
531
|
+
}
|
|
532
|
+
async function createTurnkeySolanaSigner(creds) {
|
|
533
|
+
const { createNoopSigner, address: solAddress } = await import("@solana/kit");
|
|
534
|
+
return createNoopSigner(solAddress(creds.solanaAddress));
|
|
535
|
+
}
|
|
536
|
+
function createTurnkeySvmScheme(creds, noopSigner) {
|
|
537
|
+
const inner = new ExactSvmScheme(noopSigner, { rpcUrl: "https://api.mainnet-beta.solana.com" });
|
|
538
|
+
return {
|
|
539
|
+
scheme: "exact",
|
|
540
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
541
|
+
const payload = await inner.createPaymentPayload(x402Version, paymentRequirements);
|
|
542
|
+
const wrapped = payload;
|
|
543
|
+
const txB64 = wrapped.payload.transaction;
|
|
544
|
+
const txBytes = new Uint8Array(Buffer.from(txB64, "base64"));
|
|
545
|
+
const sigCount = txBytes[0];
|
|
546
|
+
const messageStart = 1 + sigCount * 64;
|
|
547
|
+
const messageBytes = txBytes.slice(messageStart);
|
|
548
|
+
const sig = await turnkeySolanaSign(creds, messageBytes);
|
|
549
|
+
txBytes.set(sig, 1 + 64);
|
|
550
|
+
wrapped.payload.transaction = Buffer.from(txBytes).toString("base64");
|
|
551
|
+
return payload;
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function bs58Decode2(str) {
|
|
556
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
557
|
+
const BASE = 58;
|
|
558
|
+
const result = [0];
|
|
559
|
+
for (const char of str) {
|
|
560
|
+
let carry = ALPHABET.indexOf(char);
|
|
561
|
+
if (carry < 0) throw new Error(`Invalid base58 character: ${char}`);
|
|
562
|
+
for (let j = 0; j < result.length; j++) {
|
|
563
|
+
carry += result[j] * BASE;
|
|
564
|
+
result[j] = carry & 255;
|
|
565
|
+
carry >>= 8;
|
|
566
|
+
}
|
|
567
|
+
while (carry > 0) {
|
|
568
|
+
result.push(carry & 255);
|
|
569
|
+
carry >>= 8;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
for (const char of str) {
|
|
573
|
+
if (char !== "1") break;
|
|
574
|
+
result.push(0);
|
|
575
|
+
}
|
|
576
|
+
return new Uint8Array(result.reverse());
|
|
577
|
+
}
|
|
578
|
+
async function getX402Fetch(config) {
|
|
641
579
|
if (_x402Fetch) return _x402Fetch;
|
|
642
580
|
const client = new x402Client();
|
|
643
|
-
if (config.rawWallet
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
581
|
+
if (config.rawWallet) {
|
|
582
|
+
if (isEvmChain(config.rawWallet.chain)) {
|
|
583
|
+
const hex = config.rawWallet.key.startsWith("0x") ? config.rawWallet.key : `0x${config.rawWallet.key}`;
|
|
584
|
+
const account = privateKeyToAccount2(hex);
|
|
585
|
+
client.register("eip155:8453", new ExactEvmScheme(account));
|
|
586
|
+
} else if (isSolanaChain(config.rawWallet.chain)) {
|
|
587
|
+
const decoded = bs58Decode2(config.rawWallet.key);
|
|
588
|
+
const signer = await createKeyPairSignerFromBytes(new Uint8Array(decoded));
|
|
589
|
+
client.register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", new ExactSvmScheme(signer, { rpcUrl: "https://api.mainnet-beta.solana.com" }));
|
|
590
|
+
}
|
|
591
|
+
} else if (config.turnkey?.organizationId) {
|
|
592
|
+
const preferredChain = config.walletChain ?? "base";
|
|
593
|
+
if (isEvmChain(preferredChain) && config.turnkey.evmAddress) {
|
|
594
|
+
const account = createTurnkeyPaymentAccount(config.turnkey);
|
|
595
|
+
client.register("eip155:8453", new ExactEvmScheme(account));
|
|
596
|
+
} else if (isSolanaChain(preferredChain) && config.turnkey.solanaAddress) {
|
|
597
|
+
const noopSigner = await createTurnkeySolanaSigner(config.turnkey);
|
|
598
|
+
const scheme = createTurnkeySvmScheme(config.turnkey, noopSigner);
|
|
599
|
+
client.register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", scheme);
|
|
600
|
+
}
|
|
650
601
|
}
|
|
651
602
|
_x402Fetch = wrapFetchWithPayment(fetch, client);
|
|
652
603
|
return _x402Fetch;
|
|
653
604
|
}
|
|
654
|
-
async function autoPurchaseOnDemand(config, plan = "nano") {
|
|
655
|
-
const x402Fetch = getX402Fetch(config);
|
|
656
|
-
process.stderr.write(`[chainstream] No active subscription. Auto-purchasing ${plan} plan...
|
|
657
|
-
`);
|
|
658
|
-
try {
|
|
659
|
-
const resp = await x402Fetch(`${config.baseUrl}/x402/purchase?plan=${encodeURIComponent(plan)}`);
|
|
660
|
-
if (!resp.ok) {
|
|
661
|
-
const text = await resp.text().catch(() => "");
|
|
662
|
-
process.stderr.write(`[chainstream] Purchase failed (${resp.status}): ${text}
|
|
663
|
-
`);
|
|
664
|
-
return { success: false };
|
|
665
|
-
}
|
|
666
|
-
const result = await resp.json();
|
|
667
|
-
process.stderr.write(`[chainstream] Subscription activated: ${result.plan} (expires: ${result.expires_at})
|
|
668
|
-
`);
|
|
669
|
-
return {
|
|
670
|
-
success: true,
|
|
671
|
-
plan: result.plan,
|
|
672
|
-
expiresAt: result.expires_at
|
|
673
|
-
};
|
|
674
|
-
} catch (err) {
|
|
675
|
-
process.stderr.write(`[chainstream] Auto-purchase error: ${err instanceof Error ? err.message : err}
|
|
676
|
-
`);
|
|
677
|
-
return { success: false };
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
605
|
async function getPricing(baseUrl) {
|
|
681
606
|
const resp = await fetch(`${baseUrl}/x402/pricing`);
|
|
682
607
|
if (!resp.ok) throw new Error(`Failed to get pricing (${resp.status})`);
|
|
683
608
|
return resp.json();
|
|
684
609
|
}
|
|
610
|
+
async function selectPlanInteractive(baseUrl) {
|
|
611
|
+
const pricing = await getPricing(baseUrl);
|
|
612
|
+
const plans = pricing.plans.sort((a, b) => a.price_usd - b.price_usd);
|
|
613
|
+
process.stderr.write("\n[chainstream] No active subscription. Available plans:\n\n");
|
|
614
|
+
process.stderr.write(" # Plan Price Quota Duration\n");
|
|
615
|
+
process.stderr.write(" \u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
|
|
616
|
+
for (let i = 0; i < plans.length; i++) {
|
|
617
|
+
const p = plans[i];
|
|
618
|
+
const num = String(i + 1).padStart(2);
|
|
619
|
+
const name = p.name.padEnd(10);
|
|
620
|
+
const price = `$${p.price_usd}`.padEnd(8);
|
|
621
|
+
const quota = p.quota_total.toLocaleString().padStart(14) + " CU";
|
|
622
|
+
const days = `${p.duration_days} days`;
|
|
623
|
+
process.stderr.write(` ${num} ${name} ${price} ${quota} ${days}
|
|
624
|
+
`);
|
|
625
|
+
}
|
|
626
|
+
process.stderr.write("\n");
|
|
627
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
628
|
+
const answer = await rl.question(`Select plan (1-${plans.length}): `);
|
|
629
|
+
rl.close();
|
|
630
|
+
const idx = parseInt(answer.trim(), 10) - 1;
|
|
631
|
+
if (idx < 0 || idx >= plans.length || Number.isNaN(idx)) {
|
|
632
|
+
throw new Error("Invalid plan selection. Aborting purchase.");
|
|
633
|
+
}
|
|
634
|
+
return plans[idx].name;
|
|
635
|
+
}
|
|
685
636
|
var _x402Fetch;
|
|
686
637
|
var init_x402 = __esm({
|
|
687
638
|
"src/lib/x402.ts"() {
|
|
688
639
|
"use strict";
|
|
640
|
+
init_config();
|
|
689
641
|
init_turnkey();
|
|
690
642
|
_x402Fetch = null;
|
|
691
643
|
}
|
|
@@ -721,7 +673,7 @@ function createClient() {
|
|
|
721
673
|
return _client;
|
|
722
674
|
}
|
|
723
675
|
throw new Error(
|
|
724
|
-
"Not authenticated. Run one of:\n chainstream login #
|
|
676
|
+
"Not authenticated. Run one of:\n chainstream login # Create Turnkey wallet (no email)\n chainstream login --email # Email OTP login\n chainstream wallet set-raw # Import private key (dev)\n chainstream config set --key apiKey --value <key> # API key only"
|
|
725
677
|
);
|
|
726
678
|
}
|
|
727
679
|
function requireWallet() {
|
|
@@ -738,27 +690,78 @@ async function callWithAutoPayment(fn, retried = false) {
|
|
|
738
690
|
try {
|
|
739
691
|
return await fn();
|
|
740
692
|
} catch (err) {
|
|
741
|
-
const
|
|
742
|
-
if (
|
|
693
|
+
const paymentInfo = extractPaymentInfo(err);
|
|
694
|
+
if (paymentInfo && !retried && _wallet) {
|
|
743
695
|
const config = loadConfig();
|
|
744
|
-
|
|
745
|
-
if (
|
|
696
|
+
let purchaseUrl = resolvePurchaseUrl(config.baseUrl, paymentInfo);
|
|
697
|
+
if (config.plan) {
|
|
698
|
+
purchaseUrl = replacePlanInUrl(purchaseUrl, config.plan);
|
|
699
|
+
process.stderr.write(`[chainstream] No active subscription. Purchasing ${config.plan} plan...
|
|
700
|
+
`);
|
|
701
|
+
} else if (process.stdin.isTTY) {
|
|
702
|
+
const chosenPlan = await selectPlanInteractive(config.baseUrl);
|
|
703
|
+
purchaseUrl = replacePlanInUrl(purchaseUrl, chosenPlan);
|
|
704
|
+
process.stderr.write(`[chainstream] Purchasing ${chosenPlan} plan...
|
|
705
|
+
`);
|
|
706
|
+
} else {
|
|
707
|
+
process.stderr.write(`[chainstream] No active subscription. Purchasing via x402...
|
|
708
|
+
`);
|
|
709
|
+
process.stderr.write(`[chainstream] Resource: ${purchaseUrl}
|
|
710
|
+
`);
|
|
711
|
+
}
|
|
712
|
+
const x402Fetch = await getX402Fetch(config);
|
|
713
|
+
const resp = await x402Fetch(purchaseUrl);
|
|
714
|
+
if (resp.ok) {
|
|
715
|
+
const result = await resp.json();
|
|
716
|
+
process.stderr.write(`[chainstream] Subscription activated: ${result.plan} (expires: ${result.expires_at})
|
|
717
|
+
`);
|
|
746
718
|
_client = null;
|
|
747
719
|
createClient();
|
|
748
720
|
return callWithAutoPayment(fn, true);
|
|
749
721
|
}
|
|
722
|
+
const text = await resp.text().catch(() => "");
|
|
723
|
+
process.stderr.write(`[chainstream] Purchase failed (${resp.status}): ${text}
|
|
724
|
+
`);
|
|
750
725
|
}
|
|
751
726
|
throw err;
|
|
752
727
|
}
|
|
753
728
|
}
|
|
754
|
-
function
|
|
755
|
-
if (!err || typeof err !== "object") return
|
|
729
|
+
function extractPaymentInfo(err) {
|
|
730
|
+
if (!err || typeof err !== "object") return null;
|
|
756
731
|
const axiosErr = err;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
732
|
+
const status = axiosErr.response?.status ?? axiosErr.status;
|
|
733
|
+
const message = axiosErr.message ?? "";
|
|
734
|
+
if (status === 402) {
|
|
735
|
+
const header = axiosErr.response?.headers?.["payment-required"] ?? axiosErr.response?.headers?.["x-payment-required"];
|
|
736
|
+
if (header) {
|
|
737
|
+
try {
|
|
738
|
+
const decoded = JSON.parse(Buffer.from(header, "base64").toString());
|
|
739
|
+
return { resourceUrl: decoded.resource?.url };
|
|
740
|
+
} catch {
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return {};
|
|
744
|
+
}
|
|
745
|
+
if (message.includes("402") || message.includes("PAYMENT_REQUIRED") || message.includes("no active x402 subscription")) {
|
|
746
|
+
return {};
|
|
747
|
+
}
|
|
748
|
+
return null;
|
|
749
|
+
}
|
|
750
|
+
function resolvePurchaseUrl(baseUrl, info) {
|
|
751
|
+
if (info.resourceUrl) {
|
|
752
|
+
if (info.resourceUrl.startsWith("http")) return info.resourceUrl;
|
|
753
|
+
return `${baseUrl}${info.resourceUrl}`;
|
|
754
|
+
}
|
|
755
|
+
return `${baseUrl}/x402/purchase?plan=nano`;
|
|
756
|
+
}
|
|
757
|
+
function replacePlanInUrl(url, plan) {
|
|
758
|
+
try {
|
|
759
|
+
const u = new URL(url);
|
|
760
|
+
u.searchParams.set("plan", plan);
|
|
761
|
+
return u.toString();
|
|
762
|
+
} catch {
|
|
763
|
+
return url.replace(/plan=[^&]+/, `plan=${encodeURIComponent(plan)}`);
|
|
764
|
+
}
|
|
762
765
|
}
|
|
763
766
|
|
|
764
767
|
// src/lib/validate.ts
|
|
@@ -971,7 +974,7 @@ function registerMarketCommands(program2) {
|
|
|
971
974
|
|
|
972
975
|
// src/commands/wallet.ts
|
|
973
976
|
init_config();
|
|
974
|
-
import * as
|
|
977
|
+
import * as readline2 from "readline/promises";
|
|
975
978
|
function registerWalletCommands(program2) {
|
|
976
979
|
const wallet = program2.command("wallet").description("Wallet analytics and management");
|
|
977
980
|
wallet.command("profile").description("Wallet profile: PnL + net worth + top holdings").requiredOption("--chain <chain>", "Chain: sol/bsc/eth").requiredOption("--address <address>", "Wallet address").option("--raw", "Single-line JSON output").action(async (opts) => {
|
|
@@ -1040,22 +1043,22 @@ function registerWalletCommands(program2) {
|
|
|
1040
1043
|
const walletMode = (await Promise.resolve().then(() => (init_config(), config_exports))).getWalletMode(config);
|
|
1041
1044
|
if (walletMode === "turnkey" && config.turnkey) {
|
|
1042
1045
|
if (config.turnkey.evmAddress || config.turnkey.solanaAddress) {
|
|
1043
|
-
if (config.turnkey.evmAddress) process.stdout.write(`
|
|
1046
|
+
if (config.turnkey.evmAddress) process.stdout.write(`Base: ${config.turnkey.evmAddress}
|
|
1044
1047
|
`);
|
|
1045
1048
|
if (config.turnkey.solanaAddress) process.stdout.write(`Solana: ${config.turnkey.solanaAddress}
|
|
1046
1049
|
`);
|
|
1047
1050
|
} else {
|
|
1048
1051
|
const { createWalletWithAddresses: createWalletWithAddresses2 } = await Promise.resolve().then(() => (init_wallet(), wallet_exports));
|
|
1049
1052
|
const wallets = await createWalletWithAddresses2(config, "turnkey");
|
|
1050
|
-
if (wallets.
|
|
1053
|
+
if (wallets.base) process.stdout.write(`Base: ${wallets.base.address}
|
|
1051
1054
|
`);
|
|
1052
|
-
if (wallets.
|
|
1055
|
+
if (wallets.sol) process.stdout.write(`Solana: ${wallets.sol.address}
|
|
1053
1056
|
`);
|
|
1054
1057
|
}
|
|
1055
1058
|
} else if (config.rawWallet) {
|
|
1056
1059
|
const { createWallet: createWallet2 } = await Promise.resolve().then(() => (init_wallet(), wallet_exports));
|
|
1057
1060
|
const w = createWallet2(config, "raw");
|
|
1058
|
-
process.stdout.write(`${w.
|
|
1061
|
+
process.stdout.write(`${w.paymentChain.toUpperCase()}: ${w.address}
|
|
1059
1062
|
`);
|
|
1060
1063
|
} else {
|
|
1061
1064
|
process.stdout.write("No wallet configured. Run: chainstream login\n");
|
|
@@ -1091,9 +1094,9 @@ function registerWalletCommands(program2) {
|
|
|
1091
1094
|
exitOnError(err);
|
|
1092
1095
|
}
|
|
1093
1096
|
});
|
|
1094
|
-
wallet.command("set-raw").description("Set raw private key (dev/testing only)").requiredOption("--chain <chain>", "Chain:
|
|
1097
|
+
wallet.command("set-raw").description("Set raw private key (dev/testing only)").requiredOption("--chain <chain>", "Chain: base/sol").action(async (opts) => {
|
|
1095
1098
|
try {
|
|
1096
|
-
const rl =
|
|
1099
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
|
|
1097
1100
|
process.stdout.write("\u26A0 WARNING: Raw private key will be stored in plaintext.\n");
|
|
1098
1101
|
process.stdout.write(" Use Turnkey (chainstream login) for production.\n\n");
|
|
1099
1102
|
const key = await rl.question("Enter private key: ");
|
|
@@ -1139,7 +1142,7 @@ function registerKytCommands(program2) {
|
|
|
1139
1142
|
}
|
|
1140
1143
|
|
|
1141
1144
|
// src/commands/dex.ts
|
|
1142
|
-
import * as
|
|
1145
|
+
import * as readline3 from "readline/promises";
|
|
1143
1146
|
function registerDexCommands(program2) {
|
|
1144
1147
|
const dex = program2.command("dex").description("DEX swap, quote, and token creation");
|
|
1145
1148
|
dex.command("quote").description("Get swap quote (read-only)").requiredOption("--chain <chain>", "Chain: sol/bsc/eth").requiredOption("--input-token <addr>", "Input token (address or SOL/ETH/BNB/USDC)").requiredOption("--output-token <addr>", "Output token (address or SOL/ETH/BNB/USDC)").requiredOption("--amount <amount>", "Input amount (smallest unit)").option("--raw", "Single-line JSON output").action(async (opts) => {
|
|
@@ -1191,7 +1194,7 @@ function registerDexCommands(program2) {
|
|
|
1191
1194
|
`);
|
|
1192
1195
|
process.stderr.write("--------------------\n\n");
|
|
1193
1196
|
if (!opts.yes) {
|
|
1194
|
-
const rl =
|
|
1197
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
1195
1198
|
const answer = await rl.question("Confirm swap? (y/N): ");
|
|
1196
1199
|
rl.close();
|
|
1197
1200
|
if (answer.toLowerCase() !== "y") {
|
|
@@ -1346,7 +1349,7 @@ function saveKey(keyPair, profile = DEFAULT_PROFILE) {
|
|
|
1346
1349
|
// src/commands/auth.ts
|
|
1347
1350
|
init_turnkey();
|
|
1348
1351
|
init_constants();
|
|
1349
|
-
import * as
|
|
1352
|
+
import * as readline4 from "readline/promises";
|
|
1350
1353
|
async function resolveAndStoreAddresses(turnkeyCreds) {
|
|
1351
1354
|
process.stderr.write("Resolving wallet addresses...\n");
|
|
1352
1355
|
const [evmAddress, solanaAddress] = await Promise.all([
|
|
@@ -1358,7 +1361,7 @@ async function resolveAndStoreAddresses(turnkeyCreds) {
|
|
|
1358
1361
|
});
|
|
1359
1362
|
}
|
|
1360
1363
|
function registerAuthCommands(program2) {
|
|
1361
|
-
program2.command("login").description("Create wallet (default) or login via email OTP
|
|
1364
|
+
program2.command("login").description("Create wallet (default: --key) or login via email OTP").argument("[email]", "Email address for OTP login").option("--key", "Key-based login \u2014 no email required (default)").option("--email", "Email OTP login").action(async (emailArg, opts) => {
|
|
1362
1365
|
try {
|
|
1363
1366
|
if (opts.email || emailArg) {
|
|
1364
1367
|
await doEmailLogin(emailArg);
|
|
@@ -1371,35 +1374,7 @@ function registerAuthCommands(program2) {
|
|
|
1371
1374
|
});
|
|
1372
1375
|
program2.command("verify").description("Verify email OTP (second step of login)").requiredOption("--otp-id <id>", "OTP ID from login step").requiredOption("--code <code>", "OTP code from email").requiredOption("--email <email>", "Email used in login step").action(async (opts) => {
|
|
1373
1376
|
try {
|
|
1374
|
-
|
|
1375
|
-
if (!configId) throw new Error("TURNKEY_AUTH_PROXY_CONFIG_ID not set.");
|
|
1376
|
-
process.stderr.write("Verifying OTP...\n");
|
|
1377
|
-
const result = await completeLogin(opts.otpId, opts.code, configId, opts.email);
|
|
1378
|
-
saveKey({
|
|
1379
|
-
publicKeyHex: result.keyPair.publicKeyHex,
|
|
1380
|
-
uncompressedPublicKeyHex: result.keyPair.uncompressedPublicKeyHex,
|
|
1381
|
-
privateKeyDer: result.keyPair.privateKeyDer,
|
|
1382
|
-
organizationId: result.organizationId
|
|
1383
|
-
});
|
|
1384
|
-
const turnkeyCreds = {
|
|
1385
|
-
publicKeyHex: result.keyPair.publicKeyHex,
|
|
1386
|
-
privateKeyDer: result.keyPair.privateKeyDer,
|
|
1387
|
-
organizationId: result.organizationId,
|
|
1388
|
-
sessionToken: result.session,
|
|
1389
|
-
sessionExpiry: result.sessionExpiry
|
|
1390
|
-
};
|
|
1391
|
-
updateConfig({ turnkey: turnkeyCreds });
|
|
1392
|
-
await resolveAndStoreAddresses(turnkeyCreds);
|
|
1393
|
-
const config = loadConfig();
|
|
1394
|
-
if (result.isNewUser) {
|
|
1395
|
-
process.stdout.write("Welcome! Your ChainStream wallet has been created.\n");
|
|
1396
|
-
} else {
|
|
1397
|
-
process.stdout.write("Logged in successfully.\n");
|
|
1398
|
-
}
|
|
1399
|
-
process.stdout.write(` EVM: ${config.turnkey?.evmAddress}
|
|
1400
|
-
`);
|
|
1401
|
-
process.stdout.write(` Solana: ${config.turnkey?.solanaAddress}
|
|
1402
|
-
`);
|
|
1377
|
+
await completeEmailLogin(opts.otpId, opts.code, opts.email);
|
|
1403
1378
|
} catch (err) {
|
|
1404
1379
|
exitOnError(err);
|
|
1405
1380
|
}
|
|
@@ -1433,12 +1408,12 @@ function signTimestamp(timestampMs, privateKeyDerBase64) {
|
|
|
1433
1408
|
});
|
|
1434
1409
|
return sig.toString("hex");
|
|
1435
1410
|
}
|
|
1436
|
-
async function
|
|
1411
|
+
async function authServiceRequest(path, body) {
|
|
1437
1412
|
const authUrl = loadConfig().authUrl ?? CHAINSTREAM_AUTH_URL;
|
|
1438
|
-
const res = await fetch(`${authUrl}
|
|
1413
|
+
const res = await fetch(`${authUrl}${path}`, {
|
|
1439
1414
|
method: "POST",
|
|
1440
1415
|
headers: { "Content-Type": "application/json" },
|
|
1441
|
-
body: JSON.stringify(
|
|
1416
|
+
body: JSON.stringify(body)
|
|
1442
1417
|
});
|
|
1443
1418
|
if (!res.ok) {
|
|
1444
1419
|
const text = await res.text().catch(() => "");
|
|
@@ -1451,6 +1426,15 @@ async function callKeyAuth(publicKeyHex, uncompressedPublicKeyHex, timestampMs,
|
|
|
1451
1426
|
}
|
|
1452
1427
|
return res.json();
|
|
1453
1428
|
}
|
|
1429
|
+
async function callKeyAuth(publicKeyHex, uncompressedPublicKeyHex, timestampMs, signature) {
|
|
1430
|
+
return authServiceRequest("/api/auth/key", { publicKeyHex, uncompressedPublicKeyHex, timestampMs, signature });
|
|
1431
|
+
}
|
|
1432
|
+
async function callOtpInit(email, organizationId) {
|
|
1433
|
+
return authServiceRequest("/api/auth/otp-init", { email, ...organizationId && { organizationId } });
|
|
1434
|
+
}
|
|
1435
|
+
async function callOtpVerify(otpId, otpCode, targetPublicKey, organizationId) {
|
|
1436
|
+
return authServiceRequest("/api/auth/otp-verify", { otpId, otpCode, targetPublicKey, ...organizationId && { organizationId } });
|
|
1437
|
+
}
|
|
1454
1438
|
async function doKeyLogin() {
|
|
1455
1439
|
const existingKey = loadKey();
|
|
1456
1440
|
if (existingKey?.organizationId) {
|
|
@@ -1537,10 +1521,8 @@ async function doKeyLogin() {
|
|
|
1537
1521
|
`);
|
|
1538
1522
|
}
|
|
1539
1523
|
async function doEmailLogin(email) {
|
|
1540
|
-
const configId = TURNKEY_CONFIG_ID;
|
|
1541
|
-
if (!configId) throw new Error("TURNKEY_AUTH_PROXY_CONFIG_ID not set. Contact ChainStream support.");
|
|
1542
1524
|
if (!email) {
|
|
1543
|
-
const rl2 =
|
|
1525
|
+
const rl2 = readline4.createInterface({ input: process.stdin, output: process.stderr });
|
|
1544
1526
|
email = await rl2.question("Enter your email: ");
|
|
1545
1527
|
rl2.close();
|
|
1546
1528
|
if (!email?.trim()) throw new Error("Email required.");
|
|
@@ -1548,7 +1530,7 @@ async function doEmailLogin(email) {
|
|
|
1548
1530
|
}
|
|
1549
1531
|
process.stderr.write(`Sending OTP to ${email}...
|
|
1550
1532
|
`);
|
|
1551
|
-
const { otpId } = await
|
|
1533
|
+
const { otpId } = await callOtpInit(email);
|
|
1552
1534
|
if (!process.stdin.isTTY) {
|
|
1553
1535
|
process.stdout.write(JSON.stringify({ otpId, email }) + "\n");
|
|
1554
1536
|
process.stderr.write("Non-interactive mode: complete login with:\n");
|
|
@@ -1556,36 +1538,47 @@ async function doEmailLogin(email) {
|
|
|
1556
1538
|
`);
|
|
1557
1539
|
return;
|
|
1558
1540
|
}
|
|
1559
|
-
const rl =
|
|
1541
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
|
|
1560
1542
|
const code = await rl.question("Enter OTP code: ");
|
|
1561
1543
|
rl.close();
|
|
1562
1544
|
if (!code?.trim()) throw new Error("OTP code required.");
|
|
1545
|
+
await completeEmailLogin(otpId, code.trim(), email);
|
|
1546
|
+
}
|
|
1547
|
+
async function completeEmailLogin(otpId, code, email) {
|
|
1548
|
+
const keyPair = generateP256KeyPair();
|
|
1563
1549
|
process.stderr.write("Verifying...\n");
|
|
1564
|
-
const result = await
|
|
1550
|
+
const result = await authServiceRequest("/api/auth/email-login", {
|
|
1551
|
+
otpId,
|
|
1552
|
+
otpCode: code,
|
|
1553
|
+
email,
|
|
1554
|
+
publicKeyHex: keyPair.publicKeyHex,
|
|
1555
|
+
uncompressedPublicKeyHex: keyPair.uncompressedPublicKeyHex
|
|
1556
|
+
});
|
|
1565
1557
|
saveKey({
|
|
1566
|
-
publicKeyHex:
|
|
1567
|
-
uncompressedPublicKeyHex:
|
|
1568
|
-
privateKeyDer:
|
|
1569
|
-
organizationId: result.
|
|
1558
|
+
publicKeyHex: keyPair.publicKeyHex,
|
|
1559
|
+
uncompressedPublicKeyHex: keyPair.uncompressedPublicKeyHex,
|
|
1560
|
+
privateKeyDer: keyPair.privateKeyDer,
|
|
1561
|
+
organizationId: result.orgId
|
|
1562
|
+
});
|
|
1563
|
+
updateConfig({
|
|
1564
|
+
turnkey: {
|
|
1565
|
+
publicKeyHex: keyPair.publicKeyHex,
|
|
1566
|
+
privateKeyDer: keyPair.privateKeyDer,
|
|
1567
|
+
organizationId: result.orgId,
|
|
1568
|
+
sessionToken: "",
|
|
1569
|
+
sessionExpiry: 0,
|
|
1570
|
+
evmAddress: result.evmAddress,
|
|
1571
|
+
solanaAddress: result.solanaAddress
|
|
1572
|
+
}
|
|
1570
1573
|
});
|
|
1571
|
-
const turnkeyCreds = {
|
|
1572
|
-
publicKeyHex: result.keyPair.publicKeyHex,
|
|
1573
|
-
privateKeyDer: result.keyPair.privateKeyDer,
|
|
1574
|
-
organizationId: result.organizationId,
|
|
1575
|
-
sessionToken: result.session,
|
|
1576
|
-
sessionExpiry: result.sessionExpiry
|
|
1577
|
-
};
|
|
1578
|
-
updateConfig({ turnkey: turnkeyCreds });
|
|
1579
|
-
await resolveAndStoreAddresses(turnkeyCreds);
|
|
1580
|
-
const config = loadConfig();
|
|
1581
1574
|
if (result.isNewUser) {
|
|
1582
1575
|
process.stdout.write("Welcome! Your ChainStream wallet has been created.\n");
|
|
1583
1576
|
} else {
|
|
1584
1577
|
process.stdout.write("Logged in successfully.\n");
|
|
1585
1578
|
}
|
|
1586
|
-
process.stdout.write(` EVM: ${
|
|
1579
|
+
process.stdout.write(` EVM: ${result.evmAddress}
|
|
1587
1580
|
`);
|
|
1588
|
-
process.stdout.write(` Solana: ${
|
|
1581
|
+
process.stdout.write(` Solana: ${result.solanaAddress}
|
|
1589
1582
|
`);
|
|
1590
1583
|
}
|
|
1591
1584
|
async function doBindEmail(email) {
|
|
@@ -1593,10 +1586,8 @@ async function doBindEmail(email) {
|
|
|
1593
1586
|
if (!config.turnkey?.organizationId) {
|
|
1594
1587
|
throw new Error("No wallet found. Run 'chainstream login' first.");
|
|
1595
1588
|
}
|
|
1596
|
-
const configId = TURNKEY_CONFIG_ID;
|
|
1597
|
-
if (!configId) throw new Error("TURNKEY_AUTH_PROXY_CONFIG_ID not set.");
|
|
1598
1589
|
if (!email) {
|
|
1599
|
-
const rl2 =
|
|
1590
|
+
const rl2 = readline4.createInterface({ input: process.stdin, output: process.stderr });
|
|
1600
1591
|
email = await rl2.question("Enter email to bind: ");
|
|
1601
1592
|
rl2.close();
|
|
1602
1593
|
if (!email?.trim()) throw new Error("Email required.");
|
|
@@ -1604,24 +1595,29 @@ async function doBindEmail(email) {
|
|
|
1604
1595
|
}
|
|
1605
1596
|
process.stderr.write(`Sending verification code to ${email}...
|
|
1606
1597
|
`);
|
|
1607
|
-
const { otpId } = await
|
|
1598
|
+
const { otpId } = await callOtpInit(email);
|
|
1608
1599
|
if (!process.stdin.isTTY) {
|
|
1609
|
-
process.stdout.write(JSON.stringify({ otpId, email }) + "\n");
|
|
1610
|
-
process.stderr.write("Non-interactive mode.
|
|
1611
|
-
process.stderr.write(
|
|
1600
|
+
process.stdout.write(JSON.stringify({ otpId, email, step: "verify" }) + "\n");
|
|
1601
|
+
process.stderr.write("Non-interactive mode. Complete with:\n");
|
|
1602
|
+
process.stderr.write(` chainstream bind-email-verify --otp-id ${otpId} --code <code> --email ${email}
|
|
1603
|
+
`);
|
|
1612
1604
|
return;
|
|
1613
1605
|
}
|
|
1614
|
-
const rl =
|
|
1606
|
+
const rl = readline4.createInterface({ input: process.stdin, output: process.stderr });
|
|
1615
1607
|
const code = await rl.question("Enter verification code: ");
|
|
1616
1608
|
rl.close();
|
|
1617
1609
|
if (!code?.trim()) throw new Error("Verification code required.");
|
|
1618
1610
|
process.stderr.write("Verifying...\n");
|
|
1619
|
-
const { verificationToken } = await
|
|
1620
|
-
|
|
1611
|
+
const { verificationToken } = await callOtpVerify(
|
|
1612
|
+
otpId,
|
|
1613
|
+
code.trim(),
|
|
1614
|
+
config.turnkey.publicKeyHex
|
|
1615
|
+
);
|
|
1616
|
+
process.stderr.write("Binding email to wallet...\n");
|
|
1621
1617
|
await updateTurnkeyUserEmail(email, verificationToken, config.turnkey);
|
|
1622
1618
|
process.stdout.write(`Email ${email} bound successfully.
|
|
1623
1619
|
`);
|
|
1624
|
-
process.stdout.write("You can now use
|
|
1620
|
+
process.stdout.write("You can now use 'chainstream login --email' with this email.\n");
|
|
1625
1621
|
}
|
|
1626
1622
|
|
|
1627
1623
|
// src/commands/config-cmd.ts
|
|
@@ -1630,7 +1626,7 @@ function registerConfigCommands(program2) {
|
|
|
1630
1626
|
const config = program2.command("config").description("Configuration management");
|
|
1631
1627
|
config.command("set").description("Set a configuration value").requiredOption("--key <key>", "Config key (apiKey, baseUrl)").requiredOption("--value <value>", "Config value").action((opts) => {
|
|
1632
1628
|
try {
|
|
1633
|
-
const allowedKeys = ["apiKey", "baseUrl"];
|
|
1629
|
+
const allowedKeys = ["apiKey", "baseUrl", "walletChain", "plan"];
|
|
1634
1630
|
if (!allowedKeys.includes(opts.key)) {
|
|
1635
1631
|
throw new Error(`Invalid key "${opts.key}". Allowed: ${allowedKeys.join(", ")}`);
|
|
1636
1632
|
}
|
|
@@ -1690,7 +1686,7 @@ function registerConfigCommands(program2) {
|
|
|
1690
1686
|
} else {
|
|
1691
1687
|
process.stdout.write("Auth: Not configured\n");
|
|
1692
1688
|
process.stdout.write(" Run: chainstream login # Turnkey wallet\n");
|
|
1693
|
-
process.stdout.write(" Run: chainstream config set apiKey <key> # API key\n");
|
|
1689
|
+
process.stdout.write(" Run: chainstream config set --key apiKey --value <key> # API key\n");
|
|
1694
1690
|
}
|
|
1695
1691
|
} catch (err) {
|
|
1696
1692
|
exitOnError(err);
|