@elizaos/plugin-wallet 2.0.0-beta.1 → 2.0.3-beta.6
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/README.md +70 -45
- package/auto-enable.ts +1 -1
- package/dist/actions/failure-codes.d.ts +12 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/analytics/birdeye/actions/wallet-search-address.d.ts +7 -0
- package/dist/analytics/birdeye/birdeye-task.d.ts +27 -0
- package/dist/analytics/birdeye/birdeye.d.ts +140 -0
- package/dist/analytics/birdeye/constants.d.ts +68 -0
- package/dist/analytics/birdeye/providers/agent-portfolio-provider.d.ts +8 -0
- package/dist/analytics/birdeye/providers/market.d.ts +18 -0
- package/dist/analytics/birdeye/providers/portfolio-factory.d.ts +10 -0
- package/dist/analytics/birdeye/providers/trending.d.ts +19 -0
- package/dist/analytics/birdeye/providers/wallet.d.ts +5 -0
- package/dist/analytics/birdeye/search-category.d.ts +52 -0
- package/dist/analytics/birdeye/service.d.ts +94 -0
- package/dist/analytics/birdeye/types/api/common.d.ts +199 -0
- package/dist/analytics/birdeye/types/api/defi.d.ts +187 -0
- package/dist/analytics/birdeye/types/api/pair.d.ts +182 -0
- package/dist/analytics/birdeye/types/api/search.d.ts +64 -0
- package/dist/analytics/birdeye/types/api/token.d.ts +580 -0
- package/dist/analytics/birdeye/types/api/trader.d.ts +70 -0
- package/dist/analytics/birdeye/types/api/wallet.d.ts +161 -0
- package/dist/analytics/birdeye/types/shared.d.ts +83 -0
- package/dist/analytics/birdeye/utils.d.ts +74 -0
- package/dist/analytics/dexscreener/errors.d.ts +2 -0
- package/dist/analytics/dexscreener/index.d.ts +3 -0
- package/dist/analytics/dexscreener/search-category.d.ts +3 -0
- package/dist/analytics/dexscreener/service.d.ts +34 -0
- package/dist/analytics/dexscreener/types.d.ts +131 -0
- package/dist/analytics/lpinfo/index.d.ts +31 -0
- package/dist/analytics/lpinfo/kamino/index.d.ts +7 -0
- package/dist/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.d.ts +6 -0
- package/dist/analytics/lpinfo/kamino/providers/kaminoPoolProvider.d.ts +6 -0
- package/dist/analytics/lpinfo/kamino/providers/kaminoProvider.d.ts +6 -0
- package/dist/analytics/lpinfo/kamino/services/kaminoLiquidityService.d.ts +203 -0
- package/dist/analytics/lpinfo/kamino/services/kaminoService.d.ts +171 -0
- package/dist/analytics/lpinfo/steer/index.d.ts +7 -0
- package/dist/analytics/lpinfo/steer/providers/steerLiquidityProvider.d.ts +6 -0
- package/dist/analytics/lpinfo/steer/services/steerLiquidityService.d.ts +208 -0
- package/dist/analytics/lpinfo/steer/steer-display-types.d.ts +97 -0
- package/dist/analytics/news/index.d.ts +32 -0
- package/dist/analytics/news/interfaces/types.d.ts +177 -0
- package/dist/analytics/news/providers/defiNewsProvider.d.ts +106 -0
- package/dist/analytics/news/services/newsDataService.d.ts +72 -0
- package/dist/analytics/news/utils/formatters.d.ts +54 -0
- package/dist/analytics/token-info/action.d.ts +6 -0
- package/dist/analytics/token-info/index.d.ts +3 -0
- package/dist/analytics/token-info/params.d.ts +10 -0
- package/dist/analytics/token-info/providers.d.ts +4 -0
- package/dist/analytics/token-info/service.d.ts +19 -0
- package/dist/analytics/token-info/types.d.ts +45 -0
- package/dist/api/wallet-routes.d.ts +100 -0
- package/dist/audit/audit-log.d.ts +29 -0
- package/dist/browser-shim/build-shim.d.ts +31 -0
- package/dist/browser-shim/index.d.ts +1 -0
- package/dist/chains/evm/actions/helpers.d.ts +31 -0
- package/dist/chains/evm/actions/swap.d.ts +53 -0
- package/dist/chains/evm/actions/transfer.d.ts +10 -0
- package/dist/chains/evm/bridge-router.d.ts +44 -0
- package/dist/chains/evm/build.d.ts +2 -0
- package/dist/chains/evm/chain-handler.d.ts +37 -0
- package/dist/chains/evm/constants.d.ts +16 -0
- package/dist/chains/evm/dex/aerodrome/index.d.ts +6 -0
- package/dist/chains/evm/dex/aerodrome/services/AerodromeLpService.d.ts +27 -0
- package/dist/chains/evm/dex/aerodrome/types.d.ts +435 -0
- package/dist/chains/evm/dex/pancakeswp/index.d.ts +6 -0
- package/dist/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.d.ts +28 -0
- package/dist/chains/evm/dex/pancakeswp/types.d.ts +19 -0
- package/dist/chains/evm/dex/uniswap/index.d.ts +6 -0
- package/dist/chains/evm/dex/uniswap/services/UniswapV3LpService.d.ts +28 -0
- package/dist/chains/evm/dex/uniswap/types.d.ts +458 -0
- package/dist/chains/evm/generated/specs/spec-helpers.d.ts +35 -0
- package/dist/chains/evm/generated/specs/specs.d.ts +99 -0
- package/dist/chains/evm/gov-router.d.ts +6 -0
- package/dist/chains/evm/index.browser.d.ts +3 -0
- package/dist/chains/evm/index.d.ts +6 -0
- package/dist/chains/evm/prompts.d.ts +24 -0
- package/dist/chains/evm/providers/get-balance.d.ts +2 -0
- package/dist/chains/evm/providers/wallet.d.ts +35 -0
- package/dist/chains/evm/routes/sign.d.ts +13 -0
- package/dist/chains/evm/rpc-providers.d.ts +26 -0
- package/dist/chains/evm/service.d.ts +26 -0
- package/dist/chains/evm/templates/index.d.ts +1 -0
- package/dist/chains/evm/types/index.d.ts +296 -0
- package/dist/chains/evm/vitest.config.d.ts +2 -0
- package/dist/chains/registry.d.ts +3 -0
- package/dist/chains/solana/actions/confirmation.d.ts +9 -0
- package/dist/chains/solana/bn.d.ts +6 -0
- package/dist/chains/solana/build.d.ts +2 -0
- package/dist/chains/solana/constants.d.ts +2 -0
- package/dist/chains/solana/dex/meteora/e2e/scenarios.d.ts +10 -0
- package/dist/chains/solana/dex/meteora/e2e/test-utils.d.ts +28 -0
- package/dist/chains/solana/dex/meteora/index.d.ts +3 -0
- package/dist/chains/solana/dex/meteora/providers/positionProvider.d.ts +9 -0
- package/dist/chains/solana/dex/meteora/services/MeteoraLpService.d.ts +37 -0
- package/dist/chains/solana/dex/meteora/utils/dlmm.d.ts +6 -0
- package/dist/chains/solana/dex/meteora/utils/loadWallet.d.ts +14 -0
- package/dist/chains/solana/dex/meteora/utils/sendTransaction.d.ts +2 -0
- package/dist/chains/solana/dex/orca/index.d.ts +6 -0
- package/dist/chains/solana/dex/orca/providers/positionProvider.d.ts +10 -0
- package/dist/chains/solana/dex/orca/services/srv_orca.d.ts +10 -0
- package/dist/chains/solana/dex/orca/types.d.ts +61 -0
- package/dist/chains/solana/dex/orca/utils/loadWallet.d.ts +1 -0
- package/dist/chains/solana/dex/orca/utils/sendTransaction.d.ts +2 -0
- package/dist/chains/solana/dex/raydium/index.d.ts +6 -0
- package/dist/chains/solana/dex/raydium/providers/positionProvider.d.ts +10 -0
- package/dist/chains/solana/dex/raydium/services/srv_raydium.d.ts +104 -0
- package/dist/chains/solana/dex/raydium/types.d.ts +42 -0
- package/dist/chains/solana/environment.d.ts +15 -0
- package/dist/chains/solana/generated/specs/spec-helpers.d.ts +35 -0
- package/dist/chains/solana/generated/specs/specs.d.ts +73 -0
- package/dist/chains/solana/index.browser.d.ts +3 -0
- package/dist/chains/solana/index.d.ts +7 -0
- package/dist/chains/solana/keypairUtils.d.ts +7 -0
- package/dist/chains/solana/prompts.d.ts +12 -0
- package/dist/chains/solana/providers/wallet.d.ts +2 -0
- package/dist/chains/solana/routes/index.d.ts +2 -0
- package/dist/chains/solana/routes/sign.d.ts +16 -0
- package/dist/chains/solana/service.d.ts +237 -0
- package/dist/chains/solana/types.d.ts +377 -0
- package/dist/chains/solana/vitest.config.d.ts +2 -0
- package/dist/chains/wallet-action.d.ts +3 -0
- package/dist/contracts.d.ts +58 -0
- package/dist/core-augmentation.d.ts +9 -0
- package/dist/index.d.mts +27 -34727
- package/dist/index.d.ts +27 -0
- package/dist/index.mjs +28246 -21186
- package/dist/index.mjs.map +153 -0
- package/dist/lib/server-wallet-trade.d.ts +22 -0
- package/dist/lib/server-wallet-trade.js +333 -0
- package/dist/lib/server-wallet-trade.js.map +11 -0
- package/dist/lib/wallet-export-guard.d.ts +45 -0
- package/dist/lp/actions/liquidity.d.ts +2 -0
- package/dist/lp/e2e/real-token-tests.d.ts +6 -0
- package/dist/lp/e2e/scenarios.d.ts +9 -0
- package/dist/lp/e2e/test-utils.d.ts +28 -0
- package/dist/lp/lp-manager-entry.d.ts +18 -0
- package/dist/lp/services/ConcentratedLiquidityService.d.ts +32 -0
- package/dist/lp/services/DexInteractionService.d.ts +34 -0
- package/dist/lp/services/LpManagementService.d.ts +116 -0
- package/dist/lp/services/UserLpProfileService.d.ts +18 -0
- package/dist/lp/services/VaultService.d.ts +21 -0
- package/dist/lp/services/YieldOptimizationService.d.ts +59 -0
- package/dist/lp/services/__tests__/MockLpService.d.ts +17 -0
- package/dist/lp/types.d.ts +439 -0
- package/dist/lp/utils/solanaClient.d.ts +26 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/policy/policy.d.ts +14 -0
- package/dist/providers/canonical-provider.d.ts +19 -0
- package/dist/providers/wallet-provider.d.ts +5 -0
- package/dist/register-routes.d.ts +1 -0
- package/dist/routes/plugin.d.ts +14 -0
- package/dist/routes/wallet-market-overview-route.d.ts +7 -0
- package/dist/sdk/abi.d.ts +396 -0
- package/dist/sdk/bridge/abis.d.ts +63 -0
- package/dist/sdk/bridge/client.d.ts +48 -0
- package/dist/sdk/bridge/index.d.ts +14 -0
- package/dist/sdk/bridge/solana.d.ts +131 -0
- package/dist/sdk/bridge/types.d.ts +92 -0
- package/dist/sdk/convenience.d.ts +104 -0
- package/dist/sdk/escrow/MutualStakeEscrow.d.ts +75 -0
- package/dist/sdk/escrow/types.d.ts +58 -0
- package/dist/sdk/escrow/verifiers.d.ts +25 -0
- package/dist/sdk/identity/erc8004.d.ts +304 -0
- package/dist/sdk/identity/reputation.d.ts +317 -0
- package/dist/sdk/identity/uaid.d.ts +192 -0
- package/dist/sdk/identity/validation.d.ts +282 -0
- package/dist/sdk/index.d.ts +133 -0
- package/dist/sdk/index.js +5284 -0
- package/dist/sdk/index.js.map +40 -0
- package/dist/sdk/policy/SpendingPolicy.d.ts +105 -0
- package/dist/sdk/policy/UptoBillingPolicy.d.ts +87 -0
- package/dist/sdk/router/PaymentRouter.d.ts +77 -0
- package/dist/sdk/router/index.d.ts +2 -0
- package/dist/sdk/swap/SwapModule.d.ts +47 -0
- package/dist/sdk/swap/abi.d.ts +50 -0
- package/dist/sdk/swap/index.d.ts +11 -0
- package/dist/sdk/swap/types.d.ts +101 -0
- package/dist/sdk/tokens/decimals.d.ts +64 -0
- package/dist/sdk/tokens/registry.d.ts +81 -0
- package/dist/sdk/tokens/solana.d.ts +107 -0
- package/dist/sdk/tokens/transfers.d.ts +94 -0
- package/dist/sdk/types.d.ts +129 -0
- package/dist/sdk/wallet-core.d.ts +29450 -0
- package/dist/sdk/x402/budget.d.ts +51 -0
- package/dist/sdk/x402/chains/abstract/index.d.ts +134 -0
- package/dist/sdk/x402/client.d.ts +66 -0
- package/dist/sdk/x402/index.d.ts +8 -0
- package/dist/sdk/x402/middleware.d.ts +37 -0
- package/dist/sdk/x402/multi-asset.d.ts +53 -0
- package/dist/sdk/x402/types.d.ts +109 -0
- package/dist/security/wallet-context-safety.d.ts +7 -0
- package/dist/security/wallet-financial-confirmation.d.ts +23 -0
- package/dist/services/wallet-backend-service.d.ts +39 -0
- package/dist/types/wallet-router.d.ts +130 -0
- package/dist/utils/intent-trajectory.d.ts +34 -0
- package/dist/wallet/backend.d.ts +41 -0
- package/dist/wallet/errors.d.ts +17 -0
- package/dist/wallet/index.d.ts +6 -0
- package/dist/wallet/local-eoa-backend.d.ts +37 -0
- package/dist/wallet/pending.d.ts +47 -0
- package/dist/wallet/select-backend.d.ts +11 -0
- package/dist/wallet/steward-backend.d.ts +22 -0
- package/dist/wallet-action.d.ts +1 -0
- package/dist/wallet-action.js +6292 -0
- package/dist/wallet-action.js.map +44 -0
- package/package.json +35 -21
- package/registry-entry.json +134 -0
- package/src/analytics/birdeye/actions/wallet-search-address.ts +85 -5
- package/src/analytics/birdeye/birdeye-task.ts +25 -9
- package/src/analytics/birdeye/birdeye.ts +6 -7
- package/src/analytics/birdeye/constants.ts +0 -1
- package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +0 -1
- package/src/analytics/birdeye/providers/market.ts +51 -45
- package/src/analytics/birdeye/providers/portfolio-factory.ts +79 -38
- package/src/analytics/birdeye/providers/trending.ts +43 -46
- package/src/analytics/birdeye/providers/wallet.ts +0 -1
- package/src/analytics/birdeye/search-category.test.ts +1 -1
- package/src/analytics/birdeye/search-category.ts +77 -12
- package/src/analytics/birdeye/service.test.ts +146 -0
- package/src/analytics/birdeye/service.ts +220 -105
- package/src/analytics/birdeye/types/api/common.ts +0 -1
- package/src/analytics/birdeye/types/api/defi.ts +0 -1
- package/src/analytics/birdeye/types/api/pair.ts +0 -1
- package/src/analytics/birdeye/types/api/search.ts +0 -1
- package/src/analytics/birdeye/types/api/token.ts +0 -1
- package/src/analytics/birdeye/types/api/trader.ts +0 -1
- package/src/analytics/birdeye/types/api/wallet.ts +0 -1
- package/src/analytics/birdeye/types/shared.ts +0 -11
- package/src/analytics/birdeye/utils.test.ts +69 -0
- package/src/analytics/birdeye/utils.ts +11 -8
- package/src/analytics/dexscreener/search-category.ts +0 -1
- package/src/analytics/dexscreener/service.ts +7 -12
- package/src/analytics/dexscreener/types.ts +0 -1
- package/src/analytics/lpinfo/index.ts +5 -2
- package/src/analytics/lpinfo/kamino/README.md +2 -2
- package/src/analytics/lpinfo/kamino/index.ts +9 -2
- package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +6 -26
- package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +11 -12
- package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +76 -32
- package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +78 -38
- package/src/analytics/lpinfo/kamino/services/kaminoService.ts +71 -31
- package/src/analytics/lpinfo/steer/index.ts +7 -2
- package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +25 -26
- package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +367 -149
- package/src/analytics/news/index.ts +7 -2
- package/src/analytics/news/interfaces/types.ts +0 -1
- package/src/analytics/news/providers/defiNewsProvider.ts +17 -44
- package/src/analytics/news/services/newsDataService.ts +1 -22
- package/src/analytics/news/utils/formatters.test.ts +60 -0
- package/src/analytics/news/utils/formatters.ts +0 -1
- package/src/analytics/token-info/action.ts +52 -212
- package/src/analytics/token-info/index.ts +1 -1
- package/src/analytics/token-info/params.test.ts +69 -0
- package/src/analytics/token-info/params.ts +13 -11
- package/src/analytics/token-info/providers.ts +46 -17
- package/src/analytics/token-info/service.ts +3 -3
- package/src/analytics/token-info/types.ts +2 -2
- package/src/api/wallet-routes.test.ts +56 -0
- package/src/api/wallet-routes.ts +1728 -0
- package/src/audit/audit-log.ts +57 -2
- package/src/browser-shim/build-shim.ts +1 -1
- package/src/browser-shim/shim.template.js +107 -117
- package/src/chains/{wallet-router.test.ts → __tests__/wallet-router.test.ts} +57 -10
- package/src/chains/evm/actions/helpers.ts +9 -7
- package/src/chains/evm/actions/swap.ts +176 -22
- package/src/chains/evm/actions/transfer.ts +29 -22
- package/src/chains/evm/biome.json +1 -1
- package/src/chains/evm/build.ts +6 -1
- package/src/chains/evm/constants.ts +19 -0
- package/src/chains/evm/contracts/artifacts/OZGovernor.json +25 -1682
- package/src/chains/evm/dex/aerodrome/index.ts +6 -1
- package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +41 -15
- package/src/chains/evm/dex/aerodrome/types.ts +1 -2
- package/src/chains/evm/dex/pancakeswp/index.ts +6 -1
- package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +54 -17
- package/src/chains/evm/dex/pancakeswp/types.ts +1 -2
- package/src/chains/evm/dex/uniswap/index.ts +6 -1
- package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +20 -9
- package/src/chains/evm/dex/uniswap/types.ts +1 -2
- package/src/chains/evm/gov-router.ts +3 -1
- package/src/chains/evm/index.browser.ts +1 -1
- package/src/chains/evm/index.ts +5 -1
- package/src/chains/evm/prompts.ts +5 -0
- package/src/chains/evm/providers/get-balance.ts +1 -1
- package/src/chains/evm/providers/wallet.ts +80 -9
- package/src/chains/evm/routes/sign.ts +35 -26
- package/src/chains/evm/rpc-providers.ts +1 -1
- package/src/chains/evm/types/index.ts +22 -2
- package/src/chains/registry.ts +1 -1
- package/src/chains/wallet-action.ts +301 -91
- package/src/index.ts +9 -5
- package/src/lib/wallet-export-guard.test.ts +233 -0
- package/src/lib/wallet-export-guard.ts +1 -1
- package/src/lp/actions/liquidity.ts +53 -26
- package/src/lp/e2e/real-token-tests.ts +0 -1
- package/src/lp/e2e/scenarios.ts +1 -2
- package/src/lp/e2e/test-utils.ts +20 -7
- package/src/lp/lp-manager-entry.ts +2 -5
- package/src/lp/services/ConcentratedLiquidityService.ts +3 -10
- package/src/lp/services/DexInteractionService.ts +1 -2
- package/src/lp/services/LpManagementService.test.ts +0 -1
- package/src/lp/services/LpManagementService.ts +75 -35
- package/src/lp/services/UserLpProfileService.ts +2 -3
- package/src/lp/services/VaultService.ts +10 -4
- package/src/lp/services/YieldOptimizationService.ts +29 -13
- package/src/lp/services/__tests__/MockLpService.ts +1 -2
- package/src/lp/types.ts +9 -13
- package/src/lp/utils/solanaClient.ts +4 -2
- package/src/plugin.routes.test.ts +24 -0
- package/src/plugin.ts +30 -13
- package/src/providers/canonical-provider.ts +1 -1
- package/src/providers/{unified-wallet-provider.ts → wallet-provider.ts} +3 -3
- package/src/routes/__fixtures__/coingecko-markets.recorded.json +97 -0
- package/src/routes/wallet-market-overview-route.ts +1 -1
- package/src/routes/wallet-market-overview.contract.test.ts +139 -0
- package/src/routes/wallet-market-overview.real.test.ts +83 -0
- package/src/sdk/escrow/MutualStakeEscrow.ts +1 -2
- package/src/sdk/identity/erc8004.ts +1 -1
- package/src/sdk/identity/validation.ts +3 -4
- package/src/sdk/index.ts +2 -2
- package/src/sdk/policy/SpendingPolicy.ts +1 -1
- package/src/sdk/router/PaymentRouter.ts +8 -11
- package/src/sdk/swap/SwapModule.ts +1 -1
- package/src/sdk/tokens/registry.ts +1 -1
- package/src/sdk/x402/middleware.ts +2 -8
- package/src/security/__tests__/wallet-context-safety.test.ts +79 -0
- package/src/security/__tests__/wallet-financial-confirmation.test.ts +88 -0
- package/src/security/wallet-context-safety.ts +128 -0
- package/src/security/wallet-financial-confirmation.ts +150 -0
- package/src/services/wallet-backend-service.ts +15 -1
- package/src/utils/intent-trajectory.ts +2 -2
- package/src/wallet/steward-backend.ts +4 -4
- package/dist/LpManagementService-BWrQ5-cO.mjs +0 -353
- package/dist/MockLpService-D_Apn4Fd.mjs +0 -99
- package/dist/aerodrome-CfnESC32.mjs +0 -890
- package/dist/chunk-hT5z_Zn9.mjs +0 -35
- package/dist/lib/server-wallet-trade.d.mts +0 -34
- package/dist/lib/server-wallet-trade.mjs +0 -306
- package/dist/meteora-BPX39hZo.mjs +0 -22640
- package/dist/orca-Bybp1HXO.mjs +0 -249
- package/dist/pancakeswp-CkEXlXti.mjs +0 -604
- package/dist/plugin-ZO_MTyd0.mjs +0 -529
- package/dist/raydium-rfaM9yEf.mjs +0 -539
- package/dist/sdk/index.d.mts +0 -32492
- package/dist/sdk/index.mjs +0 -6415
- package/dist/types-D5252NZk.mjs +0 -487
- package/dist/uniswap-CReXgXVN.mjs +0 -573
- package/dist/wallet-action.d.mts +0 -6
- package/dist/wallet-action.mjs +0 -820
- package/src/analytics/birdeye/tasks/birdeye.ts +0 -232
- package/src/analytics/lpinfo/index.d.ts +0 -7
- package/src/chains/evm/contracts/artifacts/TimelockController.json +0 -1007
- package/src/chains/evm/contracts/artifacts/VoteToken.json +0 -895
- package/src/lp/tasks/LpAutoRebalanceTask.ts +0 -117
- package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +0 -370
|
@@ -0,0 +1,1728 @@
|
|
|
1
|
+
// === Wallet routes extracted from packages/agent ===
|
|
2
|
+
//
|
|
3
|
+
// This handler is consumed by the agent HTTP server (packages/agent/src/api/server.ts).
|
|
4
|
+
// To keep `@elizaos/plugin-wallet` free of `@elizaos/agent` imports (and thus
|
|
5
|
+
// avoid a static-import cycle), every agent-internal helper this route needs
|
|
6
|
+
// is injected via `WalletRouteContext.deps` / context fields. Agent's
|
|
7
|
+
// `server.ts` is the single wiring site that constructs the context.
|
|
8
|
+
import crypto from "node:crypto";
|
|
9
|
+
import type http from "node:http";
|
|
10
|
+
import type { AgentRuntime, RouteRequestMeta } from "@elizaos/core";
|
|
11
|
+
import { logger } from "@elizaos/core";
|
|
12
|
+
import type {
|
|
13
|
+
ElizaConfig,
|
|
14
|
+
RouteHelpers,
|
|
15
|
+
WalletBalancesResponse,
|
|
16
|
+
WalletChain,
|
|
17
|
+
WalletChainKind,
|
|
18
|
+
WalletConfigStatus,
|
|
19
|
+
WalletEntry,
|
|
20
|
+
WalletExportRejection as WalletExportRejectionLike,
|
|
21
|
+
WalletExportRequestBody,
|
|
22
|
+
WalletNftsResponse,
|
|
23
|
+
WalletPrimaryMap,
|
|
24
|
+
WalletRpcChain,
|
|
25
|
+
WalletSource,
|
|
26
|
+
} from "@elizaos/shared";
|
|
27
|
+
|
|
28
|
+
// Mirrors `WalletRpcReadiness` from `packages/agent/src/api/wallet-rpc.ts`.
|
|
29
|
+
// Defined structurally here so this plugin module stays free of
|
|
30
|
+
// `@elizaos/agent` imports.
|
|
31
|
+
export interface WalletRpcReadinessSnapshot {
|
|
32
|
+
walletNetwork: "mainnet" | "testnet";
|
|
33
|
+
cloudManagedAccess: boolean;
|
|
34
|
+
managedBscRpcReady: boolean;
|
|
35
|
+
evmBalanceReady: boolean;
|
|
36
|
+
solanaBalanceReady: boolean;
|
|
37
|
+
selectedRpcProviders: WalletRpcSelections;
|
|
38
|
+
legacyCustomChains: WalletRpcChain[];
|
|
39
|
+
bscRpcUrls: string[];
|
|
40
|
+
ethereumRpcUrls: string[];
|
|
41
|
+
baseRpcUrls: string[];
|
|
42
|
+
avalancheRpcUrls: string[];
|
|
43
|
+
solanaRpcUrls: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
import {
|
|
47
|
+
normalizeWalletRpcSelections,
|
|
48
|
+
PostWalletGenerateRequestSchema,
|
|
49
|
+
PostWalletImportRequestSchema,
|
|
50
|
+
PostWalletPrimaryRequestSchema,
|
|
51
|
+
type WalletConfigUpdateRequest,
|
|
52
|
+
type WalletRpcSelections,
|
|
53
|
+
} from "@elizaos/shared";
|
|
54
|
+
import * as ethers from "ethers";
|
|
55
|
+
|
|
56
|
+
type CloudWalletProvider = "privy" | "steward";
|
|
57
|
+
interface CloudWalletDescriptor {
|
|
58
|
+
agentWalletId: string;
|
|
59
|
+
walletAddress: string;
|
|
60
|
+
walletProvider: CloudWalletProvider;
|
|
61
|
+
chainType: WalletChainKind;
|
|
62
|
+
balance?: string | number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cloud helpers are loaded lazily via `import()` rather than referenced at
|
|
66
|
+
// module top-level so that this file is safe to import in browser builds.
|
|
67
|
+
type CloudHelperBundle = {
|
|
68
|
+
ElizaCloudClient: new (baseUrl: string, apiKey: string) => unknown;
|
|
69
|
+
getOrCreateClientAddressKey: () => Promise<{ address: string }>;
|
|
70
|
+
normalizeCloudSiteUrl: (value: string) => string;
|
|
71
|
+
persistCloudWalletCache: (
|
|
72
|
+
config: unknown,
|
|
73
|
+
descriptors: Partial<Record<WalletChainKind, CloudWalletDescriptor>>,
|
|
74
|
+
) => void;
|
|
75
|
+
provisionCloudWalletsBestEffort: (
|
|
76
|
+
bridge: unknown,
|
|
77
|
+
args: {
|
|
78
|
+
agentId: string;
|
|
79
|
+
clientAddress: string;
|
|
80
|
+
chains: readonly WalletChainKind[];
|
|
81
|
+
},
|
|
82
|
+
) => Promise<{
|
|
83
|
+
descriptors: Partial<Record<WalletChainKind, CloudWalletDescriptor>>;
|
|
84
|
+
failures: Array<{ chain: WalletChainKind; error: unknown }>;
|
|
85
|
+
warnings: string[];
|
|
86
|
+
}>;
|
|
87
|
+
resolveCloudApiKey: (
|
|
88
|
+
config: ElizaConfig,
|
|
89
|
+
runtime: AgentRuntime | null,
|
|
90
|
+
) => string | null;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
async function loadCloudHelpers(): Promise<CloudHelperBundle> {
|
|
94
|
+
return (await import(
|
|
95
|
+
"@elizaos/plugin-elizacloud"
|
|
96
|
+
)) as unknown as CloudHelperBundle;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const WALLET_CONFIG_COMPAT_KEYS = new Set([
|
|
100
|
+
"ALCHEMY_API_KEY",
|
|
101
|
+
"INFURA_API_KEY",
|
|
102
|
+
"ANKR_API_KEY",
|
|
103
|
+
"ETHEREUM_RPC_URL",
|
|
104
|
+
"BASE_RPC_URL",
|
|
105
|
+
"AVALANCHE_RPC_URL",
|
|
106
|
+
"HELIUS_API_KEY",
|
|
107
|
+
"BIRDEYE_API_KEY",
|
|
108
|
+
"NODEREAL_BSC_RPC_URL",
|
|
109
|
+
"QUICKNODE_BSC_RPC_URL",
|
|
110
|
+
"BSC_RPC_URL",
|
|
111
|
+
"SOLANA_RPC_URL",
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
function resolveWalletConfigUpdateRequest(
|
|
115
|
+
body: unknown,
|
|
116
|
+
currentSelections: WalletRpcSelections,
|
|
117
|
+
): WalletConfigUpdateRequest | null {
|
|
118
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const record = body as Record<string, unknown>;
|
|
123
|
+
if (
|
|
124
|
+
record.selections &&
|
|
125
|
+
typeof record.selections === "object" &&
|
|
126
|
+
!Array.isArray(record.selections)
|
|
127
|
+
) {
|
|
128
|
+
const walletNetwork =
|
|
129
|
+
record.walletNetwork === "testnet" || record.walletNetwork === "mainnet"
|
|
130
|
+
? record.walletNetwork
|
|
131
|
+
: undefined;
|
|
132
|
+
const credentials =
|
|
133
|
+
record.credentials &&
|
|
134
|
+
typeof record.credentials === "object" &&
|
|
135
|
+
!Array.isArray(record.credentials)
|
|
136
|
+
? Object.fromEntries(
|
|
137
|
+
Object.entries(
|
|
138
|
+
record.credentials as Record<string, unknown>,
|
|
139
|
+
).filter(([, value]) => typeof value === "string"),
|
|
140
|
+
)
|
|
141
|
+
: undefined;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
selections: normalizeWalletRpcSelections(
|
|
145
|
+
record.selections as Partial<Record<keyof WalletRpcSelections, string>>,
|
|
146
|
+
),
|
|
147
|
+
walletNetwork,
|
|
148
|
+
credentials: credentials as WalletConfigUpdateRequest["credentials"],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const compatCredentials = Object.fromEntries(
|
|
153
|
+
Object.entries(record).filter(
|
|
154
|
+
([key, value]) =>
|
|
155
|
+
WALLET_CONFIG_COMPAT_KEYS.has(key) && typeof value === "string",
|
|
156
|
+
),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (Object.keys(compatCredentials).length === 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
selections: currentSelections,
|
|
165
|
+
walletNetwork:
|
|
166
|
+
record.walletNetwork === "testnet" || record.walletNetwork === "mainnet"
|
|
167
|
+
? record.walletNetwork
|
|
168
|
+
: undefined,
|
|
169
|
+
credentials: compatCredentials as WalletConfigUpdateRequest["credentials"],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ── Wallet route dependency injection ─────────────────────────────────
|
|
174
|
+
//
|
|
175
|
+
// The route handler is consumed by the agent HTTP server, but the file
|
|
176
|
+
// itself must not import from `@elizaos/agent`. Every helper the handler
|
|
177
|
+
// invokes is therefore supplied by the agent caller as a function on
|
|
178
|
+
// `WalletRouteDependencies` / `WalletRouteContext`. See
|
|
179
|
+
// `packages/agent/src/api/server.ts` for the single wiring site.
|
|
180
|
+
|
|
181
|
+
export interface WalletAddressesSnapshot {
|
|
182
|
+
evmAddress: string | null;
|
|
183
|
+
solanaAddress: string | null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface FetchEvmBalancesOptions {
|
|
187
|
+
alchemyKey?: string | null;
|
|
188
|
+
ankrKey?: string | null;
|
|
189
|
+
cloudManagedAccess?: boolean;
|
|
190
|
+
bscRpcUrls?: string[];
|
|
191
|
+
ethereumRpcUrls?: string[];
|
|
192
|
+
baseRpcUrls?: string[];
|
|
193
|
+
avaxRpcUrls?: string[];
|
|
194
|
+
nodeRealBscRpcUrl?: string;
|
|
195
|
+
quickNodeBscRpcUrl?: string;
|
|
196
|
+
bscRpcUrl?: string;
|
|
197
|
+
ethereumRpcUrl?: string;
|
|
198
|
+
baseRpcUrl?: string;
|
|
199
|
+
avaxRpcUrl?: string;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export interface IntegrationTelemetrySpan {
|
|
203
|
+
success: (attributes?: Record<string, unknown>) => void;
|
|
204
|
+
failure: (attributes: { error: unknown } & Record<string, unknown>) => void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface CreateIntegrationTelemetrySpanArgs {
|
|
208
|
+
boundary: string;
|
|
209
|
+
operation: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface WalletRouteDependencies {
|
|
213
|
+
getWalletAddresses: () => WalletAddressesSnapshot;
|
|
214
|
+
fetchEvmBalances: (
|
|
215
|
+
address: string,
|
|
216
|
+
options: FetchEvmBalancesOptions,
|
|
217
|
+
) => Promise<NonNullable<WalletBalancesResponse["evm"]>["chains"]>;
|
|
218
|
+
fetchSolanaBalances: (
|
|
219
|
+
address: string,
|
|
220
|
+
heliusKey: string,
|
|
221
|
+
) => Promise<Omit<NonNullable<WalletBalancesResponse["solana"]>, "address">>;
|
|
222
|
+
fetchSolanaNativeBalanceViaRpc: (
|
|
223
|
+
address: string,
|
|
224
|
+
rpcUrls: string[],
|
|
225
|
+
) => Promise<Omit<NonNullable<WalletBalancesResponse["solana"]>, "address">>;
|
|
226
|
+
validatePrivateKey: (privateKey: string) => { chain: WalletChain };
|
|
227
|
+
importWallet: (
|
|
228
|
+
chain: WalletChain,
|
|
229
|
+
privateKey: string,
|
|
230
|
+
) => {
|
|
231
|
+
success: boolean;
|
|
232
|
+
chain: WalletChain;
|
|
233
|
+
address: string | null;
|
|
234
|
+
error: string | null;
|
|
235
|
+
};
|
|
236
|
+
generateWalletForChain: (chain: WalletChain) => {
|
|
237
|
+
privateKey: string;
|
|
238
|
+
address: string;
|
|
239
|
+
};
|
|
240
|
+
deriveSolanaAddress: (privateKey: string) => string;
|
|
241
|
+
setSolanaWalletEnv: (privateKey: string) => void;
|
|
242
|
+
resolveWalletRpcReadiness: (
|
|
243
|
+
config: ElizaConfig,
|
|
244
|
+
) => WalletRpcReadinessSnapshot;
|
|
245
|
+
resolveWalletNetworkMode: (config: ElizaConfig) => "mainnet" | "testnet";
|
|
246
|
+
getStoredWalletRpcSelections: (config: ElizaConfig) => WalletRpcSelections;
|
|
247
|
+
applyWalletRpcConfigUpdate: (
|
|
248
|
+
config: ElizaConfig,
|
|
249
|
+
update: WalletConfigUpdateRequest,
|
|
250
|
+
) => void;
|
|
251
|
+
resolveWalletCapabilityStatus: (args: {
|
|
252
|
+
config: ElizaConfig;
|
|
253
|
+
runtime: AgentRuntime | null;
|
|
254
|
+
getWalletAddresses: () => WalletAddressesSnapshot;
|
|
255
|
+
}) => {
|
|
256
|
+
walletSource: WalletConfigStatus["walletSource"];
|
|
257
|
+
automationMode: WalletConfigStatus["automationMode"];
|
|
258
|
+
pluginEvmLoaded: WalletConfigStatus["pluginEvmLoaded"];
|
|
259
|
+
pluginEvmRequired: WalletConfigStatus["pluginEvmRequired"];
|
|
260
|
+
executionReady: WalletConfigStatus["executionReady"];
|
|
261
|
+
executionBlockedReason: WalletConfigStatus["executionBlockedReason"];
|
|
262
|
+
evmSigningCapability: WalletConfigStatus["evmSigningCapability"];
|
|
263
|
+
evmSigningReason: WalletConfigStatus["evmSigningReason"];
|
|
264
|
+
};
|
|
265
|
+
isCloudWalletEnabled: () => boolean;
|
|
266
|
+
persistConfigEnv: (key: string, value: string) => Promise<void>;
|
|
267
|
+
createIntegrationTelemetrySpan: (
|
|
268
|
+
args: CreateIntegrationTelemetrySpanArgs,
|
|
269
|
+
) => IntegrationTelemetrySpan;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Dual-wallet response shape ────
|
|
273
|
+
// Types imported from @elizaos/shared: WalletSource, WalletChainKind, WalletProviderKind,
|
|
274
|
+
// WalletEntry, WalletPrimaryMap
|
|
275
|
+
|
|
276
|
+
interface CachedCloudWalletDescriptor {
|
|
277
|
+
agentWalletId?: string | null;
|
|
278
|
+
walletAddress?: string | null;
|
|
279
|
+
walletProvider?: string | null;
|
|
280
|
+
balance?: string | number | null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function readCloudWalletCache(
|
|
284
|
+
config: ElizaConfig,
|
|
285
|
+
): Partial<Record<WalletChainKind, CachedCloudWalletDescriptor>> {
|
|
286
|
+
const wallet = config.wallet;
|
|
287
|
+
if (!wallet || typeof wallet !== "object") return {};
|
|
288
|
+
const cloud = (wallet as { cloud?: unknown }).cloud;
|
|
289
|
+
if (!cloud || typeof cloud !== "object") return {};
|
|
290
|
+
return cloud as Partial<Record<WalletChainKind, CachedCloudWalletDescriptor>>;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function readPrimaryMap(config: ElizaConfig): WalletPrimaryMap {
|
|
294
|
+
const wallet = config.wallet;
|
|
295
|
+
const raw =
|
|
296
|
+
wallet && typeof wallet === "object"
|
|
297
|
+
? (wallet as { primary?: unknown }).primary
|
|
298
|
+
: undefined;
|
|
299
|
+
const out: WalletPrimaryMap = { evm: "local", solana: "local" };
|
|
300
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
301
|
+
const record = raw as Record<string, unknown>;
|
|
302
|
+
if (record.evm === "cloud" || record.evm === "local") out.evm = record.evm;
|
|
303
|
+
if (record.solana === "cloud" || record.solana === "local") {
|
|
304
|
+
out.solana = record.solana;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return out;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function coerceCloudProvider(value: unknown): CloudWalletProvider {
|
|
311
|
+
return value === "privy" || value === "steward" ? value : "privy";
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Build the dual-wallet `{ wallets[], primary }` block. Returns `null`
|
|
316
|
+
* when the cloud-wallet flag is off so callers can omit both fields and
|
|
317
|
+
* preserve the pre-flag response shape exactly.
|
|
318
|
+
*/
|
|
319
|
+
function buildDualWalletShape(
|
|
320
|
+
config: ElizaConfig,
|
|
321
|
+
addresses: { evmAddress: string | null; solanaAddress: string | null },
|
|
322
|
+
isCloudWalletEnabled: () => boolean,
|
|
323
|
+
): { wallets: WalletEntry[]; primary: WalletPrimaryMap } | null {
|
|
324
|
+
if (!isCloudWalletEnabled()) return null;
|
|
325
|
+
|
|
326
|
+
const primary = readPrimaryMap(config);
|
|
327
|
+
const wallets: WalletEntry[] = [];
|
|
328
|
+
|
|
329
|
+
if (addresses.evmAddress) {
|
|
330
|
+
wallets.push({
|
|
331
|
+
source: "local",
|
|
332
|
+
chain: "evm",
|
|
333
|
+
address: addresses.evmAddress,
|
|
334
|
+
provider: "local",
|
|
335
|
+
primary: primary.evm === "local",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
if (addresses.solanaAddress) {
|
|
339
|
+
wallets.push({
|
|
340
|
+
source: "local",
|
|
341
|
+
chain: "solana",
|
|
342
|
+
address: addresses.solanaAddress,
|
|
343
|
+
provider: "local",
|
|
344
|
+
primary: primary.solana === "local",
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const cloud = readCloudWalletCache(config);
|
|
349
|
+
for (const chain of ["evm", "solana"] as const) {
|
|
350
|
+
const descriptor = cloud[chain];
|
|
351
|
+
const address = descriptor?.walletAddress;
|
|
352
|
+
if (typeof address === "string" && address.length > 0) {
|
|
353
|
+
wallets.push({
|
|
354
|
+
source: "cloud",
|
|
355
|
+
chain,
|
|
356
|
+
address,
|
|
357
|
+
provider: coerceCloudProvider(descriptor?.walletProvider),
|
|
358
|
+
primary: primary[chain] === "cloud",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return { wallets, primary };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function readCloudWalletAddress(
|
|
367
|
+
descriptor: CachedCloudWalletDescriptor | undefined,
|
|
368
|
+
): string | null {
|
|
369
|
+
if (typeof descriptor?.walletAddress !== "string") return null;
|
|
370
|
+
const trimmed = descriptor.walletAddress.trim();
|
|
371
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function readCachedCloudWalletDescriptor(
|
|
375
|
+
config: ElizaConfig,
|
|
376
|
+
chain: WalletChainKind,
|
|
377
|
+
): CloudWalletDescriptor | null {
|
|
378
|
+
const descriptor = readCloudWalletCache(config)[chain];
|
|
379
|
+
const walletAddress = readCloudWalletAddress(descriptor);
|
|
380
|
+
if (!walletAddress) return null;
|
|
381
|
+
return {
|
|
382
|
+
agentWalletId:
|
|
383
|
+
typeof descriptor?.agentWalletId === "string" &&
|
|
384
|
+
descriptor.agentWalletId.trim().length > 0
|
|
385
|
+
? descriptor.agentWalletId
|
|
386
|
+
: `cached-${chain}`,
|
|
387
|
+
walletAddress,
|
|
388
|
+
walletProvider: coerceCloudProvider(descriptor?.walletProvider),
|
|
389
|
+
chainType: chain,
|
|
390
|
+
balance: descriptor?.balance ?? undefined,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function readCachedCloudWalletDescriptors(
|
|
395
|
+
config: ElizaConfig,
|
|
396
|
+
): Partial<Record<WalletChainKind, CloudWalletDescriptor>> {
|
|
397
|
+
const evm = readCachedCloudWalletDescriptor(config, "evm");
|
|
398
|
+
const solana = readCachedCloudWalletDescriptor(config, "solana");
|
|
399
|
+
return {
|
|
400
|
+
...(evm ? { evm } : {}),
|
|
401
|
+
...(solana ? { solana } : {}),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function resolvePrimaryWalletAddresses(
|
|
406
|
+
config: ElizaConfig,
|
|
407
|
+
addresses: { evmAddress: string | null; solanaAddress: string | null },
|
|
408
|
+
): { evmAddress: string | null; solanaAddress: string | null } {
|
|
409
|
+
const primary = readPrimaryMap(config);
|
|
410
|
+
const cloud = readCloudWalletCache(config);
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
evmAddress:
|
|
414
|
+
primary.evm === "cloud"
|
|
415
|
+
? readCloudWalletAddress(cloud.evm)
|
|
416
|
+
: addresses.evmAddress,
|
|
417
|
+
solanaAddress:
|
|
418
|
+
primary.solana === "cloud"
|
|
419
|
+
? readCloudWalletAddress(cloud.solana)
|
|
420
|
+
: addresses.solanaAddress,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function persistPrimarySelection(
|
|
425
|
+
config: ElizaConfig,
|
|
426
|
+
chain: WalletChainKind,
|
|
427
|
+
source: WalletSource,
|
|
428
|
+
): void {
|
|
429
|
+
if (!config.wallet) {
|
|
430
|
+
config.wallet = {};
|
|
431
|
+
}
|
|
432
|
+
const wallet = config.wallet;
|
|
433
|
+
const primary = { ...((wallet.primary as Record<string, unknown>) ?? {}) };
|
|
434
|
+
primary[chain] = source;
|
|
435
|
+
wallet.primary = primary as typeof wallet.primary;
|
|
436
|
+
config.wallet = wallet;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export interface WalletRouteContext
|
|
440
|
+
extends RouteRequestMeta,
|
|
441
|
+
Pick<RouteHelpers, "readJsonBody" | "json" | "error"> {
|
|
442
|
+
config: ElizaConfig;
|
|
443
|
+
saveConfig: (config: ElizaConfig) => void;
|
|
444
|
+
ensureWalletKeysInEnvAndConfig: (config: ElizaConfig) => boolean;
|
|
445
|
+
resolveWalletExportRejection: (
|
|
446
|
+
req: http.IncomingMessage,
|
|
447
|
+
body: WalletExportRequestBody,
|
|
448
|
+
) => WalletExportRejectionLike | null;
|
|
449
|
+
restartRuntime?: (reason: string) => Promise<boolean>;
|
|
450
|
+
scheduleRuntimeRestart?: (reason: string) => void;
|
|
451
|
+
deps: WalletRouteDependencies;
|
|
452
|
+
runtime?: AgentRuntime | null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function triggerWalletRuntimeReload(
|
|
456
|
+
ctx: WalletRouteContext,
|
|
457
|
+
reason: string,
|
|
458
|
+
): Promise<boolean> {
|
|
459
|
+
const restarted = ctx.restartRuntime
|
|
460
|
+
? await ctx.restartRuntime(reason)
|
|
461
|
+
: false;
|
|
462
|
+
if (!restarted) {
|
|
463
|
+
ctx.scheduleRuntimeRestart?.(reason);
|
|
464
|
+
}
|
|
465
|
+
return restarted;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const LOCAL_WALLET_SOURCE_ENV_KEYS: Record<WalletChain, string> = {
|
|
469
|
+
evm: "WALLET_SOURCE_EVM",
|
|
470
|
+
solana: "WALLET_SOURCE_SOLANA",
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
type BrowserSolanaCluster = "mainnet" | "devnet" | "testnet";
|
|
474
|
+
|
|
475
|
+
interface BrowserEvmTransactionRequest {
|
|
476
|
+
broadcast: boolean;
|
|
477
|
+
chainId: number;
|
|
478
|
+
data?: string;
|
|
479
|
+
to: string;
|
|
480
|
+
value: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
interface BrowserSolanaWeb3Module {
|
|
484
|
+
Keypair: {
|
|
485
|
+
fromSeed(seed: Uint8Array): unknown;
|
|
486
|
+
};
|
|
487
|
+
VersionedTransaction: {
|
|
488
|
+
deserialize(bytes: Uint8Array): {
|
|
489
|
+
sign(signers: unknown[]): void;
|
|
490
|
+
serialize(): Uint8Array;
|
|
491
|
+
};
|
|
492
|
+
};
|
|
493
|
+
Transaction: {
|
|
494
|
+
from(bytes: Uint8Array): {
|
|
495
|
+
partialSign(...signers: unknown[]): void;
|
|
496
|
+
serialize(): Uint8Array;
|
|
497
|
+
};
|
|
498
|
+
};
|
|
499
|
+
Connection: new (
|
|
500
|
+
endpoint: string,
|
|
501
|
+
commitment: string,
|
|
502
|
+
) => {
|
|
503
|
+
sendRawTransaction(bytes: Uint8Array): Promise<string>;
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const SOLANA_PKCS8_DER_PREFIX = Buffer.from(
|
|
508
|
+
"302e020100300506032b657004220420",
|
|
509
|
+
"hex",
|
|
510
|
+
);
|
|
511
|
+
const BASE58_ALPHABET =
|
|
512
|
+
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
513
|
+
|
|
514
|
+
function normalizeBrowserString(value: unknown): string | undefined {
|
|
515
|
+
if (typeof value !== "string") {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
const trimmed = value.trim();
|
|
519
|
+
return trimmed ? trimmed : undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function normalizeBrowserBoolean(value: unknown, fallback: boolean): boolean {
|
|
523
|
+
return typeof value === "boolean" ? value : fallback;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function normalizeBrowserHexData(value: unknown): string | undefined {
|
|
527
|
+
const trimmed = normalizeBrowserString(value);
|
|
528
|
+
if (!trimmed) {
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
return trimmed.startsWith("0x") ? trimmed : `0x${trimmed}`;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function safeParseBrowserBigInt(value: string): bigint {
|
|
535
|
+
try {
|
|
536
|
+
return BigInt(value);
|
|
537
|
+
} catch {
|
|
538
|
+
throw new Error(
|
|
539
|
+
`Invalid transaction value: expected an integer or hex string, got "${value}"`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function resolveLocalBrowserEvmWallet(): ethers.Wallet {
|
|
545
|
+
const evmKey = normalizeBrowserString(process.env.EVM_PRIVATE_KEY);
|
|
546
|
+
if (!evmKey) {
|
|
547
|
+
throw new Error("Local EVM wallet signing is unavailable.");
|
|
548
|
+
}
|
|
549
|
+
return new ethers.Wallet(evmKey.startsWith("0x") ? evmKey : `0x${evmKey}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function base58DecodeBrowser(value: string): Buffer {
|
|
553
|
+
if (!value.length) {
|
|
554
|
+
return Buffer.alloc(0);
|
|
555
|
+
}
|
|
556
|
+
let number = 0n;
|
|
557
|
+
for (const character of value) {
|
|
558
|
+
const index = BASE58_ALPHABET.indexOf(character);
|
|
559
|
+
if (index === -1) {
|
|
560
|
+
throw new Error(`Invalid base58 character: ${character}`);
|
|
561
|
+
}
|
|
562
|
+
number = number * 58n + BigInt(index);
|
|
563
|
+
}
|
|
564
|
+
const hex = number.toString(16);
|
|
565
|
+
const bytes = Buffer.from(hex.length % 2 === 0 ? hex : `0${hex}`, "hex");
|
|
566
|
+
let leadingZeroes = 0;
|
|
567
|
+
for (const character of value) {
|
|
568
|
+
if (character !== "1") {
|
|
569
|
+
break;
|
|
570
|
+
}
|
|
571
|
+
leadingZeroes += 1;
|
|
572
|
+
}
|
|
573
|
+
return leadingZeroes
|
|
574
|
+
? Buffer.concat([Buffer.alloc(leadingZeroes), bytes])
|
|
575
|
+
: bytes;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function decodeLocalBrowserSolanaPrivateKey(value: string): Buffer {
|
|
579
|
+
const trimmed = value.trim();
|
|
580
|
+
if (
|
|
581
|
+
trimmed.startsWith("[") &&
|
|
582
|
+
trimmed.endsWith("]") &&
|
|
583
|
+
/^\[\s*\d/.test(trimmed)
|
|
584
|
+
) {
|
|
585
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
586
|
+
if (
|
|
587
|
+
!Array.isArray(parsed) ||
|
|
588
|
+
!parsed.every((entry) => typeof entry === "number")
|
|
589
|
+
) {
|
|
590
|
+
throw new Error("Invalid Solana private key JSON array.");
|
|
591
|
+
}
|
|
592
|
+
return Buffer.from(parsed);
|
|
593
|
+
}
|
|
594
|
+
return base58DecodeBrowser(trimmed);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function resolveLocalBrowserSolanaSeed(
|
|
598
|
+
deriveSolanaAddress: WalletRouteDependencies["deriveSolanaAddress"],
|
|
599
|
+
): { address: string; seed: Buffer } {
|
|
600
|
+
const solanaKey = normalizeBrowserString(process.env.SOLANA_PRIVATE_KEY);
|
|
601
|
+
if (!solanaKey) {
|
|
602
|
+
throw new Error("Local Solana signing is unavailable.");
|
|
603
|
+
}
|
|
604
|
+
const decoded = decodeLocalBrowserSolanaPrivateKey(solanaKey);
|
|
605
|
+
const seed =
|
|
606
|
+
decoded.length === 64
|
|
607
|
+
? decoded.subarray(0, 32)
|
|
608
|
+
: decoded.length === 32
|
|
609
|
+
? decoded
|
|
610
|
+
: null;
|
|
611
|
+
if (!seed) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
`Invalid Solana private key length: expected 32 or 64 bytes, got ${decoded.length}.`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
address: deriveSolanaAddress(solanaKey),
|
|
618
|
+
seed,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function resolveBrowserSolanaMessageBytes(
|
|
623
|
+
body: Record<string, unknown>,
|
|
624
|
+
): Buffer {
|
|
625
|
+
const messageBase64 = normalizeBrowserString(body.messageBase64);
|
|
626
|
+
if (messageBase64) {
|
|
627
|
+
return Buffer.from(messageBase64, "base64");
|
|
628
|
+
}
|
|
629
|
+
const message = normalizeBrowserString(body.message);
|
|
630
|
+
if (!message) {
|
|
631
|
+
throw new Error("message or messageBase64 is required.");
|
|
632
|
+
}
|
|
633
|
+
return Buffer.from(message, "utf8");
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function resolveBrowserWalletMessagePayload(
|
|
637
|
+
message: string,
|
|
638
|
+
): string | Uint8Array {
|
|
639
|
+
const trimmed = message.trim();
|
|
640
|
+
if (
|
|
641
|
+
trimmed.startsWith("0x") &&
|
|
642
|
+
trimmed.length >= 4 &&
|
|
643
|
+
trimmed.length % 2 === 0
|
|
644
|
+
) {
|
|
645
|
+
try {
|
|
646
|
+
return ethers.getBytes(trimmed);
|
|
647
|
+
} catch {
|
|
648
|
+
return message;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return message;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
async function signLocalBrowserWalletMessage(message: string): Promise<{
|
|
655
|
+
mode: "local-key";
|
|
656
|
+
signature: string;
|
|
657
|
+
}> {
|
|
658
|
+
const wallet = resolveLocalBrowserEvmWallet();
|
|
659
|
+
return {
|
|
660
|
+
mode: "local-key",
|
|
661
|
+
signature: await wallet.signMessage(
|
|
662
|
+
resolveBrowserWalletMessagePayload(message),
|
|
663
|
+
),
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function signLocalBrowserSolanaMessage(
|
|
668
|
+
body: Record<string, unknown>,
|
|
669
|
+
deriveSolanaAddress: WalletRouteDependencies["deriveSolanaAddress"],
|
|
670
|
+
): Promise<{
|
|
671
|
+
address: string;
|
|
672
|
+
mode: "local-key";
|
|
673
|
+
signatureBase64: string;
|
|
674
|
+
}> {
|
|
675
|
+
const { address, seed } = resolveLocalBrowserSolanaSeed(deriveSolanaAddress);
|
|
676
|
+
const privateKey = crypto.createPrivateKey({
|
|
677
|
+
key: Buffer.concat([SOLANA_PKCS8_DER_PREFIX, seed]),
|
|
678
|
+
format: "der",
|
|
679
|
+
type: "pkcs8",
|
|
680
|
+
});
|
|
681
|
+
const signature = crypto.sign(
|
|
682
|
+
null,
|
|
683
|
+
resolveBrowserSolanaMessageBytes(body),
|
|
684
|
+
privateKey,
|
|
685
|
+
);
|
|
686
|
+
return {
|
|
687
|
+
address,
|
|
688
|
+
mode: "local-key",
|
|
689
|
+
signatureBase64: signature.toString("base64"),
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function normalizeBrowserSolanaCluster(value: unknown): BrowserSolanaCluster {
|
|
694
|
+
if (value === "devnet" || value === "testnet" || value === "mainnet") {
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
return "mainnet";
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function browserSolanaClusterRpcUrl(cluster: BrowserSolanaCluster): string {
|
|
701
|
+
switch (cluster) {
|
|
702
|
+
case "devnet":
|
|
703
|
+
return "https://api.devnet.solana.com";
|
|
704
|
+
case "testnet":
|
|
705
|
+
return "https://api.testnet.solana.com";
|
|
706
|
+
default:
|
|
707
|
+
return "https://api.mainnet-beta.solana.com";
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function loadBrowserSolanaWeb3(): Promise<BrowserSolanaWeb3Module> {
|
|
712
|
+
return (await import("@solana/web3.js")) as BrowserSolanaWeb3Module;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function signLocalBrowserSolanaTransaction(
|
|
716
|
+
body: Record<string, unknown>,
|
|
717
|
+
deriveSolanaAddress: WalletRouteDependencies["deriveSolanaAddress"],
|
|
718
|
+
): Promise<{
|
|
719
|
+
address: string;
|
|
720
|
+
mode: "local-key";
|
|
721
|
+
signedTransactionBase64: string;
|
|
722
|
+
signature?: string;
|
|
723
|
+
cluster: BrowserSolanaCluster;
|
|
724
|
+
}> {
|
|
725
|
+
const transactionBase64 = normalizeBrowserString(body.transactionBase64);
|
|
726
|
+
if (!transactionBase64) {
|
|
727
|
+
throw new Error("transactionBase64 is required.");
|
|
728
|
+
}
|
|
729
|
+
const broadcast = normalizeBrowserBoolean(body.broadcast, false);
|
|
730
|
+
const cluster = normalizeBrowserSolanaCluster(body.cluster);
|
|
731
|
+
const { address, seed } = resolveLocalBrowserSolanaSeed(deriveSolanaAddress);
|
|
732
|
+
|
|
733
|
+
const { Keypair, VersionedTransaction, Transaction, Connection } =
|
|
734
|
+
await loadBrowserSolanaWeb3();
|
|
735
|
+
const keypair = Keypair.fromSeed(new Uint8Array(seed));
|
|
736
|
+
const txBytes = Buffer.from(transactionBase64, "base64");
|
|
737
|
+
|
|
738
|
+
let signedBytes: Uint8Array;
|
|
739
|
+
let broadcastSignature: string | undefined;
|
|
740
|
+
try {
|
|
741
|
+
const versioned = VersionedTransaction.deserialize(txBytes);
|
|
742
|
+
versioned.sign([keypair]);
|
|
743
|
+
signedBytes = versioned.serialize();
|
|
744
|
+
if (broadcast) {
|
|
745
|
+
const connection = new Connection(
|
|
746
|
+
browserSolanaClusterRpcUrl(cluster),
|
|
747
|
+
"confirmed",
|
|
748
|
+
);
|
|
749
|
+
broadcastSignature = await connection.sendRawTransaction(signedBytes);
|
|
750
|
+
}
|
|
751
|
+
} catch (_error) {
|
|
752
|
+
const legacy = Transaction.from(txBytes);
|
|
753
|
+
legacy.partialSign(keypair);
|
|
754
|
+
signedBytes = legacy.serialize();
|
|
755
|
+
if (broadcast) {
|
|
756
|
+
const connection = new Connection(
|
|
757
|
+
browserSolanaClusterRpcUrl(cluster),
|
|
758
|
+
"confirmed",
|
|
759
|
+
);
|
|
760
|
+
broadcastSignature = await connection.sendRawTransaction(signedBytes);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
return {
|
|
765
|
+
address,
|
|
766
|
+
mode: "local-key",
|
|
767
|
+
signedTransactionBase64: Buffer.from(signedBytes).toString("base64"),
|
|
768
|
+
...(broadcastSignature ? { signature: broadcastSignature } : {}),
|
|
769
|
+
cluster,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function resolvePreferredBrowserRpcUrl(
|
|
774
|
+
config: ElizaConfig,
|
|
775
|
+
chainId: number,
|
|
776
|
+
resolveWalletRpcReadiness: WalletRouteDependencies["resolveWalletRpcReadiness"],
|
|
777
|
+
): string | null {
|
|
778
|
+
const readiness = resolveWalletRpcReadiness(config);
|
|
779
|
+
switch (chainId) {
|
|
780
|
+
case 1:
|
|
781
|
+
return readiness.ethereumRpcUrls[0] ?? null;
|
|
782
|
+
case 56:
|
|
783
|
+
case 97:
|
|
784
|
+
return readiness.bscRpcUrls[0] ?? null;
|
|
785
|
+
case 8453:
|
|
786
|
+
return readiness.baseRpcUrls[0] ?? null;
|
|
787
|
+
case 43114:
|
|
788
|
+
return readiness.avalancheRpcUrls[0] ?? null;
|
|
789
|
+
default:
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function sendLocalBrowserWalletTransaction(
|
|
795
|
+
config: ElizaConfig,
|
|
796
|
+
request: BrowserEvmTransactionRequest,
|
|
797
|
+
resolveWalletRpcReadiness: WalletRouteDependencies["resolveWalletRpcReadiness"],
|
|
798
|
+
): Promise<{
|
|
799
|
+
approved: true;
|
|
800
|
+
mode: "local-key";
|
|
801
|
+
pending: false;
|
|
802
|
+
txHash: string;
|
|
803
|
+
}> {
|
|
804
|
+
if (request.broadcast === false) {
|
|
805
|
+
throw new Error(
|
|
806
|
+
"Local browser wallet signing currently requires broadcast=true.",
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
const rpcUrl = resolvePreferredBrowserRpcUrl(
|
|
810
|
+
config,
|
|
811
|
+
request.chainId,
|
|
812
|
+
resolveWalletRpcReadiness,
|
|
813
|
+
);
|
|
814
|
+
if (!rpcUrl) {
|
|
815
|
+
throw new Error(`No RPC URL configured for chain ${request.chainId}.`);
|
|
816
|
+
}
|
|
817
|
+
const provider = new ethers.JsonRpcProvider(rpcUrl);
|
|
818
|
+
try {
|
|
819
|
+
const wallet = resolveLocalBrowserEvmWallet().connect(provider);
|
|
820
|
+
const txResponse = await wallet.sendTransaction({
|
|
821
|
+
chainId: request.chainId,
|
|
822
|
+
data: request.data,
|
|
823
|
+
to: request.to,
|
|
824
|
+
value: safeParseBrowserBigInt(request.value),
|
|
825
|
+
});
|
|
826
|
+
return {
|
|
827
|
+
approved: true,
|
|
828
|
+
mode: "local-key",
|
|
829
|
+
pending: false,
|
|
830
|
+
txHash: txResponse.hash,
|
|
831
|
+
};
|
|
832
|
+
} finally {
|
|
833
|
+
provider.destroy();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
export async function handleWalletRoutes(
|
|
838
|
+
ctx: WalletRouteContext,
|
|
839
|
+
): Promise<boolean> {
|
|
840
|
+
const {
|
|
841
|
+
req,
|
|
842
|
+
res,
|
|
843
|
+
method,
|
|
844
|
+
pathname,
|
|
845
|
+
config,
|
|
846
|
+
saveConfig,
|
|
847
|
+
readJsonBody,
|
|
848
|
+
json,
|
|
849
|
+
error,
|
|
850
|
+
deps,
|
|
851
|
+
} = ctx;
|
|
852
|
+
const {
|
|
853
|
+
isCloudWalletEnabled,
|
|
854
|
+
persistConfigEnv,
|
|
855
|
+
createIntegrationTelemetrySpan,
|
|
856
|
+
resolveWalletRpcReadiness,
|
|
857
|
+
resolveWalletNetworkMode,
|
|
858
|
+
resolveWalletCapabilityStatus,
|
|
859
|
+
getStoredWalletRpcSelections,
|
|
860
|
+
applyWalletRpcConfigUpdate,
|
|
861
|
+
deriveSolanaAddress,
|
|
862
|
+
setSolanaWalletEnv,
|
|
863
|
+
} = deps;
|
|
864
|
+
|
|
865
|
+
// GET /api/wallet/addresses
|
|
866
|
+
if (method === "GET" && pathname === "/api/wallet/addresses") {
|
|
867
|
+
json(res, deps.getWalletAddresses());
|
|
868
|
+
return true;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// GET /api/wallet/balances
|
|
872
|
+
if (method === "GET" && pathname === "/api/wallet/balances") {
|
|
873
|
+
const addresses = deps.getWalletAddresses();
|
|
874
|
+
const rpcReadiness = resolveWalletRpcReadiness(config);
|
|
875
|
+
const alchemyKey = process.env.ALCHEMY_API_KEY?.trim() || null;
|
|
876
|
+
const ankrKey = process.env.ANKR_API_KEY?.trim() || null;
|
|
877
|
+
const heliusKey = process.env.HELIUS_API_KEY?.trim() || null;
|
|
878
|
+
|
|
879
|
+
const result: WalletBalancesResponse = { evm: null, solana: null };
|
|
880
|
+
|
|
881
|
+
if (addresses.evmAddress && rpcReadiness.evmBalanceReady) {
|
|
882
|
+
const evmBalancesSpan = createIntegrationTelemetrySpan({
|
|
883
|
+
boundary: "wallet",
|
|
884
|
+
operation: "fetch_evm_balances",
|
|
885
|
+
});
|
|
886
|
+
try {
|
|
887
|
+
const chains = await deps.fetchEvmBalances(addresses.evmAddress, {
|
|
888
|
+
alchemyKey,
|
|
889
|
+
ankrKey,
|
|
890
|
+
cloudManagedAccess: rpcReadiness.cloudManagedAccess,
|
|
891
|
+
bscRpcUrls: rpcReadiness.bscRpcUrls,
|
|
892
|
+
ethereumRpcUrls: rpcReadiness.ethereumRpcUrls,
|
|
893
|
+
baseRpcUrls: rpcReadiness.baseRpcUrls,
|
|
894
|
+
avaxRpcUrls: rpcReadiness.avalancheRpcUrls,
|
|
895
|
+
nodeRealBscRpcUrl: process.env.NODEREAL_BSC_RPC_URL,
|
|
896
|
+
quickNodeBscRpcUrl: process.env.QUICKNODE_BSC_RPC_URL,
|
|
897
|
+
bscRpcUrl: process.env.BSC_RPC_URL,
|
|
898
|
+
ethereumRpcUrl: process.env.ETHEREUM_RPC_URL,
|
|
899
|
+
baseRpcUrl: process.env.BASE_RPC_URL,
|
|
900
|
+
avaxRpcUrl: process.env.AVALANCHE_RPC_URL,
|
|
901
|
+
});
|
|
902
|
+
result.evm = { address: addresses.evmAddress, chains };
|
|
903
|
+
evmBalancesSpan.success();
|
|
904
|
+
} catch (err) {
|
|
905
|
+
evmBalancesSpan.failure({ error: err });
|
|
906
|
+
logger.warn(`[wallet] EVM balance fetch failed: ${err}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (addresses.solanaAddress && rpcReadiness.solanaBalanceReady) {
|
|
911
|
+
const solanaBalancesSpan = createIntegrationTelemetrySpan({
|
|
912
|
+
boundary: "wallet",
|
|
913
|
+
operation: "fetch_solana_balances",
|
|
914
|
+
});
|
|
915
|
+
try {
|
|
916
|
+
const solanaData = heliusKey
|
|
917
|
+
? await deps.fetchSolanaBalances(addresses.solanaAddress, heliusKey)
|
|
918
|
+
: await deps.fetchSolanaNativeBalanceViaRpc(
|
|
919
|
+
addresses.solanaAddress,
|
|
920
|
+
rpcReadiness.solanaRpcUrls,
|
|
921
|
+
);
|
|
922
|
+
result.solana = { address: addresses.solanaAddress, ...solanaData };
|
|
923
|
+
solanaBalancesSpan.success();
|
|
924
|
+
} catch (err) {
|
|
925
|
+
solanaBalancesSpan.failure({ error: err });
|
|
926
|
+
logger.warn(`[wallet] Solana balance fetch failed: ${err}`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
json(res, result);
|
|
931
|
+
return true;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// GET /api/wallet/nfts
|
|
935
|
+
// The always-loaded plugin-wallet has no NFT data source wired in — NFT
|
|
936
|
+
// indexing lives in the opt-in steward-app routes. Return an empty,
|
|
937
|
+
// well-typed collection (shape-matched to that handler) so the wallet and
|
|
938
|
+
// inventory views render cleanly instead of hitting an unhandled 404.
|
|
939
|
+
if (method === "GET" && pathname === "/api/wallet/nfts") {
|
|
940
|
+
const empty: WalletNftsResponse = { evm: [], solana: null };
|
|
941
|
+
json(res, empty);
|
|
942
|
+
return true;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// POST /api/wallet/import
|
|
946
|
+
if (method === "POST" && pathname === "/api/wallet/import") {
|
|
947
|
+
const rawImport = await readJsonBody<Record<string, unknown>>(req, res);
|
|
948
|
+
if (rawImport === null) return true;
|
|
949
|
+
const parsedImport = PostWalletImportRequestSchema.safeParse(rawImport);
|
|
950
|
+
if (!parsedImport.success) {
|
|
951
|
+
error(
|
|
952
|
+
res,
|
|
953
|
+
parsedImport.error.issues[0]?.message ?? "Invalid request body",
|
|
954
|
+
400,
|
|
955
|
+
);
|
|
956
|
+
return true;
|
|
957
|
+
}
|
|
958
|
+
const body = parsedImport.data;
|
|
959
|
+
|
|
960
|
+
const chain: WalletChain = body.chain
|
|
961
|
+
? body.chain
|
|
962
|
+
: deps.validatePrivateKey(body.privateKey).chain;
|
|
963
|
+
|
|
964
|
+
// When steward is configured, warn that keys should be imported via vault
|
|
965
|
+
const stewardWarning = process.env.STEWARD_API_URL?.trim()
|
|
966
|
+
? "Steward vault is configured. Consider importing keys directly into the vault instead of storing plaintext keys locally."
|
|
967
|
+
: undefined;
|
|
968
|
+
|
|
969
|
+
const result = deps.importWallet(chain, body.privateKey);
|
|
970
|
+
|
|
971
|
+
if (!result.success) {
|
|
972
|
+
error(res, result.error ?? "Import failed", 422);
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (!config.env) config.env = {};
|
|
977
|
+
const envKey = chain === "evm" ? "EVM_PRIVATE_KEY" : "SOLANA_PRIVATE_KEY";
|
|
978
|
+
(config.env as Record<string, string>)[envKey] = process.env[envKey] ?? "";
|
|
979
|
+
persistPrimarySelection(config, chain, "local");
|
|
980
|
+
|
|
981
|
+
let configSaveWarning: string | undefined;
|
|
982
|
+
try {
|
|
983
|
+
saveConfig(config);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
const msg = `Config save failed: ${String(err)}`;
|
|
986
|
+
logger.warn(`[api] ${msg}`);
|
|
987
|
+
configSaveWarning = msg;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const warnings: string[] = [];
|
|
991
|
+
if (configSaveWarning) warnings.push(configSaveWarning);
|
|
992
|
+
if (stewardWarning) warnings.push(stewardWarning);
|
|
993
|
+
|
|
994
|
+
const walletSourceEnvKey = LOCAL_WALLET_SOURCE_ENV_KEYS[chain];
|
|
995
|
+
process.env[walletSourceEnvKey] = "local";
|
|
996
|
+
try {
|
|
997
|
+
await persistConfigEnv(walletSourceEnvKey, "local");
|
|
998
|
+
} catch (err) {
|
|
999
|
+
error(
|
|
1000
|
+
res,
|
|
1001
|
+
`Failed to persist ${walletSourceEnvKey}: ${String(err)}`,
|
|
1002
|
+
500,
|
|
1003
|
+
);
|
|
1004
|
+
return true;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const restarted = await triggerWalletRuntimeReload(
|
|
1008
|
+
ctx,
|
|
1009
|
+
"Wallet configuration updated",
|
|
1010
|
+
);
|
|
1011
|
+
|
|
1012
|
+
json(res, {
|
|
1013
|
+
ok: true,
|
|
1014
|
+
chain,
|
|
1015
|
+
address: result.address,
|
|
1016
|
+
restarting: restarted,
|
|
1017
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
1018
|
+
});
|
|
1019
|
+
return true;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// POST /api/wallet/generate
|
|
1023
|
+
if (method === "POST" && pathname === "/api/wallet/generate") {
|
|
1024
|
+
const rawGen = await readJsonBody<Record<string, unknown>>(req, res);
|
|
1025
|
+
if (rawGen === null) return true;
|
|
1026
|
+
const parsedGen = PostWalletGenerateRequestSchema.safeParse(rawGen);
|
|
1027
|
+
if (!parsedGen.success) {
|
|
1028
|
+
error(
|
|
1029
|
+
res,
|
|
1030
|
+
parsedGen.error.issues[0]?.message ?? "Invalid request body",
|
|
1031
|
+
400,
|
|
1032
|
+
);
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
const body = parsedGen.data;
|
|
1036
|
+
const requestedSource = body.source;
|
|
1037
|
+
const targetChain = body.chain ?? "both";
|
|
1038
|
+
|
|
1039
|
+
// ── Steward-first: delegate wallet generation to steward ──────────
|
|
1040
|
+
const stewardApiUrl = process.env.STEWARD_API_URL?.trim();
|
|
1041
|
+
if (stewardApiUrl && requestedSource !== "local") {
|
|
1042
|
+
try {
|
|
1043
|
+
const agentId =
|
|
1044
|
+
process.env.STEWARD_AGENT_ID?.trim() ||
|
|
1045
|
+
process.env.ELIZA_STEWARD_AGENT_ID?.trim() ||
|
|
1046
|
+
null;
|
|
1047
|
+
|
|
1048
|
+
if (!agentId) {
|
|
1049
|
+
error(
|
|
1050
|
+
res,
|
|
1051
|
+
"Steward is configured but no agent ID is set (STEWARD_AGENT_ID).",
|
|
1052
|
+
500,
|
|
1053
|
+
);
|
|
1054
|
+
return true;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// Build auth headers
|
|
1058
|
+
const headers: Record<string, string> = {
|
|
1059
|
+
Accept: "application/json",
|
|
1060
|
+
"Content-Type": "application/json",
|
|
1061
|
+
};
|
|
1062
|
+
const bearerToken = process.env.STEWARD_AGENT_TOKEN?.trim();
|
|
1063
|
+
const apiKey = process.env.STEWARD_API_KEY?.trim();
|
|
1064
|
+
const tenantId = process.env.STEWARD_TENANT_ID?.trim();
|
|
1065
|
+
if (bearerToken) {
|
|
1066
|
+
headers.Authorization = `Bearer ${bearerToken}`;
|
|
1067
|
+
} else if (apiKey) {
|
|
1068
|
+
headers["X-Steward-Key"] = apiKey;
|
|
1069
|
+
}
|
|
1070
|
+
if (tenantId) {
|
|
1071
|
+
headers["X-Steward-Tenant"] = tenantId;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// Check if agent already exists (has wallets)
|
|
1075
|
+
let agentEvm: string | null = null;
|
|
1076
|
+
let agentSolana: string | null = null;
|
|
1077
|
+
let agentExists = false;
|
|
1078
|
+
|
|
1079
|
+
try {
|
|
1080
|
+
const agentRes = await fetch(
|
|
1081
|
+
`${stewardApiUrl}/agents/${encodeURIComponent(agentId)}`,
|
|
1082
|
+
{ headers: { ...headers }, signal: AbortSignal.timeout(15_000) },
|
|
1083
|
+
);
|
|
1084
|
+
if (agentRes.ok) {
|
|
1085
|
+
agentExists = true;
|
|
1086
|
+
const agentBody = (await agentRes.json()) as {
|
|
1087
|
+
data?: {
|
|
1088
|
+
walletAddress?: string;
|
|
1089
|
+
walletAddresses?: { evm?: string; solana?: string };
|
|
1090
|
+
};
|
|
1091
|
+
walletAddress?: string;
|
|
1092
|
+
walletAddresses?: { evm?: string; solana?: string };
|
|
1093
|
+
};
|
|
1094
|
+
const agent = agentBody.data ?? agentBody;
|
|
1095
|
+
agentEvm =
|
|
1096
|
+
agent.walletAddresses?.evm?.trim() ||
|
|
1097
|
+
agent.walletAddress?.trim() ||
|
|
1098
|
+
null;
|
|
1099
|
+
agentSolana = agent.walletAddresses?.solana?.trim() || null;
|
|
1100
|
+
}
|
|
1101
|
+
} catch {
|
|
1102
|
+
// agent doesn't exist or fetch failed — will try to create
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// If agent doesn't exist, create it (steward auto-generates wallets)
|
|
1106
|
+
if (!agentExists) {
|
|
1107
|
+
const createRes = await fetch(`${stewardApiUrl}/agents`, {
|
|
1108
|
+
method: "POST",
|
|
1109
|
+
headers,
|
|
1110
|
+
body: JSON.stringify({ id: agentId, name: agentId }),
|
|
1111
|
+
signal: AbortSignal.timeout(15_000),
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
if (!createRes.ok) {
|
|
1115
|
+
const errText = await createRes.text().catch(() => "Unknown error");
|
|
1116
|
+
error(res, `Steward agent creation failed: ${errText}`, 502);
|
|
1117
|
+
return true;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const createBody = (await createRes.json()) as {
|
|
1121
|
+
ok?: boolean;
|
|
1122
|
+
data?: {
|
|
1123
|
+
walletAddress?: string;
|
|
1124
|
+
walletAddresses?: { evm?: string; solana?: string };
|
|
1125
|
+
};
|
|
1126
|
+
walletAddress?: string;
|
|
1127
|
+
walletAddresses?: { evm?: string; solana?: string };
|
|
1128
|
+
};
|
|
1129
|
+
const created = createBody.data ?? createBody;
|
|
1130
|
+
agentEvm =
|
|
1131
|
+
created.walletAddresses?.evm?.trim() ||
|
|
1132
|
+
created.walletAddress?.trim() ||
|
|
1133
|
+
null;
|
|
1134
|
+
agentSolana = created.walletAddresses?.solana?.trim() || null;
|
|
1135
|
+
|
|
1136
|
+
logger.info(
|
|
1137
|
+
`[wallet] Created steward agent "${agentId}" with wallets`,
|
|
1138
|
+
);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Cache steward addresses in env for synchronous access
|
|
1142
|
+
const generated: Array<{ chain: WalletChain; address: string }> = [];
|
|
1143
|
+
if (agentEvm && (targetChain === "both" || targetChain === "evm")) {
|
|
1144
|
+
process.env.STEWARD_EVM_ADDRESS = agentEvm;
|
|
1145
|
+
generated.push({ chain: "evm", address: agentEvm });
|
|
1146
|
+
logger.info(`[wallet] Steward EVM wallet: ${agentEvm}`);
|
|
1147
|
+
}
|
|
1148
|
+
if (
|
|
1149
|
+
agentSolana &&
|
|
1150
|
+
(targetChain === "both" || targetChain === "solana")
|
|
1151
|
+
) {
|
|
1152
|
+
process.env.STEWARD_SOLANA_ADDRESS = agentSolana;
|
|
1153
|
+
generated.push({ chain: "solana", address: agentSolana });
|
|
1154
|
+
logger.info(`[wallet] Steward Solana wallet: ${agentSolana}`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
json(res, {
|
|
1158
|
+
ok: true,
|
|
1159
|
+
wallets: generated,
|
|
1160
|
+
source: "steward",
|
|
1161
|
+
});
|
|
1162
|
+
return true;
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
logger.warn(
|
|
1165
|
+
`[wallet] Steward wallet generation failed, falling back to local: ${err}`,
|
|
1166
|
+
);
|
|
1167
|
+
// Fall through to local generation
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// ── Legacy local key generation (fallback) ────────────────────────
|
|
1172
|
+
if (!config.env) config.env = {};
|
|
1173
|
+
|
|
1174
|
+
const generated: Array<{ chain: WalletChain; address: string }> = [];
|
|
1175
|
+
const generatedChains: WalletChain[] = [];
|
|
1176
|
+
|
|
1177
|
+
if (targetChain === "both" || targetChain === "evm") {
|
|
1178
|
+
const result = deps.generateWalletForChain("evm");
|
|
1179
|
+
process.env.EVM_PRIVATE_KEY = result.privateKey;
|
|
1180
|
+
(config.env as Record<string, string>).EVM_PRIVATE_KEY =
|
|
1181
|
+
result.privateKey;
|
|
1182
|
+
persistPrimarySelection(config, "evm", "local");
|
|
1183
|
+
generatedChains.push("evm");
|
|
1184
|
+
generated.push({ chain: "evm", address: result.address });
|
|
1185
|
+
logger.info(`[eliza-api] Generated EVM wallet: ${result.address}`);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (targetChain === "both" || targetChain === "solana") {
|
|
1189
|
+
const result = deps.generateWalletForChain("solana");
|
|
1190
|
+
setSolanaWalletEnv(result.privateKey);
|
|
1191
|
+
(config.env as Record<string, string>).SOLANA_PRIVATE_KEY =
|
|
1192
|
+
result.privateKey;
|
|
1193
|
+
persistPrimarySelection(config, "solana", "local");
|
|
1194
|
+
generatedChains.push("solana");
|
|
1195
|
+
generated.push({ chain: "solana", address: result.address });
|
|
1196
|
+
logger.info(`[eliza-api] Generated Solana wallet: ${result.address}`);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
let configSaveWarning: string | undefined;
|
|
1200
|
+
try {
|
|
1201
|
+
saveConfig(config);
|
|
1202
|
+
} catch (err) {
|
|
1203
|
+
const msg = `Config save failed: ${String(err)}`;
|
|
1204
|
+
logger.warn(`[api] ${msg}`);
|
|
1205
|
+
configSaveWarning = msg;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
for (const chainName of generatedChains) {
|
|
1209
|
+
const walletSourceEnvKey = LOCAL_WALLET_SOURCE_ENV_KEYS[chainName];
|
|
1210
|
+
process.env[walletSourceEnvKey] = "local";
|
|
1211
|
+
try {
|
|
1212
|
+
await persistConfigEnv(walletSourceEnvKey, "local");
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
error(
|
|
1215
|
+
res,
|
|
1216
|
+
`Failed to persist ${walletSourceEnvKey}: ${String(err)}`,
|
|
1217
|
+
500,
|
|
1218
|
+
);
|
|
1219
|
+
return true;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const restarted = await triggerWalletRuntimeReload(
|
|
1224
|
+
ctx,
|
|
1225
|
+
"Wallet configuration updated",
|
|
1226
|
+
);
|
|
1227
|
+
|
|
1228
|
+
json(res, {
|
|
1229
|
+
ok: true,
|
|
1230
|
+
wallets: generated,
|
|
1231
|
+
source: "local",
|
|
1232
|
+
restarting: restarted,
|
|
1233
|
+
...(configSaveWarning ? { warnings: [configSaveWarning] } : {}),
|
|
1234
|
+
});
|
|
1235
|
+
return true;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// GET /api/wallet/config
|
|
1239
|
+
if (method === "GET" && pathname === "/api/wallet/config") {
|
|
1240
|
+
const addresses = deps.getWalletAddresses();
|
|
1241
|
+
const primary = readPrimaryMap(config);
|
|
1242
|
+
const primaryAddresses = resolvePrimaryWalletAddresses(config, addresses);
|
|
1243
|
+
const rpcReadiness = resolveWalletRpcReadiness(config);
|
|
1244
|
+
const localSolanaSignerAvailable = Boolean(
|
|
1245
|
+
process.env.SOLANA_PRIVATE_KEY?.trim(),
|
|
1246
|
+
);
|
|
1247
|
+
const capability = resolveWalletCapabilityStatus({
|
|
1248
|
+
config,
|
|
1249
|
+
runtime: ctx.runtime ?? null,
|
|
1250
|
+
getWalletAddresses: () => primaryAddresses,
|
|
1251
|
+
});
|
|
1252
|
+
const alchemyKeySet = Boolean(process.env.ALCHEMY_API_KEY?.trim());
|
|
1253
|
+
const ankrKeySet = Boolean(process.env.ANKR_API_KEY?.trim());
|
|
1254
|
+
const nodeRealSet = Boolean(process.env.NODEREAL_BSC_RPC_URL?.trim());
|
|
1255
|
+
const quickNodeSet = Boolean(process.env.QUICKNODE_BSC_RPC_URL?.trim());
|
|
1256
|
+
const configStatus: WalletConfigStatus = {
|
|
1257
|
+
selectedRpcProviders: rpcReadiness.selectedRpcProviders,
|
|
1258
|
+
walletNetwork: resolveWalletNetworkMode(config),
|
|
1259
|
+
legacyCustomChains: rpcReadiness.legacyCustomChains,
|
|
1260
|
+
alchemyKeySet,
|
|
1261
|
+
infuraKeySet: Boolean(process.env.INFURA_API_KEY?.trim()),
|
|
1262
|
+
ankrKeySet,
|
|
1263
|
+
nodeRealBscRpcSet: nodeRealSet,
|
|
1264
|
+
quickNodeBscRpcSet: quickNodeSet,
|
|
1265
|
+
managedBscRpcReady: rpcReadiness.managedBscRpcReady,
|
|
1266
|
+
cloudManagedAccess: rpcReadiness.cloudManagedAccess,
|
|
1267
|
+
evmBalanceReady: rpcReadiness.evmBalanceReady,
|
|
1268
|
+
ethereumBalanceReady:
|
|
1269
|
+
alchemyKeySet || rpcReadiness.ethereumRpcUrls.length > 0,
|
|
1270
|
+
baseBalanceReady: alchemyKeySet || rpcReadiness.baseRpcUrls.length > 0,
|
|
1271
|
+
bscBalanceReady: ankrKeySet || rpcReadiness.bscRpcUrls.length > 0,
|
|
1272
|
+
avalancheBalanceReady:
|
|
1273
|
+
alchemyKeySet || rpcReadiness.avalancheRpcUrls.length > 0,
|
|
1274
|
+
solanaBalanceReady: rpcReadiness.solanaBalanceReady,
|
|
1275
|
+
heliusKeySet: Boolean(process.env.HELIUS_API_KEY?.trim()),
|
|
1276
|
+
birdeyeKeySet: Boolean(process.env.BIRDEYE_API_KEY?.trim()),
|
|
1277
|
+
evmChains: [
|
|
1278
|
+
"Ethereum",
|
|
1279
|
+
"Base",
|
|
1280
|
+
"Arbitrum",
|
|
1281
|
+
"Optimism",
|
|
1282
|
+
"Polygon",
|
|
1283
|
+
"BSC",
|
|
1284
|
+
"Avalanche",
|
|
1285
|
+
],
|
|
1286
|
+
evmAddress: primaryAddresses.evmAddress,
|
|
1287
|
+
solanaAddress: primaryAddresses.solanaAddress,
|
|
1288
|
+
walletSource: capability.walletSource,
|
|
1289
|
+
automationMode: capability.automationMode,
|
|
1290
|
+
pluginEvmLoaded: capability.pluginEvmLoaded,
|
|
1291
|
+
pluginEvmRequired: capability.pluginEvmRequired,
|
|
1292
|
+
executionReady: capability.executionReady,
|
|
1293
|
+
executionBlockedReason: capability.executionBlockedReason,
|
|
1294
|
+
evmSigningCapability: capability.evmSigningCapability,
|
|
1295
|
+
evmSigningReason: capability.evmSigningReason,
|
|
1296
|
+
solanaSigningAvailable: primaryAddresses.solanaAddress
|
|
1297
|
+
? localSolanaSignerAvailable || primary.solana === "cloud"
|
|
1298
|
+
: false,
|
|
1299
|
+
};
|
|
1300
|
+
const dual = buildDualWalletShape(config, addresses, isCloudWalletEnabled);
|
|
1301
|
+
if (dual) {
|
|
1302
|
+
json(res, {
|
|
1303
|
+
...configStatus,
|
|
1304
|
+
wallets: dual.wallets,
|
|
1305
|
+
primary: dual.primary,
|
|
1306
|
+
});
|
|
1307
|
+
} else {
|
|
1308
|
+
json(res, configStatus);
|
|
1309
|
+
}
|
|
1310
|
+
return true;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
if (
|
|
1314
|
+
method === "POST" &&
|
|
1315
|
+
(pathname === "/api/wallet/browser-transaction" ||
|
|
1316
|
+
pathname === "/api/wallet/browser-sign-message" ||
|
|
1317
|
+
pathname === "/api/wallet/browser-solana-sign-message" ||
|
|
1318
|
+
pathname === "/api/wallet/browser-solana-transaction")
|
|
1319
|
+
) {
|
|
1320
|
+
const body = await readJsonBody<Record<string, unknown>>(req, res);
|
|
1321
|
+
if (!body) return true;
|
|
1322
|
+
|
|
1323
|
+
const hasLocalEvmKey = Boolean(
|
|
1324
|
+
normalizeBrowserString(process.env.EVM_PRIVATE_KEY),
|
|
1325
|
+
);
|
|
1326
|
+
const hasLocalSolanaKey = Boolean(
|
|
1327
|
+
normalizeBrowserString(process.env.SOLANA_PRIVATE_KEY),
|
|
1328
|
+
);
|
|
1329
|
+
|
|
1330
|
+
if (pathname === "/api/wallet/browser-sign-message") {
|
|
1331
|
+
const message = normalizeBrowserString(body.message);
|
|
1332
|
+
if (!message) {
|
|
1333
|
+
error(res, "message is required.", 400);
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
if (!hasLocalEvmKey) {
|
|
1337
|
+
error(res, "No browser EVM signer is available.", 503);
|
|
1338
|
+
return true;
|
|
1339
|
+
}
|
|
1340
|
+
try {
|
|
1341
|
+
json(res, await signLocalBrowserWalletMessage(message));
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
error(res, err instanceof Error ? err.message : String(err), 503);
|
|
1344
|
+
}
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (pathname === "/api/wallet/browser-solana-sign-message") {
|
|
1349
|
+
if (!hasLocalSolanaKey) {
|
|
1350
|
+
error(res, "No browser Solana signer is available.", 503);
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
try {
|
|
1354
|
+
json(
|
|
1355
|
+
res,
|
|
1356
|
+
await signLocalBrowserSolanaMessage(body, deriveSolanaAddress),
|
|
1357
|
+
);
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
error(res, err instanceof Error ? err.message : String(err), 503);
|
|
1360
|
+
}
|
|
1361
|
+
return true;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
if (pathname === "/api/wallet/browser-solana-transaction") {
|
|
1365
|
+
if (!hasLocalSolanaKey) {
|
|
1366
|
+
error(res, "No browser Solana transaction signer is available.", 503);
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
try {
|
|
1370
|
+
json(
|
|
1371
|
+
res,
|
|
1372
|
+
await signLocalBrowserSolanaTransaction(body, deriveSolanaAddress),
|
|
1373
|
+
);
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
error(res, err instanceof Error ? err.message : String(err), 503);
|
|
1376
|
+
}
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (!hasLocalEvmKey) {
|
|
1381
|
+
error(res, "No browser EVM transaction signer is available.", 503);
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
const request: BrowserEvmTransactionRequest = {
|
|
1386
|
+
broadcast: normalizeBrowserBoolean(body.broadcast, true),
|
|
1387
|
+
chainId:
|
|
1388
|
+
typeof body.chainId === "number" && Number.isFinite(body.chainId)
|
|
1389
|
+
? body.chainId
|
|
1390
|
+
: Number.NaN,
|
|
1391
|
+
data: normalizeBrowserHexData(body.data),
|
|
1392
|
+
to: normalizeBrowserString(body.to) ?? "",
|
|
1393
|
+
value: normalizeBrowserString(body.value) ?? "0",
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
if (!request.to || !Number.isFinite(request.chainId)) {
|
|
1397
|
+
error(res, "to and a valid chainId are required.", 400);
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
try {
|
|
1402
|
+
json(
|
|
1403
|
+
res,
|
|
1404
|
+
await sendLocalBrowserWalletTransaction(
|
|
1405
|
+
config,
|
|
1406
|
+
request,
|
|
1407
|
+
resolveWalletRpcReadiness,
|
|
1408
|
+
),
|
|
1409
|
+
);
|
|
1410
|
+
} catch (err) {
|
|
1411
|
+
error(res, err instanceof Error ? err.message : String(err), 503);
|
|
1412
|
+
}
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// POST /api/wallet/primary — flag-gated (404 when ENABLE_CLOUD_WALLET is off).
|
|
1417
|
+
// Body: { chain: "evm"|"solana", source: "local"|"cloud" }
|
|
1418
|
+
if (method === "POST" && pathname === "/api/wallet/primary") {
|
|
1419
|
+
if (!isCloudWalletEnabled()) {
|
|
1420
|
+
error(res, "Not found", 404);
|
|
1421
|
+
return true;
|
|
1422
|
+
}
|
|
1423
|
+
const rawPrimary = await readJsonBody<Record<string, unknown>>(req, res);
|
|
1424
|
+
if (rawPrimary === null) return true;
|
|
1425
|
+
const parsedPrimary = PostWalletPrimaryRequestSchema.safeParse(rawPrimary);
|
|
1426
|
+
if (!parsedPrimary.success) {
|
|
1427
|
+
error(
|
|
1428
|
+
res,
|
|
1429
|
+
parsedPrimary.error.issues[0]?.message ?? "Invalid request body",
|
|
1430
|
+
400,
|
|
1431
|
+
);
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
const chain = parsedPrimary.data.chain as WalletChainKind;
|
|
1435
|
+
const source = parsedPrimary.data.source as WalletSource;
|
|
1436
|
+
const previousPrimary = readPrimaryMap(config)[chain];
|
|
1437
|
+
|
|
1438
|
+
persistPrimarySelection(config, chain, source);
|
|
1439
|
+
|
|
1440
|
+
let configSaveWarning: string | undefined;
|
|
1441
|
+
try {
|
|
1442
|
+
saveConfig(config);
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
configSaveWarning = `Config save failed: ${String(err)}`;
|
|
1445
|
+
logger.warn(`[api] ${configSaveWarning}`);
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const envKey =
|
|
1449
|
+
chain === "evm" ? "WALLET_SOURCE_EVM" : "WALLET_SOURCE_SOLANA";
|
|
1450
|
+
try {
|
|
1451
|
+
await persistConfigEnv(envKey, source);
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
error(res, `Failed to persist ${envKey}: ${String(err)}`, 500);
|
|
1454
|
+
return true;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
const restarted =
|
|
1458
|
+
previousPrimary === source
|
|
1459
|
+
? false
|
|
1460
|
+
: await triggerWalletRuntimeReload(ctx, "primary-changed");
|
|
1461
|
+
|
|
1462
|
+
json(res, {
|
|
1463
|
+
ok: true,
|
|
1464
|
+
chain,
|
|
1465
|
+
source,
|
|
1466
|
+
restarting: restarted,
|
|
1467
|
+
...(configSaveWarning ? { warnings: [configSaveWarning] } : {}),
|
|
1468
|
+
});
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// POST /api/wallet/refresh-cloud — flag-gated.
|
|
1473
|
+
// Re-queries the Eliza Cloud bridge for per-chain wallet descriptors and
|
|
1474
|
+
// refreshes `config.wallet.cloud.*`. Provision is best-effort so one bad
|
|
1475
|
+
// chain does not discard the other imported wallet(s). This is a refresh
|
|
1476
|
+
// operation, so we re-fetch all chains to pick up any upstream changes
|
|
1477
|
+
// (address rotation, migration, etc.), not just new chains.
|
|
1478
|
+
if (method === "POST" && pathname === "/api/wallet/refresh-cloud") {
|
|
1479
|
+
if (!isCloudWalletEnabled()) {
|
|
1480
|
+
error(res, "Not found", 404);
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const cloud = config.cloud;
|
|
1485
|
+
const cloudHelpers = await loadCloudHelpers();
|
|
1486
|
+
const {
|
|
1487
|
+
ElizaCloudClient,
|
|
1488
|
+
getOrCreateClientAddressKey,
|
|
1489
|
+
normalizeCloudSiteUrl,
|
|
1490
|
+
persistCloudWalletCache,
|
|
1491
|
+
provisionCloudWalletsBestEffort,
|
|
1492
|
+
resolveCloudApiKey,
|
|
1493
|
+
} = cloudHelpers;
|
|
1494
|
+
const apiKey = resolveCloudApiKey(config, ctx.runtime ?? null) ?? "";
|
|
1495
|
+
const baseUrl = cloud?.baseUrl
|
|
1496
|
+
? normalizeCloudSiteUrl(cloud.baseUrl)
|
|
1497
|
+
: "https://www.elizacloud.ai";
|
|
1498
|
+
if (!apiKey) {
|
|
1499
|
+
error(res, "Cloud not linked — sign in to Eliza Cloud first", 400);
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const agentEntry = config.agents?.list?.[0];
|
|
1504
|
+
const agentId =
|
|
1505
|
+
agentEntry?.id ??
|
|
1506
|
+
(ctx.runtime as { agentId?: string } | null)?.agentId ??
|
|
1507
|
+
null;
|
|
1508
|
+
if (!agentId) {
|
|
1509
|
+
error(res, "No agent configured", 400);
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
try {
|
|
1514
|
+
const { address: clientAddress } = await getOrCreateClientAddressKey();
|
|
1515
|
+
const bridge = new ElizaCloudClient(baseUrl, apiKey);
|
|
1516
|
+
const cachedDescriptors = readCachedCloudWalletDescriptors(config);
|
|
1517
|
+
const chainsToProvision = (["evm", "solana"] as const).filter(
|
|
1518
|
+
(chain) => !cachedDescriptors[chain],
|
|
1519
|
+
);
|
|
1520
|
+
const descriptors: Partial<
|
|
1521
|
+
Record<WalletChainKind, CloudWalletDescriptor>
|
|
1522
|
+
> = { ...cachedDescriptors };
|
|
1523
|
+
const warnings: string[] = [];
|
|
1524
|
+
const previousPrimary = readPrimaryMap(config);
|
|
1525
|
+
const previousEvmAddress = readCloudWalletAddress(cachedDescriptors.evm);
|
|
1526
|
+
const previousSolanaAddress = readCloudWalletAddress(
|
|
1527
|
+
cachedDescriptors.solana,
|
|
1528
|
+
);
|
|
1529
|
+
if (chainsToProvision.length > 0) {
|
|
1530
|
+
const provisionResult = await provisionCloudWalletsBestEffort(bridge, {
|
|
1531
|
+
agentId,
|
|
1532
|
+
clientAddress,
|
|
1533
|
+
chains: chainsToProvision,
|
|
1534
|
+
});
|
|
1535
|
+
Object.assign(descriptors, provisionResult.descriptors);
|
|
1536
|
+
for (const [index, failure] of provisionResult.failures.entries()) {
|
|
1537
|
+
const cached = cachedDescriptors[failure.chain];
|
|
1538
|
+
if (cached) {
|
|
1539
|
+
descriptors[failure.chain] = cached;
|
|
1540
|
+
const detail =
|
|
1541
|
+
failure.error instanceof Error
|
|
1542
|
+
? failure.error.message
|
|
1543
|
+
: String(failure.error);
|
|
1544
|
+
warnings.push(
|
|
1545
|
+
`Reused cached ${failure.chain} cloud wallet after refresh failed: ${detail}`,
|
|
1546
|
+
);
|
|
1547
|
+
continue;
|
|
1548
|
+
}
|
|
1549
|
+
warnings.push(
|
|
1550
|
+
provisionResult.warnings[index] ??
|
|
1551
|
+
`Cloud ${failure.chain} wallet import failed`,
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (!descriptors.evm && !descriptors.solana) {
|
|
1556
|
+
throw new Error(
|
|
1557
|
+
warnings[0] ?? "Failed to provision any cloud wallet descriptors",
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
persistCloudWalletCache(config as never, descriptors);
|
|
1561
|
+
|
|
1562
|
+
process.env.ENABLE_CLOUD_WALLET = "1";
|
|
1563
|
+
await persistConfigEnv("ENABLE_CLOUD_WALLET", "1");
|
|
1564
|
+
|
|
1565
|
+
const cloudConfig: Record<string, unknown> = { ...(cloud ?? {}) };
|
|
1566
|
+
cloudConfig.clientAddressPublicKey = clientAddress;
|
|
1567
|
+
config.cloud = cloudConfig as typeof config.cloud;
|
|
1568
|
+
|
|
1569
|
+
if (descriptors.evm?.walletAddress) {
|
|
1570
|
+
process.env.ELIZA_CLOUD_EVM_ADDRESS = descriptors.evm.walletAddress;
|
|
1571
|
+
await persistConfigEnv(
|
|
1572
|
+
"ELIZA_CLOUD_EVM_ADDRESS",
|
|
1573
|
+
descriptors.evm.walletAddress,
|
|
1574
|
+
);
|
|
1575
|
+
process.env.WALLET_SOURCE_EVM = "cloud";
|
|
1576
|
+
await persistConfigEnv("WALLET_SOURCE_EVM", "cloud");
|
|
1577
|
+
persistPrimarySelection(config, "evm", "cloud");
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
if (descriptors.solana?.walletAddress) {
|
|
1581
|
+
process.env.ELIZA_CLOUD_SOLANA_ADDRESS =
|
|
1582
|
+
descriptors.solana.walletAddress;
|
|
1583
|
+
await persistConfigEnv(
|
|
1584
|
+
"ELIZA_CLOUD_SOLANA_ADDRESS",
|
|
1585
|
+
descriptors.solana.walletAddress,
|
|
1586
|
+
);
|
|
1587
|
+
process.env.WALLET_SOURCE_SOLANA = "cloud";
|
|
1588
|
+
await persistConfigEnv("WALLET_SOURCE_SOLANA", "cloud");
|
|
1589
|
+
persistPrimarySelection(config, "solana", "cloud");
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
let configSaveWarning: string | undefined;
|
|
1593
|
+
try {
|
|
1594
|
+
saveConfig(config);
|
|
1595
|
+
} catch (err) {
|
|
1596
|
+
configSaveWarning = `Config save failed: ${String(err)}`;
|
|
1597
|
+
logger.warn(`[api] ${configSaveWarning}`);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const responseWarnings = [...warnings];
|
|
1601
|
+
if (configSaveWarning) {
|
|
1602
|
+
responseWarnings.push(configSaveWarning);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const nextPrimary = readPrimaryMap(config);
|
|
1606
|
+
const nextEvmAddress = descriptors.evm?.walletAddress ?? null;
|
|
1607
|
+
const nextSolanaAddress = descriptors.solana?.walletAddress ?? null;
|
|
1608
|
+
const walletBindingChanged =
|
|
1609
|
+
previousPrimary.evm !== nextPrimary.evm ||
|
|
1610
|
+
previousPrimary.solana !== nextPrimary.solana ||
|
|
1611
|
+
previousEvmAddress !== nextEvmAddress ||
|
|
1612
|
+
previousSolanaAddress !== nextSolanaAddress;
|
|
1613
|
+
const restarted = walletBindingChanged
|
|
1614
|
+
? await triggerWalletRuntimeReload(ctx, "cloud-refreshed")
|
|
1615
|
+
: false;
|
|
1616
|
+
|
|
1617
|
+
json(res, {
|
|
1618
|
+
ok: true,
|
|
1619
|
+
restarting: restarted,
|
|
1620
|
+
wallets: {
|
|
1621
|
+
evm: descriptors.evm
|
|
1622
|
+
? {
|
|
1623
|
+
address: descriptors.evm.walletAddress,
|
|
1624
|
+
provider: descriptors.evm.walletProvider,
|
|
1625
|
+
}
|
|
1626
|
+
: null,
|
|
1627
|
+
solana: descriptors.solana
|
|
1628
|
+
? {
|
|
1629
|
+
address: descriptors.solana.walletAddress,
|
|
1630
|
+
provider: descriptors.solana.walletProvider,
|
|
1631
|
+
}
|
|
1632
|
+
: null,
|
|
1633
|
+
},
|
|
1634
|
+
...(responseWarnings.length > 0 ? { warnings: responseWarnings } : {}),
|
|
1635
|
+
});
|
|
1636
|
+
} catch (err) {
|
|
1637
|
+
logger.warn(`[api] cloud wallet refresh failed: ${String(err)}`);
|
|
1638
|
+
error(res, `Cloud wallet refresh failed: ${String(err)}`, 502);
|
|
1639
|
+
}
|
|
1640
|
+
return true;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
// PUT /api/wallet/config
|
|
1644
|
+
if (method === "PUT" && pathname === "/api/wallet/config") {
|
|
1645
|
+
const body = await readJsonBody<Record<string, unknown>>(req, res);
|
|
1646
|
+
if (!body) return true;
|
|
1647
|
+
|
|
1648
|
+
const updateRequest = resolveWalletConfigUpdateRequest(
|
|
1649
|
+
body,
|
|
1650
|
+
getStoredWalletRpcSelections(config),
|
|
1651
|
+
);
|
|
1652
|
+
if (!updateRequest) {
|
|
1653
|
+
error(res, "Invalid wallet config update");
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
applyWalletRpcConfigUpdate(config, updateRequest);
|
|
1658
|
+
|
|
1659
|
+
const selectedProviders = normalizeWalletRpcSelections(
|
|
1660
|
+
updateRequest.selections,
|
|
1661
|
+
);
|
|
1662
|
+
const shouldEnableCloudWallet = Object.values(selectedProviders).every(
|
|
1663
|
+
(provider) => provider === "eliza-cloud",
|
|
1664
|
+
);
|
|
1665
|
+
|
|
1666
|
+
if (shouldEnableCloudWallet) {
|
|
1667
|
+
process.env.ENABLE_CLOUD_WALLET = "1";
|
|
1668
|
+
try {
|
|
1669
|
+
await persistConfigEnv("ENABLE_CLOUD_WALLET", "1");
|
|
1670
|
+
} catch (err) {
|
|
1671
|
+
error(
|
|
1672
|
+
res,
|
|
1673
|
+
`Failed to persist ENABLE_CLOUD_WALLET: ${String(err)}`,
|
|
1674
|
+
500,
|
|
1675
|
+
);
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
let configSaveWarning: string | undefined;
|
|
1681
|
+
try {
|
|
1682
|
+
saveConfig(config);
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
const msg = `Config save failed: ${String(err)}`;
|
|
1685
|
+
logger.warn(`[api] ${msg}`);
|
|
1686
|
+
configSaveWarning = msg;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
json(res, {
|
|
1690
|
+
ok: true,
|
|
1691
|
+
...(configSaveWarning ? { warnings: [configSaveWarning] } : {}),
|
|
1692
|
+
});
|
|
1693
|
+
return true;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
if (method === "GET" && pathname === "/api/wallet/approvals/stream") {
|
|
1697
|
+
error(
|
|
1698
|
+
res,
|
|
1699
|
+
"Wallet approval streaming requires the Steward wallet route bridge.",
|
|
1700
|
+
503,
|
|
1701
|
+
);
|
|
1702
|
+
return true;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
const approvalDecision = /^\/api\/wallet\/approvals\/([^/]+)\/decision$/.exec(
|
|
1706
|
+
pathname,
|
|
1707
|
+
);
|
|
1708
|
+
if (method === "POST" && approvalDecision) {
|
|
1709
|
+
error(
|
|
1710
|
+
res,
|
|
1711
|
+
"Wallet approval decisions require the Steward wallet route bridge.",
|
|
1712
|
+
503,
|
|
1713
|
+
);
|
|
1714
|
+
return true;
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// POST /api/wallet/export — removed (no plaintext key export from the agent API).
|
|
1718
|
+
if (method === "POST" && pathname === "/api/wallet/export") {
|
|
1719
|
+
error(
|
|
1720
|
+
res,
|
|
1721
|
+
"Private key export has been removed. Use Steward or OS-backed custody flows.",
|
|
1722
|
+
410,
|
|
1723
|
+
);
|
|
1724
|
+
return true;
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|