nansen-cli 1.35.0 → 1.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +34 -1
- package/package.json +1 -1
- package/skills/nansen-trading/SKILL.md +49 -1
- package/src/api.js +3 -1
- package/src/bridge.js +1102 -0
- package/src/cli.js +130 -7
- package/src/hl-action.js +528 -0
- package/src/hl-client.js +168 -0
- package/src/hl-env.js +37 -0
- package/src/keychain.js +6 -2
- package/src/limit-order.js +18 -4
- package/src/perp.js +835 -0
- package/src/rpc-urls.js +18 -11
- package/src/schema.json +415 -6
- package/src/trading.js +162 -17
- package/src/wallet-signing.js +87 -0
package/src/trading.js
CHANGED
|
@@ -80,7 +80,7 @@ export function resolveTokenAddress(symbolOrAddress, chainName) {
|
|
|
80
80
|
* @returns {Promise<*>} Parsed result value
|
|
81
81
|
* @throws {Error} If chain has no configured RPC or the RPC returns an error
|
|
82
82
|
*/
|
|
83
|
-
async function evmRpcCall(chain, method, params = []) {
|
|
83
|
+
export async function evmRpcCall(chain, method, params = []) {
|
|
84
84
|
const rpcUrl = CHAIN_RPCS[chain];
|
|
85
85
|
if (!rpcUrl) throw new Error(`No RPC URL configured for chain: ${chain}`);
|
|
86
86
|
const res = await fetch(rpcUrl, {
|
|
@@ -99,13 +99,13 @@ async function evmRpcCall(chain, method, params = []) {
|
|
|
99
99
|
return body.result;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function getQuotesDir() {
|
|
102
|
+
export function getQuotesDir() {
|
|
103
103
|
const configDir = path.join(process.env.HOME || process.env.USERPROFILE || '', '.nansen');
|
|
104
104
|
return path.join(configDir, 'quotes');
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Resolve a filename inside the quotes dir, rejecting path traversal.
|
|
108
|
-
function safeQuotesPath(filename) {
|
|
108
|
+
export function safeQuotesPath(filename) {
|
|
109
109
|
const base = path.resolve(getQuotesDir());
|
|
110
110
|
const target = path.resolve(base, filename);
|
|
111
111
|
if (path.relative(base, target).startsWith('..')) return null;
|
|
@@ -390,7 +390,7 @@ export function saveQuote(quoteResponse, chain, signerType = 'local', privyWalle
|
|
|
390
390
|
const hash = crypto.randomBytes(4).toString('hex');
|
|
391
391
|
const quoteId = `${timestamp}-${hash}`;
|
|
392
392
|
|
|
393
|
-
const data = { quoteId, chain, timestamp, signerType, response: quoteResponse };
|
|
393
|
+
const data = { quoteId, type: 'swap', chain, timestamp, signerType, response: quoteResponse };
|
|
394
394
|
if (toChain) data.toChain = toChain;
|
|
395
395
|
if (privyWalletIds) data.privyWalletIds = privyWalletIds;
|
|
396
396
|
|
|
@@ -412,6 +412,11 @@ export function loadQuote(quoteId) {
|
|
|
412
412
|
fs.unlinkSync(filePath);
|
|
413
413
|
throw new Error('Quote has expired. Please request a new quote.');
|
|
414
414
|
}
|
|
415
|
+
// Guard against running a bridge quote through the swap path. Older swap
|
|
416
|
+
// quotes predate the `type` field, so only reject a known-mismatched type.
|
|
417
|
+
if (data.type && data.type !== 'swap') {
|
|
418
|
+
throw new Error(`Quote "${quoteId}" is a ${data.type} quote. Use the matching command (e.g. "nansen bridge execute" for a bridge quote).`);
|
|
419
|
+
}
|
|
415
420
|
return data;
|
|
416
421
|
}
|
|
417
422
|
|
|
@@ -498,25 +503,29 @@ export function signSolanaTransaction(transactionBase64, privateKeyHex) {
|
|
|
498
503
|
* { to, data, value?, gas?, gasPrice? }
|
|
499
504
|
*
|
|
500
505
|
* The nonce must be fetched from the chain RPC.
|
|
501
|
-
* Signs as a legacy (type 0) transaction with gasPrice (matching the e2e tests).
|
|
502
506
|
*
|
|
503
|
-
*
|
|
507
|
+
* Emits an EIP-1559 (type 2) transaction when the quote supplies fee-cap
|
|
508
|
+
* fields, and a legacy (type 0) one otherwise. Quotes from the trading API and
|
|
509
|
+
* from Relay both carry maxFeePerGas/maxPriorityFeePerGas, so type 2 is the
|
|
510
|
+
* normal path; flattening those into a single legacy gasPrice — as this used to
|
|
511
|
+
* do — discards the fee cap the aggregator computed and leaves the transaction
|
|
512
|
+
* unincludable the moment the base fee rises past it.
|
|
513
|
+
*
|
|
514
|
+
* @param {object} txData - Transaction fields from a quote { to, data, value, gas, gasPrice | maxFeePerGas + maxPriorityFeePerGas }
|
|
504
515
|
* @param {string} privateKeyHex - 64-char hex (32-byte secp256k1 private key)
|
|
505
516
|
* @param {string} chain - Chain name
|
|
506
517
|
* @param {number} nonce - Account nonce
|
|
507
518
|
* @returns {string} 0x-prefixed signed transaction hex
|
|
508
519
|
*/
|
|
509
520
|
// ⚠️ SECURITY: EVM transaction signing - requires thorough review before production use
|
|
510
|
-
// TODO: Always signs as legacy (type 0) transactions. Do we need EIP-1559 (type 2) support?
|
|
511
521
|
export function signEvmTransaction(txData, privateKeyHex, chain, nonce) {
|
|
512
522
|
const chainConfig = CHAIN_MAP[chain];
|
|
513
523
|
if (!chainConfig || chainConfig.type !== 'evm') {
|
|
514
524
|
throw new Error(`Unsupported EVM chain: ${chain}`);
|
|
515
525
|
}
|
|
516
526
|
|
|
517
|
-
const
|
|
527
|
+
const common = {
|
|
518
528
|
nonce,
|
|
519
|
-
gasPrice: toHex(txData.gasPrice || txData.maxFeePerGas || '1'),
|
|
520
529
|
gasLimit: toHex(txData.gas || txData.gasLimit || '210000'),
|
|
521
530
|
to: txData.to,
|
|
522
531
|
value: toHex(txData.value || '0'),
|
|
@@ -524,18 +533,82 @@ export function signEvmTransaction(txData, privateKeyHex, chain, nonce) {
|
|
|
524
533
|
chainId: chainConfig.chainId,
|
|
525
534
|
};
|
|
526
535
|
|
|
527
|
-
|
|
536
|
+
if (txData.maxFeePerGas) {
|
|
537
|
+
return signEip1559Transaction({
|
|
538
|
+
...common,
|
|
539
|
+
maxFeePerGas: toHex(txData.maxFeePerGas),
|
|
540
|
+
// A zero priority fee is a valid choice but not a sane default, so fall
|
|
541
|
+
// back to the fee cap rather than to nothing when the quote omits it.
|
|
542
|
+
maxPriorityFeePerGas: toHex(txData.maxPriorityFeePerGas || txData.maxFeePerGas),
|
|
543
|
+
}, privateKeyHex);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Previously this fell back to a gasPrice of 1 wei, which signs a transaction
|
|
547
|
+
// that can never be mined and burns the nonce. Refuse instead: a quote with no
|
|
548
|
+
// fee information at all is a bug upstream, not something to sign through.
|
|
549
|
+
if (!txData.gasPrice) {
|
|
550
|
+
throw new Error(
|
|
551
|
+
'Quote supplied no gas price (expected gasPrice or maxFeePerGas), so any signed transaction would be unmineable. Refusing to sign.',
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return signLegacyTransaction({ ...common, gasPrice: toHex(txData.gasPrice) }, privateKeyHex);
|
|
528
556
|
}
|
|
529
557
|
|
|
558
|
+
// How many queued-but-unmined transactions we are willing to sign past.
|
|
559
|
+
//
|
|
560
|
+
// `pending` counts mempool-queued transactions as well as mined ones, and that is
|
|
561
|
+
// what callers want: the bridge signs its approve and deposit steps back to back,
|
|
562
|
+
// so the second has to be numbered after the first while the first is still
|
|
563
|
+
// pending. But a transaction that *cannot* be mined — priced below what the chain
|
|
564
|
+
// is currently including — keeps the count elevated for as long as it sits there,
|
|
565
|
+
// and every later signature is numbered behind it, unexecutable until it clears.
|
|
566
|
+
//
|
|
567
|
+
// One or two in flight is normal for a multi-step run. Beyond that, something is
|
|
568
|
+
// wedged, and adding another transaction to the queue cannot help.
|
|
569
|
+
const MAX_PENDING_NONCE_GAP = 2;
|
|
570
|
+
|
|
530
571
|
/**
|
|
531
|
-
* Fetch the
|
|
572
|
+
* Fetch the next nonce for an EVM address, reconciled against the mined count.
|
|
573
|
+
*
|
|
574
|
+
* Returns a DECIMAL number, not a hex string — callers must not decode it again.
|
|
575
|
+
* (bridge.js did, and `parseInt(20, 16)` is 32: a wallet at nonce 20 signed at
|
|
576
|
+
* 32, which no node can execute. It only showed up past nonce 9, where decimal
|
|
577
|
+
* and hex digits diverge.)
|
|
578
|
+
*
|
|
532
579
|
* @param {string} chain - Chain name
|
|
533
580
|
* @param {string} address - 0x address
|
|
534
|
-
* @returns {Promise<number>}
|
|
581
|
+
* @returns {Promise<number>} Next nonce, decimal
|
|
535
582
|
*/
|
|
536
583
|
export async function getEvmNonce(chain, address) {
|
|
537
|
-
const
|
|
538
|
-
|
|
584
|
+
const [pendingHex, latestHex] = await Promise.all([
|
|
585
|
+
evmRpcCall(chain, 'eth_getTransactionCount', [address, 'pending']),
|
|
586
|
+
evmRpcCall(chain, 'eth_getTransactionCount', [address, 'latest']),
|
|
587
|
+
]);
|
|
588
|
+
const pending = parseInt(pendingHex, 16);
|
|
589
|
+
const latest = parseInt(latestHex, 16);
|
|
590
|
+
if (!Number.isInteger(pending) || !Number.isInteger(latest)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`Could not read the nonce for ${address} on ${chain} (pending: ${pendingHex}, latest: ${latestHex}).`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const gap = pending - latest;
|
|
597
|
+
if (gap > MAX_PENDING_NONCE_GAP) {
|
|
598
|
+
// Refuse rather than pile on. Signing at `pending` here produces a
|
|
599
|
+
// transaction that cannot execute until everything ahead of it does, and the
|
|
600
|
+
// symptom the operator sees is only "no receipt" — no indication that the
|
|
601
|
+
// real problem is a transaction from an earlier run.
|
|
602
|
+
throw new Error(
|
|
603
|
+
`${address} has ${gap} unmined transactions queued on ${chain} (next mined nonce ${latest}, next pending ${pending}). `
|
|
604
|
+
+ `Signing another would queue behind them and stay unexecutable until they clear. `
|
|
605
|
+
+ `Replace the transaction at nonce ${latest} with a higher fee first: request a fresh quote and run `
|
|
606
|
+
+ `"nansen bridge execute --quote <id> --nonce ${latest} --priority-fee <gwei>". `
|
|
607
|
+
+ `Note that a load-balanced public RPC may deny holding a transaction it does in fact hold, so do not diagnose from one endpoint.`,
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return pending;
|
|
539
612
|
}
|
|
540
613
|
|
|
541
614
|
/**
|
|
@@ -544,11 +617,18 @@ export async function getEvmNonce(chain, address) {
|
|
|
544
617
|
*
|
|
545
618
|
* @param {string} chain - Chain name
|
|
546
619
|
* @param {string} txHash - Transaction hash (0x...)
|
|
547
|
-
*
|
|
620
|
+
* The default window is deliberately generous: by the time this is called the
|
|
621
|
+
* transaction is already broadcast, so giving up early converts "still
|
|
622
|
+
* confirming" into a hard failure the caller has to interpret, without undoing
|
|
623
|
+
* anything. A tight 30s window did exactly that during a real Base deposit.
|
|
624
|
+
*
|
|
625
|
+
* @param {string} chain - Chain name
|
|
626
|
+
* @param {string} txHash - Transaction hash (0x...)
|
|
627
|
+
* @param {number} [timeoutMs=180000] - Max wait time
|
|
548
628
|
* @param {number} [pollMs=2000] - Poll interval
|
|
549
629
|
* @returns {Promise<object>} Transaction receipt
|
|
550
630
|
*/
|
|
551
|
-
export async function waitForReceipt(chain, txHash, timeoutMs =
|
|
631
|
+
export async function waitForReceipt(chain, txHash, timeoutMs = 180000, pollMs = 2000) {
|
|
552
632
|
const start = Date.now();
|
|
553
633
|
while (Date.now() - start < timeoutMs) {
|
|
554
634
|
try {
|
|
@@ -741,6 +821,49 @@ export function signLegacyTransaction(tx, privateKeyHex) {
|
|
|
741
821
|
return '0x' + rlpEncode(signedFields).toString('hex');
|
|
742
822
|
}
|
|
743
823
|
|
|
824
|
+
/**
|
|
825
|
+
* Sign an EIP-1559 (type 2) transaction.
|
|
826
|
+
*
|
|
827
|
+
* Envelope: 0x02 || RLP([chainId, nonce, maxPriorityFeePerGas, maxFeePerGas,
|
|
828
|
+
* gasLimit, to, value, data, accessList, yParity, r, s]).
|
|
829
|
+
*
|
|
830
|
+
* Type 2 exists here because a legacy transaction pays exactly `gasPrice`: once
|
|
831
|
+
* the base fee rises above it the transaction is not slow, it is permanently
|
|
832
|
+
* unincludable at that nonce. A type-2 transaction pays baseFee + priority
|
|
833
|
+
* capped at maxFeePerGas, so it rides fee movement instead of dying.
|
|
834
|
+
*
|
|
835
|
+
* Note yParity is the raw recovery bit (0/1), not EIP-155's chainId*2+35+bit —
|
|
836
|
+
* the chain id is already a first-class field in the payload.
|
|
837
|
+
*/
|
|
838
|
+
export function signEip1559Transaction(tx, privateKeyHex) {
|
|
839
|
+
const payloadFields = [
|
|
840
|
+
rlpNormalize(tx.chainId),
|
|
841
|
+
rlpNormalize(tx.nonce),
|
|
842
|
+
rlpNormalize(tx.maxPriorityFeePerGas),
|
|
843
|
+
rlpNormalize(tx.maxFeePerGas),
|
|
844
|
+
rlpNormalize(tx.gasLimit),
|
|
845
|
+
toBuffer(tx.to),
|
|
846
|
+
rlpNormalize(tx.value),
|
|
847
|
+
toBuffer(tx.data || '0x'),
|
|
848
|
+
[], // accessList — always empty; we never build access-listed transactions
|
|
849
|
+
];
|
|
850
|
+
|
|
851
|
+
const msgHash = keccak256(Buffer.concat([Buffer.from([0x02]), rlpEncode(payloadFields)]));
|
|
852
|
+
const { r, s, v: recoveryBit } = signSecp256k1(msgHash, Buffer.from(privateKeyHex, 'hex'));
|
|
853
|
+
|
|
854
|
+
const signed = Buffer.concat([
|
|
855
|
+
Buffer.from([0x02]),
|
|
856
|
+
rlpEncode([
|
|
857
|
+
...payloadFields,
|
|
858
|
+
rlpNormalize(recoveryBit),
|
|
859
|
+
stripLeadingZeros(r),
|
|
860
|
+
stripLeadingZeros(s),
|
|
861
|
+
]),
|
|
862
|
+
]);
|
|
863
|
+
|
|
864
|
+
return '0x' + signed.toString('hex');
|
|
865
|
+
}
|
|
866
|
+
|
|
744
867
|
export function toBuffer(v) {
|
|
745
868
|
if (Buffer.isBuffer(v)) return v;
|
|
746
869
|
if (typeof v === 'string') {
|
|
@@ -1074,6 +1197,19 @@ export function buildTradingCommands(deps = {}) {
|
|
|
1074
1197
|
'INVALID_AGGREGATOR'
|
|
1075
1198
|
);
|
|
1076
1199
|
}
|
|
1200
|
+
// Slippage is a decimal fraction (0.03 = 3%). Reject non-numeric or
|
|
1201
|
+
// out-of-range values so a percent-vs-decimal mix-up (e.g. "3" meaning 3%)
|
|
1202
|
+
// can't become a 300% slippage tolerance.
|
|
1203
|
+
for (const [optName, optVal] of [['slippage', slippage], ['max-auto-slippage', maxAutoSlippage]]) {
|
|
1204
|
+
if (optVal == null) continue;
|
|
1205
|
+
const n = Number(optVal);
|
|
1206
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
1207
|
+
throw new CommandError(
|
|
1208
|
+
`Invalid --${optName} "${optVal}". Use a decimal between 0 and 1 (e.g. 0.03 for 3%).`,
|
|
1209
|
+
'INVALID_SLIPPAGE'
|
|
1210
|
+
);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1077
1213
|
|
|
1078
1214
|
if (!chain || !from || !to || !amount) {
|
|
1079
1215
|
throw new CommandError(`
|
|
@@ -1399,7 +1535,16 @@ EXAMPLES:
|
|
|
1399
1535
|
}
|
|
1400
1536
|
|
|
1401
1537
|
// --quote-index pins a specific quote (no fallback)
|
|
1402
|
-
|
|
1538
|
+
let pinIndex = null;
|
|
1539
|
+
if (options['quote-index'] != null) {
|
|
1540
|
+
pinIndex = parseInt(options['quote-index'], 10);
|
|
1541
|
+
if (!Number.isInteger(pinIndex) || pinIndex < 0 || pinIndex >= allQuotes.length) {
|
|
1542
|
+
throw new CommandError(
|
|
1543
|
+
`❌ Invalid --quote-index "${options['quote-index']}". Must be an integer between 0 and ${allQuotes.length - 1}.`,
|
|
1544
|
+
'INVALID_QUOTE_INDEX',
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1403
1548
|
const startIndex = pinIndex ?? 0;
|
|
1404
1549
|
const endIndex = pinIndex != null ? startIndex + 1 : allQuotes.length;
|
|
1405
1550
|
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nansen CLI — shared wallet resolution for the money paths (perp, bridge).
|
|
3
|
+
*
|
|
4
|
+
* Every command that signs needs the same three things: the wallet's EVM
|
|
5
|
+
* address, a clear error when the wallet is encrypted but no password reached
|
|
6
|
+
* us, and the signing material (a local private key, or a Privy handle).
|
|
7
|
+
*
|
|
8
|
+
* These lived twice — once in perp.js, once in bridge.js — and the copies had
|
|
9
|
+
* already drifted: perp.js grew an explicit PASSWORD_REQUIRED error while
|
|
10
|
+
* bridge.js kept calling exportWallet(name, null), which reports "Incorrect
|
|
11
|
+
* password" for a password that was never entered. One copy means the next fix
|
|
12
|
+
* can't land on one path and miss the other.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { CommandError } from './api.js';
|
|
16
|
+
import { retrievePassword } from './keychain.js';
|
|
17
|
+
import { exportWallet, getWalletConfig, showWallet } from './wallet.js';
|
|
18
|
+
|
|
19
|
+
const EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
|
|
20
|
+
|
|
21
|
+
// Resolve --wallet (or the configured default) to a wallet with a usable EVM
|
|
22
|
+
// address. `context` names the feature in the error so the message stays
|
|
23
|
+
// actionable ("Hyperliquid perp trading requires an EVM wallet").
|
|
24
|
+
//
|
|
25
|
+
// Returns the resolved name alongside the address so callers can hand the same
|
|
26
|
+
// resolution to resolveSigningCredentials instead of looking the wallet up a
|
|
27
|
+
// second time — a second lookup can read a different wallet if the default
|
|
28
|
+
// changed in between, and it re-reads the wallet file for no reason.
|
|
29
|
+
export function resolveEvmWallet(walletName, context = 'This command') {
|
|
30
|
+
const config = getWalletConfig();
|
|
31
|
+
const name = walletName || config.defaultWallet;
|
|
32
|
+
const wallet = name ? showWallet(name) : undefined;
|
|
33
|
+
if (!wallet) {
|
|
34
|
+
throw new CommandError('No wallet found. Create one with: nansen wallet create', 'NO_WALLET');
|
|
35
|
+
}
|
|
36
|
+
if (!wallet.evm || !EVM_ADDRESS_RE.test(wallet.evm)) {
|
|
37
|
+
throw new CommandError(
|
|
38
|
+
`Wallet "${wallet.name || name}" has no valid EVM address. ${context} requires an EVM wallet.`,
|
|
39
|
+
'INVALID_WALLET',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
name: wallet.name || name,
|
|
44
|
+
address: wallet.evm,
|
|
45
|
+
provider: wallet.provider || 'local',
|
|
46
|
+
privyWalletIds: wallet.privyWalletIds || null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The local private key for an already-resolved wallet.
|
|
51
|
+
export function resolvePrivateKey(wallet) {
|
|
52
|
+
const config = getWalletConfig();
|
|
53
|
+
let password = null;
|
|
54
|
+
if (config.passwordHash) {
|
|
55
|
+
const { password: pw, source } = retrievePassword();
|
|
56
|
+
if (source === 'file') {
|
|
57
|
+
process.stderr.write('⚠️ Password loaded from ~/.nansen/wallets/.credentials (insecure).\n');
|
|
58
|
+
}
|
|
59
|
+
password = pw;
|
|
60
|
+
// Distinguish "no password reached us" from "wrong password": without this,
|
|
61
|
+
// exportWallet(name, null) fails with the misleading "Incorrect password"
|
|
62
|
+
// even though nothing was entered. Mirror trade/limit-order's
|
|
63
|
+
// PASSWORD_REQUIRED.
|
|
64
|
+
if (!password) {
|
|
65
|
+
throw new CommandError('Wallet is encrypted and no password was found.', 'PASSWORD_REQUIRED', {
|
|
66
|
+
error: 'PASSWORD_REQUIRED',
|
|
67
|
+
message: 'Wallet is encrypted and no password was found.',
|
|
68
|
+
resolution: [
|
|
69
|
+
'Set NANSEN_WALLET_PASSWORD environment variable',
|
|
70
|
+
'Or run: nansen wallet create (password is saved to OS keychain automatically)',
|
|
71
|
+
],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const exported = exportWallet(wallet.name, password);
|
|
76
|
+
return exported.evm.privateKey;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Signing material for an already-resolved wallet. Privy wallets have no
|
|
80
|
+
// exportable key — the caller signs through the Privy client instead, using the
|
|
81
|
+
// privyWalletIds already on `wallet`.
|
|
82
|
+
export function resolveSigningCredentials(wallet) {
|
|
83
|
+
if (wallet.provider === 'privy') {
|
|
84
|
+
return { provider: 'privy', privateKey: null };
|
|
85
|
+
}
|
|
86
|
+
return { provider: 'local', privateKey: resolvePrivateKey(wallet) };
|
|
87
|
+
}
|