@elizaos/autonomous 2.0.0-alpha.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/LICENSE +21 -0
- package/package.json +270 -0
- package/src/actions/emote.ts +101 -0
- package/src/actions/restart.ts +101 -0
- package/src/actions/send-message.ts +168 -0
- package/src/actions/stream-control.ts +439 -0
- package/src/actions/switch-stream-source.ts +126 -0
- package/src/actions/terminal.ts +186 -0
- package/src/api/agent-admin-routes.ts +178 -0
- package/src/api/agent-lifecycle-routes.ts +129 -0
- package/src/api/agent-model.ts +143 -0
- package/src/api/agent-transfer-routes.ts +211 -0
- package/src/api/apps-routes.ts +210 -0
- package/src/api/auth-routes.ts +90 -0
- package/src/api/bsc-trade.ts +736 -0
- package/src/api/bug-report-routes.ts +161 -0
- package/src/api/character-routes.ts +421 -0
- package/src/api/cloud-billing-routes.ts +598 -0
- package/src/api/cloud-compat-routes.ts +192 -0
- package/src/api/cloud-routes.ts +529 -0
- package/src/api/cloud-status-routes.ts +234 -0
- package/src/api/compat-utils.ts +154 -0
- package/src/api/connector-health.ts +135 -0
- package/src/api/coordinator-wiring.ts +179 -0
- package/src/api/credit-detection.ts +47 -0
- package/src/api/database.ts +1357 -0
- package/src/api/diagnostics-routes.ts +389 -0
- package/src/api/drop-service.ts +205 -0
- package/src/api/early-logs.ts +111 -0
- package/src/api/http-helpers.ts +252 -0
- package/src/api/index.ts +85 -0
- package/src/api/knowledge-routes.ts +1189 -0
- package/src/api/knowledge-service-loader.ts +92 -0
- package/src/api/memory-bounds.ts +121 -0
- package/src/api/memory-routes.ts +349 -0
- package/src/api/merkle-tree.ts +239 -0
- package/src/api/models-routes.ts +72 -0
- package/src/api/nfa-routes.ts +169 -0
- package/src/api/nft-verify.ts +188 -0
- package/src/api/og-tracker.ts +72 -0
- package/src/api/parse-action-block.ts +145 -0
- package/src/api/permissions-routes.ts +222 -0
- package/src/api/plugin-validation.ts +355 -0
- package/src/api/provider-switch-config.ts +455 -0
- package/src/api/registry-routes.ts +165 -0
- package/src/api/registry-service.ts +292 -0
- package/src/api/route-helpers.ts +21 -0
- package/src/api/sandbox-routes.ts +1480 -0
- package/src/api/server.ts +17674 -0
- package/src/api/signal-routes.ts +265 -0
- package/src/api/stream-persistence.ts +297 -0
- package/src/api/stream-route-state.ts +48 -0
- package/src/api/stream-routes.ts +1046 -0
- package/src/api/stream-voice-routes.ts +208 -0
- package/src/api/streaming-text.ts +129 -0
- package/src/api/streaming-types.ts +23 -0
- package/src/api/subscription-routes.ts +283 -0
- package/src/api/terminal-run-limits.ts +31 -0
- package/src/api/training-backend-check.ts +40 -0
- package/src/api/training-routes.ts +314 -0
- package/src/api/training-service-like.ts +46 -0
- package/src/api/trajectory-routes.ts +714 -0
- package/src/api/trigger-routes.ts +438 -0
- package/src/api/twitter-verify.ts +226 -0
- package/src/api/tx-service.ts +193 -0
- package/src/api/wallet-dex-prices.ts +206 -0
- package/src/api/wallet-evm-balance.ts +989 -0
- package/src/api/wallet-routes.ts +505 -0
- package/src/api/wallet-rpc.ts +523 -0
- package/src/api/wallet-trading-profile.ts +694 -0
- package/src/api/wallet.ts +745 -0
- package/src/api/whatsapp-routes.ts +282 -0
- package/src/api/zip-utils.ts +130 -0
- package/src/auth/anthropic.ts +63 -0
- package/src/auth/apply-stealth.ts +38 -0
- package/src/auth/claude-code-stealth.ts +141 -0
- package/src/auth/credentials.ts +226 -0
- package/src/auth/index.ts +18 -0
- package/src/auth/openai-codex.ts +94 -0
- package/src/auth/types.ts +24 -0
- package/src/awareness/registry.ts +220 -0
- package/src/bin.ts +10 -0
- package/src/cli/index.ts +36 -0
- package/src/cli/parse-duration.ts +43 -0
- package/src/cloud/auth.test.ts +370 -0
- package/src/cloud/auth.ts +176 -0
- package/src/cloud/backup.test.ts +150 -0
- package/src/cloud/backup.ts +50 -0
- package/src/cloud/base-url.ts +45 -0
- package/src/cloud/bridge-client.test.ts +481 -0
- package/src/cloud/bridge-client.ts +307 -0
- package/src/cloud/cloud-manager.test.ts +223 -0
- package/src/cloud/cloud-manager.ts +151 -0
- package/src/cloud/cloud-proxy.test.ts +122 -0
- package/src/cloud/cloud-proxy.ts +52 -0
- package/src/cloud/index.ts +23 -0
- package/src/cloud/reconnect.test.ts +178 -0
- package/src/cloud/reconnect.ts +108 -0
- package/src/cloud/validate-url.test.ts +147 -0
- package/src/cloud/validate-url.ts +176 -0
- package/src/config/character-schema.ts +44 -0
- package/src/config/config.ts +149 -0
- package/src/config/env-vars.ts +86 -0
- package/src/config/includes.ts +196 -0
- package/src/config/index.ts +15 -0
- package/src/config/object-utils.ts +10 -0
- package/src/config/paths.ts +92 -0
- package/src/config/plugin-auto-enable.ts +520 -0
- package/src/config/schema.ts +1342 -0
- package/src/config/telegram-custom-commands.ts +99 -0
- package/src/config/types.agent-defaults.ts +342 -0
- package/src/config/types.agents.ts +112 -0
- package/src/config/types.gateway.ts +243 -0
- package/src/config/types.hooks.ts +124 -0
- package/src/config/types.messages.ts +201 -0
- package/src/config/types.milady.ts +791 -0
- package/src/config/types.tools.ts +416 -0
- package/src/config/types.ts +7 -0
- package/src/config/zod-schema.agent-runtime.ts +777 -0
- package/src/config/zod-schema.core.ts +778 -0
- package/src/config/zod-schema.hooks.ts +139 -0
- package/src/config/zod-schema.providers-core.ts +1126 -0
- package/src/config/zod-schema.session.ts +98 -0
- package/src/config/zod-schema.ts +865 -0
- package/src/contracts/apps.ts +46 -0
- package/src/contracts/awareness.ts +56 -0
- package/src/contracts/config.ts +172 -0
- package/src/contracts/drop.ts +21 -0
- package/src/contracts/index.ts +8 -0
- package/src/contracts/onboarding.ts +592 -0
- package/src/contracts/permissions.ts +52 -0
- package/src/contracts/verification.ts +9 -0
- package/src/contracts/wallet.ts +503 -0
- package/src/diagnostics/integration-observability.ts +132 -0
- package/src/emotes/catalog.ts +655 -0
- package/src/external-modules.d.ts +7 -0
- package/src/hooks/discovery.test.ts +357 -0
- package/src/hooks/discovery.ts +231 -0
- package/src/hooks/eligibility.ts +146 -0
- package/src/hooks/hooks.test.ts +320 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/loader.test.ts +418 -0
- package/src/hooks/loader.ts +256 -0
- package/src/hooks/registry.test.ts +168 -0
- package/src/hooks/registry.ts +74 -0
- package/src/hooks/types.ts +121 -0
- package/src/index.ts +19 -0
- package/src/onboarding-presets.ts +828 -0
- package/src/plugins/custom-rtmp/index.ts +40 -0
- package/src/providers/admin-trust.ts +76 -0
- package/src/providers/session-bridge.ts +143 -0
- package/src/providers/session-utils.ts +42 -0
- package/src/providers/simple-mode.ts +113 -0
- package/src/providers/ui-catalog.ts +135 -0
- package/src/providers/workspace-provider.ts +213 -0
- package/src/providers/workspace.ts +497 -0
- package/src/runtime/agent-event-service.ts +57 -0
- package/src/runtime/cloud-onboarding.test.ts +489 -0
- package/src/runtime/cloud-onboarding.ts +408 -0
- package/src/runtime/core-plugins.ts +53 -0
- package/src/runtime/custom-actions.ts +605 -0
- package/src/runtime/eliza.ts +4941 -0
- package/src/runtime/embedding-presets.ts +73 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/milady-plugin.ts +180 -0
- package/src/runtime/onboarding-names.ts +76 -0
- package/src/runtime/release-plugin-policy.ts +119 -0
- package/src/runtime/restart.ts +59 -0
- package/src/runtime/trajectory-persistence.ts +2584 -0
- package/src/runtime/version.ts +6 -0
- package/src/security/audit-log.ts +222 -0
- package/src/security/network-policy.ts +91 -0
- package/src/server/index.ts +6 -0
- package/src/services/agent-export.ts +976 -0
- package/src/services/app-manager.ts +755 -0
- package/src/services/browser-capture.ts +215 -0
- package/src/services/coding-agent-context.ts +355 -0
- package/src/services/fallback-training-service.ts +196 -0
- package/src/services/index.ts +17 -0
- package/src/services/mcp-marketplace.ts +327 -0
- package/src/services/plugin-manager-types.ts +185 -0
- package/src/services/privy-wallets.ts +352 -0
- package/src/services/registry-client-app-meta.ts +201 -0
- package/src/services/registry-client-endpoints.ts +253 -0
- package/src/services/registry-client-local.ts +485 -0
- package/src/services/registry-client-network.ts +173 -0
- package/src/services/registry-client-queries.ts +176 -0
- package/src/services/registry-client-types.ts +104 -0
- package/src/services/registry-client.ts +366 -0
- package/src/services/remote-signing-service.ts +261 -0
- package/src/services/sandbox-engine.ts +753 -0
- package/src/services/sandbox-manager.ts +503 -0
- package/src/services/self-updater.ts +213 -0
- package/src/services/signal-pairing.ts +189 -0
- package/src/services/signing-policy.ts +230 -0
- package/src/services/skill-catalog-client.ts +195 -0
- package/src/services/skill-marketplace.ts +909 -0
- package/src/services/stream-manager.ts +707 -0
- package/src/services/tts-stream-bridge.ts +465 -0
- package/src/services/update-checker.ts +163 -0
- package/src/services/version-compat.ts +367 -0
- package/src/services/whatsapp-pairing.ts +279 -0
- package/src/shared/ui-catalog-prompt.ts +1158 -0
- package/src/test-support/process-helpers.ts +35 -0
- package/src/test-support/route-test-helpers.ts +113 -0
- package/src/test-support/test-helpers.ts +304 -0
- package/src/testing/index.ts +3 -0
- package/src/triggers/action.ts +342 -0
- package/src/triggers/runtime.ts +432 -0
- package/src/triggers/scheduling.ts +472 -0
- package/src/triggers/types.ts +133 -0
- package/src/types/app-hyperscape-routes-shim.d.ts +29 -0
- package/src/types/external-modules.d.ts +7 -0
- package/src/utils/exec-safety.ts +23 -0
- package/src/utils/number-parsing.ts +112 -0
- package/src/utils/spoken-text.ts +65 -0
- package/src/version-resolver.ts +60 -0
- package/test/api/agent-admin-routes.test.ts +160 -0
- package/test/api/agent-lifecycle-routes.test.ts +164 -0
- package/test/api/agent-transfer-routes.test.ts +136 -0
- package/test/api/apps-routes.test.ts +140 -0
- package/test/api/auth-routes.test.ts +160 -0
- package/test/api/bug-report-routes.test.ts +88 -0
- package/test/api/knowledge-routes.test.ts +73 -0
- package/test/api/lifecycle.test.ts +342 -0
- package/test/api/memory-routes.test.ts +74 -0
- package/test/api/models-routes.test.ts +112 -0
- package/test/api/nfa-routes.test.ts +78 -0
- package/test/api/permissions-routes.test.ts +185 -0
- package/test/api/registry-routes.test.ts +157 -0
- package/test/api/signal-routes.test.ts +113 -0
- package/test/api/subscription-routes.test.ts +90 -0
- package/test/api/trigger-routes.test.ts +87 -0
- package/test/api/wallet-routes.observability.test.ts +191 -0
- package/test/api/wallet-routes.test.ts +502 -0
- package/test/diagnostics/integration-observability.test.ts +135 -0
- package/test/security/audit-log.test.ts +229 -0
- package/test/security/network-policy.test.ts +143 -0
- package/test/services/version-compat.test.ts +127 -0
- package/tsconfig.build.json +21 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVM balance fetching — Alchemy, Ankr, and direct-RPC fallback paths.
|
|
3
|
+
*
|
|
4
|
+
* Handles multi-chain EVM balance + NFT retrieval with provider-key resolution
|
|
5
|
+
* and automatic fallback to public RPC endpoints when premium APIs are unavailable.
|
|
6
|
+
*/
|
|
7
|
+
import { logger } from "@elizaos/core";
|
|
8
|
+
import type {
|
|
9
|
+
EvmChainBalance,
|
|
10
|
+
EvmNft,
|
|
11
|
+
EvmTokenBalance,
|
|
12
|
+
} from "../contracts/wallet";
|
|
13
|
+
import {
|
|
14
|
+
computeValueUsd,
|
|
15
|
+
type DexTokenMeta,
|
|
16
|
+
fetchDexPrices,
|
|
17
|
+
WRAPPED_NATIVE,
|
|
18
|
+
} from "./wallet-dex-prices";
|
|
19
|
+
import {
|
|
20
|
+
resolveAvalancheRpcUrls,
|
|
21
|
+
resolveBaseRpcUrls,
|
|
22
|
+
resolveBscRpcUrls,
|
|
23
|
+
resolveEthereumRpcUrls,
|
|
24
|
+
} from "./wallet-rpc";
|
|
25
|
+
|
|
26
|
+
// ── Constants ─────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
29
|
+
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
30
|
+
|
|
31
|
+
// ── Types ─────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
type EvmChainProvider = "alchemy" | "ankr";
|
|
34
|
+
|
|
35
|
+
export interface EvmChainConfig {
|
|
36
|
+
name: string;
|
|
37
|
+
subdomain: string;
|
|
38
|
+
chainId: number;
|
|
39
|
+
nativeSymbol: string;
|
|
40
|
+
provider: EvmChainProvider;
|
|
41
|
+
ankrChain?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EvmProviderKeys {
|
|
45
|
+
alchemyKey?: string | null;
|
|
46
|
+
ankrKey?: string | null;
|
|
47
|
+
cloudManagedAccess?: boolean | null;
|
|
48
|
+
bscRpcUrls?: string[] | null;
|
|
49
|
+
ethereumRpcUrls?: string[] | null;
|
|
50
|
+
baseRpcUrls?: string[] | null;
|
|
51
|
+
avaxRpcUrls?: string[] | null;
|
|
52
|
+
nodeRealBscRpcUrl?: string | null;
|
|
53
|
+
quickNodeBscRpcUrl?: string | null;
|
|
54
|
+
/** Standard elizaOS EVM plugin env key for BSC. */
|
|
55
|
+
bscRpcUrl?: string | null;
|
|
56
|
+
/** Standard elizaOS EVM plugin env key for Ethereum mainnet. */
|
|
57
|
+
ethereumRpcUrl?: string | null;
|
|
58
|
+
/** Standard elizaOS EVM plugin env key for Base. */
|
|
59
|
+
baseRpcUrl?: string | null;
|
|
60
|
+
/** Standard elizaOS EVM plugin env key for Avalanche C-Chain. */
|
|
61
|
+
avaxRpcUrl?: string | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface EvmProviderKeyset {
|
|
65
|
+
alchemyKey: string | null;
|
|
66
|
+
ankrKey: string | null;
|
|
67
|
+
cloudManagedAccess: boolean;
|
|
68
|
+
bscRpcUrls: string[];
|
|
69
|
+
ethereumRpcUrls: string[];
|
|
70
|
+
baseRpcUrls: string[];
|
|
71
|
+
avaxRpcUrls: string[];
|
|
72
|
+
nodeRealBscRpcUrl: string | null;
|
|
73
|
+
quickNodeBscRpcUrl: string | null;
|
|
74
|
+
bscRpcUrl: string | null;
|
|
75
|
+
ethereumRpcUrl: string | null;
|
|
76
|
+
baseRpcUrl: string | null;
|
|
77
|
+
avaxRpcUrl: string | null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface AlchemyTokenBalance {
|
|
81
|
+
contractAddress: string;
|
|
82
|
+
tokenBalance: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface AlchemyTokenMeta {
|
|
86
|
+
name: string;
|
|
87
|
+
symbol: string;
|
|
88
|
+
decimals: number;
|
|
89
|
+
logo: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface AnkrTokenAsset {
|
|
93
|
+
contractAddress?: string;
|
|
94
|
+
tokenName?: string;
|
|
95
|
+
tokenSymbol?: string;
|
|
96
|
+
tokenDecimals?: number | string;
|
|
97
|
+
tokenType?: string;
|
|
98
|
+
tokenBalance?: string | number;
|
|
99
|
+
balance?: string | number;
|
|
100
|
+
balanceRawInteger?: string | number;
|
|
101
|
+
balanceUsd?: string | number;
|
|
102
|
+
thumbnail?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface AnkrNftAsset {
|
|
106
|
+
contractAddress?: string;
|
|
107
|
+
tokenId?: string | number;
|
|
108
|
+
name?: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
imageUrl?: string;
|
|
111
|
+
imagePreviewUrl?: string;
|
|
112
|
+
imageOriginalUrl?: string;
|
|
113
|
+
collectionName?: string;
|
|
114
|
+
contractName?: string;
|
|
115
|
+
tokenType?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Default chain configuration ───────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export const DEFAULT_EVM_CHAINS: readonly EvmChainConfig[] = [
|
|
121
|
+
{
|
|
122
|
+
name: "Ethereum",
|
|
123
|
+
subdomain: "eth-mainnet",
|
|
124
|
+
chainId: 1,
|
|
125
|
+
nativeSymbol: "ETH",
|
|
126
|
+
provider: "alchemy",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "Base",
|
|
130
|
+
subdomain: "base-mainnet",
|
|
131
|
+
chainId: 8453,
|
|
132
|
+
nativeSymbol: "ETH",
|
|
133
|
+
provider: "alchemy",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "Arbitrum",
|
|
137
|
+
subdomain: "arb-mainnet",
|
|
138
|
+
chainId: 42161,
|
|
139
|
+
nativeSymbol: "ETH",
|
|
140
|
+
provider: "alchemy",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "Optimism",
|
|
144
|
+
subdomain: "opt-mainnet",
|
|
145
|
+
chainId: 10,
|
|
146
|
+
nativeSymbol: "ETH",
|
|
147
|
+
provider: "alchemy",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "Polygon",
|
|
151
|
+
subdomain: "polygon-mainnet",
|
|
152
|
+
chainId: 137,
|
|
153
|
+
nativeSymbol: "POL",
|
|
154
|
+
provider: "alchemy",
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
name: "BSC",
|
|
158
|
+
subdomain: "bnb-mainnet",
|
|
159
|
+
chainId: 56,
|
|
160
|
+
nativeSymbol: "BNB",
|
|
161
|
+
provider: "alchemy",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "Avalanche",
|
|
165
|
+
subdomain: "avax-mainnet",
|
|
166
|
+
chainId: 43114,
|
|
167
|
+
nativeSymbol: "AVAX",
|
|
168
|
+
provider: "alchemy",
|
|
169
|
+
},
|
|
170
|
+
] as const;
|
|
171
|
+
|
|
172
|
+
// ── Internal helpers ──────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
/** Parse JSON from a fetch response. If the body isn't JSON, throw with the raw text. */
|
|
175
|
+
async function jsonOrThrow<T>(res: Response): Promise<T> {
|
|
176
|
+
const text = await res.text();
|
|
177
|
+
if (!res.ok) throw new Error(text.slice(0, 200) || `HTTP ${res.status}`);
|
|
178
|
+
try {
|
|
179
|
+
return JSON.parse(text) as T;
|
|
180
|
+
} catch {
|
|
181
|
+
throw new Error(text.slice(0, 200) || "Invalid JSON");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeApiKey(value: string | null | undefined): string | null {
|
|
186
|
+
if (typeof value !== "string") return null;
|
|
187
|
+
const trimmed = value.trim();
|
|
188
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeStringArray(
|
|
192
|
+
values: ReadonlyArray<string | null | undefined> | null | undefined,
|
|
193
|
+
): string[] {
|
|
194
|
+
return [
|
|
195
|
+
...new Set(
|
|
196
|
+
(values ?? [])
|
|
197
|
+
.map((value) => normalizeApiKey(value))
|
|
198
|
+
.filter((value): value is string => Boolean(value)),
|
|
199
|
+
),
|
|
200
|
+
];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function resolveEvmProviderKeys(
|
|
204
|
+
alchemyOrKeys: string | EvmProviderKeys | null | undefined,
|
|
205
|
+
maybeAnkrKey?: string | null,
|
|
206
|
+
): EvmProviderKeyset {
|
|
207
|
+
if (typeof alchemyOrKeys === "string" || alchemyOrKeys == null) {
|
|
208
|
+
return {
|
|
209
|
+
alchemyKey: normalizeApiKey(alchemyOrKeys),
|
|
210
|
+
ankrKey: normalizeApiKey(maybeAnkrKey),
|
|
211
|
+
cloudManagedAccess: false,
|
|
212
|
+
bscRpcUrls: resolveBscRpcUrls({ cloudManagedAccess: false }),
|
|
213
|
+
ethereumRpcUrls: resolveEthereumRpcUrls({ cloudManagedAccess: false }),
|
|
214
|
+
baseRpcUrls: resolveBaseRpcUrls({ cloudManagedAccess: false }),
|
|
215
|
+
avaxRpcUrls: resolveAvalancheRpcUrls({ cloudManagedAccess: false }),
|
|
216
|
+
nodeRealBscRpcUrl: normalizeApiKey(
|
|
217
|
+
process.env.NODEREAL_BSC_RPC_URL ?? null,
|
|
218
|
+
),
|
|
219
|
+
quickNodeBscRpcUrl: normalizeApiKey(
|
|
220
|
+
process.env.QUICKNODE_BSC_RPC_URL ?? null,
|
|
221
|
+
),
|
|
222
|
+
bscRpcUrl: normalizeApiKey(process.env.BSC_RPC_URL ?? null),
|
|
223
|
+
ethereumRpcUrl: normalizeApiKey(process.env.ETHEREUM_RPC_URL ?? null),
|
|
224
|
+
baseRpcUrl: normalizeApiKey(process.env.BASE_RPC_URL ?? null),
|
|
225
|
+
avaxRpcUrl: normalizeApiKey(process.env.AVALANCHE_RPC_URL ?? null),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
const cloudManagedAccess = Boolean(alchemyOrKeys.cloudManagedAccess);
|
|
229
|
+
return {
|
|
230
|
+
alchemyKey: normalizeApiKey(alchemyOrKeys.alchemyKey),
|
|
231
|
+
ankrKey: normalizeApiKey(alchemyOrKeys.ankrKey ?? maybeAnkrKey),
|
|
232
|
+
cloudManagedAccess,
|
|
233
|
+
bscRpcUrls: normalizeStringArray([
|
|
234
|
+
...(alchemyOrKeys.bscRpcUrls ?? []),
|
|
235
|
+
alchemyOrKeys.nodeRealBscRpcUrl ?? process.env.NODEREAL_BSC_RPC_URL,
|
|
236
|
+
alchemyOrKeys.quickNodeBscRpcUrl ?? process.env.QUICKNODE_BSC_RPC_URL,
|
|
237
|
+
alchemyOrKeys.bscRpcUrl ?? process.env.BSC_RPC_URL,
|
|
238
|
+
...resolveBscRpcUrls({ cloudManagedAccess }),
|
|
239
|
+
]),
|
|
240
|
+
ethereumRpcUrls: normalizeStringArray([
|
|
241
|
+
...(alchemyOrKeys.ethereumRpcUrls ?? []),
|
|
242
|
+
alchemyOrKeys.ethereumRpcUrl ?? process.env.ETHEREUM_RPC_URL,
|
|
243
|
+
...resolveEthereumRpcUrls({ cloudManagedAccess }),
|
|
244
|
+
]),
|
|
245
|
+
baseRpcUrls: normalizeStringArray([
|
|
246
|
+
...(alchemyOrKeys.baseRpcUrls ?? []),
|
|
247
|
+
alchemyOrKeys.baseRpcUrl ?? process.env.BASE_RPC_URL,
|
|
248
|
+
...resolveBaseRpcUrls({ cloudManagedAccess }),
|
|
249
|
+
]),
|
|
250
|
+
avaxRpcUrls: normalizeStringArray([
|
|
251
|
+
...(alchemyOrKeys.avaxRpcUrls ?? []),
|
|
252
|
+
alchemyOrKeys.avaxRpcUrl ?? process.env.AVALANCHE_RPC_URL,
|
|
253
|
+
...resolveAvalancheRpcUrls({ cloudManagedAccess }),
|
|
254
|
+
]),
|
|
255
|
+
nodeRealBscRpcUrl: normalizeApiKey(
|
|
256
|
+
alchemyOrKeys.nodeRealBscRpcUrl ?? process.env.NODEREAL_BSC_RPC_URL,
|
|
257
|
+
),
|
|
258
|
+
quickNodeBscRpcUrl: normalizeApiKey(
|
|
259
|
+
alchemyOrKeys.quickNodeBscRpcUrl ?? process.env.QUICKNODE_BSC_RPC_URL,
|
|
260
|
+
),
|
|
261
|
+
bscRpcUrl: normalizeApiKey(
|
|
262
|
+
alchemyOrKeys.bscRpcUrl ?? process.env.BSC_RPC_URL,
|
|
263
|
+
),
|
|
264
|
+
ethereumRpcUrl: normalizeApiKey(
|
|
265
|
+
alchemyOrKeys.ethereumRpcUrl ?? process.env.ETHEREUM_RPC_URL,
|
|
266
|
+
),
|
|
267
|
+
baseRpcUrl: normalizeApiKey(
|
|
268
|
+
alchemyOrKeys.baseRpcUrl ?? process.env.BASE_RPC_URL,
|
|
269
|
+
),
|
|
270
|
+
avaxRpcUrl: normalizeApiKey(
|
|
271
|
+
alchemyOrKeys.avaxRpcUrl ?? process.env.AVALANCHE_RPC_URL,
|
|
272
|
+
),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isBscChain(chain: EvmChainConfig): boolean {
|
|
277
|
+
return (
|
|
278
|
+
chain.chainId === 56 || (chain.ankrChain ?? "").toLowerCase() === "bsc"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function describeRpcEndpoint(url: string): string {
|
|
283
|
+
try {
|
|
284
|
+
return new URL(url).host;
|
|
285
|
+
} catch {
|
|
286
|
+
return "rpc";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function makeEvmChainFailure(
|
|
291
|
+
chain: EvmChainConfig,
|
|
292
|
+
message: string,
|
|
293
|
+
): EvmChainBalance {
|
|
294
|
+
return {
|
|
295
|
+
chain: chain.name,
|
|
296
|
+
chainId: chain.chainId,
|
|
297
|
+
nativeBalance: "0",
|
|
298
|
+
nativeSymbol: chain.nativeSymbol,
|
|
299
|
+
nativeValueUsd: "0",
|
|
300
|
+
tokens: [],
|
|
301
|
+
error: message,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function rpcJsonRequest(body: string): RequestInit {
|
|
306
|
+
return {
|
|
307
|
+
method: "POST",
|
|
308
|
+
headers: { "Content-Type": "application/json" },
|
|
309
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
310
|
+
body,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function asString(value: unknown): string {
|
|
315
|
+
if (typeof value === "string") return value.trim();
|
|
316
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
317
|
+
return "";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function parseTokenDecimals(value: unknown, fallback = 18): number {
|
|
321
|
+
const num =
|
|
322
|
+
typeof value === "number"
|
|
323
|
+
? value
|
|
324
|
+
: typeof value === "string"
|
|
325
|
+
? Number.parseInt(value, 10)
|
|
326
|
+
: Number.NaN;
|
|
327
|
+
if (!Number.isFinite(num) || num < 0) return fallback;
|
|
328
|
+
return Math.trunc(num);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseAnkrBalance(asset: AnkrTokenAsset, decimals: number): string {
|
|
332
|
+
const tokenBalance = asString(asset.tokenBalance);
|
|
333
|
+
if (tokenBalance) {
|
|
334
|
+
if (/^\d+$/.test(tokenBalance))
|
|
335
|
+
return formatWei(BigInt(tokenBalance), decimals);
|
|
336
|
+
return tokenBalance;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const displayBalance = asString(asset.balance);
|
|
340
|
+
if (displayBalance) {
|
|
341
|
+
if (/^\d+$/.test(displayBalance))
|
|
342
|
+
return formatWei(BigInt(displayBalance), decimals);
|
|
343
|
+
return displayBalance;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const rawBalance = asString(asset.balanceRawInteger);
|
|
347
|
+
if (rawBalance && /^\d+$/.test(rawBalance))
|
|
348
|
+
return formatWei(BigInt(rawBalance), decimals);
|
|
349
|
+
|
|
350
|
+
return "0";
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function isZeroBalance(balance: string): boolean {
|
|
354
|
+
if (!balance) return true;
|
|
355
|
+
if (/^0+(\.0+)?$/.test(balance)) return true;
|
|
356
|
+
const parsed = Number.parseFloat(balance);
|
|
357
|
+
return Number.isFinite(parsed) ? parsed <= 0 : false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function isAnkrNativeAsset(asset: AnkrTokenAsset): boolean {
|
|
361
|
+
const tokenType = (asset.tokenType ?? "").toUpperCase();
|
|
362
|
+
const symbol = (asset.tokenSymbol ?? "").toUpperCase();
|
|
363
|
+
const contract = (asset.contractAddress ?? "").toLowerCase();
|
|
364
|
+
if (tokenType === "NATIVE") return true;
|
|
365
|
+
return symbol === "BNB" && (!contract || contract === ZERO_ADDRESS);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function formatWei(wei: bigint, decimals: number): string {
|
|
369
|
+
if (wei <= 0n || decimals <= 0) return wei <= 0n ? "0" : wei.toString();
|
|
370
|
+
const divisor = 10n ** BigInt(decimals);
|
|
371
|
+
const whole = wei / divisor;
|
|
372
|
+
const rem = wei % divisor;
|
|
373
|
+
if (rem === 0n) return whole.toString();
|
|
374
|
+
return `${whole}.${rem.toString().padStart(decimals, "0").replace(/0+$/, "")}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// ── Alchemy balance fetching ──────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
async function fetchAlchemyChainBalances(
|
|
380
|
+
chain: EvmChainConfig,
|
|
381
|
+
address: string,
|
|
382
|
+
alchemyKey: string,
|
|
383
|
+
): Promise<EvmChainBalance> {
|
|
384
|
+
const url = `https://${chain.subdomain}.g.alchemy.com/v2/${alchemyKey}`;
|
|
385
|
+
|
|
386
|
+
const nativeData = await jsonOrThrow<{ result?: string }>(
|
|
387
|
+
await fetch(
|
|
388
|
+
url,
|
|
389
|
+
rpcJsonRequest(
|
|
390
|
+
JSON.stringify({
|
|
391
|
+
jsonrpc: "2.0",
|
|
392
|
+
id: 1,
|
|
393
|
+
method: "eth_getBalance",
|
|
394
|
+
params: [address, "latest"],
|
|
395
|
+
}),
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
);
|
|
399
|
+
const nativeBalance = formatWei(
|
|
400
|
+
nativeData.result ? BigInt(nativeData.result) : 0n,
|
|
401
|
+
18,
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const tokenData = await jsonOrThrow<{
|
|
405
|
+
result?: { tokenBalances?: AlchemyTokenBalance[] };
|
|
406
|
+
}>(
|
|
407
|
+
await fetch(
|
|
408
|
+
url,
|
|
409
|
+
rpcJsonRequest(
|
|
410
|
+
JSON.stringify({
|
|
411
|
+
jsonrpc: "2.0",
|
|
412
|
+
id: 2,
|
|
413
|
+
method: "alchemy_getTokenBalances",
|
|
414
|
+
params: [address, "DEFAULT_TOKENS"],
|
|
415
|
+
}),
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
const nonZero = (tokenData.result?.tokenBalances ?? []).filter(
|
|
420
|
+
(t) =>
|
|
421
|
+
t.tokenBalance && t.tokenBalance !== "0x0" && t.tokenBalance !== "0x",
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const metaResults = await Promise.allSettled(
|
|
425
|
+
nonZero.slice(0, 50).map(async (tok): Promise<EvmTokenBalance> => {
|
|
426
|
+
const meta = (
|
|
427
|
+
await jsonOrThrow<{ result?: AlchemyTokenMeta }>(
|
|
428
|
+
await fetch(
|
|
429
|
+
url,
|
|
430
|
+
rpcJsonRequest(
|
|
431
|
+
JSON.stringify({
|
|
432
|
+
jsonrpc: "2.0",
|
|
433
|
+
id: 3,
|
|
434
|
+
method: "alchemy_getTokenMetadata",
|
|
435
|
+
params: [tok.contractAddress],
|
|
436
|
+
}),
|
|
437
|
+
),
|
|
438
|
+
),
|
|
439
|
+
)
|
|
440
|
+
).result;
|
|
441
|
+
const decimals = meta?.decimals ?? 18;
|
|
442
|
+
return {
|
|
443
|
+
symbol: meta?.symbol ?? "???",
|
|
444
|
+
name: meta?.name ?? "Unknown Token",
|
|
445
|
+
contractAddress: tok.contractAddress,
|
|
446
|
+
balance: formatWei(BigInt(tok.tokenBalance), decimals),
|
|
447
|
+
decimals,
|
|
448
|
+
valueUsd: "0",
|
|
449
|
+
logoUrl: meta?.logo ?? "",
|
|
450
|
+
};
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
const tokens = metaResults
|
|
454
|
+
.filter(
|
|
455
|
+
(r): r is PromiseFulfilledResult<EvmTokenBalance> =>
|
|
456
|
+
r.status === "fulfilled",
|
|
457
|
+
)
|
|
458
|
+
.map((r) => r.value);
|
|
459
|
+
|
|
460
|
+
// Fetch DEX prices for all tokens + native token.
|
|
461
|
+
const allAddresses = tokens.map((t) => t.contractAddress);
|
|
462
|
+
const wrappedNative = WRAPPED_NATIVE[chain.chainId];
|
|
463
|
+
if (wrappedNative) allAddresses.push(wrappedNative);
|
|
464
|
+
const dexPrices = await fetchDexPrices(chain.chainId, allAddresses);
|
|
465
|
+
|
|
466
|
+
for (const tok of tokens) {
|
|
467
|
+
const meta = dexPrices.get(tok.contractAddress.toLowerCase());
|
|
468
|
+
if (meta) {
|
|
469
|
+
tok.valueUsd = computeValueUsd(tok.balance, meta.price);
|
|
470
|
+
if (meta.logoUrl && !tok.logoUrl) tok.logoUrl = meta.logoUrl;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const nativeMeta = wrappedNative
|
|
474
|
+
? dexPrices.get(wrappedNative.toLowerCase())
|
|
475
|
+
: undefined;
|
|
476
|
+
const nativeValueUsd = nativeMeta
|
|
477
|
+
? computeValueUsd(nativeBalance, nativeMeta.price)
|
|
478
|
+
: "0";
|
|
479
|
+
|
|
480
|
+
return {
|
|
481
|
+
chain: chain.name,
|
|
482
|
+
chainId: chain.chainId,
|
|
483
|
+
nativeBalance,
|
|
484
|
+
nativeSymbol: chain.nativeSymbol,
|
|
485
|
+
nativeValueUsd,
|
|
486
|
+
tokens,
|
|
487
|
+
error: null,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ── Ankr balance fetching ─────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
async function fetchAnkrChainBalances(
|
|
494
|
+
chain: EvmChainConfig,
|
|
495
|
+
address: string,
|
|
496
|
+
ankrKey: string,
|
|
497
|
+
): Promise<EvmChainBalance> {
|
|
498
|
+
const res = await fetch(
|
|
499
|
+
`https://rpc.ankr.com/multichain/${ankrKey}`,
|
|
500
|
+
rpcJsonRequest(
|
|
501
|
+
JSON.stringify({
|
|
502
|
+
jsonrpc: "2.0",
|
|
503
|
+
id: 1,
|
|
504
|
+
method: "ankr_getAccountBalance",
|
|
505
|
+
params: {
|
|
506
|
+
walletAddress: address,
|
|
507
|
+
blockchain: [chain.ankrChain ?? "bsc"],
|
|
508
|
+
onlyWhitelisted: false,
|
|
509
|
+
},
|
|
510
|
+
}),
|
|
511
|
+
),
|
|
512
|
+
);
|
|
513
|
+
const data = await jsonOrThrow<{ result?: { assets?: AnkrTokenAsset[] } }>(
|
|
514
|
+
res,
|
|
515
|
+
);
|
|
516
|
+
const assets = data.result?.assets ?? [];
|
|
517
|
+
const nativeAsset = assets.find(isAnkrNativeAsset);
|
|
518
|
+
const nativeBalance = nativeAsset
|
|
519
|
+
? parseAnkrBalance(
|
|
520
|
+
nativeAsset,
|
|
521
|
+
parseTokenDecimals(nativeAsset.tokenDecimals),
|
|
522
|
+
)
|
|
523
|
+
: "0";
|
|
524
|
+
const tokens: EvmTokenBalance[] = [];
|
|
525
|
+
for (const asset of assets) {
|
|
526
|
+
if (isAnkrNativeAsset(asset)) continue;
|
|
527
|
+
const decimals = parseTokenDecimals(asset.tokenDecimals);
|
|
528
|
+
const balance = parseAnkrBalance(asset, decimals);
|
|
529
|
+
if (isZeroBalance(balance)) continue;
|
|
530
|
+
tokens.push({
|
|
531
|
+
symbol: asset.tokenSymbol ?? "???",
|
|
532
|
+
name: asset.tokenName ?? "Unknown Token",
|
|
533
|
+
contractAddress: asset.contractAddress ?? "",
|
|
534
|
+
balance,
|
|
535
|
+
decimals,
|
|
536
|
+
valueUsd: "0",
|
|
537
|
+
logoUrl: asset.thumbnail ?? "",
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// All pricing via DexScreener/DexPaprika (Ankr only provides balances).
|
|
542
|
+
const allAddresses = tokens
|
|
543
|
+
.filter((t) => t.contractAddress)
|
|
544
|
+
.map((t) => t.contractAddress);
|
|
545
|
+
const wrappedNative = WRAPPED_NATIVE[chain.chainId];
|
|
546
|
+
if (wrappedNative) allAddresses.push(wrappedNative);
|
|
547
|
+
logger.info(
|
|
548
|
+
`[wallet] Fetching DEX prices for ${chain.name}: ${allAddresses.length} addresses (native=${nativeBalance})`,
|
|
549
|
+
);
|
|
550
|
+
const dexPrices = await fetchDexPrices(chain.chainId, allAddresses);
|
|
551
|
+
logger.info(
|
|
552
|
+
`[wallet] DEX prices result for ${chain.name}: ${dexPrices.size} prices found`,
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
for (const tok of tokens) {
|
|
556
|
+
const meta = dexPrices.get(tok.contractAddress.toLowerCase());
|
|
557
|
+
if (meta) {
|
|
558
|
+
tok.valueUsd = computeValueUsd(tok.balance, meta.price);
|
|
559
|
+
if (meta.logoUrl && !tok.logoUrl) tok.logoUrl = meta.logoUrl;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const nativeMeta = wrappedNative
|
|
563
|
+
? dexPrices.get(wrappedNative.toLowerCase())
|
|
564
|
+
: undefined;
|
|
565
|
+
const nativeValueUsd = nativeMeta
|
|
566
|
+
? computeValueUsd(nativeBalance, nativeMeta.price)
|
|
567
|
+
: "0";
|
|
568
|
+
|
|
569
|
+
return {
|
|
570
|
+
chain: chain.name,
|
|
571
|
+
chainId: chain.chainId,
|
|
572
|
+
nativeBalance,
|
|
573
|
+
nativeSymbol: chain.nativeSymbol,
|
|
574
|
+
nativeValueUsd,
|
|
575
|
+
tokens,
|
|
576
|
+
error: null,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ── Direct RPC balance fetching ───────────────────────────────────────
|
|
581
|
+
|
|
582
|
+
async function fetchNativeBalanceViaRpc(
|
|
583
|
+
rpcUrl: string,
|
|
584
|
+
address: string,
|
|
585
|
+
): Promise<string> {
|
|
586
|
+
const data = await jsonOrThrow<{
|
|
587
|
+
result?: string;
|
|
588
|
+
error?: { message?: string };
|
|
589
|
+
}>(
|
|
590
|
+
await fetch(
|
|
591
|
+
rpcUrl,
|
|
592
|
+
rpcJsonRequest(
|
|
593
|
+
JSON.stringify({
|
|
594
|
+
jsonrpc: "2.0",
|
|
595
|
+
id: 1,
|
|
596
|
+
method: "eth_getBalance",
|
|
597
|
+
params: [address, "latest"],
|
|
598
|
+
}),
|
|
599
|
+
),
|
|
600
|
+
),
|
|
601
|
+
);
|
|
602
|
+
|
|
603
|
+
if (data.error?.message) {
|
|
604
|
+
throw new Error(data.error.message);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const raw = typeof data.result === "string" ? data.result : "0x0";
|
|
608
|
+
const wei = raw.startsWith("0x") ? BigInt(raw) : BigInt(raw || "0");
|
|
609
|
+
return formatWei(wei, 18);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* Query ERC-20 balanceOf, symbol, and decimals for a single token via RPC.
|
|
614
|
+
* Returns null if the token has zero balance or the call fails.
|
|
615
|
+
*/
|
|
616
|
+
async function fetchErc20BalanceViaRpc(
|
|
617
|
+
rpcUrl: string,
|
|
618
|
+
walletAddress: string,
|
|
619
|
+
contractAddress: string,
|
|
620
|
+
): Promise<EvmTokenBalance | null> {
|
|
621
|
+
const paddedWallet = walletAddress
|
|
622
|
+
.toLowerCase()
|
|
623
|
+
.replace("0x", "")
|
|
624
|
+
.padStart(64, "0");
|
|
625
|
+
// balanceOf(address) — paddedWallet is already 64 hex chars (24 zero prefix + 40 addr)
|
|
626
|
+
const balanceOfData = `0x70a08231${paddedWallet}`;
|
|
627
|
+
// symbol()
|
|
628
|
+
const symbolData = "0x95d89b41";
|
|
629
|
+
// decimals()
|
|
630
|
+
const decimalsData = "0x313ce567";
|
|
631
|
+
|
|
632
|
+
const makeCall = (to: string, data: string) =>
|
|
633
|
+
fetch(rpcUrl, {
|
|
634
|
+
method: "POST",
|
|
635
|
+
headers: { "Content-Type": "application/json" },
|
|
636
|
+
body: JSON.stringify({
|
|
637
|
+
jsonrpc: "2.0",
|
|
638
|
+
id: 1,
|
|
639
|
+
method: "eth_call",
|
|
640
|
+
params: [{ to, data }, "latest"],
|
|
641
|
+
}),
|
|
642
|
+
signal: AbortSignal.timeout(8_000),
|
|
643
|
+
}).then((r) => r.json() as Promise<{ result?: string }>);
|
|
644
|
+
|
|
645
|
+
try {
|
|
646
|
+
const [balRes, symRes, decRes] = await Promise.all([
|
|
647
|
+
makeCall(contractAddress, balanceOfData),
|
|
648
|
+
makeCall(contractAddress, symbolData),
|
|
649
|
+
makeCall(contractAddress, decimalsData),
|
|
650
|
+
]);
|
|
651
|
+
|
|
652
|
+
const rawBal = balRes.result;
|
|
653
|
+
if (!rawBal || rawBal === "0x" || rawBal === "0x0" || BigInt(rawBal) === 0n)
|
|
654
|
+
return null;
|
|
655
|
+
|
|
656
|
+
let decimals = 18;
|
|
657
|
+
if (decRes.result && decRes.result !== "0x") {
|
|
658
|
+
const d = Number(BigInt(decRes.result));
|
|
659
|
+
if (Number.isFinite(d) && d >= 0 && d <= 36) decimals = d;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let symbol = "TOKEN";
|
|
663
|
+
if (symRes.result && symRes.result.length > 2) {
|
|
664
|
+
try {
|
|
665
|
+
// ABI-encoded string: offset (32 bytes) + length (32 bytes) + data
|
|
666
|
+
const hex = symRes.result.slice(2);
|
|
667
|
+
if (hex.length >= 128) {
|
|
668
|
+
const len = Number(BigInt(`0x${hex.slice(64, 128)}`));
|
|
669
|
+
const bytes = Buffer.from(hex.slice(128, 128 + len * 2), "hex");
|
|
670
|
+
const decoded = bytes.toString("utf-8").replace(/\0/g, "").trim();
|
|
671
|
+
if (decoded) symbol = decoded;
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
// Fall through with default symbol.
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const balance = formatWei(BigInt(rawBal), decimals);
|
|
679
|
+
return {
|
|
680
|
+
symbol,
|
|
681
|
+
name: symbol,
|
|
682
|
+
contractAddress,
|
|
683
|
+
balance,
|
|
684
|
+
decimals,
|
|
685
|
+
valueUsd: "0",
|
|
686
|
+
logoUrl: "",
|
|
687
|
+
};
|
|
688
|
+
} catch {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
async function fetchEvmChainBalancesViaRpc(
|
|
694
|
+
chain: EvmChainConfig,
|
|
695
|
+
address: string,
|
|
696
|
+
rpcUrls: string[],
|
|
697
|
+
knownTokenAddresses?: string[],
|
|
698
|
+
): Promise<EvmChainBalance> {
|
|
699
|
+
const errors: string[] = [];
|
|
700
|
+
for (const rpcUrl of rpcUrls) {
|
|
701
|
+
try {
|
|
702
|
+
const nativeBalance = await fetchNativeBalanceViaRpc(rpcUrl, address);
|
|
703
|
+
|
|
704
|
+
// Query known ERC-20 tokens (e.g. from trade ledger).
|
|
705
|
+
const tokens: EvmTokenBalance[] = [];
|
|
706
|
+
if (knownTokenAddresses && knownTokenAddresses.length > 0) {
|
|
707
|
+
const results = await Promise.allSettled(
|
|
708
|
+
knownTokenAddresses
|
|
709
|
+
.slice(0, 30)
|
|
710
|
+
.map((addr) => fetchErc20BalanceViaRpc(rpcUrl, address, addr)),
|
|
711
|
+
);
|
|
712
|
+
for (const r of results) {
|
|
713
|
+
if (r.status === "fulfilled" && r.value) tokens.push(r.value);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Price native + tokens via DEX.
|
|
718
|
+
const wrappedNative = WRAPPED_NATIVE[chain.chainId];
|
|
719
|
+
const priceAddresses = tokens.map((t) => t.contractAddress);
|
|
720
|
+
if (wrappedNative) priceAddresses.push(wrappedNative);
|
|
721
|
+
|
|
722
|
+
const dexPrices =
|
|
723
|
+
priceAddresses.length > 0
|
|
724
|
+
? await fetchDexPrices(chain.chainId, priceAddresses)
|
|
725
|
+
: new Map<string, DexTokenMeta>();
|
|
726
|
+
|
|
727
|
+
let nativeValueUsd = "0";
|
|
728
|
+
if (wrappedNative) {
|
|
729
|
+
const nativeMeta = dexPrices.get(wrappedNative.toLowerCase());
|
|
730
|
+
if (nativeMeta)
|
|
731
|
+
nativeValueUsd = computeValueUsd(nativeBalance, nativeMeta.price);
|
|
732
|
+
logger.info(
|
|
733
|
+
`[wallet] RPC path: ${chain.name} native=${nativeBalance} price=${nativeMeta?.price ?? "none"} value=$${nativeValueUsd}`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
for (const tok of tokens) {
|
|
738
|
+
const meta = dexPrices.get(tok.contractAddress.toLowerCase());
|
|
739
|
+
if (meta) {
|
|
740
|
+
tok.valueUsd = computeValueUsd(tok.balance, meta.price);
|
|
741
|
+
if (meta.logoUrl) tok.logoUrl = meta.logoUrl;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
if (tokens.length > 0) {
|
|
746
|
+
logger.info(
|
|
747
|
+
`[wallet] RPC path: ${chain.name} found ${tokens.length} tokens with balance`,
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return {
|
|
752
|
+
chain: chain.name,
|
|
753
|
+
chainId: chain.chainId,
|
|
754
|
+
nativeBalance,
|
|
755
|
+
nativeSymbol: chain.nativeSymbol,
|
|
756
|
+
nativeValueUsd,
|
|
757
|
+
tokens,
|
|
758
|
+
error: null,
|
|
759
|
+
};
|
|
760
|
+
} catch (err) {
|
|
761
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
762
|
+
errors.push(`${describeRpcEndpoint(rpcUrl)}: ${msg}`);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
throw new Error(
|
|
767
|
+
errors.join(" | ").slice(0, 400) || `${chain.name} RPC unavailable`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// ── Alchemy NFT fetching ──────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
async function fetchAlchemyChainNfts(
|
|
774
|
+
chain: EvmChainConfig,
|
|
775
|
+
address: string,
|
|
776
|
+
alchemyKey: string,
|
|
777
|
+
): Promise<{ chain: string; nfts: EvmNft[] }> {
|
|
778
|
+
const res = await fetch(
|
|
779
|
+
`https://${chain.subdomain}.g.alchemy.com/nft/v3/${alchemyKey}/getNFTsForOwner?owner=${address}&withMetadata=true&pageSize=50`,
|
|
780
|
+
{ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) },
|
|
781
|
+
);
|
|
782
|
+
const data = await jsonOrThrow<{
|
|
783
|
+
ownedNfts?: Array<{
|
|
784
|
+
contract?: {
|
|
785
|
+
address?: string;
|
|
786
|
+
name?: string;
|
|
787
|
+
openSeaMetadata?: { collectionName?: string };
|
|
788
|
+
};
|
|
789
|
+
tokenId?: string;
|
|
790
|
+
name?: string;
|
|
791
|
+
description?: string;
|
|
792
|
+
image?: {
|
|
793
|
+
cachedUrl?: string;
|
|
794
|
+
thumbnailUrl?: string;
|
|
795
|
+
originalUrl?: string;
|
|
796
|
+
};
|
|
797
|
+
tokenType?: string;
|
|
798
|
+
}>;
|
|
799
|
+
}>(res);
|
|
800
|
+
return {
|
|
801
|
+
chain: chain.name,
|
|
802
|
+
nfts: (data.ownedNfts ?? []).map((nft) => ({
|
|
803
|
+
contractAddress: nft.contract?.address ?? "",
|
|
804
|
+
tokenId: nft.tokenId ?? "",
|
|
805
|
+
name: nft.name ?? "Untitled",
|
|
806
|
+
description: (nft.description ?? "").slice(0, 200),
|
|
807
|
+
imageUrl:
|
|
808
|
+
nft.image?.cachedUrl ??
|
|
809
|
+
nft.image?.thumbnailUrl ??
|
|
810
|
+
nft.image?.originalUrl ??
|
|
811
|
+
"",
|
|
812
|
+
collectionName:
|
|
813
|
+
nft.contract?.openSeaMetadata?.collectionName ??
|
|
814
|
+
nft.contract?.name ??
|
|
815
|
+
"",
|
|
816
|
+
tokenType: nft.tokenType ?? "ERC721",
|
|
817
|
+
})),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// ── Ankr NFT fetching ─────────────────────────────────────────────────
|
|
822
|
+
|
|
823
|
+
async function fetchAnkrChainNfts(
|
|
824
|
+
chain: EvmChainConfig,
|
|
825
|
+
address: string,
|
|
826
|
+
ankrKey: string,
|
|
827
|
+
): Promise<{ chain: string; nfts: EvmNft[] }> {
|
|
828
|
+
const res = await fetch(
|
|
829
|
+
`https://rpc.ankr.com/multichain/${ankrKey}`,
|
|
830
|
+
rpcJsonRequest(
|
|
831
|
+
JSON.stringify({
|
|
832
|
+
jsonrpc: "2.0",
|
|
833
|
+
id: 1,
|
|
834
|
+
method: "ankr_getNFTsByOwner",
|
|
835
|
+
params: {
|
|
836
|
+
walletAddress: address,
|
|
837
|
+
blockchain: [chain.ankrChain ?? "bsc"],
|
|
838
|
+
pageSize: 50,
|
|
839
|
+
},
|
|
840
|
+
}),
|
|
841
|
+
),
|
|
842
|
+
);
|
|
843
|
+
const data = await jsonOrThrow<{ result?: { assets?: AnkrNftAsset[] } }>(res);
|
|
844
|
+
return {
|
|
845
|
+
chain: chain.name,
|
|
846
|
+
nfts: (data.result?.assets ?? []).map((nft) => ({
|
|
847
|
+
contractAddress: nft.contractAddress ?? "",
|
|
848
|
+
tokenId: String(nft.tokenId ?? ""),
|
|
849
|
+
name: nft.name ?? "Untitled",
|
|
850
|
+
description: (nft.description ?? "").slice(0, 200),
|
|
851
|
+
imageUrl:
|
|
852
|
+
nft.imageUrl ?? nft.imagePreviewUrl ?? nft.imageOriginalUrl ?? "",
|
|
853
|
+
collectionName: nft.collectionName ?? nft.contractName ?? "",
|
|
854
|
+
tokenType: nft.tokenType ?? "ERC721",
|
|
855
|
+
})),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ── Public API ────────────────────────────────────────────────────────
|
|
860
|
+
|
|
861
|
+
export async function fetchEvmBalances(
|
|
862
|
+
address: string,
|
|
863
|
+
alchemyOrKeys: string | EvmProviderKeys | null | undefined,
|
|
864
|
+
maybeAnkrKey?: string | null,
|
|
865
|
+
knownTokenAddresses?: string[],
|
|
866
|
+
): Promise<EvmChainBalance[]> {
|
|
867
|
+
const keys = resolveEvmProviderKeys(alchemyOrKeys, maybeAnkrKey);
|
|
868
|
+
const bscRpcUrls = keys.bscRpcUrls;
|
|
869
|
+
const ethRpcUrls = keys.ethereumRpcUrls;
|
|
870
|
+
const baseRpcUrls = keys.baseRpcUrls;
|
|
871
|
+
const avaxRpcUrls = keys.avaxRpcUrls;
|
|
872
|
+
|
|
873
|
+
const hasManagedBscRpc = bscRpcUrls.length > 0;
|
|
874
|
+
const activeChains = DEFAULT_EVM_CHAINS.filter((chain) => {
|
|
875
|
+
if (chain.provider === "ankr") {
|
|
876
|
+
return Boolean(keys.ankrKey) || (isBscChain(chain) && hasManagedBscRpc);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Prefer Alchemy when available (tokens + USD value). Otherwise, fall back to
|
|
880
|
+
// public RPC for native balances on the chains we support out-of-box.
|
|
881
|
+
if (keys.alchemyKey) return true;
|
|
882
|
+
if (chain.chainId === 1) return ethRpcUrls.length > 0;
|
|
883
|
+
if (chain.chainId === 8453) return baseRpcUrls.length > 0;
|
|
884
|
+
if (chain.chainId === 56) return hasManagedBscRpc;
|
|
885
|
+
if (chain.chainId === 43114) return avaxRpcUrls.length > 0;
|
|
886
|
+
return false;
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
return Promise.all(
|
|
890
|
+
activeChains.map(async (chain): Promise<EvmChainBalance> => {
|
|
891
|
+
try {
|
|
892
|
+
if (chain.provider === "ankr") {
|
|
893
|
+
if (keys.ankrKey) {
|
|
894
|
+
return await fetchAnkrChainBalances(chain, address, keys.ankrKey);
|
|
895
|
+
}
|
|
896
|
+
if (isBscChain(chain) && hasManagedBscRpc) {
|
|
897
|
+
return await fetchEvmChainBalancesViaRpc(
|
|
898
|
+
chain,
|
|
899
|
+
address,
|
|
900
|
+
bscRpcUrls,
|
|
901
|
+
knownTokenAddresses,
|
|
902
|
+
);
|
|
903
|
+
}
|
|
904
|
+
return makeEvmChainFailure(chain, "Missing ANKR_API_KEY");
|
|
905
|
+
}
|
|
906
|
+
if (!keys.alchemyKey) {
|
|
907
|
+
if (chain.chainId === 1 && ethRpcUrls.length > 0) {
|
|
908
|
+
return await fetchEvmChainBalancesViaRpc(
|
|
909
|
+
chain,
|
|
910
|
+
address,
|
|
911
|
+
ethRpcUrls,
|
|
912
|
+
knownTokenAddresses,
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
if (chain.chainId === 8453 && baseRpcUrls.length > 0) {
|
|
916
|
+
return await fetchEvmChainBalancesViaRpc(
|
|
917
|
+
chain,
|
|
918
|
+
address,
|
|
919
|
+
baseRpcUrls,
|
|
920
|
+
knownTokenAddresses,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
if (chain.chainId === 56 && hasManagedBscRpc) {
|
|
924
|
+
return await fetchEvmChainBalancesViaRpc(
|
|
925
|
+
chain,
|
|
926
|
+
address,
|
|
927
|
+
bscRpcUrls,
|
|
928
|
+
knownTokenAddresses,
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
if (chain.chainId === 43114 && avaxRpcUrls.length > 0) {
|
|
932
|
+
return await fetchEvmChainBalancesViaRpc(
|
|
933
|
+
chain,
|
|
934
|
+
address,
|
|
935
|
+
avaxRpcUrls,
|
|
936
|
+
knownTokenAddresses,
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
return makeEvmChainFailure(chain, "Missing ALCHEMY_API_KEY");
|
|
940
|
+
}
|
|
941
|
+
return await fetchAlchemyChainBalances(chain, address, keys.alchemyKey);
|
|
942
|
+
} catch (err) {
|
|
943
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
944
|
+
logger.warn(`EVM balance fetch failed for ${chain.name}: ${msg}`);
|
|
945
|
+
return makeEvmChainFailure(chain, msg);
|
|
946
|
+
}
|
|
947
|
+
}),
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export async function fetchEvmNfts(
|
|
952
|
+
address: string,
|
|
953
|
+
alchemyOrKeys: string | EvmProviderKeys | null | undefined,
|
|
954
|
+
maybeAnkrKey?: string | null,
|
|
955
|
+
): Promise<Array<{ chain: string; nfts: EvmNft[] }>> {
|
|
956
|
+
const keys = resolveEvmProviderKeys(alchemyOrKeys, maybeAnkrKey);
|
|
957
|
+
const hasManagedBscRpc = keys.bscRpcUrls.length > 0;
|
|
958
|
+
const activeChains = DEFAULT_EVM_CHAINS.filter((chain) => {
|
|
959
|
+
if (chain.provider === "ankr") {
|
|
960
|
+
return (isBscChain(chain) && hasManagedBscRpc) || Boolean(keys.ankrKey);
|
|
961
|
+
}
|
|
962
|
+
if (keys.alchemyKey) return true;
|
|
963
|
+
// BSC without Alchemy: include if managed RPC exists (NFTs will be empty)
|
|
964
|
+
if (chain.chainId === 56) return hasManagedBscRpc;
|
|
965
|
+
return false;
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
return Promise.all(
|
|
969
|
+
activeChains.map(
|
|
970
|
+
async (chain): Promise<{ chain: string; nfts: EvmNft[] }> => {
|
|
971
|
+
try {
|
|
972
|
+
if (chain.provider === "ankr") {
|
|
973
|
+
if (!keys.ankrKey) {
|
|
974
|
+
// Managed NodeReal/QuickNode mode currently provides native-balance
|
|
975
|
+
// readiness only; token/NFT indexing is added in later phases.
|
|
976
|
+
return { chain: chain.name, nfts: [] };
|
|
977
|
+
}
|
|
978
|
+
return await fetchAnkrChainNfts(chain, address, keys.ankrKey);
|
|
979
|
+
}
|
|
980
|
+
if (!keys.alchemyKey) return { chain: chain.name, nfts: [] };
|
|
981
|
+
return await fetchAlchemyChainNfts(chain, address, keys.alchemyKey);
|
|
982
|
+
} catch (err) {
|
|
983
|
+
logger.warn(`EVM NFT fetch failed for ${chain.name}: ${err}`);
|
|
984
|
+
return { chain: chain.name, nfts: [] };
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
),
|
|
988
|
+
);
|
|
989
|
+
}
|