@catalyst-team/poly-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env +0 -0
- package/README.md +803 -0
- package/dist/__tests__/clob-api.test.d.ts +5 -0
- package/dist/__tests__/clob-api.test.d.ts.map +1 -0
- package/dist/__tests__/clob-api.test.js +240 -0
- package/dist/__tests__/clob-api.test.js.map +1 -0
- package/dist/__tests__/integration/bridge-client.integration.test.d.ts +11 -0
- package/dist/__tests__/integration/bridge-client.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/bridge-client.integration.test.js +260 -0
- package/dist/__tests__/integration/bridge-client.integration.test.js.map +1 -0
- package/dist/__tests__/integration/clob-api.integration.test.d.ts +13 -0
- package/dist/__tests__/integration/clob-api.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/clob-api.integration.test.js +170 -0
- package/dist/__tests__/integration/clob-api.integration.test.js.map +1 -0
- package/dist/__tests__/integration/ctf-client.integration.test.d.ts +17 -0
- package/dist/__tests__/integration/ctf-client.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/ctf-client.integration.test.js +234 -0
- package/dist/__tests__/integration/ctf-client.integration.test.js.map +1 -0
- package/dist/__tests__/integration/data-api.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/data-api.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/data-api.integration.test.js +161 -0
- package/dist/__tests__/integration/data-api.integration.test.js.map +1 -0
- package/dist/__tests__/integration/gamma-api.integration.test.d.ts +9 -0
- package/dist/__tests__/integration/gamma-api.integration.test.d.ts.map +1 -0
- package/dist/__tests__/integration/gamma-api.integration.test.js +170 -0
- package/dist/__tests__/integration/gamma-api.integration.test.js.map +1 -0
- package/dist/__tests__/test-utils.d.ts +92 -0
- package/dist/__tests__/test-utils.d.ts.map +1 -0
- package/dist/__tests__/test-utils.js +143 -0
- package/dist/__tests__/test-utils.js.map +1 -0
- package/dist/clients/bridge-client.d.ts +388 -0
- package/dist/clients/bridge-client.d.ts.map +1 -0
- package/dist/clients/bridge-client.js +587 -0
- package/dist/clients/bridge-client.js.map +1 -0
- package/dist/clients/clob-api.d.ts +318 -0
- package/dist/clients/clob-api.d.ts.map +1 -0
- package/dist/clients/clob-api.js +388 -0
- package/dist/clients/clob-api.js.map +1 -0
- package/dist/clients/ctf-client.d.ts +473 -0
- package/dist/clients/ctf-client.d.ts.map +1 -0
- package/dist/clients/ctf-client.js +915 -0
- package/dist/clients/ctf-client.js.map +1 -0
- package/dist/clients/data-api.d.ts +134 -0
- package/dist/clients/data-api.d.ts.map +1 -0
- package/dist/clients/data-api.js +265 -0
- package/dist/clients/data-api.js.map +1 -0
- package/dist/clients/gamma-api.d.ts +401 -0
- package/dist/clients/gamma-api.d.ts.map +1 -0
- package/dist/clients/gamma-api.js +352 -0
- package/dist/clients/gamma-api.js.map +1 -0
- package/dist/clients/trading-client.d.ts +252 -0
- package/dist/clients/trading-client.d.ts.map +1 -0
- package/dist/clients/trading-client.js +543 -0
- package/dist/clients/trading-client.js.map +1 -0
- package/dist/clients/websocket-manager.d.ts +100 -0
- package/dist/clients/websocket-manager.d.ts.map +1 -0
- package/dist/clients/websocket-manager.js +193 -0
- package/dist/clients/websocket-manager.js.map +1 -0
- package/dist/core/cache-adapter-bridge.d.ts +36 -0
- package/dist/core/cache-adapter-bridge.d.ts.map +1 -0
- package/dist/core/cache-adapter-bridge.js +81 -0
- package/dist/core/cache-adapter-bridge.js.map +1 -0
- package/dist/core/cache.d.ts +40 -0
- package/dist/core/cache.d.ts.map +1 -0
- package/dist/core/cache.js +71 -0
- package/dist/core/cache.js.map +1 -0
- package/dist/core/errors.d.ts +38 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +84 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +31 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +70 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/types.d.ts +314 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +19 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/unified-cache.d.ts +63 -0
- package/dist/core/unified-cache.d.ts.map +1 -0
- package/dist/core/unified-cache.js +114 -0
- package/dist/core/unified-cache.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +258 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/errors.d.ts +33 -0
- package/dist/mcp/errors.d.ts.map +1 -0
- package/dist/mcp/errors.js +86 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/index.d.ts +62 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +173 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +17 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +155 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools/guide.d.ts +12 -0
- package/dist/mcp/tools/guide.d.ts.map +1 -0
- package/dist/mcp/tools/guide.js +801 -0
- package/dist/mcp/tools/guide.js.map +1 -0
- package/dist/mcp/tools/index.d.ts +11 -0
- package/dist/mcp/tools/index.d.ts.map +1 -0
- package/dist/mcp/tools/index.js +27 -0
- package/dist/mcp/tools/index.js.map +1 -0
- package/dist/mcp/tools/market.d.ts +11 -0
- package/dist/mcp/tools/market.d.ts.map +1 -0
- package/dist/mcp/tools/market.js +314 -0
- package/dist/mcp/tools/market.js.map +1 -0
- package/dist/mcp/tools/order.d.ts +10 -0
- package/dist/mcp/tools/order.d.ts.map +1 -0
- package/dist/mcp/tools/order.js +258 -0
- package/dist/mcp/tools/order.js.map +1 -0
- package/dist/mcp/tools/trade.d.ts +38 -0
- package/dist/mcp/tools/trade.d.ts.map +1 -0
- package/dist/mcp/tools/trade.js +314 -0
- package/dist/mcp/tools/trade.js.map +1 -0
- package/dist/mcp/tools/trader.d.ts +11 -0
- package/dist/mcp/tools/trader.d.ts.map +1 -0
- package/dist/mcp/tools/trader.js +277 -0
- package/dist/mcp/tools/trader.js.map +1 -0
- package/dist/mcp/tools/wallet.d.ts +274 -0
- package/dist/mcp/tools/wallet.d.ts.map +1 -0
- package/dist/mcp/tools/wallet.js +579 -0
- package/dist/mcp/tools/wallet.js.map +1 -0
- package/dist/mcp/types.d.ts +413 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +5 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/services/authorization-service.d.ts +97 -0
- package/dist/services/authorization-service.d.ts.map +1 -0
- package/dist/services/authorization-service.js +279 -0
- package/dist/services/authorization-service.js.map +1 -0
- package/dist/services/market-service.d.ts +108 -0
- package/dist/services/market-service.d.ts.map +1 -0
- package/dist/services/market-service.js +458 -0
- package/dist/services/market-service.js.map +1 -0
- package/dist/services/realtime-service.d.ts +82 -0
- package/dist/services/realtime-service.d.ts.map +1 -0
- package/dist/services/realtime-service.js +150 -0
- package/dist/services/realtime-service.js.map +1 -0
- package/dist/services/swap-service.d.ts +217 -0
- package/dist/services/swap-service.d.ts.map +1 -0
- package/dist/services/swap-service.js +695 -0
- package/dist/services/swap-service.js.map +1 -0
- package/dist/services/wallet-service.d.ts +94 -0
- package/dist/services/wallet-service.d.ts.map +1 -0
- package/dist/services/wallet-service.js +173 -0
- package/dist/services/wallet-service.js.map +1 -0
- package/dist/utils/price-utils.d.ts +153 -0
- package/dist/utils/price-utils.d.ts.map +1 -0
- package/dist/utils/price-utils.js +236 -0
- package/dist/utils/price-utils.js.map +1 -0
- package/docs/00-design.md +760 -0
- package/docs/01-mcp.md +2041 -0
- package/docs/02-API.md +1148 -0
- package/docs/e2e/01-trader-tools.md +159 -0
- package/docs/e2e/02-market-tools.md +180 -0
- package/docs/e2e/03-order-tools.md +166 -0
- package/docs/e2e/04-wallet-tools.md +224 -0
- package/docs/e2e/05-trading-tools.md +327 -0
- package/docs/e2e/06-integration-scenarios.md +481 -0
- package/docs/e2e/coordinator.md +376 -0
- package/examples/01-basic-usage.ts +68 -0
- package/examples/02-smart-money.ts +95 -0
- package/examples/03-market-analysis.ts +108 -0
- package/examples/04-kline-aggregation.ts +158 -0
- package/examples/05-follow-wallet-strategy.ts +156 -0
- package/examples/06-services-demo.ts +124 -0
- package/examples/07-realtime-websocket.ts +117 -0
- package/examples/08-trading-orders.ts +278 -0
- package/examples/09-rewards-tracking.ts +187 -0
- package/examples/10-ctf-operations.ts +336 -0
- package/examples/11-live-arbitrage-scan.ts +221 -0
- package/examples/12-trending-arb-monitor.ts +406 -0
- package/examples/README.md +179 -0
- package/package.json +62 -0
- package/scripts/README.md +163 -0
- package/scripts/approvals/approve-erc1155.ts +129 -0
- package/scripts/approvals/approve-neg-risk-erc1155.ts +149 -0
- package/scripts/approvals/approve-neg-risk.ts +102 -0
- package/scripts/approvals/check-all-allowances.ts +150 -0
- package/scripts/approvals/check-allowance.ts +129 -0
- package/scripts/approvals/check-ctf-approval.ts +158 -0
- package/scripts/datas/001-report.md +486 -0
- package/scripts/datas/clone-modal-screenshot.png +0 -0
- package/scripts/deposit/deposit-native-usdc.ts +179 -0
- package/scripts/deposit/deposit-usdc.ts +155 -0
- package/scripts/deposit/swap-usdc-to-usdce.ts +375 -0
- package/scripts/research/research-markets.ts +166 -0
- package/scripts/trading/check-orders.ts +50 -0
- package/scripts/trading/sell-nvidia-positions.ts +206 -0
- package/scripts/trading/test-order.ts +172 -0
- package/scripts/truth.md +440 -0
- package/scripts/verify/test-approve-trading.ts +98 -0
- package/scripts/verify/test-provider-fix.ts +43 -0
- package/scripts/verify/test-search-mcp.ts +113 -0
- package/scripts/verify/verify-all-apis.ts +160 -0
- package/scripts/wallet/check-wallet-balances.ts +75 -0
- package/scripts/wallet/test-wallet-operations.ts +191 -0
- package/scripts/wallet/verify-wallet-tools.ts +124 -0
- package/src/__tests__/clob-api.test.ts +301 -0
- package/src/__tests__/integration/bridge-client.integration.test.ts +314 -0
- package/src/__tests__/integration/clob-api.integration.test.ts +218 -0
- package/src/__tests__/integration/ctf-client.integration.test.ts +331 -0
- package/src/__tests__/integration/data-api.integration.test.ts +194 -0
- package/src/__tests__/integration/gamma-api.integration.test.ts +206 -0
- package/src/__tests__/test-utils.ts +170 -0
- package/src/clients/bridge-client.ts +841 -0
- package/src/clients/clob-api.ts +629 -0
- package/src/clients/ctf-client.ts +1216 -0
- package/src/clients/data-api.ts +469 -0
- package/src/clients/gamma-api.ts +597 -0
- package/src/clients/trading-client.ts +749 -0
- package/src/clients/websocket-manager.ts +267 -0
- package/src/core/cache-adapter-bridge.ts +94 -0
- package/src/core/cache.ts +85 -0
- package/src/core/errors.ts +117 -0
- package/src/core/rate-limiter.ts +74 -0
- package/src/core/types.ts +360 -0
- package/src/core/unified-cache.ts +153 -0
- package/src/index.ts +455 -0
- package/src/mcp/README.md +380 -0
- package/src/mcp/errors.ts +124 -0
- package/src/mcp/index.ts +309 -0
- package/src/mcp/server.ts +183 -0
- package/src/mcp/tools/guide.ts +821 -0
- package/src/mcp/tools/index.ts +73 -0
- package/src/mcp/tools/market.ts +363 -0
- package/src/mcp/tools/order.ts +326 -0
- package/src/mcp/tools/trade.ts +417 -0
- package/src/mcp/tools/trader.ts +322 -0
- package/src/mcp/tools/wallet.ts +683 -0
- package/src/mcp/types.ts +472 -0
- package/src/services/authorization-service.ts +357 -0
- package/src/services/market-service.ts +544 -0
- package/src/services/realtime-service.ts +196 -0
- package/src/services/swap-service.ts +896 -0
- package/src/services/wallet-service.ts +259 -0
- package/src/utils/price-utils.ts +307 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +19 -0
- package/vitest.integration.config.ts +18 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swap Service
|
|
3
|
+
*
|
|
4
|
+
* Provides DEX swap functionality on Polygon using QuickSwap V3.
|
|
5
|
+
* Supports swapping between various tokens including MATIC, WETH, USDC, USDC.e, USDT, DAI.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ethers, Contract, BigNumber } from 'ethers';
|
|
9
|
+
|
|
10
|
+
// QuickSwap V3 Contracts on Polygon
|
|
11
|
+
export const QUICKSWAP_ROUTER = '0xf5b509bB0909a69B1c207E495f687a596C168E12';
|
|
12
|
+
export const QUICKSWAP_QUOTER = '0xa15F0D7377B2A0C0c10db057f641beD21028FC89';
|
|
13
|
+
export const QUICKSWAP_FACTORY = '0x411b0fAcC3489691f28ad58c47006AF5E3Ab3A28';
|
|
14
|
+
|
|
15
|
+
// Wrapped MATIC for swapping native MATIC
|
|
16
|
+
export const WMATIC = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Supported tokens on Polygon
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ IMPORTANT: USDC vs USDC.e for Polymarket CTF
|
|
22
|
+
*
|
|
23
|
+
* | Token | Address | Polymarket CTF |
|
|
24
|
+
* |-------------|--------------------------------------------|-----------------
|
|
25
|
+
* | USDC_E | 0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174 | ✅ Required |
|
|
26
|
+
* | USDC/NATIVE | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 | ❌ Not accepted|
|
|
27
|
+
*
|
|
28
|
+
* For Polymarket CTF operations (split/merge/redeem):
|
|
29
|
+
* - Use transferUsdcE() to send USDC.e
|
|
30
|
+
* - Use swap('USDC', 'USDC_E', amount) to convert native USDC to USDC.e
|
|
31
|
+
*
|
|
32
|
+
* For general transfers:
|
|
33
|
+
* - transferUsdc() sends native USDC (most DEXs, CEXs use this)
|
|
34
|
+
* - transferUsdcE() sends bridged USDC.e (Polymarket CTF requires this)
|
|
35
|
+
*/
|
|
36
|
+
export const POLYGON_TOKENS = {
|
|
37
|
+
// Native MATIC (use WMATIC address for swaps)
|
|
38
|
+
MATIC: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
|
|
39
|
+
WMATIC: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270',
|
|
40
|
+
// USDC variants - SEE ABOVE FOR POLYMARKET COMPATIBILITY
|
|
41
|
+
USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // Native USDC - NOT for Polymarket CTF
|
|
42
|
+
NATIVE_USDC: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', // Alias for USDC
|
|
43
|
+
USDC_E: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', // Bridged USDC.e - REQUIRED for Polymarket CTF
|
|
44
|
+
// Other stables
|
|
45
|
+
USDT: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F',
|
|
46
|
+
DAI: '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063',
|
|
47
|
+
// ETH
|
|
48
|
+
WETH: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619',
|
|
49
|
+
} as const;
|
|
50
|
+
|
|
51
|
+
// Token decimals
|
|
52
|
+
export const TOKEN_DECIMALS: Record<string, number> = {
|
|
53
|
+
MATIC: 18,
|
|
54
|
+
WMATIC: 18,
|
|
55
|
+
USDC: 6,
|
|
56
|
+
NATIVE_USDC: 6,
|
|
57
|
+
USDC_E: 6,
|
|
58
|
+
USDT: 6,
|
|
59
|
+
DAI: 18,
|
|
60
|
+
WETH: 18,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export type SupportedToken = keyof typeof POLYGON_TOKENS;
|
|
64
|
+
|
|
65
|
+
// ABIs
|
|
66
|
+
const ERC20_ABI = [
|
|
67
|
+
'function balanceOf(address) view returns (uint256)',
|
|
68
|
+
'function decimals() view returns (uint8)',
|
|
69
|
+
'function symbol() view returns (string)',
|
|
70
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
71
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
72
|
+
'function transfer(address to, uint256 amount) returns (bool)',
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const QUICKSWAP_ROUTER_ABI = [
|
|
76
|
+
'function exactInputSingle((address tokenIn, address tokenOut, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum, uint160 limitSqrtPrice)) external payable returns (uint256 amountOut)',
|
|
77
|
+
'function exactInput((bytes path, address recipient, uint256 deadline, uint256 amountIn, uint256 amountOutMinimum)) external payable returns (uint256 amountOut)',
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const QUICKSWAP_QUOTER_ABI = [
|
|
81
|
+
'function quoteExactInputSingle(address tokenIn, address tokenOut, uint256 amountIn, uint160 limitSqrtPrice) external returns (uint256 amountOut, uint16 fee)',
|
|
82
|
+
'function quoteExactInput(bytes path, uint256 amountIn) external returns (uint256 amountOut, uint16[] fees)',
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const QUICKSWAP_FACTORY_ABI = [
|
|
86
|
+
'function poolByPair(address tokenA, address tokenB) external view returns (address pool)',
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
const WMATIC_ABI = [
|
|
90
|
+
'function deposit() external payable',
|
|
91
|
+
'function withdraw(uint256 amount) external',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
export interface SwapQuote {
|
|
95
|
+
tokenIn: string;
|
|
96
|
+
tokenOut: string;
|
|
97
|
+
amountIn: string;
|
|
98
|
+
estimatedAmountOut: string;
|
|
99
|
+
minAmountOut: string;
|
|
100
|
+
slippage: number;
|
|
101
|
+
priceImpact: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Quote result from Quoter contract */
|
|
105
|
+
export interface QuoteResult {
|
|
106
|
+
possible: boolean;
|
|
107
|
+
tokenIn: string;
|
|
108
|
+
tokenOut: string;
|
|
109
|
+
amountIn: string;
|
|
110
|
+
amountOut: string | null;
|
|
111
|
+
route: string[];
|
|
112
|
+
poolExists: boolean;
|
|
113
|
+
reason?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Pool info */
|
|
117
|
+
export interface PoolInfo {
|
|
118
|
+
tokenA: string;
|
|
119
|
+
tokenB: string;
|
|
120
|
+
poolAddress: string | null;
|
|
121
|
+
exists: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface SwapResult {
|
|
125
|
+
success: boolean;
|
|
126
|
+
transactionHash: string;
|
|
127
|
+
tokenIn: string;
|
|
128
|
+
tokenOut: string;
|
|
129
|
+
amountIn: string;
|
|
130
|
+
amountOut: string;
|
|
131
|
+
gasUsed: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface TokenBalance {
|
|
135
|
+
token: string;
|
|
136
|
+
symbol: string;
|
|
137
|
+
balance: string;
|
|
138
|
+
decimals: number;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface TransferResult {
|
|
142
|
+
success: boolean;
|
|
143
|
+
transactionHash: string;
|
|
144
|
+
token: string;
|
|
145
|
+
to: string;
|
|
146
|
+
amount: string;
|
|
147
|
+
gasUsed: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class SwapService {
|
|
151
|
+
private signer: ethers.Wallet;
|
|
152
|
+
private provider: ethers.providers.Provider;
|
|
153
|
+
private router: Contract;
|
|
154
|
+
private quoter: Contract;
|
|
155
|
+
private factory: Contract;
|
|
156
|
+
|
|
157
|
+
constructor(signer: ethers.Wallet) {
|
|
158
|
+
// Use signer's provider if available, otherwise create a default Polygon provider
|
|
159
|
+
this.provider = signer.provider || new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
|
|
160
|
+
// Ensure signer is connected to the provider
|
|
161
|
+
this.signer = signer.provider ? signer : signer.connect(this.provider);
|
|
162
|
+
this.router = new Contract(QUICKSWAP_ROUTER, QUICKSWAP_ROUTER_ABI, this.signer);
|
|
163
|
+
this.quoter = new Contract(QUICKSWAP_QUOTER, QUICKSWAP_QUOTER_ABI, this.provider);
|
|
164
|
+
this.factory = new Contract(QUICKSWAP_FACTORY, QUICKSWAP_FACTORY_ABI, this.provider);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get dynamic gas options for Polygon network
|
|
169
|
+
* Uses RPC fee data with minimum priority fee of 30 gwei
|
|
170
|
+
*/
|
|
171
|
+
private async getGasOptions(): Promise<{
|
|
172
|
+
maxPriorityFeePerGas: BigNumber;
|
|
173
|
+
maxFeePerGas: BigNumber;
|
|
174
|
+
}> {
|
|
175
|
+
const feeData = await this.provider.getFeeData();
|
|
176
|
+
const baseFee = feeData.lastBaseFeePerGas || feeData.gasPrice || ethers.utils.parseUnits('100', 'gwei');
|
|
177
|
+
const minPriorityFee = ethers.utils.parseUnits('30', 'gwei');
|
|
178
|
+
const maxPriorityFeePerGas = feeData.maxPriorityFeePerGas && feeData.maxPriorityFeePerGas.gt(minPriorityFee)
|
|
179
|
+
? feeData.maxPriorityFeePerGas
|
|
180
|
+
: minPriorityFee;
|
|
181
|
+
const maxFeePerGas = baseFee.mul(3).div(2).add(maxPriorityFeePerGas);
|
|
182
|
+
return { maxPriorityFeePerGas, maxFeePerGas };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Get the wallet address
|
|
187
|
+
*/
|
|
188
|
+
get address(): string {
|
|
189
|
+
return this.signer.address;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get token address from symbol
|
|
194
|
+
*/
|
|
195
|
+
getTokenAddress(token: string): string {
|
|
196
|
+
const upperToken = token.toUpperCase() as SupportedToken;
|
|
197
|
+
const address = POLYGON_TOKENS[upperToken];
|
|
198
|
+
if (!address) {
|
|
199
|
+
// Check if it's already an address
|
|
200
|
+
if (token.startsWith('0x') && token.length === 42) {
|
|
201
|
+
return token;
|
|
202
|
+
}
|
|
203
|
+
throw new Error(`Unknown token: ${token}. Supported: ${Object.keys(POLYGON_TOKENS).join(', ')}`);
|
|
204
|
+
}
|
|
205
|
+
return address;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Get token decimals
|
|
210
|
+
*/
|
|
211
|
+
getTokenDecimals(token: string): number {
|
|
212
|
+
const upperToken = token.toUpperCase();
|
|
213
|
+
return TOKEN_DECIMALS[upperToken] || 18;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if a pool exists for a token pair
|
|
218
|
+
*/
|
|
219
|
+
async checkPool(tokenA: string, tokenB: string): Promise<PoolInfo> {
|
|
220
|
+
const addressA = this.getTokenAddress(tokenA);
|
|
221
|
+
const addressB = this.getTokenAddress(tokenB);
|
|
222
|
+
|
|
223
|
+
try {
|
|
224
|
+
const poolAddress = await this.factory.poolByPair(addressA, addressB);
|
|
225
|
+
const exists = poolAddress !== ethers.constants.AddressZero;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
tokenA: tokenA.toUpperCase(),
|
|
229
|
+
tokenB: tokenB.toUpperCase(),
|
|
230
|
+
poolAddress: exists ? poolAddress : null,
|
|
231
|
+
exists,
|
|
232
|
+
};
|
|
233
|
+
} catch {
|
|
234
|
+
return {
|
|
235
|
+
tokenA: tokenA.toUpperCase(),
|
|
236
|
+
tokenB: tokenB.toUpperCase(),
|
|
237
|
+
poolAddress: null,
|
|
238
|
+
exists: false,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Get all available pools for supported tokens
|
|
245
|
+
*/
|
|
246
|
+
async getAvailablePools(): Promise<PoolInfo[]> {
|
|
247
|
+
const tokens = Object.keys(POLYGON_TOKENS).filter(
|
|
248
|
+
(t) => t !== 'NATIVE_USDC' && t !== 'WMATIC' // Skip aliases
|
|
249
|
+
);
|
|
250
|
+
const pools: PoolInfo[] = [];
|
|
251
|
+
|
|
252
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
253
|
+
for (let j = i + 1; j < tokens.length; j++) {
|
|
254
|
+
const pool = await this.checkPool(tokens[i], tokens[j]);
|
|
255
|
+
if (pool.exists) {
|
|
256
|
+
pools.push(pool);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return pools;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get a quote for a swap (checks if route is possible)
|
|
266
|
+
*/
|
|
267
|
+
async getQuote(
|
|
268
|
+
tokenIn: string,
|
|
269
|
+
tokenOut: string,
|
|
270
|
+
amountIn: string
|
|
271
|
+
): Promise<QuoteResult> {
|
|
272
|
+
const upperTokenIn = tokenIn.toUpperCase();
|
|
273
|
+
const upperTokenOut = tokenOut.toUpperCase();
|
|
274
|
+
|
|
275
|
+
// Handle MATIC → need to use WMATIC for the pool
|
|
276
|
+
const actualTokenIn = upperTokenIn === 'MATIC' ? 'WMATIC' : upperTokenIn;
|
|
277
|
+
const actualTokenOut = upperTokenOut === 'MATIC' ? 'WMATIC' : upperTokenOut;
|
|
278
|
+
|
|
279
|
+
const addressIn = this.getTokenAddress(actualTokenIn);
|
|
280
|
+
const addressOut = this.getTokenAddress(actualTokenOut);
|
|
281
|
+
const decimalsIn = this.getTokenDecimals(actualTokenIn);
|
|
282
|
+
const decimalsOut = this.getTokenDecimals(actualTokenOut);
|
|
283
|
+
const amountInWei = ethers.utils.parseUnits(amountIn, decimalsIn);
|
|
284
|
+
|
|
285
|
+
// First check if direct pool exists
|
|
286
|
+
const directPool = await this.checkPool(actualTokenIn, actualTokenOut);
|
|
287
|
+
|
|
288
|
+
if (directPool.exists) {
|
|
289
|
+
// Try direct quote
|
|
290
|
+
try {
|
|
291
|
+
const result = await this.quoter.callStatic.quoteExactInputSingle(
|
|
292
|
+
addressIn,
|
|
293
|
+
addressOut,
|
|
294
|
+
amountInWei,
|
|
295
|
+
0 // no price limit
|
|
296
|
+
);
|
|
297
|
+
const amountOut = ethers.utils.formatUnits(result.amountOut, decimalsOut);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
possible: true,
|
|
301
|
+
tokenIn: upperTokenIn,
|
|
302
|
+
tokenOut: upperTokenOut,
|
|
303
|
+
amountIn,
|
|
304
|
+
amountOut,
|
|
305
|
+
route: [upperTokenIn, upperTokenOut],
|
|
306
|
+
poolExists: true,
|
|
307
|
+
};
|
|
308
|
+
} catch {
|
|
309
|
+
// Pool exists but quote failed (maybe low liquidity)
|
|
310
|
+
return {
|
|
311
|
+
possible: false,
|
|
312
|
+
tokenIn: upperTokenIn,
|
|
313
|
+
tokenOut: upperTokenOut,
|
|
314
|
+
amountIn,
|
|
315
|
+
amountOut: null,
|
|
316
|
+
route: [upperTokenIn, upperTokenOut],
|
|
317
|
+
poolExists: true,
|
|
318
|
+
reason: 'Pool exists but insufficient liquidity for this amount',
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Try multi-hop through USDC or WMATIC
|
|
324
|
+
const intermediates = ['USDC', 'WMATIC', 'WETH'];
|
|
325
|
+
for (const mid of intermediates) {
|
|
326
|
+
if (mid === actualTokenIn || mid === actualTokenOut) continue;
|
|
327
|
+
|
|
328
|
+
const pool1 = await this.checkPool(actualTokenIn, mid);
|
|
329
|
+
const pool2 = await this.checkPool(mid, actualTokenOut);
|
|
330
|
+
|
|
331
|
+
if (pool1.exists && pool2.exists) {
|
|
332
|
+
// Try multi-hop quote
|
|
333
|
+
try {
|
|
334
|
+
const midAddress = this.getTokenAddress(mid);
|
|
335
|
+
const path = ethers.utils.solidityPack(
|
|
336
|
+
['address', 'address', 'address'],
|
|
337
|
+
[addressIn, midAddress, addressOut]
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const result = await this.quoter.callStatic.quoteExactInput(path, amountInWei);
|
|
341
|
+
const amountOut = ethers.utils.formatUnits(result.amountOut, decimalsOut);
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
|
+
possible: true,
|
|
345
|
+
tokenIn: upperTokenIn,
|
|
346
|
+
tokenOut: upperTokenOut,
|
|
347
|
+
amountIn,
|
|
348
|
+
amountOut,
|
|
349
|
+
route: [upperTokenIn, mid, upperTokenOut],
|
|
350
|
+
poolExists: true,
|
|
351
|
+
};
|
|
352
|
+
} catch {
|
|
353
|
+
// Continue to try other routes
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// No route found
|
|
359
|
+
return {
|
|
360
|
+
possible: false,
|
|
361
|
+
tokenIn: upperTokenIn,
|
|
362
|
+
tokenOut: upperTokenOut,
|
|
363
|
+
amountIn,
|
|
364
|
+
amountOut: null,
|
|
365
|
+
route: [],
|
|
366
|
+
poolExists: false,
|
|
367
|
+
reason: 'No liquidity pool or route available for this pair',
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Execute a multi-hop swap
|
|
373
|
+
*/
|
|
374
|
+
async swapMultiHop(
|
|
375
|
+
tokenIn: string,
|
|
376
|
+
tokenOut: string,
|
|
377
|
+
amountIn: string,
|
|
378
|
+
route: string[],
|
|
379
|
+
options: { slippage?: number; deadline?: number } = {}
|
|
380
|
+
): Promise<SwapResult> {
|
|
381
|
+
const { slippage = 0.5, deadline = 300 } = options;
|
|
382
|
+
|
|
383
|
+
if (route.length < 2) {
|
|
384
|
+
throw new Error('Route must have at least 2 tokens');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const upperTokenIn = tokenIn.toUpperCase();
|
|
388
|
+
const upperTokenOut = tokenOut.toUpperCase();
|
|
389
|
+
|
|
390
|
+
// Handle MATIC wrapping
|
|
391
|
+
let wrappedAmount = amountIn;
|
|
392
|
+
if (upperTokenIn === 'MATIC') {
|
|
393
|
+
await this.wrapMatic(amountIn);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Build path
|
|
397
|
+
const addresses = route.map((t) => {
|
|
398
|
+
const upper = t.toUpperCase();
|
|
399
|
+
return this.getTokenAddress(upper === 'MATIC' ? 'WMATIC' : upper);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const path = ethers.utils.solidityPack(
|
|
403
|
+
addresses.map(() => 'address'),
|
|
404
|
+
addresses
|
|
405
|
+
);
|
|
406
|
+
|
|
407
|
+
const decimalsIn = this.getTokenDecimals(route[0] === 'MATIC' ? 'WMATIC' : route[0]);
|
|
408
|
+
const decimalsOut = this.getTokenDecimals(route[route.length - 1] === 'MATIC' ? 'WMATIC' : route[route.length - 1]);
|
|
409
|
+
const amountInWei = ethers.utils.parseUnits(wrappedAmount, decimalsIn);
|
|
410
|
+
|
|
411
|
+
// Get gas options
|
|
412
|
+
const gasOptions = await this.getGasOptions();
|
|
413
|
+
|
|
414
|
+
// Check and approve if needed
|
|
415
|
+
const tokenInAddress = addresses[0];
|
|
416
|
+
const tokenContract = new Contract(tokenInAddress, ERC20_ABI, this.signer);
|
|
417
|
+
const currentAllowance = await tokenContract.allowance(this.signer.address, QUICKSWAP_ROUTER);
|
|
418
|
+
|
|
419
|
+
if (currentAllowance.lt(amountInWei)) {
|
|
420
|
+
const approveTx = await tokenContract.approve(QUICKSWAP_ROUTER, ethers.constants.MaxUint256, gasOptions);
|
|
421
|
+
await approveTx.wait();
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Execute multi-hop swap
|
|
425
|
+
const swapParams = {
|
|
426
|
+
path,
|
|
427
|
+
recipient: this.signer.address,
|
|
428
|
+
deadline: Math.floor(Date.now() / 1000) + deadline,
|
|
429
|
+
amountIn: amountInWei,
|
|
430
|
+
amountOutMinimum: 0, // For simplicity; in production use quote with slippage
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const tx = await this.router.exactInput(swapParams, { ...gasOptions, gasLimit: 500000 });
|
|
434
|
+
const receipt = await tx.wait();
|
|
435
|
+
|
|
436
|
+
// Get actual output amount
|
|
437
|
+
const tokenOutAddress = addresses[addresses.length - 1];
|
|
438
|
+
const tokenOutContract = new Contract(tokenOutAddress, ERC20_ABI, this.provider);
|
|
439
|
+
const finalBalance = await tokenOutContract.balanceOf(this.signer.address);
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
success: receipt.status === 1,
|
|
443
|
+
transactionHash: receipt.transactionHash,
|
|
444
|
+
tokenIn: upperTokenIn,
|
|
445
|
+
tokenOut: upperTokenOut,
|
|
446
|
+
amountIn,
|
|
447
|
+
amountOut: ethers.utils.formatUnits(finalBalance, decimalsOut),
|
|
448
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get balances for all supported tokens
|
|
454
|
+
*/
|
|
455
|
+
async getBalances(): Promise<TokenBalance[]> {
|
|
456
|
+
const balances: TokenBalance[] = [];
|
|
457
|
+
|
|
458
|
+
// Get native MATIC balance
|
|
459
|
+
const maticBalance = await this.provider.getBalance(this.signer.address);
|
|
460
|
+
balances.push({
|
|
461
|
+
token: 'MATIC',
|
|
462
|
+
symbol: 'MATIC',
|
|
463
|
+
balance: ethers.utils.formatEther(maticBalance),
|
|
464
|
+
decimals: 18,
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Get ERC20 balances
|
|
468
|
+
const tokens = ['USDC', 'USDC_E', 'USDT', 'DAI', 'WETH', 'WMATIC'];
|
|
469
|
+
for (const tokenSymbol of tokens) {
|
|
470
|
+
const address = POLYGON_TOKENS[tokenSymbol as SupportedToken];
|
|
471
|
+
const contract = new Contract(address, ERC20_ABI, this.provider);
|
|
472
|
+
try {
|
|
473
|
+
const balance = await contract.balanceOf(this.signer.address);
|
|
474
|
+
const decimals = TOKEN_DECIMALS[tokenSymbol];
|
|
475
|
+
balances.push({
|
|
476
|
+
token: tokenSymbol,
|
|
477
|
+
symbol: tokenSymbol,
|
|
478
|
+
balance: ethers.utils.formatUnits(balance, decimals),
|
|
479
|
+
decimals,
|
|
480
|
+
});
|
|
481
|
+
} catch {
|
|
482
|
+
// Skip if token query fails
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return balances;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get balance for a specific token
|
|
491
|
+
*/
|
|
492
|
+
async getBalance(token: string): Promise<string> {
|
|
493
|
+
const upperToken = token.toUpperCase();
|
|
494
|
+
|
|
495
|
+
if (upperToken === 'MATIC') {
|
|
496
|
+
const balance = await this.provider.getBalance(this.signer.address);
|
|
497
|
+
return ethers.utils.formatEther(balance);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const address = this.getTokenAddress(token);
|
|
501
|
+
const contract = new Contract(address, ERC20_ABI, this.provider);
|
|
502
|
+
const balance = await contract.balanceOf(this.signer.address);
|
|
503
|
+
const decimals = this.getTokenDecimals(token);
|
|
504
|
+
return ethers.utils.formatUnits(balance, decimals);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Wrap native MATIC to WMATIC
|
|
509
|
+
*/
|
|
510
|
+
async wrapMatic(amount: string): Promise<SwapResult> {
|
|
511
|
+
const amountWei = ethers.utils.parseEther(amount);
|
|
512
|
+
const wmatic = new Contract(WMATIC, WMATIC_ABI, this.signer);
|
|
513
|
+
const gasOptions = await this.getGasOptions();
|
|
514
|
+
|
|
515
|
+
const tx = await wmatic.deposit({ value: amountWei, ...gasOptions });
|
|
516
|
+
const receipt = await tx.wait();
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
success: true,
|
|
520
|
+
transactionHash: receipt.transactionHash,
|
|
521
|
+
tokenIn: 'MATIC',
|
|
522
|
+
tokenOut: 'WMATIC',
|
|
523
|
+
amountIn: amount,
|
|
524
|
+
amountOut: amount,
|
|
525
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Unwrap WMATIC to native MATIC
|
|
531
|
+
*/
|
|
532
|
+
async unwrapMatic(amount: string): Promise<SwapResult> {
|
|
533
|
+
const amountWei = ethers.utils.parseEther(amount);
|
|
534
|
+
const wmatic = new Contract(WMATIC, WMATIC_ABI, this.signer);
|
|
535
|
+
const gasOptions = await this.getGasOptions();
|
|
536
|
+
|
|
537
|
+
const tx = await wmatic.withdraw(amountWei, gasOptions);
|
|
538
|
+
const receipt = await tx.wait();
|
|
539
|
+
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
transactionHash: receipt.transactionHash,
|
|
543
|
+
tokenIn: 'WMATIC',
|
|
544
|
+
tokenOut: 'MATIC',
|
|
545
|
+
amountIn: amount,
|
|
546
|
+
amountOut: amount,
|
|
547
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Execute a token swap using QuickSwap V3
|
|
553
|
+
*/
|
|
554
|
+
async swap(
|
|
555
|
+
tokenIn: string,
|
|
556
|
+
tokenOut: string,
|
|
557
|
+
amountIn: string,
|
|
558
|
+
options: {
|
|
559
|
+
slippage?: number; // Default 0.5%
|
|
560
|
+
deadline?: number; // Default 5 minutes
|
|
561
|
+
} = {}
|
|
562
|
+
): Promise<SwapResult> {
|
|
563
|
+
const { slippage = 0.5, deadline = 300 } = options;
|
|
564
|
+
|
|
565
|
+
const upperTokenIn = tokenIn.toUpperCase();
|
|
566
|
+
const upperTokenOut = tokenOut.toUpperCase();
|
|
567
|
+
|
|
568
|
+
// Handle native MATIC swaps
|
|
569
|
+
if (upperTokenIn === 'MATIC' && upperTokenOut === 'WMATIC') {
|
|
570
|
+
return this.wrapMatic(amountIn);
|
|
571
|
+
}
|
|
572
|
+
if (upperTokenIn === 'WMATIC' && upperTokenOut === 'MATIC') {
|
|
573
|
+
return this.unwrapMatic(amountIn);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// For MATIC input, first wrap to WMATIC
|
|
577
|
+
let actualTokenIn = upperTokenIn;
|
|
578
|
+
let wrappedAmount = amountIn;
|
|
579
|
+
if (upperTokenIn === 'MATIC') {
|
|
580
|
+
await this.wrapMatic(amountIn);
|
|
581
|
+
actualTokenIn = 'WMATIC';
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const tokenInAddress = this.getTokenAddress(actualTokenIn);
|
|
585
|
+
const tokenOutAddress = this.getTokenAddress(tokenOut);
|
|
586
|
+
const decimalsIn = this.getTokenDecimals(actualTokenIn);
|
|
587
|
+
const decimalsOut = this.getTokenDecimals(tokenOut);
|
|
588
|
+
|
|
589
|
+
const amountInWei = ethers.utils.parseUnits(wrappedAmount, decimalsIn);
|
|
590
|
+
|
|
591
|
+
// Get gas options for all transactions
|
|
592
|
+
const gasOptions = await this.getGasOptions();
|
|
593
|
+
|
|
594
|
+
// Check and approve if needed
|
|
595
|
+
const tokenContract = new Contract(tokenInAddress, ERC20_ABI, this.signer);
|
|
596
|
+
const currentAllowance = await tokenContract.allowance(this.signer.address, QUICKSWAP_ROUTER);
|
|
597
|
+
|
|
598
|
+
if (currentAllowance.lt(amountInWei)) {
|
|
599
|
+
const approveTx = await tokenContract.approve(QUICKSWAP_ROUTER, ethers.constants.MaxUint256, gasOptions);
|
|
600
|
+
await approveTx.wait();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Calculate min output with slippage
|
|
604
|
+
// Only stablecoin pairs can use ~1:1 ratio estimation
|
|
605
|
+
const stablecoins = ['USDC', 'NATIVE_USDC', 'USDC_E', 'USDT', 'DAI'];
|
|
606
|
+
const isStablecoinPair = stablecoins.includes(actualTokenIn) && stablecoins.includes(upperTokenOut);
|
|
607
|
+
|
|
608
|
+
let minAmountOut: BigNumber;
|
|
609
|
+
if (isStablecoinPair) {
|
|
610
|
+
// For stablecoin pairs, assume ~1:1 ratio with slippage
|
|
611
|
+
let estimatedOut = amountInWei;
|
|
612
|
+
if (decimalsIn !== decimalsOut) {
|
|
613
|
+
if (decimalsIn > decimalsOut) {
|
|
614
|
+
estimatedOut = amountInWei.div(BigNumber.from(10).pow(decimalsIn - decimalsOut));
|
|
615
|
+
} else {
|
|
616
|
+
estimatedOut = amountInWei.mul(BigNumber.from(10).pow(decimalsOut - decimalsIn));
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const slippageBps = Math.floor(slippage * 100);
|
|
620
|
+
minAmountOut = estimatedOut.mul(10000 - slippageBps).div(10000);
|
|
621
|
+
} else {
|
|
622
|
+
// For non-stablecoin pairs (like MATIC → USDC), set minAmountOut to 0
|
|
623
|
+
// The actual protection comes from the DEX's price oracle
|
|
624
|
+
// In production, you should use a quoter contract for accurate price
|
|
625
|
+
minAmountOut = BigNumber.from(0);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Execute swap
|
|
629
|
+
const swapParams = {
|
|
630
|
+
tokenIn: tokenInAddress,
|
|
631
|
+
tokenOut: tokenOutAddress,
|
|
632
|
+
recipient: this.signer.address,
|
|
633
|
+
deadline: Math.floor(Date.now() / 1000) + deadline,
|
|
634
|
+
amountIn: amountInWei,
|
|
635
|
+
amountOutMinimum: minAmountOut,
|
|
636
|
+
limitSqrtPrice: 0,
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
const tx = await this.router.exactInputSingle(swapParams, { ...gasOptions, gasLimit: 300000 });
|
|
640
|
+
const receipt = await tx.wait();
|
|
641
|
+
|
|
642
|
+
// Get actual output amount
|
|
643
|
+
const tokenOutContract = new Contract(tokenOutAddress, ERC20_ABI, this.provider);
|
|
644
|
+
const finalBalance = await tokenOutContract.balanceOf(this.signer.address);
|
|
645
|
+
|
|
646
|
+
return {
|
|
647
|
+
success: true,
|
|
648
|
+
transactionHash: receipt.transactionHash,
|
|
649
|
+
tokenIn: upperTokenIn,
|
|
650
|
+
tokenOut: upperTokenOut,
|
|
651
|
+
amountIn,
|
|
652
|
+
amountOut: ethers.utils.formatUnits(finalBalance, decimalsOut),
|
|
653
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Swap any supported token to USDC (for deposit)
|
|
659
|
+
*/
|
|
660
|
+
async swapToUsdc(
|
|
661
|
+
tokenIn: string,
|
|
662
|
+
amountIn: string,
|
|
663
|
+
options: {
|
|
664
|
+
usdcType?: 'NATIVE_USDC' | 'USDC_E';
|
|
665
|
+
slippage?: number;
|
|
666
|
+
} = {}
|
|
667
|
+
): Promise<SwapResult> {
|
|
668
|
+
const { usdcType = 'NATIVE_USDC', slippage = 0.5 } = options;
|
|
669
|
+
|
|
670
|
+
const upperTokenIn = tokenIn.toUpperCase();
|
|
671
|
+
|
|
672
|
+
// If already USDC, no swap needed
|
|
673
|
+
if (upperTokenIn === 'USDC' || upperTokenIn === 'NATIVE_USDC') {
|
|
674
|
+
if (usdcType === 'NATIVE_USDC') {
|
|
675
|
+
return {
|
|
676
|
+
success: true,
|
|
677
|
+
transactionHash: '',
|
|
678
|
+
tokenIn: upperTokenIn,
|
|
679
|
+
tokenOut: 'NATIVE_USDC',
|
|
680
|
+
amountIn,
|
|
681
|
+
amountOut: amountIn,
|
|
682
|
+
gasUsed: '0',
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
// Swap USDC to USDC.e
|
|
686
|
+
return this.swap('USDC', 'USDC_E', amountIn, { slippage });
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (upperTokenIn === 'USDC_E') {
|
|
690
|
+
if (usdcType === 'USDC_E') {
|
|
691
|
+
return {
|
|
692
|
+
success: true,
|
|
693
|
+
transactionHash: '',
|
|
694
|
+
tokenIn: upperTokenIn,
|
|
695
|
+
tokenOut: 'USDC_E',
|
|
696
|
+
amountIn,
|
|
697
|
+
amountOut: amountIn,
|
|
698
|
+
gasUsed: '0',
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
// Swap USDC.e to USDC
|
|
702
|
+
return this.swap('USDC_E', 'USDC', amountIn, { slippage });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Swap other tokens to USDC
|
|
706
|
+
const targetUsdc = usdcType === 'NATIVE_USDC' ? 'USDC' : 'USDC_E';
|
|
707
|
+
return this.swap(tokenIn, targetUsdc, amountIn, { slippage });
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Get list of supported tokens
|
|
712
|
+
*/
|
|
713
|
+
getSupportedTokens(): string[] {
|
|
714
|
+
return Object.keys(POLYGON_TOKENS);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Get balances for any wallet address (static method, no signer required)
|
|
719
|
+
*/
|
|
720
|
+
static async getWalletBalances(
|
|
721
|
+
address: string,
|
|
722
|
+
provider?: ethers.providers.Provider
|
|
723
|
+
): Promise<TokenBalance[]> {
|
|
724
|
+
const rpcProvider = provider || new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
|
|
725
|
+
const balances: TokenBalance[] = [];
|
|
726
|
+
|
|
727
|
+
// Get native MATIC balance
|
|
728
|
+
const maticBalance = await rpcProvider.getBalance(address);
|
|
729
|
+
balances.push({
|
|
730
|
+
token: 'MATIC',
|
|
731
|
+
symbol: 'MATIC',
|
|
732
|
+
balance: ethers.utils.formatEther(maticBalance),
|
|
733
|
+
decimals: 18,
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Get ERC20 balances
|
|
737
|
+
const tokens = ['USDC', 'USDC_E', 'USDT', 'DAI', 'WETH', 'WMATIC'];
|
|
738
|
+
for (const tokenSymbol of tokens) {
|
|
739
|
+
const tokenAddress = POLYGON_TOKENS[tokenSymbol as SupportedToken];
|
|
740
|
+
const contract = new Contract(tokenAddress, ERC20_ABI, rpcProvider);
|
|
741
|
+
try {
|
|
742
|
+
const balance = await contract.balanceOf(address);
|
|
743
|
+
const decimals = TOKEN_DECIMALS[tokenSymbol];
|
|
744
|
+
balances.push({
|
|
745
|
+
token: tokenSymbol,
|
|
746
|
+
symbol: tokenSymbol,
|
|
747
|
+
balance: ethers.utils.formatUnits(balance, decimals),
|
|
748
|
+
decimals,
|
|
749
|
+
});
|
|
750
|
+
} catch {
|
|
751
|
+
// Skip if token query fails
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return balances;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Get balance for a specific token for any wallet (static)
|
|
760
|
+
*/
|
|
761
|
+
static async getWalletBalance(
|
|
762
|
+
address: string,
|
|
763
|
+
token: string,
|
|
764
|
+
provider?: ethers.providers.Provider
|
|
765
|
+
): Promise<string> {
|
|
766
|
+
const rpcProvider = provider || new ethers.providers.JsonRpcProvider('https://polygon-rpc.com');
|
|
767
|
+
const upperToken = token.toUpperCase();
|
|
768
|
+
|
|
769
|
+
if (upperToken === 'MATIC') {
|
|
770
|
+
const balance = await rpcProvider.getBalance(address);
|
|
771
|
+
return ethers.utils.formatEther(balance);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const tokenAddress = POLYGON_TOKENS[upperToken as SupportedToken];
|
|
775
|
+
if (!tokenAddress) {
|
|
776
|
+
throw new Error(`Unknown token: ${token}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const contract = new Contract(tokenAddress, ERC20_ABI, rpcProvider);
|
|
780
|
+
const balance = await contract.balanceOf(address);
|
|
781
|
+
const decimals = TOKEN_DECIMALS[upperToken] || 18;
|
|
782
|
+
return ethers.utils.formatUnits(balance, decimals);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ============= Transfer Methods =============
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Transfer native MATIC (POL) to another address
|
|
789
|
+
*/
|
|
790
|
+
async transferMatic(to: string, amount: string): Promise<TransferResult> {
|
|
791
|
+
const amountWei = ethers.utils.parseEther(amount);
|
|
792
|
+
|
|
793
|
+
// Check balance
|
|
794
|
+
const balance = await this.provider.getBalance(this.signer.address);
|
|
795
|
+
if (balance.lt(amountWei)) {
|
|
796
|
+
throw new Error(`Insufficient MATIC balance: have ${ethers.utils.formatEther(balance)}, need ${amount}`);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const gasOptions = await this.getGasOptions();
|
|
800
|
+
|
|
801
|
+
const tx = await this.signer.sendTransaction({
|
|
802
|
+
to,
|
|
803
|
+
value: amountWei,
|
|
804
|
+
...gasOptions,
|
|
805
|
+
gasLimit: 21000, // Standard ETH transfer gas limit
|
|
806
|
+
});
|
|
807
|
+
const receipt = await tx.wait();
|
|
808
|
+
|
|
809
|
+
return {
|
|
810
|
+
success: true,
|
|
811
|
+
transactionHash: receipt.transactionHash,
|
|
812
|
+
token: 'MATIC',
|
|
813
|
+
to,
|
|
814
|
+
amount,
|
|
815
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Transfer an ERC20 token to another address
|
|
821
|
+
*/
|
|
822
|
+
async transfer(token: string, to: string, amount: string): Promise<TransferResult> {
|
|
823
|
+
const upperToken = token.toUpperCase();
|
|
824
|
+
|
|
825
|
+
// For native MATIC, use transferMatic
|
|
826
|
+
if (upperToken === 'MATIC') {
|
|
827
|
+
return this.transferMatic(to, amount);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const tokenAddress = this.getTokenAddress(token);
|
|
831
|
+
const decimals = this.getTokenDecimals(token);
|
|
832
|
+
const amountWei = ethers.utils.parseUnits(amount, decimals);
|
|
833
|
+
|
|
834
|
+
const contract = new Contract(tokenAddress, ERC20_ABI, this.signer);
|
|
835
|
+
|
|
836
|
+
// Check balance
|
|
837
|
+
const balance = await contract.balanceOf(this.signer.address);
|
|
838
|
+
if (balance.lt(amountWei)) {
|
|
839
|
+
throw new Error(`Insufficient ${upperToken} balance: have ${ethers.utils.formatUnits(balance, decimals)}, need ${amount}`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const gasOptions = await this.getGasOptions();
|
|
843
|
+
|
|
844
|
+
const tx = await contract.transfer(to, amountWei, {
|
|
845
|
+
...gasOptions,
|
|
846
|
+
gasLimit: 100000, // ERC20 transfer gas limit (USDC.e needs ~71k)
|
|
847
|
+
});
|
|
848
|
+
const receipt = await tx.wait();
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
success: true,
|
|
852
|
+
transactionHash: receipt.transactionHash,
|
|
853
|
+
token: upperToken,
|
|
854
|
+
to,
|
|
855
|
+
amount,
|
|
856
|
+
gasUsed: receipt.gasUsed.toString(),
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Transfer native USDC to another address
|
|
862
|
+
*
|
|
863
|
+
* ⚠️ WARNING: This transfers NATIVE USDC (0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359)
|
|
864
|
+
*
|
|
865
|
+
* For Polymarket CTF operations, you need USDC.e instead.
|
|
866
|
+
* Use transferUsdcE() for Polymarket CTF compatibility.
|
|
867
|
+
*
|
|
868
|
+
* @see transferUsdcE - For Polymarket CTF operations
|
|
869
|
+
*/
|
|
870
|
+
async transferUsdc(to: string, amount: string): Promise<TransferResult> {
|
|
871
|
+
return this.transfer('USDC', to, amount);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* Transfer USDC.e (bridged USDC) to another address
|
|
876
|
+
*
|
|
877
|
+
* ✅ This is the correct method for Polymarket CTF operations.
|
|
878
|
+
*
|
|
879
|
+
* Polymarket's Conditional Token Framework (CTF) only accepts
|
|
880
|
+
* USDC.e (0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174).
|
|
881
|
+
*
|
|
882
|
+
* If you're funding a wallet for CTF trading, use this method.
|
|
883
|
+
*
|
|
884
|
+
* @example
|
|
885
|
+
* ```typescript
|
|
886
|
+
* // Fund a session wallet for Polymarket trading
|
|
887
|
+
* await swapService.transferUsdcE(sessionWallet, '100');
|
|
888
|
+
*
|
|
889
|
+
* // The session wallet can now perform CTF operations
|
|
890
|
+
* await ctf.split(conditionId, '100');
|
|
891
|
+
* ```
|
|
892
|
+
*/
|
|
893
|
+
async transferUsdcE(to: string, amount: string): Promise<TransferResult> {
|
|
894
|
+
return this.transfer('USDC_E', to, amount);
|
|
895
|
+
}
|
|
896
|
+
}
|