@elizaos/plugin-steward-app 2.0.3-beta.6 → 2.0.3-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ApprovalQueue.d.ts +18 -0
- package/dist/ApprovalQueue.d.ts.map +1 -0
- package/dist/ApprovalQueue.js +420 -0
- package/dist/ApprovalQueue.js.map +1 -0
- package/dist/StewardLogo.d.ts +11 -0
- package/dist/StewardLogo.d.ts.map +1 -0
- package/dist/StewardLogo.js +36 -0
- package/dist/StewardLogo.js.map +1 -0
- package/dist/StewardView.d.ts +13 -0
- package/dist/StewardView.d.ts.map +1 -0
- package/dist/StewardView.helpers.d.ts +15 -0
- package/dist/StewardView.helpers.d.ts.map +1 -0
- package/dist/StewardView.helpers.js +45 -0
- package/dist/StewardView.helpers.js.map +1 -0
- package/dist/StewardView.interact.d.ts +2 -0
- package/dist/StewardView.interact.d.ts.map +1 -0
- package/dist/StewardView.interact.js +54 -0
- package/dist/StewardView.interact.js.map +1 -0
- package/dist/StewardView.js +249 -0
- package/dist/StewardView.js.map +1 -0
- package/dist/TransactionHistory.d.ts +22 -0
- package/dist/TransactionHistory.d.ts.map +1 -0
- package/dist/TransactionHistory.js +361 -0
- package/dist/TransactionHistory.js.map +1 -0
- package/dist/__fixtures__/steward-sdk-fixtures.d.ts +10 -0
- package/dist/__fixtures__/steward-sdk-fixtures.d.ts.map +1 -0
- package/dist/__fixtures__/steward-sdk-fixtures.js +60 -0
- package/dist/__fixtures__/steward-sdk-fixtures.js.map +1 -0
- package/dist/actions/wallet-action-shared.d.ts +15 -0
- package/dist/actions/wallet-action-shared.d.ts.map +1 -0
- package/dist/actions/wallet-action-shared.js +16 -0
- package/dist/actions/wallet-action-shared.js.map +1 -0
- package/dist/api/binance-skill-helpers.d.ts +21 -0
- package/dist/api/binance-skill-helpers.d.ts.map +1 -0
- package/dist/api/binance-skill-helpers.js +790 -0
- package/dist/api/binance-skill-helpers.js.map +1 -0
- package/dist/api/bsc-trade.d.ts +36 -0
- package/dist/api/bsc-trade.d.ts.map +1 -0
- package/dist/api/bsc-trade.js +796 -0
- package/dist/api/bsc-trade.js.map +1 -0
- package/dist/api/trade-safety.d.ts +35 -0
- package/dist/api/trade-safety.d.ts.map +1 -0
- package/dist/api/trade-safety.js +56 -0
- package/dist/api/trade-safety.js.map +1 -0
- package/dist/api/tx-service.d.ts +53 -0
- package/dist/api/tx-service.d.ts.map +1 -0
- package/dist/api/tx-service.js +206 -0
- package/dist/api/tx-service.js.map +1 -0
- package/dist/api/wallet-bsc-routes.d.ts +63 -0
- package/dist/api/wallet-bsc-routes.d.ts.map +1 -0
- package/dist/api/wallet-bsc-routes.js +337 -0
- package/dist/api/wallet-bsc-routes.js.map +1 -0
- package/dist/api/wallet-capability.d.ts +2 -0
- package/dist/api/wallet-capability.d.ts.map +1 -0
- package/dist/api/wallet-capability.js +15 -0
- package/dist/api/wallet-capability.js.map +1 -0
- package/dist/api/wallet-dex-prices.d.ts +43 -0
- package/dist/api/wallet-dex-prices.d.ts.map +1 -0
- package/dist/api/wallet-dex-prices.js +132 -0
- package/dist/api/wallet-dex-prices.js.map +1 -0
- package/dist/api/wallet-evm-balance.d.ts +72 -0
- package/dist/api/wallet-evm-balance.d.ts.map +1 -0
- package/dist/api/wallet-evm-balance.js +697 -0
- package/dist/api/wallet-evm-balance.js.map +1 -0
- package/dist/api/wallet-routes.d.ts +27 -0
- package/dist/api/wallet-routes.d.ts.map +1 -0
- package/dist/api/wallet-routes.js +556 -0
- package/dist/api/wallet-routes.js.map +1 -0
- package/dist/api/wallet-rpc.d.ts +73 -0
- package/dist/api/wallet-rpc.d.ts.map +1 -0
- package/dist/api/wallet-rpc.js +460 -0
- package/dist/api/wallet-rpc.js.map +1 -0
- package/dist/api/wallet-trade-routes.d.ts +104 -0
- package/dist/api/wallet-trade-routes.d.ts.map +1 -0
- package/dist/api/wallet-trade-routes.js +353 -0
- package/dist/api/wallet-trade-routes.js.map +1 -0
- package/dist/api/wallet-trading-profile.d.ts +31 -0
- package/dist/api/wallet-trading-profile.d.ts.map +1 -0
- package/dist/api/wallet-trading-profile.js +500 -0
- package/dist/api/wallet-trading-profile.js.map +1 -0
- package/dist/api/wallet.d.ts +60 -0
- package/dist/api/wallet.d.ts.map +1 -0
- package/dist/api/wallet.js +617 -0
- package/dist/api/wallet.js.map +1 -0
- package/dist/chain-utils.d.ts +10 -0
- package/dist/chain-utils.d.ts.map +1 -0
- package/dist/chain-utils.js +81 -0
- package/dist/chain-utils.js.map +1 -0
- package/dist/components/StewardSpatialView.d.ts +74 -0
- package/dist/components/StewardSpatialView.d.ts.map +1 -0
- package/dist/components/StewardSpatialView.js +309 -0
- package/dist/components/StewardSpatialView.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +21 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +319 -0
- package/dist/plugin.js.map +1 -0
- package/dist/providers/steward-balance.d.ts +12 -0
- package/dist/providers/steward-balance.d.ts.map +1 -0
- package/dist/providers/steward-balance.js +85 -0
- package/dist/providers/steward-balance.js.map +1 -0
- package/dist/providers/steward-receive-address.d.ts +12 -0
- package/dist/providers/steward-receive-address.d.ts.map +1 -0
- package/dist/providers/steward-receive-address.js +47 -0
- package/dist/providers/steward-receive-address.js.map +1 -0
- package/dist/register-routes.d.ts +2 -0
- package/dist/register-routes.d.ts.map +1 -0
- package/dist/register-routes.js +6 -0
- package/dist/register-routes.js.map +1 -0
- package/dist/register-terminal-view.d.ts +15 -0
- package/dist/register-terminal-view.d.ts.map +1 -0
- package/dist/register-terminal-view.js +34 -0
- package/dist/register-terminal-view.js.map +1 -0
- package/dist/routes/steward-bridge.d.ts +202 -0
- package/dist/routes/steward-bridge.d.ts.map +1 -0
- package/dist/routes/steward-bridge.js +776 -0
- package/dist/routes/steward-bridge.js.map +1 -0
- package/dist/routes/steward-compat-routes.d.ts +21 -0
- package/dist/routes/steward-compat-routes.d.ts.map +1 -0
- package/dist/routes/steward-compat-routes.js +350 -0
- package/dist/routes/steward-compat-routes.js.map +1 -0
- package/dist/routes/wallet-browser-compat-routes.d.ts +6 -0
- package/dist/routes/wallet-browser-compat-routes.d.ts.map +1 -0
- package/dist/routes/wallet-browser-compat-routes.js +402 -0
- package/dist/routes/wallet-browser-compat-routes.js.map +1 -0
- package/dist/routes/wallet-bsc-core-routes.d.ts +15 -0
- package/dist/routes/wallet-bsc-core-routes.d.ts.map +1 -0
- package/dist/routes/wallet-bsc-core-routes.js +59 -0
- package/dist/routes/wallet-bsc-core-routes.js.map +1 -0
- package/dist/routes/wallet-compat-routes.d.ts +13 -0
- package/dist/routes/wallet-compat-routes.d.ts.map +1 -0
- package/dist/routes/wallet-compat-routes.js +206 -0
- package/dist/routes/wallet-compat-routes.js.map +1 -0
- package/dist/routes/wallet-core-routes.d.ts +16 -0
- package/dist/routes/wallet-core-routes.d.ts.map +1 -0
- package/dist/routes/wallet-core-routes.js +48 -0
- package/dist/routes/wallet-core-routes.js.map +1 -0
- package/dist/routes/wallet-trade-compat-routes.d.ts +11 -0
- package/dist/routes/wallet-trade-compat-routes.d.ts.map +1 -0
- package/dist/routes/wallet-trade-compat-routes.js +570 -0
- package/dist/routes/wallet-trade-compat-routes.js.map +1 -0
- package/dist/security/hydrate-wallet-keys-from-platform-store.d.ts +7 -0
- package/dist/security/hydrate-wallet-keys-from-platform-store.d.ts.map +1 -0
- package/dist/security/hydrate-wallet-keys-from-platform-store.js +43 -0
- package/dist/security/hydrate-wallet-keys-from-platform-store.js.map +1 -0
- package/dist/security/wallet-os-store-actions.d.ts +14 -0
- package/dist/security/wallet-os-store-actions.d.ts.map +1 -0
- package/dist/security/wallet-os-store-actions.js +63 -0
- package/dist/security/wallet-os-store-actions.js.map +1 -0
- package/dist/services/steward-credentials.d.ts +2 -0
- package/dist/services/steward-credentials.d.ts.map +1 -0
- package/dist/services/steward-credentials.js +2 -0
- package/dist/services/steward-credentials.js.map +1 -0
- package/dist/services/steward-evm-account.d.ts +75 -0
- package/dist/services/steward-evm-account.d.ts.map +1 -0
- package/dist/services/steward-evm-account.js +279 -0
- package/dist/services/steward-evm-account.js.map +1 -0
- package/dist/services/steward-evm-bridge.d.ts +36 -0
- package/dist/services/steward-evm-bridge.d.ts.map +1 -0
- package/dist/services/steward-evm-bridge.js +78 -0
- package/dist/services/steward-evm-bridge.js.map +1 -0
- package/dist/services/steward-sidecar/health-check.d.ts +2 -0
- package/dist/services/steward-sidecar/health-check.d.ts.map +1 -0
- package/dist/services/steward-sidecar/health-check.js +2 -0
- package/dist/services/steward-sidecar/health-check.js.map +1 -0
- package/dist/services/steward-sidecar/helpers.d.ts +2 -0
- package/dist/services/steward-sidecar/helpers.d.ts.map +1 -0
- package/dist/services/steward-sidecar/helpers.js +2 -0
- package/dist/services/steward-sidecar/helpers.js.map +1 -0
- package/dist/services/steward-sidecar/process-management.d.ts +2 -0
- package/dist/services/steward-sidecar/process-management.d.ts.map +1 -0
- package/dist/services/steward-sidecar/process-management.js +2 -0
- package/dist/services/steward-sidecar/process-management.js.map +1 -0
- package/dist/services/steward-sidecar/types.d.ts +2 -0
- package/dist/services/steward-sidecar/types.d.ts.map +1 -0
- package/dist/services/steward-sidecar/types.js +2 -0
- package/dist/services/steward-sidecar/types.js.map +1 -0
- package/dist/services/steward-sidecar/wallet-setup.d.ts +2 -0
- package/dist/services/steward-sidecar/wallet-setup.d.ts.map +1 -0
- package/dist/services/steward-sidecar/wallet-setup.js +2 -0
- package/dist/services/steward-sidecar/wallet-setup.js.map +1 -0
- package/dist/services/steward-sidecar.d.ts +2 -0
- package/dist/services/steward-sidecar.d.ts.map +1 -0
- package/dist/services/steward-sidecar.js +2 -0
- package/dist/services/steward-sidecar.js.map +1 -0
- package/dist/services/steward-wallet.d.ts +25 -0
- package/dist/services/steward-wallet.d.ts.map +1 -0
- package/dist/services/steward-wallet.js +333 -0
- package/dist/services/steward-wallet.js.map +1 -0
- package/dist/steward-ui-state.d.ts +14 -0
- package/dist/steward-ui-state.d.ts.map +1 -0
- package/dist/steward-ui-state.js +46 -0
- package/dist/steward-ui-state.js.map +1 -0
- package/dist/steward-view-bundle.d.ts +3 -0
- package/dist/steward-view-bundle.d.ts.map +1 -0
- package/dist/steward-view-bundle.js +7 -0
- package/dist/steward-view-bundle.js.map +1 -0
- package/dist/types/bsc-trade.d.ts +180 -0
- package/dist/types/bsc-trade.d.ts.map +1 -0
- package/dist/types/bsc-trade.js +1 -0
- package/dist/types/bsc-trade.js.map +1 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/steward.d.ts +83 -0
- package/dist/types/steward.d.ts.map +1 -0
- package/dist/types/steward.js +1 -0
- package/dist/types/steward.js.map +1 -0
- package/dist/ui.d.ts +7 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +7 -0
- package/dist/ui.js.map +1 -0
- package/dist/views/bundle.js +601 -0
- package/dist/views/bundle.js.map +1 -0
- package/package.json +8 -8
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
import { logger } from "@elizaos/core";
|
|
2
|
+
import * as ethers from "ethers";
|
|
3
|
+
import {
|
|
4
|
+
normalizeRpcUrl,
|
|
5
|
+
resolveBscRpcUrls as resolveWalletBscRpcUrls,
|
|
6
|
+
resolveWalletNetworkMode
|
|
7
|
+
} from "./wallet-rpc.js";
|
|
8
|
+
const FETCH_TIMEOUT_MS = 15e3;
|
|
9
|
+
const BSC_MAINNET_CHAIN_ID = 56;
|
|
10
|
+
const BSC_TESTNET_CHAIN_ID = 97;
|
|
11
|
+
const MIN_GAS_BNB = "0.005";
|
|
12
|
+
const DEFAULT_SLIPPAGE_BPS = 300;
|
|
13
|
+
const MAX_SLIPPAGE_BPS = 1e3;
|
|
14
|
+
const SLIPPAGE_WARNING_THRESHOLD_BPS = 300;
|
|
15
|
+
const ZEROX_API_BASE_URL = "https://bsc.api.0x.org";
|
|
16
|
+
const ZEROX_QUOTE_TIMEOUT_MS = 8e3;
|
|
17
|
+
const PANCAKE_SWAP_V2_ROUTER = ethers.getAddress(
|
|
18
|
+
"0x10ED43C718714eb63d5aA57B78B54704E256024E"
|
|
19
|
+
);
|
|
20
|
+
const BSC_WBNB_FALLBACK = ethers.getAddress(
|
|
21
|
+
"0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c"
|
|
22
|
+
);
|
|
23
|
+
const BSC_TESTNET_EXPLORER_BASE_URL = "https://testnet.bscscan.com";
|
|
24
|
+
const ROUTER_IFACE = new ethers.Interface([
|
|
25
|
+
"function WETH() view returns (address)",
|
|
26
|
+
"function getAmountsOut(uint256 amountIn, address[] calldata path) view returns (uint256[] memory amounts)",
|
|
27
|
+
"function swapExactETHForTokensSupportingFeeOnTransferTokens(uint amountOutMin, address[] path, address to, uint deadline)",
|
|
28
|
+
"function swapExactTokensForETHSupportingFeeOnTransferTokens(uint amountIn, uint amountOutMin, address[] path, address to, uint deadline)"
|
|
29
|
+
]);
|
|
30
|
+
const ERC20_IFACE = new ethers.Interface([
|
|
31
|
+
"function symbol() view returns (string)",
|
|
32
|
+
"function decimals() view returns (uint8)",
|
|
33
|
+
"function balanceOf(address owner) view returns (uint256)",
|
|
34
|
+
"function approve(address spender, uint256 amount) returns (bool)"
|
|
35
|
+
]);
|
|
36
|
+
function resolveBscExecutionContext() {
|
|
37
|
+
const walletNetwork = resolveWalletNetworkMode();
|
|
38
|
+
if (walletNetwork === "mainnet") {
|
|
39
|
+
return {
|
|
40
|
+
walletNetwork,
|
|
41
|
+
chainId: BSC_MAINNET_CHAIN_ID,
|
|
42
|
+
routerAddress: PANCAKE_SWAP_V2_ROUTER,
|
|
43
|
+
wrappedNativeFallback: BSC_WBNB_FALLBACK,
|
|
44
|
+
explorerBaseUrl: "https://bscscan.com"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const configuredChainIdRaw = process.env.BSC_TESTNET_CHAIN_ID?.trim();
|
|
48
|
+
const configuredChainId = configuredChainIdRaw ? Number.parseInt(configuredChainIdRaw, 10) : BSC_TESTNET_CHAIN_ID;
|
|
49
|
+
const chainId = Number.isFinite(configuredChainId) && configuredChainId > 0 ? configuredChainId : BSC_TESTNET_CHAIN_ID;
|
|
50
|
+
const routerAddress = normalizeAddress(
|
|
51
|
+
process.env.BSC_TESTNET_SWAP_ROUTER_ADDRESS ?? process.env.BSC_SWAP_ROUTER_ADDRESS
|
|
52
|
+
);
|
|
53
|
+
if (!routerAddress) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"BSC testnet router not configured. Set BSC_TESTNET_SWAP_ROUTER_ADDRESS."
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const wrappedNativeFallback = normalizeAddress(
|
|
59
|
+
process.env.BSC_TESTNET_WRAPPED_NATIVE_ADDRESS ?? process.env.BSC_WRAPPED_NATIVE_ADDRESS
|
|
60
|
+
);
|
|
61
|
+
const explorerBaseUrlRaw = process.env.BSC_TESTNET_EXPLORER_BASE_URL ?? BSC_TESTNET_EXPLORER_BASE_URL;
|
|
62
|
+
const explorerBaseUrl = (() => {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(explorerBaseUrlRaw);
|
|
65
|
+
return parsed.toString().replace(/\/+$/, "");
|
|
66
|
+
} catch {
|
|
67
|
+
return BSC_TESTNET_EXPLORER_BASE_URL;
|
|
68
|
+
}
|
|
69
|
+
})();
|
|
70
|
+
return {
|
|
71
|
+
walletNetwork,
|
|
72
|
+
chainId,
|
|
73
|
+
routerAddress,
|
|
74
|
+
wrappedNativeFallback,
|
|
75
|
+
explorerBaseUrl
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function resolveBscRpcUrls(input) {
|
|
79
|
+
const walletNetwork = resolveWalletNetworkMode();
|
|
80
|
+
const candidates = [
|
|
81
|
+
...(input.rpcUrls ?? []).map((url) => normalizeRpcUrl(url)),
|
|
82
|
+
normalizeRpcUrl(process.env.BSC_TESTNET_RPC_URL),
|
|
83
|
+
normalizeRpcUrl(
|
|
84
|
+
input.nodeRealBscRpcUrl !== void 0 ? input.nodeRealBscRpcUrl : process.env.NODEREAL_BSC_RPC_URL
|
|
85
|
+
),
|
|
86
|
+
normalizeRpcUrl(
|
|
87
|
+
input.quickNodeBscRpcUrl !== void 0 ? input.quickNodeBscRpcUrl : process.env.QUICKNODE_BSC_RPC_URL
|
|
88
|
+
),
|
|
89
|
+
// Standard plugin env key used across elizaOS EVM tooling.
|
|
90
|
+
normalizeRpcUrl(
|
|
91
|
+
input.bscRpcUrl !== void 0 ? input.bscRpcUrl : process.env.BSC_RPC_URL
|
|
92
|
+
),
|
|
93
|
+
...resolveWalletBscRpcUrls({
|
|
94
|
+
cloudManagedAccess: input.cloudManagedAccess,
|
|
95
|
+
walletNetwork
|
|
96
|
+
})
|
|
97
|
+
].filter((v) => Boolean(v));
|
|
98
|
+
return [...new Set(candidates)];
|
|
99
|
+
}
|
|
100
|
+
function resolvePrimaryBscRpcUrl(input) {
|
|
101
|
+
const urls = resolveBscRpcUrls(input);
|
|
102
|
+
if (urls.length === 0) return null;
|
|
103
|
+
const primary = urls[0];
|
|
104
|
+
try {
|
|
105
|
+
const parsed = new URL(primary);
|
|
106
|
+
if (parsed.protocol === "http:") {
|
|
107
|
+
logger.warn(
|
|
108
|
+
`BSC RPC URL uses http: (${parsed.host}) \u2014 MITM risk for trade execution. Use https: in production.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
}
|
|
113
|
+
return primary;
|
|
114
|
+
}
|
|
115
|
+
function hostLabel(url) {
|
|
116
|
+
try {
|
|
117
|
+
return new URL(url).host;
|
|
118
|
+
} catch {
|
|
119
|
+
return "rpc";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function normalizeAddress(value) {
|
|
123
|
+
if (typeof value !== "string") return null;
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
if (!trimmed) return null;
|
|
126
|
+
try {
|
|
127
|
+
return ethers.getAddress(trimmed);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function parseRpcChainId(value) {
|
|
133
|
+
if (!value || typeof value !== "string") return null;
|
|
134
|
+
if (!value.startsWith("0x")) return null;
|
|
135
|
+
const parsed = Number.parseInt(value, 16);
|
|
136
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
137
|
+
}
|
|
138
|
+
function clampSlippageBps(value) {
|
|
139
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
140
|
+
return DEFAULT_SLIPPAGE_BPS;
|
|
141
|
+
}
|
|
142
|
+
const rounded = Math.round(value);
|
|
143
|
+
if (rounded < 1) return 1;
|
|
144
|
+
if (rounded > MAX_SLIPPAGE_BPS) {
|
|
145
|
+
console.warn(
|
|
146
|
+
`[bsc-trade] Slippage ${rounded} bps exceeds max ${MAX_SLIPPAGE_BPS} bps, clamping`
|
|
147
|
+
);
|
|
148
|
+
return MAX_SLIPPAGE_BPS;
|
|
149
|
+
}
|
|
150
|
+
if (rounded > SLIPPAGE_WARNING_THRESHOLD_BPS) {
|
|
151
|
+
console.warn(
|
|
152
|
+
`[bsc-trade] High slippage requested: ${rounded} bps (${(rounded / 100).toFixed(1)}%)`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return rounded;
|
|
156
|
+
}
|
|
157
|
+
function clampDeadlineSeconds(value) {
|
|
158
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return 600;
|
|
159
|
+
const rounded = Math.round(value);
|
|
160
|
+
if (rounded < 60) return 60;
|
|
161
|
+
if (rounded > 3600) return 3600;
|
|
162
|
+
return rounded;
|
|
163
|
+
}
|
|
164
|
+
function parsePositiveDecimal(value) {
|
|
165
|
+
const amount = Number.parseFloat(value);
|
|
166
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
167
|
+
throw new Error("Amount must be a positive number.");
|
|
168
|
+
}
|
|
169
|
+
return amount;
|
|
170
|
+
}
|
|
171
|
+
function resolveRouteProviderPreference(value) {
|
|
172
|
+
if (value === "pancakeswap-v2" || value === "0x" || value === "auto") {
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
return "auto";
|
|
176
|
+
}
|
|
177
|
+
function resolveRouteProviderOrder(requested) {
|
|
178
|
+
if (requested === "0x") {
|
|
179
|
+
return ["0x", "pancakeswap-v2"];
|
|
180
|
+
}
|
|
181
|
+
if (requested === "pancakeswap-v2") {
|
|
182
|
+
return ["pancakeswap-v2"];
|
|
183
|
+
}
|
|
184
|
+
return ["0x", "pancakeswap-v2"];
|
|
185
|
+
}
|
|
186
|
+
function formatPrice(amountIn, amountOut) {
|
|
187
|
+
const inNum = Number.parseFloat(amountIn);
|
|
188
|
+
const outNum = Number.parseFloat(amountOut);
|
|
189
|
+
if (!Number.isFinite(inNum) || !Number.isFinite(outNum) || inNum <= 0) {
|
|
190
|
+
return "0";
|
|
191
|
+
}
|
|
192
|
+
const price = outNum / inNum;
|
|
193
|
+
if (price >= 1e3) return price.toFixed(2);
|
|
194
|
+
if (price >= 1) return price.toFixed(4);
|
|
195
|
+
if (price >= 1e-3) return price.toFixed(6);
|
|
196
|
+
return price.toExponential(4);
|
|
197
|
+
}
|
|
198
|
+
async function rpcCallWithFallback(rpcUrls, method, params) {
|
|
199
|
+
if (rpcUrls.length === 0) {
|
|
200
|
+
throw new Error("No BSC RPC endpoints configured.");
|
|
201
|
+
}
|
|
202
|
+
const payload = JSON.stringify({
|
|
203
|
+
jsonrpc: "2.0",
|
|
204
|
+
id: 1,
|
|
205
|
+
method,
|
|
206
|
+
params
|
|
207
|
+
});
|
|
208
|
+
let lastError = "Unknown RPC error";
|
|
209
|
+
for (const rpcUrl of rpcUrls) {
|
|
210
|
+
try {
|
|
211
|
+
const response = await fetch(rpcUrl, {
|
|
212
|
+
method: "POST",
|
|
213
|
+
headers: { "Content-Type": "application/json" },
|
|
214
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
215
|
+
body: payload
|
|
216
|
+
});
|
|
217
|
+
const raw = await response.text();
|
|
218
|
+
if (!response.ok) {
|
|
219
|
+
throw new Error(`HTTP ${response.status}: ${raw.slice(0, 180)}`);
|
|
220
|
+
}
|
|
221
|
+
let parsed;
|
|
222
|
+
try {
|
|
223
|
+
parsed = JSON.parse(raw);
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error(`Invalid JSON response: ${raw.slice(0, 180)}`);
|
|
226
|
+
}
|
|
227
|
+
if (parsed.error) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
parsed.error.message ?? `RPC error ${parsed.error.code}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
if (parsed.result === void 0 || parsed.result === null) {
|
|
233
|
+
throw new Error("RPC returned empty result.");
|
|
234
|
+
}
|
|
235
|
+
return { result: parsed.result, rpcUrl };
|
|
236
|
+
} catch (err) {
|
|
237
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
238
|
+
lastError = `${hostLabel(rpcUrl)}: ${message}`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw new Error(lastError);
|
|
242
|
+
}
|
|
243
|
+
async function ethCall(rpcUrls, to, data) {
|
|
244
|
+
return rpcCallWithFallback(rpcUrls, "eth_call", [
|
|
245
|
+
{ to, data },
|
|
246
|
+
"latest"
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
async function readWrappedNativeAddress(rpcUrls, context) {
|
|
250
|
+
const encoded = ROUTER_IFACE.encodeFunctionData("WETH", []);
|
|
251
|
+
try {
|
|
252
|
+
const call = await ethCall(rpcUrls, context.routerAddress, encoded);
|
|
253
|
+
const decoded = ROUTER_IFACE.decodeFunctionResult("WETH", call.result);
|
|
254
|
+
const wrappedNative = decoded[0];
|
|
255
|
+
if (typeof wrappedNative !== "string" || !wrappedNative) {
|
|
256
|
+
throw new Error("Router WETH() returned an invalid address.");
|
|
257
|
+
}
|
|
258
|
+
return ethers.getAddress(wrappedNative);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
if (context.wrappedNativeFallback) {
|
|
261
|
+
return context.wrappedNativeFallback;
|
|
262
|
+
}
|
|
263
|
+
throw err;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function readTokenDecimals(rpcUrls, tokenAddress) {
|
|
267
|
+
const encoded = ERC20_IFACE.encodeFunctionData("decimals", []);
|
|
268
|
+
const call = await ethCall(rpcUrls, tokenAddress, encoded);
|
|
269
|
+
const decoded = ERC20_IFACE.decodeFunctionResult("decimals", call.result);
|
|
270
|
+
const decimals = decoded[0];
|
|
271
|
+
if (typeof decimals !== "bigint") return 18;
|
|
272
|
+
const parsed = Number(decimals);
|
|
273
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 18;
|
|
274
|
+
return parsed;
|
|
275
|
+
}
|
|
276
|
+
async function readTokenSymbol(rpcUrls, tokenAddress) {
|
|
277
|
+
const encoded = ERC20_IFACE.encodeFunctionData("symbol", []);
|
|
278
|
+
const call = await ethCall(rpcUrls, tokenAddress, encoded);
|
|
279
|
+
const decoded = ERC20_IFACE.decodeFunctionResult("symbol", call.result);
|
|
280
|
+
const symbol = decoded[0];
|
|
281
|
+
if (typeof symbol === "string" && symbol.trim()) {
|
|
282
|
+
return symbol.trim().slice(0, 16);
|
|
283
|
+
}
|
|
284
|
+
return `TKN-${tokenAddress.slice(2, 6).toUpperCase()}`;
|
|
285
|
+
}
|
|
286
|
+
async function readTokenBalanceWei(rpcUrls, tokenAddress, walletAddress) {
|
|
287
|
+
const encoded = ERC20_IFACE.encodeFunctionData("balanceOf", [walletAddress]);
|
|
288
|
+
const call = await ethCall(rpcUrls, tokenAddress, encoded);
|
|
289
|
+
const decoded = ERC20_IFACE.decodeFunctionResult("balanceOf", call.result);
|
|
290
|
+
const balance = decoded[0];
|
|
291
|
+
if (typeof balance !== "bigint") {
|
|
292
|
+
throw new Error("Token balance response is invalid.");
|
|
293
|
+
}
|
|
294
|
+
return balance;
|
|
295
|
+
}
|
|
296
|
+
async function fetchZeroXQuote(input) {
|
|
297
|
+
const apiBase = normalizeRpcUrl(process.env.ZEROX_BSC_API_BASE_URL) ? normalizeRpcUrl(process.env.ZEROX_BSC_API_BASE_URL) : ZEROX_API_BASE_URL;
|
|
298
|
+
const sellToken = input.side === "buy" ? input.wrappedNativeAddress : input.tokenAddress;
|
|
299
|
+
const buyToken = input.side === "buy" ? input.tokenAddress : input.wrappedNativeAddress;
|
|
300
|
+
const quoteUrl = new URL("/swap/v1/quote", apiBase);
|
|
301
|
+
quoteUrl.searchParams.set("sellToken", sellToken);
|
|
302
|
+
quoteUrl.searchParams.set("buyToken", buyToken);
|
|
303
|
+
quoteUrl.searchParams.set("sellAmount", input.amountInWei.toString());
|
|
304
|
+
quoteUrl.searchParams.set(
|
|
305
|
+
"slippagePercentage",
|
|
306
|
+
(input.slippageBps / 1e4).toString()
|
|
307
|
+
);
|
|
308
|
+
quoteUrl.searchParams.set("takerAddress", input.walletAddress);
|
|
309
|
+
const apiKey = process.env.ZEROX_API_KEY?.trim();
|
|
310
|
+
const response = await fetch(quoteUrl.toString(), {
|
|
311
|
+
method: "GET",
|
|
312
|
+
headers: apiKey ? { "0x-api-key": apiKey } : void 0,
|
|
313
|
+
signal: AbortSignal.timeout(ZEROX_QUOTE_TIMEOUT_MS)
|
|
314
|
+
});
|
|
315
|
+
const raw = await response.text();
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
throw new Error(`0x HTTP ${response.status}: ${raw.slice(0, 180)}`);
|
|
318
|
+
}
|
|
319
|
+
let parsed;
|
|
320
|
+
try {
|
|
321
|
+
parsed = JSON.parse(raw);
|
|
322
|
+
} catch {
|
|
323
|
+
throw new Error(`0x quote returned invalid JSON: ${raw.slice(0, 180)}`);
|
|
324
|
+
}
|
|
325
|
+
const to = normalizeAddress(parsed.to);
|
|
326
|
+
const data = typeof parsed.data === "string" && parsed.data.startsWith("0x") ? parsed.data : null;
|
|
327
|
+
const buyAmount = typeof parsed.buyAmount === "string" && /^\d+$/.test(parsed.buyAmount) ? BigInt(parsed.buyAmount) : null;
|
|
328
|
+
const sellAmount = typeof parsed.sellAmount === "string" && /^\d+$/.test(parsed.sellAmount) ? BigInt(parsed.sellAmount) : null;
|
|
329
|
+
const value = typeof parsed.value === "string" && /^\d+$/.test(parsed.value) ? parsed.value : "0";
|
|
330
|
+
if (!to || !data || !buyAmount || !sellAmount) {
|
|
331
|
+
throw new Error("0x quote missing required transaction fields.");
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
to,
|
|
335
|
+
data,
|
|
336
|
+
value,
|
|
337
|
+
allowanceTarget: normalizeAddress(parsed.allowanceTarget ?? null) ?? void 0,
|
|
338
|
+
buyAmount: buyAmount.toString(),
|
|
339
|
+
sellAmount: sellAmount.toString()
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
async function buildBscTradePreflight(input) {
|
|
343
|
+
const context = resolveBscExecutionContext();
|
|
344
|
+
const checks = {
|
|
345
|
+
walletReady: false,
|
|
346
|
+
rpcReady: false,
|
|
347
|
+
chainReady: false,
|
|
348
|
+
gasReady: false,
|
|
349
|
+
tokenAddressValid: true
|
|
350
|
+
};
|
|
351
|
+
const reasons = [];
|
|
352
|
+
const walletAddress = normalizeAddress(input.walletAddress);
|
|
353
|
+
const tokenAddressRaw = (input.tokenAddress ?? "").trim();
|
|
354
|
+
const tokenAddress = tokenAddressRaw ? normalizeAddress(tokenAddressRaw) : null;
|
|
355
|
+
const rpcUrls = resolveBscRpcUrls(input);
|
|
356
|
+
let chainId = null;
|
|
357
|
+
let bnbBalance = null;
|
|
358
|
+
let activeRpcUrl = null;
|
|
359
|
+
checks.walletReady = Boolean(walletAddress);
|
|
360
|
+
if (!checks.walletReady) {
|
|
361
|
+
reasons.push("Wallet not ready. Create or connect an EVM wallet first.");
|
|
362
|
+
}
|
|
363
|
+
if (tokenAddressRaw && !tokenAddress) {
|
|
364
|
+
checks.tokenAddressValid = false;
|
|
365
|
+
reasons.push("Token address format is invalid.");
|
|
366
|
+
}
|
|
367
|
+
if (rpcUrls.length === 0) {
|
|
368
|
+
reasons.push(
|
|
369
|
+
"BSC RPC not configured. Connect Eliza Cloud or set NODEREAL_BSC_RPC_URL, QUICKNODE_BSC_RPC_URL, or BSC_RPC_URL."
|
|
370
|
+
);
|
|
371
|
+
} else {
|
|
372
|
+
try {
|
|
373
|
+
const chainResponse = await rpcCallWithFallback(
|
|
374
|
+
rpcUrls,
|
|
375
|
+
"eth_chainId",
|
|
376
|
+
[]
|
|
377
|
+
);
|
|
378
|
+
activeRpcUrl = chainResponse.rpcUrl;
|
|
379
|
+
checks.rpcReady = true;
|
|
380
|
+
chainId = parseRpcChainId(chainResponse.result);
|
|
381
|
+
checks.chainReady = chainId === context.chainId;
|
|
382
|
+
if (!checks.chainReady) {
|
|
383
|
+
reasons.push(
|
|
384
|
+
chainId === null ? "Unable to read chain id from RPC." : `RPC chain mismatch. Expected BSC (${context.chainId}), got ${chainId}.`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
} catch (err) {
|
|
388
|
+
reasons.push(
|
|
389
|
+
`BSC RPC unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (checks.walletReady && checks.rpcReady) {
|
|
394
|
+
try {
|
|
395
|
+
const rpcCandidates = activeRpcUrl ? [activeRpcUrl, ...rpcUrls.filter((url) => url !== activeRpcUrl)] : rpcUrls;
|
|
396
|
+
const balanceResponse = await rpcCallWithFallback(
|
|
397
|
+
rpcCandidates,
|
|
398
|
+
"eth_getBalance",
|
|
399
|
+
[walletAddress, "latest"]
|
|
400
|
+
);
|
|
401
|
+
if (!activeRpcUrl) activeRpcUrl = balanceResponse.rpcUrl;
|
|
402
|
+
const balanceWei = BigInt(balanceResponse.result);
|
|
403
|
+
bnbBalance = ethers.formatEther(balanceWei);
|
|
404
|
+
checks.gasReady = balanceWei >= ethers.parseEther(MIN_GAS_BNB);
|
|
405
|
+
if (!checks.gasReady) {
|
|
406
|
+
reasons.push(
|
|
407
|
+
`Insufficient BNB gas. Keep at least ${MIN_GAS_BNB} BNB available.`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
} catch (err) {
|
|
411
|
+
reasons.push(
|
|
412
|
+
`Failed to read wallet balance: ${err instanceof Error ? err.message : String(err)}`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (tokenAddressRaw && tokenAddress && checks.rpcReady && checks.chainReady) {
|
|
417
|
+
try {
|
|
418
|
+
const rpcCandidates = activeRpcUrl ? [activeRpcUrl, ...rpcUrls.filter((url) => url !== activeRpcUrl)] : rpcUrls;
|
|
419
|
+
const codeResponse = await rpcCallWithFallback(
|
|
420
|
+
rpcCandidates,
|
|
421
|
+
"eth_getCode",
|
|
422
|
+
[tokenAddress, "latest"]
|
|
423
|
+
);
|
|
424
|
+
if (!activeRpcUrl) activeRpcUrl = codeResponse.rpcUrl;
|
|
425
|
+
const code = codeResponse.result.trim().toLowerCase();
|
|
426
|
+
if (code === "0x" || code === "0x0") {
|
|
427
|
+
checks.tokenAddressValid = false;
|
|
428
|
+
reasons.push("Token contract not found on BSC.");
|
|
429
|
+
}
|
|
430
|
+
} catch (err) {
|
|
431
|
+
checks.tokenAddressValid = false;
|
|
432
|
+
reasons.push(
|
|
433
|
+
`Token contract check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
const ok = checks.walletReady && checks.rpcReady && checks.chainReady && checks.gasReady && checks.tokenAddressValid;
|
|
438
|
+
return {
|
|
439
|
+
ok,
|
|
440
|
+
walletAddress,
|
|
441
|
+
rpcUrlHost: activeRpcUrl ? hostLabel(activeRpcUrl) : null,
|
|
442
|
+
chainId,
|
|
443
|
+
bnbBalance,
|
|
444
|
+
minGasBnb: MIN_GAS_BNB,
|
|
445
|
+
checks,
|
|
446
|
+
reasons
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
async function buildBscTradeQuote(input) {
|
|
450
|
+
const context = resolveBscExecutionContext();
|
|
451
|
+
const side = input.request.side;
|
|
452
|
+
if (side !== "buy" && side !== "sell") {
|
|
453
|
+
throw new Error('Unsupported trade side. Use "buy" or "sell".');
|
|
454
|
+
}
|
|
455
|
+
const tokenAddress = normalizeAddress(input.request.tokenAddress);
|
|
456
|
+
if (!tokenAddress) {
|
|
457
|
+
throw new Error("Token address is required.");
|
|
458
|
+
}
|
|
459
|
+
const amountInput = input.request.amount.trim();
|
|
460
|
+
parsePositiveDecimal(amountInput);
|
|
461
|
+
const slippageBps = clampSlippageBps(input.request.slippageBps);
|
|
462
|
+
const routeProviderRequested = resolveRouteProviderPreference(
|
|
463
|
+
input.request.routeProvider
|
|
464
|
+
);
|
|
465
|
+
const preflight = await buildBscTradePreflight({
|
|
466
|
+
walletAddress: input.walletAddress,
|
|
467
|
+
tokenAddress,
|
|
468
|
+
rpcUrls: input.rpcUrls,
|
|
469
|
+
nodeRealBscRpcUrl: input.nodeRealBscRpcUrl,
|
|
470
|
+
quickNodeBscRpcUrl: input.quickNodeBscRpcUrl,
|
|
471
|
+
bscRpcUrl: input.bscRpcUrl,
|
|
472
|
+
cloudManagedAccess: input.cloudManagedAccess
|
|
473
|
+
});
|
|
474
|
+
if (!preflight.ok) {
|
|
475
|
+
throw new Error(preflight.reasons[0] ?? "Trade preflight failed.");
|
|
476
|
+
}
|
|
477
|
+
const rpcUrls = resolveBscRpcUrls(input);
|
|
478
|
+
if (rpcUrls.length === 0) {
|
|
479
|
+
throw new Error("BSC RPC unavailable.");
|
|
480
|
+
}
|
|
481
|
+
const wrappedNativeAddress = await readWrappedNativeAddress(rpcUrls, context);
|
|
482
|
+
const tokenDecimals = await readTokenDecimals(rpcUrls, tokenAddress);
|
|
483
|
+
const tokenSymbol = await readTokenSymbol(rpcUrls, tokenAddress);
|
|
484
|
+
const amountInWei = side === "buy" ? ethers.parseEther(amountInput) : ethers.parseUnits(amountInput, tokenDecimals);
|
|
485
|
+
if (side === "sell") {
|
|
486
|
+
if (!preflight.walletAddress) {
|
|
487
|
+
throw new Error("Wallet not ready for sell quote.");
|
|
488
|
+
}
|
|
489
|
+
const tokenBalanceWei = await readTokenBalanceWei(
|
|
490
|
+
rpcUrls,
|
|
491
|
+
tokenAddress,
|
|
492
|
+
preflight.walletAddress
|
|
493
|
+
);
|
|
494
|
+
if (amountInWei > tokenBalanceWei) {
|
|
495
|
+
throw new Error("Insufficient token balance for sell amount.");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (side === "buy" && preflight.bnbBalance) {
|
|
499
|
+
const walletBalanceWei = ethers.parseEther(preflight.bnbBalance);
|
|
500
|
+
const gasReserveWei = ethers.parseEther(MIN_GAS_BNB);
|
|
501
|
+
if (amountInWei + gasReserveWei > walletBalanceWei) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`Insufficient BNB for amount + gas reserve (${MIN_GAS_BNB} BNB).`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
let routeProvider = "pancakeswap-v2";
|
|
508
|
+
let routeProviderFallbackUsed = false;
|
|
509
|
+
const routeProviderNotes = [];
|
|
510
|
+
let amountOutWei = null;
|
|
511
|
+
let route = [];
|
|
512
|
+
let swapTargetAddress;
|
|
513
|
+
let swapCallData;
|
|
514
|
+
let swapValueWei;
|
|
515
|
+
let allowanceTarget;
|
|
516
|
+
const providerErrors = [];
|
|
517
|
+
for (const provider of resolveRouteProviderOrder(routeProviderRequested)) {
|
|
518
|
+
try {
|
|
519
|
+
if (provider === "0x") {
|
|
520
|
+
if (!preflight.walletAddress) {
|
|
521
|
+
throw new Error("wallet address is required for 0x route");
|
|
522
|
+
}
|
|
523
|
+
const zeroX = await fetchZeroXQuote({
|
|
524
|
+
side,
|
|
525
|
+
walletAddress: preflight.walletAddress,
|
|
526
|
+
tokenAddress,
|
|
527
|
+
wrappedNativeAddress,
|
|
528
|
+
amountInWei,
|
|
529
|
+
slippageBps
|
|
530
|
+
});
|
|
531
|
+
const buyAmount = BigInt(zeroX.buyAmount ?? "0");
|
|
532
|
+
if (buyAmount <= 0n) {
|
|
533
|
+
throw new Error("0x quote returned zero output amount");
|
|
534
|
+
}
|
|
535
|
+
amountOutWei = buyAmount;
|
|
536
|
+
routeProvider = "0x";
|
|
537
|
+
route = side === "buy" ? [wrappedNativeAddress, tokenAddress] : [tokenAddress, wrappedNativeAddress];
|
|
538
|
+
swapTargetAddress = zeroX.to;
|
|
539
|
+
swapCallData = zeroX.data;
|
|
540
|
+
swapValueWei = zeroX.value ?? "0";
|
|
541
|
+
allowanceTarget = zeroX.allowanceTarget;
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
544
|
+
const candidateRoute = side === "buy" ? [wrappedNativeAddress, tokenAddress] : [tokenAddress, wrappedNativeAddress];
|
|
545
|
+
const quoteCall = ROUTER_IFACE.encodeFunctionData("getAmountsOut", [
|
|
546
|
+
amountInWei,
|
|
547
|
+
candidateRoute
|
|
548
|
+
]);
|
|
549
|
+
const quoteResponse = await ethCall(
|
|
550
|
+
rpcUrls,
|
|
551
|
+
context.routerAddress,
|
|
552
|
+
quoteCall
|
|
553
|
+
);
|
|
554
|
+
const decoded = ROUTER_IFACE.decodeFunctionResult(
|
|
555
|
+
"getAmountsOut",
|
|
556
|
+
quoteResponse.result
|
|
557
|
+
);
|
|
558
|
+
const amountsOut = decoded[0];
|
|
559
|
+
if (!Array.isArray(amountsOut) || amountsOut.length < 2) {
|
|
560
|
+
throw new Error("router returned an invalid quote");
|
|
561
|
+
}
|
|
562
|
+
const finalAmount = amountsOut[amountsOut.length - 1];
|
|
563
|
+
if (typeof finalAmount !== "bigint" || finalAmount <= 0n) {
|
|
564
|
+
throw new Error("router quote output is invalid");
|
|
565
|
+
}
|
|
566
|
+
amountOutWei = finalAmount;
|
|
567
|
+
routeProvider = "pancakeswap-v2";
|
|
568
|
+
route = candidateRoute;
|
|
569
|
+
break;
|
|
570
|
+
} catch (err) {
|
|
571
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
572
|
+
providerErrors.push(`${provider}: ${message}`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (!amountOutWei) {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Trade quote failed across providers (${providerErrors.join(" | ")})`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
if (routeProviderRequested !== routeProvider) {
|
|
581
|
+
routeProviderFallbackUsed = true;
|
|
582
|
+
routeProviderNotes.push(
|
|
583
|
+
`Requested ${routeProviderRequested}, used ${routeProvider}.`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
if (providerErrors.length > 0) {
|
|
587
|
+
routeProviderNotes.push(...providerErrors);
|
|
588
|
+
}
|
|
589
|
+
let minReceiveWei = amountOutWei * BigInt(1e4 - slippageBps) / 10000n;
|
|
590
|
+
if (minReceiveWei === 0n && amountOutWei > 0n) {
|
|
591
|
+
minReceiveWei = 1n;
|
|
592
|
+
}
|
|
593
|
+
const outDecimals = side === "buy" ? tokenDecimals : 18;
|
|
594
|
+
const inSymbol = side === "buy" ? "BNB" : tokenSymbol;
|
|
595
|
+
const outSymbol = side === "buy" ? tokenSymbol : "BNB";
|
|
596
|
+
const amountInFormatted = side === "buy" ? ethers.formatEther(amountInWei) : ethers.formatUnits(amountInWei, tokenDecimals);
|
|
597
|
+
const amountOutFormatted = ethers.formatUnits(amountOutWei, outDecimals);
|
|
598
|
+
const minReceiveFormatted = ethers.formatUnits(minReceiveWei, outDecimals);
|
|
599
|
+
return {
|
|
600
|
+
ok: true,
|
|
601
|
+
side,
|
|
602
|
+
routeProvider,
|
|
603
|
+
routeProviderRequested,
|
|
604
|
+
routeProviderFallbackUsed,
|
|
605
|
+
routeProviderNotes: routeProviderNotes.length > 0 ? routeProviderNotes : void 0,
|
|
606
|
+
routerAddress: routeProvider === "0x" ? swapTargetAddress ?? context.routerAddress : context.routerAddress,
|
|
607
|
+
wrappedNativeAddress,
|
|
608
|
+
tokenAddress,
|
|
609
|
+
slippageBps,
|
|
610
|
+
route,
|
|
611
|
+
quoteIn: {
|
|
612
|
+
symbol: inSymbol,
|
|
613
|
+
amount: amountInFormatted,
|
|
614
|
+
amountWei: amountInWei.toString()
|
|
615
|
+
},
|
|
616
|
+
quoteOut: {
|
|
617
|
+
symbol: outSymbol,
|
|
618
|
+
amount: amountOutFormatted,
|
|
619
|
+
amountWei: amountOutWei.toString()
|
|
620
|
+
},
|
|
621
|
+
minReceive: {
|
|
622
|
+
symbol: outSymbol,
|
|
623
|
+
amount: minReceiveFormatted,
|
|
624
|
+
amountWei: minReceiveWei.toString()
|
|
625
|
+
},
|
|
626
|
+
price: formatPrice(amountInFormatted, amountOutFormatted),
|
|
627
|
+
preflight,
|
|
628
|
+
swapTargetAddress,
|
|
629
|
+
swapCallData,
|
|
630
|
+
swapValueWei,
|
|
631
|
+
allowanceTarget,
|
|
632
|
+
quotedAt: Date.now()
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
function assertRouterAddress(quote, context) {
|
|
636
|
+
if (quote.routeProvider === "0x") {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (quote.routerAddress !== context.routerAddress) {
|
|
640
|
+
throw new Error(
|
|
641
|
+
`Unexpected router address in quote: ${quote.routerAddress}. Expected router ${context.routerAddress}.`
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function buildBscBuyUnsignedTx(quote, recipientAddress, deadlineSeconds) {
|
|
646
|
+
const context = resolveBscExecutionContext();
|
|
647
|
+
assertRouterAddress(quote, context);
|
|
648
|
+
if (quote.side !== "buy") {
|
|
649
|
+
throw new Error("Only buy execution is currently supported.");
|
|
650
|
+
}
|
|
651
|
+
const normalizedRecipient = normalizeAddress(recipientAddress);
|
|
652
|
+
if (!normalizedRecipient) {
|
|
653
|
+
throw new Error("Recipient wallet address is required.");
|
|
654
|
+
}
|
|
655
|
+
if (quote.routeProvider === "0x") {
|
|
656
|
+
if (!quote.swapTargetAddress || !quote.swapCallData) {
|
|
657
|
+
throw new Error("0x quote is missing swap transaction payload.");
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
chainId: context.chainId,
|
|
661
|
+
from: normalizedRecipient,
|
|
662
|
+
to: quote.swapTargetAddress,
|
|
663
|
+
data: quote.swapCallData,
|
|
664
|
+
valueWei: quote.swapValueWei ?? quote.quoteIn.amountWei,
|
|
665
|
+
deadline: Math.floor(Date.now() / 1e3) + clampDeadlineSeconds(deadlineSeconds),
|
|
666
|
+
explorerUrl: context.explorerBaseUrl
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
670
|
+
const deadline = now + clampDeadlineSeconds(deadlineSeconds);
|
|
671
|
+
const data = ROUTER_IFACE.encodeFunctionData(
|
|
672
|
+
"swapExactETHForTokensSupportingFeeOnTransferTokens",
|
|
673
|
+
[
|
|
674
|
+
BigInt(quote.minReceive.amountWei),
|
|
675
|
+
quote.route,
|
|
676
|
+
normalizedRecipient,
|
|
677
|
+
deadline
|
|
678
|
+
]
|
|
679
|
+
);
|
|
680
|
+
return {
|
|
681
|
+
chainId: context.chainId,
|
|
682
|
+
from: normalizedRecipient,
|
|
683
|
+
to: quote.routerAddress,
|
|
684
|
+
data,
|
|
685
|
+
valueWei: quote.quoteIn.amountWei,
|
|
686
|
+
deadline,
|
|
687
|
+
explorerUrl: context.explorerBaseUrl
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function buildBscSellUnsignedTx(quote, recipientAddress, deadlineSeconds) {
|
|
691
|
+
const context = resolveBscExecutionContext();
|
|
692
|
+
assertRouterAddress(quote, context);
|
|
693
|
+
if (quote.side !== "sell") {
|
|
694
|
+
throw new Error("Only sell execution is supported for this payload.");
|
|
695
|
+
}
|
|
696
|
+
const normalizedRecipient = normalizeAddress(recipientAddress);
|
|
697
|
+
if (!normalizedRecipient) {
|
|
698
|
+
throw new Error("Recipient wallet address is required.");
|
|
699
|
+
}
|
|
700
|
+
if (quote.routeProvider === "0x") {
|
|
701
|
+
if (!quote.swapTargetAddress || !quote.swapCallData) {
|
|
702
|
+
throw new Error("0x quote is missing swap transaction payload.");
|
|
703
|
+
}
|
|
704
|
+
return {
|
|
705
|
+
chainId: context.chainId,
|
|
706
|
+
from: normalizedRecipient,
|
|
707
|
+
to: quote.swapTargetAddress,
|
|
708
|
+
data: quote.swapCallData,
|
|
709
|
+
valueWei: quote.swapValueWei ?? "0",
|
|
710
|
+
deadline: Math.floor(Date.now() / 1e3) + clampDeadlineSeconds(deadlineSeconds),
|
|
711
|
+
explorerUrl: context.explorerBaseUrl
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
715
|
+
const deadline = now + clampDeadlineSeconds(deadlineSeconds);
|
|
716
|
+
const data = ROUTER_IFACE.encodeFunctionData(
|
|
717
|
+
"swapExactTokensForETHSupportingFeeOnTransferTokens",
|
|
718
|
+
[
|
|
719
|
+
BigInt(quote.quoteIn.amountWei),
|
|
720
|
+
BigInt(quote.minReceive.amountWei),
|
|
721
|
+
quote.route,
|
|
722
|
+
normalizedRecipient,
|
|
723
|
+
deadline
|
|
724
|
+
]
|
|
725
|
+
);
|
|
726
|
+
return {
|
|
727
|
+
chainId: context.chainId,
|
|
728
|
+
from: normalizedRecipient,
|
|
729
|
+
to: quote.routerAddress,
|
|
730
|
+
data,
|
|
731
|
+
valueWei: "0",
|
|
732
|
+
deadline,
|
|
733
|
+
explorerUrl: context.explorerBaseUrl
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function buildBscApproveUnsignedTx(tokenAddress, ownerAddress, spenderAddress, amountWei) {
|
|
737
|
+
const context = resolveBscExecutionContext();
|
|
738
|
+
const normalizedToken = normalizeAddress(tokenAddress);
|
|
739
|
+
if (!normalizedToken) {
|
|
740
|
+
throw new Error("Token address is invalid for approval payload.");
|
|
741
|
+
}
|
|
742
|
+
const normalizedOwner = normalizeAddress(ownerAddress);
|
|
743
|
+
if (!normalizedOwner) {
|
|
744
|
+
throw new Error("Owner wallet address is required for approval payload.");
|
|
745
|
+
}
|
|
746
|
+
const normalizedSpender = normalizeAddress(spenderAddress);
|
|
747
|
+
if (!normalizedSpender) {
|
|
748
|
+
throw new Error("Spender address is invalid for approval payload.");
|
|
749
|
+
}
|
|
750
|
+
let amount;
|
|
751
|
+
try {
|
|
752
|
+
amount = BigInt(amountWei);
|
|
753
|
+
} catch {
|
|
754
|
+
throw new Error(
|
|
755
|
+
`Invalid approval amount: expected integer string, got "${String(amountWei).slice(0, 20)}"`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (amount <= 0n) {
|
|
759
|
+
throw new Error("Approval amount must be greater than zero.");
|
|
760
|
+
}
|
|
761
|
+
const data = ERC20_IFACE.encodeFunctionData("approve", [
|
|
762
|
+
normalizedSpender,
|
|
763
|
+
amount
|
|
764
|
+
]);
|
|
765
|
+
return {
|
|
766
|
+
chainId: context.chainId,
|
|
767
|
+
from: normalizedOwner,
|
|
768
|
+
to: normalizedToken,
|
|
769
|
+
data,
|
|
770
|
+
valueWei: "0",
|
|
771
|
+
explorerUrl: context.explorerBaseUrl,
|
|
772
|
+
spender: normalizedSpender,
|
|
773
|
+
amountWei: amount.toString()
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
function resolveBscApprovalSpender(quote) {
|
|
777
|
+
if (quote.routeProvider === "0x" && quote.allowanceTarget) {
|
|
778
|
+
return quote.allowanceTarget;
|
|
779
|
+
}
|
|
780
|
+
return quote.routerAddress;
|
|
781
|
+
}
|
|
782
|
+
export {
|
|
783
|
+
BSC_TESTNET_EXPLORER_BASE_URL,
|
|
784
|
+
BSC_WBNB_FALLBACK,
|
|
785
|
+
PANCAKE_SWAP_V2_ROUTER,
|
|
786
|
+
buildBscApproveUnsignedTx,
|
|
787
|
+
buildBscBuyUnsignedTx,
|
|
788
|
+
buildBscSellUnsignedTx,
|
|
789
|
+
buildBscTradePreflight,
|
|
790
|
+
buildBscTradeQuote,
|
|
791
|
+
readTokenDecimals,
|
|
792
|
+
resolveBscApprovalSpender,
|
|
793
|
+
resolveBscRpcUrls,
|
|
794
|
+
resolvePrimaryBscRpcUrl
|
|
795
|
+
};
|
|
796
|
+
//# sourceMappingURL=bsc-trade.js.map
|