amped-defi 1.0.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/README.md +757 -0
- package/dist/__mocks__/@sodax/sdk.d.ts +24 -0
- package/dist/__mocks__/@sodax/sdk.d.ts.map +1 -0
- package/dist/__mocks__/@sodax/sdk.js +24 -0
- package/dist/__mocks__/@sodax/sdk.js.map +1 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +32 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +281 -0
- package/dist/index.js.map +1 -0
- package/dist/policy/policyEngine.d.ts +119 -0
- package/dist/policy/policyEngine.d.ts.map +1 -0
- package/dist/policy/policyEngine.js +322 -0
- package/dist/policy/policyEngine.js.map +1 -0
- package/dist/providers/spokeProviderFactory.d.ts +38 -0
- package/dist/providers/spokeProviderFactory.d.ts.map +1 -0
- package/dist/providers/spokeProviderFactory.js +212 -0
- package/dist/providers/spokeProviderFactory.js.map +1 -0
- package/dist/sodax/client.d.ts +34 -0
- package/dist/sodax/client.d.ts.map +1 -0
- package/dist/sodax/client.js +99 -0
- package/dist/sodax/client.js.map +1 -0
- package/dist/tools/bridge.d.ts +105 -0
- package/dist/tools/bridge.d.ts.map +1 -0
- package/dist/tools/bridge.js +334 -0
- package/dist/tools/bridge.js.map +1 -0
- package/dist/tools/discovery.d.ts +141 -0
- package/dist/tools/discovery.d.ts.map +1 -0
- package/dist/tools/discovery.js +777 -0
- package/dist/tools/discovery.js.map +1 -0
- package/dist/tools/moneyMarket.d.ts +227 -0
- package/dist/tools/moneyMarket.d.ts.map +1 -0
- package/dist/tools/moneyMarket.js +867 -0
- package/dist/tools/moneyMarket.js.map +1 -0
- package/dist/tools/portfolio.d.ts +43 -0
- package/dist/tools/portfolio.d.ts.map +1 -0
- package/dist/tools/portfolio.js +538 -0
- package/dist/tools/portfolio.js.map +1 -0
- package/dist/tools/swap.d.ts +71 -0
- package/dist/tools/swap.d.ts.map +1 -0
- package/dist/tools/swap.js +762 -0
- package/dist/tools/swap.js.map +1 -0
- package/dist/tools/walletManagement.d.ts +80 -0
- package/dist/tools/walletManagement.d.ts.map +1 -0
- package/dist/tools/walletManagement.js +289 -0
- package/dist/tools/walletManagement.js.map +1 -0
- package/dist/types.d.ts +205 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/errorUtils.d.ts +2 -0
- package/dist/utils/errorUtils.d.ts.map +1 -0
- package/dist/utils/errorUtils.js +19 -0
- package/dist/utils/errorUtils.js.map +1 -0
- package/dist/utils/errors.d.ts +144 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +310 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/positionAggregator.d.ts +122 -0
- package/dist/utils/positionAggregator.d.ts.map +1 -0
- package/dist/utils/positionAggregator.js +377 -0
- package/dist/utils/positionAggregator.js.map +1 -0
- package/dist/utils/priceService.d.ts +45 -0
- package/dist/utils/priceService.d.ts.map +1 -0
- package/dist/utils/priceService.js +108 -0
- package/dist/utils/priceService.js.map +1 -0
- package/dist/utils/sodaxApi.d.ts +92 -0
- package/dist/utils/sodaxApi.d.ts.map +1 -0
- package/dist/utils/sodaxApi.js +143 -0
- package/dist/utils/sodaxApi.js.map +1 -0
- package/dist/utils/tokenResolver.d.ts +54 -0
- package/dist/utils/tokenResolver.d.ts.map +1 -0
- package/dist/utils/tokenResolver.js +252 -0
- package/dist/utils/tokenResolver.js.map +1 -0
- package/dist/wallet/backendConfig.d.ts +37 -0
- package/dist/wallet/backendConfig.d.ts.map +1 -0
- package/dist/wallet/backendConfig.js +125 -0
- package/dist/wallet/backendConfig.js.map +1 -0
- package/dist/wallet/backends/BankrBackend.d.ts +73 -0
- package/dist/wallet/backends/BankrBackend.d.ts.map +1 -0
- package/dist/wallet/backends/BankrBackend.js +315 -0
- package/dist/wallet/backends/BankrBackend.js.map +1 -0
- package/dist/wallet/backends/BankrWalletProvider.d.ts +75 -0
- package/dist/wallet/backends/BankrWalletProvider.d.ts.map +1 -0
- package/dist/wallet/backends/BankrWalletProvider.js +243 -0
- package/dist/wallet/backends/BankrWalletProvider.js.map +1 -0
- package/dist/wallet/backends/EnvBackend.d.ts +50 -0
- package/dist/wallet/backends/EnvBackend.d.ts.map +1 -0
- package/dist/wallet/backends/EnvBackend.js +114 -0
- package/dist/wallet/backends/EnvBackend.js.map +1 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.d.ts +40 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.d.ts.map +1 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.js +81 -0
- package/dist/wallet/backends/EvmWalletSkillBackend.js.map +1 -0
- package/dist/wallet/backends/index.d.ts +10 -0
- package/dist/wallet/backends/index.d.ts.map +1 -0
- package/dist/wallet/backends/index.js +10 -0
- package/dist/wallet/backends/index.js.map +1 -0
- package/dist/wallet/index.d.ts +9 -0
- package/dist/wallet/index.d.ts.map +1 -0
- package/dist/wallet/index.js +12 -0
- package/dist/wallet/index.js.map +1 -0
- package/dist/wallet/providers/AmpedWalletProvider.d.ts +107 -0
- package/dist/wallet/providers/AmpedWalletProvider.d.ts.map +1 -0
- package/dist/wallet/providers/AmpedWalletProvider.js +208 -0
- package/dist/wallet/providers/AmpedWalletProvider.js.map +1 -0
- package/dist/wallet/providers/BankrBackend.d.ts +105 -0
- package/dist/wallet/providers/BankrBackend.d.ts.map +1 -0
- package/dist/wallet/providers/BankrBackend.js +327 -0
- package/dist/wallet/providers/BankrBackend.js.map +1 -0
- package/dist/wallet/providers/LocalKeyBackend.d.ts +62 -0
- package/dist/wallet/providers/LocalKeyBackend.d.ts.map +1 -0
- package/dist/wallet/providers/LocalKeyBackend.js +152 -0
- package/dist/wallet/providers/LocalKeyBackend.js.map +1 -0
- package/dist/wallet/providers/chainConfig.d.ts +209 -0
- package/dist/wallet/providers/chainConfig.d.ts.map +1 -0
- package/dist/wallet/providers/chainConfig.js +175 -0
- package/dist/wallet/providers/chainConfig.js.map +1 -0
- package/dist/wallet/providers/index.d.ts +30 -0
- package/dist/wallet/providers/index.d.ts.map +1 -0
- package/dist/wallet/providers/index.js +32 -0
- package/dist/wallet/providers/index.js.map +1 -0
- package/dist/wallet/providers/types.d.ts +156 -0
- package/dist/wallet/providers/types.d.ts.map +1 -0
- package/dist/wallet/providers/types.js +11 -0
- package/dist/wallet/providers/types.js.map +1 -0
- package/dist/wallet/skillWalletAdapter.d.ts +96 -0
- package/dist/wallet/skillWalletAdapter.d.ts.map +1 -0
- package/dist/wallet/skillWalletAdapter.js +280 -0
- package/dist/wallet/skillWalletAdapter.js.map +1 -0
- package/dist/wallet/types.d.ts +134 -0
- package/dist/wallet/types.d.ts.map +1 -0
- package/dist/wallet/types.js +138 -0
- package/dist/wallet/types.js.map +1 -0
- package/dist/wallet/walletManager.d.ts +111 -0
- package/dist/wallet/walletManager.d.ts.map +1 -0
- package/dist/wallet/walletManager.js +476 -0
- package/dist/wallet/walletManager.js.map +1 -0
- package/dist/wallet/walletRegistry.d.ts +95 -0
- package/dist/wallet/walletRegistry.d.ts.map +1 -0
- package/dist/wallet/walletRegistry.js +184 -0
- package/dist/wallet/walletRegistry.js.map +1 -0
- package/index.js +2 -0
- package/openclaw.plugin.json +37 -0
- package/package.json +69 -0
- package/src/__mocks__/@sodax/sdk.ts +28 -0
- package/src/__tests__/errors.test.ts +238 -0
- package/src/__tests__/policyEngine.test.ts +354 -0
- package/src/__tests__/positionAggregator.test.ts +271 -0
- package/src/__tests__/setup.ts +35 -0
- package/src/__tests__/sodaxApi.test.ts +203 -0
- package/src/__tests__/walletRegistry.test.ts +155 -0
- package/src/index.ts +376 -0
- package/src/policy/policyEngine.ts +389 -0
- package/src/providers/spokeProviderFactory.ts +283 -0
- package/src/sodax/client.ts +113 -0
- package/src/tools/bridge.ts +425 -0
- package/src/tools/discovery.ts +989 -0
- package/src/tools/moneyMarket.ts +1265 -0
- package/src/tools/portfolio.ts +697 -0
- package/src/tools/swap.ts +926 -0
- package/src/tools/walletManagement.ts +359 -0
- package/src/types.ts +228 -0
- package/src/utils/errorUtils.ts +16 -0
- package/src/utils/errors.ts +396 -0
- package/src/utils/positionAggregator.ts +559 -0
- package/src/utils/priceService.ts +153 -0
- package/src/utils/sodaxApi.ts +261 -0
- package/src/utils/tokenResolver.ts +286 -0
- package/src/wallet/backendConfig.ts +151 -0
- package/src/wallet/backends/BankrBackend.ts +399 -0
- package/src/wallet/backends/BankrWalletProvider.ts +329 -0
- package/src/wallet/backends/EnvBackend.ts +149 -0
- package/src/wallet/backends/EvmWalletSkillBackend.ts +110 -0
- package/src/wallet/backends/index.ts +10 -0
- package/src/wallet/index.ts +14 -0
- package/src/wallet/providers/AmpedWalletProvider.ts +267 -0
- package/src/wallet/providers/BankrBackend.ts +407 -0
- package/src/wallet/providers/LocalKeyBackend.ts +184 -0
- package/src/wallet/providers/chainConfig.ts +194 -0
- package/src/wallet/providers/index.ts +62 -0
- package/src/wallet/providers/types.ts +186 -0
- package/src/wallet/skillWalletAdapter.ts +335 -0
- package/src/wallet/types.ts +248 -0
- package/src/wallet/walletManager.ts +561 -0
- package/src/wallet/walletRegistry.ts +216 -0
|
@@ -0,0 +1,926 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swap Tools for Amped DeFi Plugin
|
|
3
|
+
*
|
|
4
|
+
* Provides OpenClaw tools for cross-chain swap operations using SODAX SDK:
|
|
5
|
+
* - amped_swap_quote: Get exact-in/exact-out quotes
|
|
6
|
+
* - amped_swap_execute: Execute swaps with policy enforcement
|
|
7
|
+
* - amped_swap_status: Poll intent status
|
|
8
|
+
* - amped_swap_cancel: Cancel active intents
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Static, Type } from '@sinclair/typebox';
|
|
12
|
+
import { serializeError } from '../utils/errorUtils';
|
|
13
|
+
// SDK types - using any for now due to beta API changes
|
|
14
|
+
import { Intent } from '@sodax/sdk';
|
|
15
|
+
type QuoteRequest = any;
|
|
16
|
+
type SwapQuote = any;
|
|
17
|
+
type IntentStatus = any;
|
|
18
|
+
import { getSodaxClient } from '../sodax/client';
|
|
19
|
+
import { getSpokeProvider } from '../providers/spokeProviderFactory';
|
|
20
|
+
import { PolicyEngine } from '../policy/policyEngine';
|
|
21
|
+
import { getWalletManager } from '../wallet/walletManager';
|
|
22
|
+
import type { AgentTools } from '../types';
|
|
23
|
+
import { toSodaxChainId } from '../wallet/types';
|
|
24
|
+
import { getSodaxApiClient } from '../utils/sodaxApi';
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// SODAX API & Explorer Links
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
const SODAX_CANARY_API = 'https://canary-api.sodax.com/v1/be';
|
|
31
|
+
|
|
32
|
+
// Chain ID to block explorer mapping
|
|
33
|
+
const CHAIN_EXPLORERS: Record<string, string> = {
|
|
34
|
+
'ethereum': 'https://etherscan.io/tx/',
|
|
35
|
+
'base': 'https://basescan.org/tx/',
|
|
36
|
+
'arbitrum': 'https://arbiscan.io/tx/',
|
|
37
|
+
'optimism': 'https://optimistic.etherscan.io/tx/',
|
|
38
|
+
'polygon': 'https://polygonscan.com/tx/',
|
|
39
|
+
'sonic': 'https://sonicscan.org/tx/',
|
|
40
|
+
'avalanche': 'https://snowtrace.io/tx/',
|
|
41
|
+
'bsc': 'https://bscscan.com/tx/',
|
|
42
|
+
'solana': 'https://solscan.io/tx/',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function getExplorerLink(chainId: string, txHash: string): string | undefined {
|
|
46
|
+
// Normalize chain ID (remove 0x prefix and suffix if present)
|
|
47
|
+
const normalizedChainId = chainId.replace(/^0x[\\da-f]+\\./, '').toLowerCase();
|
|
48
|
+
const explorer = CHAIN_EXPLORERS[normalizedChainId];
|
|
49
|
+
return explorer ? `${explorer}${txHash}` : undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function fetchIntentFromSodax(intentHash: string): Promise<any> {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(`${SODAX_CANARY_API}/intent/${intentHash}`);
|
|
55
|
+
if (!response.ok) return null;
|
|
56
|
+
return await response.json();
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Ensure intent hash is in hex format (0x prefixed)
|
|
64
|
+
*/
|
|
65
|
+
function toHexIntentHash(hash: unknown): string | undefined {
|
|
66
|
+
if (!hash) return undefined;
|
|
67
|
+
const str = String(hash);
|
|
68
|
+
// Already hex format
|
|
69
|
+
if (str.startsWith('0x')) return str;
|
|
70
|
+
// Convert decimal BigInt string to hex
|
|
71
|
+
try {
|
|
72
|
+
return '0x' + BigInt(str).toString(16);
|
|
73
|
+
} catch {
|
|
74
|
+
return str; // Return as-is if conversion fails
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getSodaxScanUrl(txHash: string): string {
|
|
79
|
+
return `https://sodaxscan.com/messages/search?value=${txHash}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getSodaxIntentApiUrl(intentHash: string): string {
|
|
83
|
+
return `${SODAX_CANARY_API}/intent/${intentHash}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// SODAX internal chain ID to block explorer mapping
|
|
87
|
+
const SODAX_CHAIN_EXPLORERS: Record<number, string> = {
|
|
88
|
+
1: 'https://solscan.io/tx/', // Solana
|
|
89
|
+
30: 'https://basescan.org/tx/', // Base
|
|
90
|
+
146: 'https://sonicscan.org/tx/', // Sonic (hub)
|
|
91
|
+
42161: 'https://arbiscan.io/tx/', // Arbitrum
|
|
92
|
+
10: 'https://optimistic.etherscan.io/tx/', // Optimism
|
|
93
|
+
137: 'https://polygonscan.com/tx/', // Polygon
|
|
94
|
+
56: 'https://bscscan.com/tx/', // BSC
|
|
95
|
+
43114: 'https://snowtrace.io/tx/', // Avalanche
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Poll SODAX API until intent is delivered, then return delivery tx explorer link
|
|
100
|
+
*/
|
|
101
|
+
async function pollForDelivery(
|
|
102
|
+
intentHash: string,
|
|
103
|
+
timeoutMs: number = 60000,
|
|
104
|
+
pollIntervalMs: number = 3000
|
|
105
|
+
): Promise<{ delivered: boolean; deliveryTxHash?: string; deliveryExplorer?: string; dstChainId?: number }> {
|
|
106
|
+
const startTime = Date.now();
|
|
107
|
+
|
|
108
|
+
while (Date.now() - startTime < timeoutMs) {
|
|
109
|
+
try {
|
|
110
|
+
const response = await fetch(`${SODAX_CANARY_API}/intent/${intentHash}`);
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data: any = await response.json();
|
|
117
|
+
|
|
118
|
+
// Check if intent is filled (closed)
|
|
119
|
+
if (data.open === false && data.events?.length > 0) {
|
|
120
|
+
const fillEvent = data.events.find((e: any) => e.eventType === 'intent-filled');
|
|
121
|
+
if (fillEvent) {
|
|
122
|
+
const dstChainId = data.intent?.dstChain;
|
|
123
|
+
const explorer = SODAX_CHAIN_EXPLORERS[dstChainId] || '';
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
delivered: true,
|
|
127
|
+
deliveryTxHash: fillEvent.txHash,
|
|
128
|
+
deliveryExplorer: explorer ? `${explorer}${fillEvent.txHash}` : undefined,
|
|
129
|
+
dstChainId
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
135
|
+
} catch {
|
|
136
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return { delivered: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
import { resolveToken, getTokenInfo } from '../utils/tokenResolver';
|
|
144
|
+
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// TypeBox Schemas
|
|
147
|
+
// ============================================================================
|
|
148
|
+
|
|
149
|
+
const SwapTypeSchema = Type.Union([
|
|
150
|
+
Type.Literal('exact_input'),
|
|
151
|
+
Type.Literal('exact_output')
|
|
152
|
+
]);
|
|
153
|
+
|
|
154
|
+
const SwapQuoteRequestSchema = Type.Object({
|
|
155
|
+
walletId: Type.String(),
|
|
156
|
+
srcChainId: Type.String(),
|
|
157
|
+
dstChainId: Type.String(),
|
|
158
|
+
srcToken: Type.String(),
|
|
159
|
+
dstToken: Type.String(),
|
|
160
|
+
amount: Type.String(),
|
|
161
|
+
type: SwapTypeSchema,
|
|
162
|
+
slippageBps: Type.Number({ default: 50, minimum: 0, maximum: 10000 }),
|
|
163
|
+
recipient: Type.Optional(Type.String({
|
|
164
|
+
description: 'Recipient address on destination chain. For cross-chain swaps to Solana, provide a Solana base58 address. Defaults to wallet address if omitted.'
|
|
165
|
+
}))
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Result schema for documentation (not used at runtime)
|
|
169
|
+
const _SwapQuoteResultSchema = Type.Object({
|
|
170
|
+
inputAmount: Type.String(),
|
|
171
|
+
outputAmount: Type.String(),
|
|
172
|
+
srcToken: Type.String(),
|
|
173
|
+
dstToken: Type.String(),
|
|
174
|
+
srcChainId: Type.String(),
|
|
175
|
+
dstChainId: Type.String(),
|
|
176
|
+
slippageBps: Type.Number(),
|
|
177
|
+
deadline: Type.Number(),
|
|
178
|
+
fees: Type.Object({
|
|
179
|
+
solverFee: Type.String(),
|
|
180
|
+
protocolFee: Type.Optional(Type.String()),
|
|
181
|
+
partnerFee: Type.Optional(Type.String())
|
|
182
|
+
}),
|
|
183
|
+
minOutputAmount: Type.Optional(Type.String()),
|
|
184
|
+
maxInputAmount: Type.Optional(Type.String()),
|
|
185
|
+
recipient: Type.Optional(Type.String())
|
|
186
|
+
});
|
|
187
|
+
void _SwapQuoteResultSchema; // Suppress unused warning
|
|
188
|
+
|
|
189
|
+
const SwapExecuteParamsSchema = Type.Object({
|
|
190
|
+
walletId: Type.String(),
|
|
191
|
+
quote: Type.Object({
|
|
192
|
+
srcChainId: Type.String(),
|
|
193
|
+
dstChainId: Type.String(),
|
|
194
|
+
srcToken: Type.String(),
|
|
195
|
+
dstToken: Type.String(),
|
|
196
|
+
inputAmount: Type.String(),
|
|
197
|
+
outputAmount: Type.String(),
|
|
198
|
+
slippageBps: Type.Number(),
|
|
199
|
+
deadline: Type.Number(),
|
|
200
|
+
minOutputAmount: Type.Optional(Type.String()),
|
|
201
|
+
maxInputAmount: Type.Optional(Type.String()),
|
|
202
|
+
recipient: Type.Optional(Type.String())
|
|
203
|
+
}),
|
|
204
|
+
maxSlippageBps: Type.Optional(Type.Number({ minimum: 0, maximum: 10000 })),
|
|
205
|
+
policyId: Type.Optional(Type.String()),
|
|
206
|
+
skipSimulation: Type.Optional(Type.Boolean({ default: false })),
|
|
207
|
+
timeoutMs: Type.Optional(Type.Number({ default: 120000 }))
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const SwapExecuteResultSchema = Type.Object({
|
|
211
|
+
spokeTxHash: Type.String(),
|
|
212
|
+
hubTxHash: Type.Optional(Type.String()),
|
|
213
|
+
intentHash: Type.Optional(Type.String()),
|
|
214
|
+
status: Type.String(),
|
|
215
|
+
message: Type.Optional(Type.String())
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const SwapStatusParamsSchema = Type.Object({
|
|
219
|
+
txHash: Type.Optional(Type.String()),
|
|
220
|
+
intentHash: Type.Optional(Type.String())
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const SwapStatusResultSchema = Type.Object({
|
|
224
|
+
status: Type.String(),
|
|
225
|
+
intentHash: Type.Optional(Type.String()),
|
|
226
|
+
spokeTxHash: Type.Optional(Type.String()),
|
|
227
|
+
hubTxHash: Type.Optional(Type.String()),
|
|
228
|
+
filledAmount: Type.Optional(Type.String()),
|
|
229
|
+
error: Type.Optional(Type.String()),
|
|
230
|
+
createdAt: Type.Optional(Type.Number()),
|
|
231
|
+
expiresAt: Type.Optional(Type.Number())
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const SwapCancelParamsSchema = Type.Object({
|
|
235
|
+
walletId: Type.String(),
|
|
236
|
+
intent: Type.Object({
|
|
237
|
+
id: Type.String(),
|
|
238
|
+
srcChainId: Type.String(),
|
|
239
|
+
dstChainId: Type.String(),
|
|
240
|
+
srcToken: Type.String(),
|
|
241
|
+
dstToken: Type.String(),
|
|
242
|
+
amount: Type.String(),
|
|
243
|
+
deadline: Type.Number()
|
|
244
|
+
}),
|
|
245
|
+
srcChainId: Type.String()
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const SwapCancelResultSchema = Type.Object({
|
|
249
|
+
success: Type.Boolean(),
|
|
250
|
+
txHash: Type.Optional(Type.String()),
|
|
251
|
+
message: Type.String()
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ============================================================================
|
|
255
|
+
// Type Definitions
|
|
256
|
+
// ============================================================================
|
|
257
|
+
|
|
258
|
+
type SwapQuoteRequest = Static<typeof SwapQuoteRequestSchema>;
|
|
259
|
+
type SwapExecuteParams = Static<typeof SwapExecuteParamsSchema>;
|
|
260
|
+
type SwapStatusParams = Static<typeof SwapStatusParamsSchema>;
|
|
261
|
+
type SwapCancelParams = Static<typeof SwapCancelParamsSchema>;
|
|
262
|
+
|
|
263
|
+
// ============================================================================
|
|
264
|
+
// Swap Quote Tool
|
|
265
|
+
// ============================================================================
|
|
266
|
+
|
|
267
|
+
async function handleSwapQuote(params: SwapQuoteRequest): Promise<Record<string, unknown>> {
|
|
268
|
+
const requestId = generateRequestId();
|
|
269
|
+
const startTime = Date.now();
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const sodaxClient = getSodaxClient();
|
|
273
|
+
|
|
274
|
+
// Resolve token symbols to addresses
|
|
275
|
+
const srcTokenAddr = await resolveToken(params.srcChainId, params.srcToken);
|
|
276
|
+
const dstTokenAddr = await resolveToken(params.dstChainId, params.dstToken);
|
|
277
|
+
|
|
278
|
+
// Get token info for decimals
|
|
279
|
+
const srcTokenInfo = await getTokenInfo(params.srcChainId, srcTokenAddr);
|
|
280
|
+
const dstTokenInfo = await getTokenInfo(params.dstChainId, dstTokenAddr);
|
|
281
|
+
|
|
282
|
+
// Get token config to determine decimals for amount conversion
|
|
283
|
+
const configService = (sodaxClient as any).configService;
|
|
284
|
+
const decimals = srcTokenInfo?.decimals ?? 18; // Default to 18 (most EVM tokens) if not found
|
|
285
|
+
|
|
286
|
+
// Convert human-readable amount to raw amount (bigint)
|
|
287
|
+
const amountFloat = parseFloat(params.amount);
|
|
288
|
+
const rawAmount = BigInt(Math.floor(amountFloat * Math.pow(10, decimals)));
|
|
289
|
+
|
|
290
|
+
// Build SDK-compatible request with snake_case parameters
|
|
291
|
+
const quoteRequest = {
|
|
292
|
+
token_src: srcTokenAddr,
|
|
293
|
+
token_src_blockchain_id: toSodaxChainId(params.srcChainId),
|
|
294
|
+
token_dst: dstTokenAddr,
|
|
295
|
+
token_dst_blockchain_id: toSodaxChainId(params.dstChainId),
|
|
296
|
+
amount: rawAmount,
|
|
297
|
+
quote_type: params.type
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
console.log('[swap_quote] SDK request:', JSON.stringify(quoteRequest, (k, v) => typeof v === 'bigint' ? v.toString() : v));
|
|
301
|
+
|
|
302
|
+
const quoteResult = await (sodaxClient as any).swaps.getQuote(quoteRequest);
|
|
303
|
+
|
|
304
|
+
// Handle Result type from SDK
|
|
305
|
+
if (quoteResult.ok === false) {
|
|
306
|
+
const errorMsg = quoteResult.error instanceof Error
|
|
307
|
+
? quoteResult.error.message
|
|
308
|
+
: typeof quoteResult.error === 'string'
|
|
309
|
+
? quoteResult.error
|
|
310
|
+
: JSON.stringify(quoteResult.error, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2);
|
|
311
|
+
throw new Error(`Quote failed: ${errorMsg}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const quote = quoteResult.ok ? quoteResult.value : quoteResult;
|
|
315
|
+
|
|
316
|
+
console.log('[swap_quote] SDK response:', JSON.stringify(quote, (k, v) => typeof v === 'bigint' ? v.toString() : v));
|
|
317
|
+
|
|
318
|
+
// Get output token decimals with multiple fallbacks
|
|
319
|
+
// SDK returns quoted_amount as bigint - convert to human-readable string
|
|
320
|
+
let dstDecimals = (quote as any).token_dst_decimals || (quote as any).tokenDstDecimals;
|
|
321
|
+
|
|
322
|
+
if (!dstDecimals && dstTokenInfo) {
|
|
323
|
+
dstDecimals = dstTokenInfo.decimals;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!dstDecimals) {
|
|
327
|
+
// Hardcoded decimals for common stablecoins
|
|
328
|
+
const KNOWN_DECIMALS: Record<string, number> = {
|
|
329
|
+
usdc: 6, USDC: 6, usdt: 6, USDT: 6, sol: 9, SOL: 9,
|
|
330
|
+
dai: 18, DAI: 18, bnusd: 18, bnUSD: 18
|
|
331
|
+
};
|
|
332
|
+
const tokenSymbol = params.dstToken.toUpperCase();
|
|
333
|
+
dstDecimals = KNOWN_DECIMALS[tokenSymbol] || 18;
|
|
334
|
+
console.warn(`[swap_quote] Using fallback decimals (${dstDecimals}) for token ${params.dstToken}`);
|
|
335
|
+
}
|
|
336
|
+
const quotedAmount = quote.quoted_amount || quote.quotedAmount || quote.outputAmount;
|
|
337
|
+
const outputAmountStr = quotedAmount
|
|
338
|
+
? (Number(quotedAmount) / Math.pow(10, dstDecimals)).toString()
|
|
339
|
+
: '0';
|
|
340
|
+
|
|
341
|
+
// Normalize and return quote (SDK uses snake_case, we return camelCase)
|
|
342
|
+
const result = {
|
|
343
|
+
inputAmount: params.amount,
|
|
344
|
+
outputAmount: outputAmountStr,
|
|
345
|
+
srcToken: srcTokenAddr,
|
|
346
|
+
dstToken: dstTokenAddr,
|
|
347
|
+
srcChainId: params.srcChainId,
|
|
348
|
+
dstChainId: params.dstChainId,
|
|
349
|
+
slippageBps: params.slippageBps,
|
|
350
|
+
deadline: quote.deadline || calculateDeadline(300), // 5 min default
|
|
351
|
+
fees: {
|
|
352
|
+
solverFee: quote.solver_fee || quote.fees?.solverFee || '0',
|
|
353
|
+
protocolFee: quote.protocol_fee || quote.fees?.protocolFee,
|
|
354
|
+
partnerFee: quote.partner_fee || quote.fees?.partnerFee
|
|
355
|
+
},
|
|
356
|
+
minOutputAmount: quote.min_output_amount || quote.minOutputAmount,
|
|
357
|
+
maxInputAmount: quote.max_input_amount || quote.maxInputAmount,
|
|
358
|
+
recipient: params.recipient, // Pass through for execute
|
|
359
|
+
// Include raw SDK response for debugging
|
|
360
|
+
_raw: JSON.parse(JSON.stringify(quote, (k, v) => typeof v === 'bigint' ? v.toString() : v))
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
logStructured({
|
|
364
|
+
requestId,
|
|
365
|
+
opType: 'swap_quote',
|
|
366
|
+
walletId: params.walletId,
|
|
367
|
+
chainIds: [params.srcChainId, params.dstChainId],
|
|
368
|
+
tokenAddresses: [params.srcToken, params.dstToken],
|
|
369
|
+
durationMs: Date.now() - startTime,
|
|
370
|
+
success: true
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
return result;
|
|
374
|
+
} catch (error) {
|
|
375
|
+
logStructured({
|
|
376
|
+
requestId,
|
|
377
|
+
opType: 'swap_quote',
|
|
378
|
+
walletId: params.walletId,
|
|
379
|
+
chainIds: [params.srcChainId, params.dstChainId],
|
|
380
|
+
durationMs: Date.now() - startTime,
|
|
381
|
+
success: false,
|
|
382
|
+
error: error instanceof Error ? error.message : String(error)
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
throw new Error(`Failed to get swap quote: ${error instanceof Error ? error.message : String(error)}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ============================================================================
|
|
390
|
+
// Swap Execute Tool
|
|
391
|
+
// ============================================================================
|
|
392
|
+
|
|
393
|
+
async function handleSwapExecute(params: SwapExecuteParams): Promise<Record<string, unknown>> {
|
|
394
|
+
const requestId = generateRequestId();
|
|
395
|
+
const startTime = Date.now();
|
|
396
|
+
|
|
397
|
+
try {
|
|
398
|
+
// 1. Initialize dependencies
|
|
399
|
+
const policyEngine = new PolicyEngine();
|
|
400
|
+
const walletManager = getWalletManager();
|
|
401
|
+
const sodaxClient = getSodaxClient();
|
|
402
|
+
|
|
403
|
+
// Resolve token symbols to addresses
|
|
404
|
+
const srcTokenAddr = await resolveToken(params.quote.srcChainId, params.quote.srcToken);
|
|
405
|
+
const dstTokenAddr = await resolveToken(params.quote.dstChainId, params.quote.dstToken);
|
|
406
|
+
|
|
407
|
+
// Get token info for decimals
|
|
408
|
+
const srcTokenInfo = await getTokenInfo(params.quote.srcChainId, srcTokenAddr);
|
|
409
|
+
const dstTokenInfo = await getTokenInfo(params.quote.dstChainId, dstTokenAddr);
|
|
410
|
+
|
|
411
|
+
// 2. Resolve wallet
|
|
412
|
+
const wallet = await walletManager.resolve(params.walletId);
|
|
413
|
+
const walletAddress = await wallet.getAddress();
|
|
414
|
+
|
|
415
|
+
// 3. Policy check
|
|
416
|
+
const policyCheck = await policyEngine.checkSwap({
|
|
417
|
+
walletId: params.walletId,
|
|
418
|
+
srcChainId: params.quote.srcChainId,
|
|
419
|
+
dstChainId: params.quote.dstChainId,
|
|
420
|
+
srcToken: params.quote.srcToken,
|
|
421
|
+
dstToken: params.quote.dstToken,
|
|
422
|
+
inputAmount: params.quote.inputAmount,
|
|
423
|
+
slippageBps: params.maxSlippageBps || params.quote.slippageBps,
|
|
424
|
+
policyId: params.policyId
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (!policyCheck.allowed) {
|
|
428
|
+
throw new Error(`Policy check failed: ${policyCheck.reason}`);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// 4. Get spoke provider for source chain
|
|
432
|
+
const spokeProvider = await getSpokeProvider(
|
|
433
|
+
params.walletId,
|
|
434
|
+
params.quote.srcChainId
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// 5. Convert amounts to bigint FIRST (needed for intentParams)
|
|
438
|
+
const srcDecimals = srcTokenInfo?.decimals ?? 18;
|
|
439
|
+
const dstDecimals = dstTokenInfo?.decimals ?? 18;
|
|
440
|
+
const inputAmountRaw = BigInt(Math.floor(parseFloat(params.quote.inputAmount) * Math.pow(10, srcDecimals)));
|
|
441
|
+
const outputAmountRaw = BigInt(Math.floor(parseFloat(params.quote.outputAmount) * Math.pow(10, dstDecimals)));
|
|
442
|
+
|
|
443
|
+
// Calculate minOutputAmount with slippage
|
|
444
|
+
const slippageBps = params.maxSlippageBps || params.quote.slippageBps || 100;
|
|
445
|
+
const minOutputAmountRaw = outputAmountRaw - (outputAmountRaw * BigInt(slippageBps) / 10000n);
|
|
446
|
+
|
|
447
|
+
console.log("[swap_execute] Amount conversion:", {
|
|
448
|
+
inputAmount: params.quote.inputAmount,
|
|
449
|
+
inputAmountRaw: inputAmountRaw.toString(),
|
|
450
|
+
outputAmount: params.quote.outputAmount,
|
|
451
|
+
outputAmountRaw: outputAmountRaw.toString(),
|
|
452
|
+
minOutputAmountRaw: minOutputAmountRaw.toString(),
|
|
453
|
+
srcDecimals,
|
|
454
|
+
dstDecimals
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// 6. Build intentParams (used for allowance check, approval, and swap)
|
|
458
|
+
const intentParams = {
|
|
459
|
+
srcAddress: walletAddress,
|
|
460
|
+
dstAddress: params.quote.recipient || walletAddress,
|
|
461
|
+
srcChain: toSodaxChainId(params.quote.srcChainId),
|
|
462
|
+
dstChain: toSodaxChainId(params.quote.dstChainId),
|
|
463
|
+
inputToken: srcTokenAddr,
|
|
464
|
+
outputToken: dstTokenAddr,
|
|
465
|
+
inputAmount: inputAmountRaw,
|
|
466
|
+
minOutputAmount: minOutputAmountRaw,
|
|
467
|
+
deadline: BigInt(params.quote.deadline),
|
|
468
|
+
allowPartialFill: false,
|
|
469
|
+
solver: "0x0000000000000000000000000000000000000000" as `0x${string}`,
|
|
470
|
+
data: "0x" as `0x${string}`
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// 7. Check allowance using SDK's expected API
|
|
474
|
+
let allowanceValid = false;
|
|
475
|
+
try {
|
|
476
|
+
const allowanceResult = await (sodaxClient as any).swaps.isAllowanceValid({
|
|
477
|
+
intentParams,
|
|
478
|
+
spokeProvider
|
|
479
|
+
});
|
|
480
|
+
allowanceValid = allowanceResult?.ok ? allowanceResult.value : !!allowanceResult;
|
|
481
|
+
} catch (e) {
|
|
482
|
+
console.warn('[swap_execute] Allowance check failed, assuming approval needed:', e);
|
|
483
|
+
allowanceValid = false;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 8. Approve if needed using SDK's expected API
|
|
487
|
+
if (!allowanceValid) {
|
|
488
|
+
logStructured({
|
|
489
|
+
requestId,
|
|
490
|
+
opType: 'swap_approve',
|
|
491
|
+
walletId: params.walletId,
|
|
492
|
+
chainId: params.quote.srcChainId,
|
|
493
|
+
token: srcTokenAddr,
|
|
494
|
+
message: 'Token approval required'
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const approvalResult = await (sodaxClient as any).swaps.approve({
|
|
498
|
+
intentParams,
|
|
499
|
+
spokeProvider
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const approvalTx = approvalResult?.ok ? approvalResult.value : approvalResult;
|
|
503
|
+
|
|
504
|
+
// Wait for approval confirmation if possible
|
|
505
|
+
if ((spokeProvider as any).walletProvider?.waitForTransactionReceipt && approvalTx) {
|
|
506
|
+
await (spokeProvider as any).walletProvider.waitForTransactionReceipt(approvalTx);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
logStructured({
|
|
510
|
+
requestId,
|
|
511
|
+
opType: 'swap_approve',
|
|
512
|
+
walletId: params.walletId,
|
|
513
|
+
chainId: params.quote.srcChainId,
|
|
514
|
+
token: srcTokenAddr,
|
|
515
|
+
approvalTx: String(approvalTx),
|
|
516
|
+
success: true
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 9. Execute swap
|
|
521
|
+
const swapResult = await (sodaxClient as any).swaps.swap({
|
|
522
|
+
intentParams,
|
|
523
|
+
spokeProvider,
|
|
524
|
+
skipSimulation: params.skipSimulation || false,
|
|
525
|
+
timeout: params.timeoutMs || 120000
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Handle Result type from SDK
|
|
529
|
+
if (swapResult.ok === false) {
|
|
530
|
+
const errorMsg = swapResult.error instanceof Error
|
|
531
|
+
? swapResult.error.message
|
|
532
|
+
: typeof swapResult.error === 'string'
|
|
533
|
+
? swapResult.error
|
|
534
|
+
: JSON.stringify(swapResult.error, (k, v) => typeof v === 'bigint' ? v.toString() : v, 2);
|
|
535
|
+
throw new Error(`Swap failed: ${errorMsg}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const value = swapResult.ok ? swapResult.value : swapResult;
|
|
539
|
+
|
|
540
|
+
// SDK may return [response, intent, deliveryInfo] tuple
|
|
541
|
+
const [solverResponse, intent, deliveryInfo] = Array.isArray(value) ? value : [value, undefined, undefined];
|
|
542
|
+
|
|
543
|
+
// Extract internal tracking info
|
|
544
|
+
const srcTxHash = deliveryInfo?.srcTxHash;
|
|
545
|
+
const intentHash = toHexIntentHash((solverResponse as any)?.intent_hash) || toHexIntentHash(intent?.intentId);
|
|
546
|
+
|
|
547
|
+
// Poll for delivery confirmation (wait up to 60s)
|
|
548
|
+
let deliveryResult: { delivered: boolean; deliveryExplorer?: string } = { delivered: false };
|
|
549
|
+
if (intentHash) {
|
|
550
|
+
console.log('[swap_execute] Waiting for delivery confirmation...');
|
|
551
|
+
deliveryResult = await pollForDelivery(intentHash, 60000, 3000);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Build user-friendly result
|
|
555
|
+
const result = {
|
|
556
|
+
status: deliveryResult.delivered ? 'delivered' : 'submitted',
|
|
557
|
+
message: deliveryResult.delivered
|
|
558
|
+
? 'Swap completed! Funds delivered to destination.'
|
|
559
|
+
: 'Swap submitted, awaiting cross-chain delivery...',
|
|
560
|
+
// User-friendly tracking link
|
|
561
|
+
sodaxScanUrl: srcTxHash ? getSodaxScanUrl(srcTxHash) : undefined,
|
|
562
|
+
// Source chain: where user initiated the swap
|
|
563
|
+
// Destination chain: where user RECEIVED funds
|
|
564
|
+
initiationTx: srcTxHash ? getExplorerLink(params.quote.srcChainId, srcTxHash) : undefined,
|
|
565
|
+
receiptTx: deliveryResult.deliveryExplorer,
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
logStructured({
|
|
569
|
+
requestId,
|
|
570
|
+
opType: 'swap_execute',
|
|
571
|
+
walletId: params.walletId,
|
|
572
|
+
chainIds: [params.quote.srcChainId, params.quote.dstChainId],
|
|
573
|
+
tokenAddresses: [params.quote.srcToken, params.quote.dstToken],
|
|
574
|
+
durationMs: Date.now() - startTime,
|
|
575
|
+
success: true
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
return result;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
logStructured({
|
|
581
|
+
requestId,
|
|
582
|
+
opType: 'swap_execute',
|
|
583
|
+
walletId: params.walletId,
|
|
584
|
+
chainIds: [params.quote.srcChainId, params.quote.dstChainId],
|
|
585
|
+
durationMs: Date.now() - startTime,
|
|
586
|
+
success: false,
|
|
587
|
+
error: error instanceof Error ? error.message : String(error)
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
throw new Error(`Swap execution failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ============================================================================
|
|
595
|
+
// Swap Status Tool
|
|
596
|
+
// ============================================================================
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
async function handleSwapStatus(params: SwapStatusParams): Promise<Record<string, unknown>> {
|
|
600
|
+
const requestId = generateRequestId();
|
|
601
|
+
const startTime = Date.now();
|
|
602
|
+
|
|
603
|
+
try {
|
|
604
|
+
if (!params.txHash && !params.intentHash) {
|
|
605
|
+
throw new Error('Either txHash or intentHash must be provided');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Use our SodaxApiClient for Backend API access (not SDK)
|
|
609
|
+
const sodaxApi = getSodaxApiClient();
|
|
610
|
+
|
|
611
|
+
let intentData: any = null;
|
|
612
|
+
|
|
613
|
+
// Try intentHash first (most reliable)
|
|
614
|
+
if (params.intentHash) {
|
|
615
|
+
try {
|
|
616
|
+
intentData = await sodaxApi.getIntentByHash(params.intentHash);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
console.warn(`[swap_status] getIntentByHash failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// If intentHash lookup failed or wasn't provided, try txHash
|
|
623
|
+
// Note: txHash should be from the HUB chain (Sonic), not spoke chain
|
|
624
|
+
if (!intentData && params.txHash) {
|
|
625
|
+
try {
|
|
626
|
+
intentData = await sodaxApi.getIntentByTxHash(params.txHash);
|
|
627
|
+
} catch (err) {
|
|
628
|
+
// txHash lookup failed - provide helpful error message
|
|
629
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
630
|
+
throw new Error(
|
|
631
|
+
`Unable to find intent for txHash: ${params.txHash}. ` +
|
|
632
|
+
`Note: The txHash must be from the HUB chain (Sonic), not the spoke chain. ` +
|
|
633
|
+
`If you have a spoke chain txHash (Base, Arbitrum, etc.), use amped_user_intents to find the intent first. ` +
|
|
634
|
+
`Error: ${errorMsg}`
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (!intentData) {
|
|
640
|
+
throw new Error('Unable to retrieve swap status. Provide a valid intentHash or hub chain txHash.');
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Extract intent details
|
|
644
|
+
const intentHash = intentData.intentHash;
|
|
645
|
+
const hubTxHash = intentData.txHash; // This is the hub chain tx that created the intent
|
|
646
|
+
const isOpen = intentData.open;
|
|
647
|
+
const intent = intentData.intent;
|
|
648
|
+
|
|
649
|
+
// Determine status from intent state
|
|
650
|
+
let status = 'unknown';
|
|
651
|
+
if (intentData.open === true) {
|
|
652
|
+
status = 'pending';
|
|
653
|
+
} else if (intentData.open === false) {
|
|
654
|
+
// Check events to determine if filled or cancelled/expired
|
|
655
|
+
const filledEvent = intentData.events?.find((e: any) => e.eventType === 'intent-filled');
|
|
656
|
+
const cancelledEvent = intentData.events?.find((e: any) =>
|
|
657
|
+
e.eventType === 'intent-cancelled' || e.eventType === 'intent-expired'
|
|
658
|
+
);
|
|
659
|
+
if (filledEvent) {
|
|
660
|
+
status = 'filled';
|
|
661
|
+
} else if (cancelledEvent) {
|
|
662
|
+
status = cancelledEvent.eventType === 'intent-cancelled' ? 'cancelled' : 'expired';
|
|
663
|
+
} else {
|
|
664
|
+
status = 'closed';
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Extract fulfillment details if available
|
|
669
|
+
const filledEvent = intentData.events?.find((e: any) => e.eventType === 'intent-filled');
|
|
670
|
+
const fulfillmentTxHash = filledEvent?.txHash;
|
|
671
|
+
const receivedOutput = filledEvent?.intentState?.receivedOutput;
|
|
672
|
+
|
|
673
|
+
// Build result
|
|
674
|
+
const result: Record<string, unknown> = {
|
|
675
|
+
status,
|
|
676
|
+
intentHash,
|
|
677
|
+
hubTxHash, // Hub chain tx that created the intent
|
|
678
|
+
spokeTxHash: params.txHash !== hubTxHash ? params.txHash : undefined, // Original spoke tx if different
|
|
679
|
+
open: isOpen,
|
|
680
|
+
// Intent details
|
|
681
|
+
srcChain: intent?.srcChain,
|
|
682
|
+
dstChain: intent?.dstChain,
|
|
683
|
+
inputToken: intent?.inputToken,
|
|
684
|
+
outputToken: intent?.outputToken,
|
|
685
|
+
inputAmount: intent?.inputAmount,
|
|
686
|
+
minOutputAmount: intent?.minOutputAmount,
|
|
687
|
+
receivedOutput: receivedOutput,
|
|
688
|
+
deadline: intent?.deadline ? new Date(parseInt(intent.deadline) * 1000).toISOString() : undefined,
|
|
689
|
+
createdAt: intentData.createdAt,
|
|
690
|
+
// Fulfillment details
|
|
691
|
+
fulfillmentTxHash,
|
|
692
|
+
fulfillmentChain: intent?.dstChain,
|
|
693
|
+
// Tracking links
|
|
694
|
+
sodaxScanUrl: `https://sodaxscan.com/intents/${intentHash}`,
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
logStructured({
|
|
698
|
+
requestId,
|
|
699
|
+
opType: 'swap_status',
|
|
700
|
+
intentHash,
|
|
701
|
+
txHash: params.txHash,
|
|
702
|
+
status,
|
|
703
|
+
durationMs: Date.now() - startTime,
|
|
704
|
+
success: true
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
return result;
|
|
708
|
+
} catch (error) {
|
|
709
|
+
logStructured({
|
|
710
|
+
requestId,
|
|
711
|
+
opType: 'swap_status',
|
|
712
|
+
intentHash: params.intentHash,
|
|
713
|
+
txHash: params.txHash,
|
|
714
|
+
durationMs: Date.now() - startTime,
|
|
715
|
+
success: false,
|
|
716
|
+
error: error instanceof Error ? error.message : String(error)
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
throw new Error(`Failed to get swap status: ${error instanceof Error ? error.message : String(error)}`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ============================================================================
|
|
724
|
+
// Swap Cancel Tool
|
|
725
|
+
// ============================================================================
|
|
726
|
+
|
|
727
|
+
async function handleSwapCancel(params: SwapCancelParams): Promise<Record<string, unknown>> {
|
|
728
|
+
const requestId = generateRequestId();
|
|
729
|
+
const startTime = Date.now();
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
const walletManager = getWalletManager();
|
|
733
|
+
const sodaxClient = getSodaxClient();
|
|
734
|
+
|
|
735
|
+
// Resolve token symbols to addresses
|
|
736
|
+
const srcTokenAddr = await resolveToken(params.intent.srcChainId, params.intent.srcToken);
|
|
737
|
+
const dstTokenAddr = await resolveToken(params.intent.dstChainId, params.intent.dstToken);
|
|
738
|
+
|
|
739
|
+
// Get token info for decimals
|
|
740
|
+
const srcTokenInfo = await getTokenInfo(params.intent.srcChainId, srcTokenAddr);
|
|
741
|
+
const dstTokenInfo = await getTokenInfo(params.intent.dstChainId, dstTokenAddr);
|
|
742
|
+
|
|
743
|
+
// Resolve wallet (validates it exists)
|
|
744
|
+
await walletManager.resolve(params.walletId);
|
|
745
|
+
|
|
746
|
+
// Get spoke provider for source chain
|
|
747
|
+
const spokeProvider = await getSpokeProvider(
|
|
748
|
+
params.walletId,
|
|
749
|
+
params.srcChainId
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
// Construct intent object for cancellation
|
|
753
|
+
const intent = {
|
|
754
|
+
id: params.intent.id,
|
|
755
|
+
srcChainId: params.intent.srcChainId,
|
|
756
|
+
dstChainId: params.intent.dstChainId,
|
|
757
|
+
srcToken: params.intent.srcToken,
|
|
758
|
+
dstToken: params.intent.dstToken,
|
|
759
|
+
amount: params.intent.amount,
|
|
760
|
+
deadline: BigInt(params.intent.deadline),
|
|
761
|
+
createdAt: Date.now(),
|
|
762
|
+
status: 'pending'
|
|
763
|
+
} as unknown as Intent;
|
|
764
|
+
|
|
765
|
+
// Cancel the intent - SDK expects (intent, spokeProvider)
|
|
766
|
+
const cancelResult = await (sodaxClient as any).swaps.cancelIntent(intent, spokeProvider);
|
|
767
|
+
|
|
768
|
+
// Handle Result type
|
|
769
|
+
if (cancelResult.ok === false) {
|
|
770
|
+
throw new Error(`Cancel failed: ${serializeError(cancelResult.error)}`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const cancelTx = cancelResult.ok ? cancelResult.value : cancelResult;
|
|
774
|
+
const cancelTxHash = typeof cancelTx === 'string' ? cancelTx : String(cancelTx);
|
|
775
|
+
|
|
776
|
+
// Wait for cancellation confirmation if possible
|
|
777
|
+
// SDK may expose waitForTransactionReceipt on the underlying wallet provider
|
|
778
|
+
if ((spokeProvider as any).walletProvider?.waitForTransactionReceipt) {
|
|
779
|
+
await (spokeProvider as any).walletProvider.waitForTransactionReceipt(cancelTxHash);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const result = {
|
|
783
|
+
success: true,
|
|
784
|
+
txHash: cancelTxHash,
|
|
785
|
+
message: 'Intent cancelled successfully'
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
logStructured({
|
|
789
|
+
requestId,
|
|
790
|
+
opType: 'swap_cancel',
|
|
791
|
+
walletId: params.walletId,
|
|
792
|
+
chainId: params.srcChainId,
|
|
793
|
+
intentId: params.intent.id,
|
|
794
|
+
txHash: cancelTxHash,
|
|
795
|
+
durationMs: Date.now() - startTime,
|
|
796
|
+
success: true
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
return result;
|
|
800
|
+
} catch (error) {
|
|
801
|
+
logStructured({
|
|
802
|
+
requestId,
|
|
803
|
+
opType: 'swap_cancel',
|
|
804
|
+
walletId: params.walletId,
|
|
805
|
+
chainId: params.srcChainId,
|
|
806
|
+
intentId: params.intent.id,
|
|
807
|
+
durationMs: Date.now() - startTime,
|
|
808
|
+
success: false,
|
|
809
|
+
error: error instanceof Error ? error.message : String(error)
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
throw new Error(`Failed to cancel swap: ${error instanceof Error ? error.message : String(error)}`);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// ============================================================================
|
|
817
|
+
// Utility Functions
|
|
818
|
+
// ============================================================================
|
|
819
|
+
|
|
820
|
+
function generateRequestId(): string {
|
|
821
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function calculateDeadline(secondsFromNow: number): number {
|
|
825
|
+
return Math.floor(Date.now() / 1000) + secondsFromNow;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
interface LogEntry {
|
|
829
|
+
requestId: string;
|
|
830
|
+
opType: string;
|
|
831
|
+
walletId?: string;
|
|
832
|
+
chainId?: string;
|
|
833
|
+
chainIds?: string[];
|
|
834
|
+
token?: string;
|
|
835
|
+
tokenAddresses?: string[];
|
|
836
|
+
intentHash?: string;
|
|
837
|
+
txHash?: string;
|
|
838
|
+
spokeTxHash?: string;
|
|
839
|
+
hubTxHash?: string;
|
|
840
|
+
approvalTx?: string;
|
|
841
|
+
status?: string;
|
|
842
|
+
durationMs?: number;
|
|
843
|
+
success?: boolean;
|
|
844
|
+
error?: string;
|
|
845
|
+
message?: string;
|
|
846
|
+
intentId?: string;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function logStructured(entry: LogEntry): void {
|
|
850
|
+
// Structured JSON logging for observability
|
|
851
|
+
// Use replacer to handle BigInt serialization
|
|
852
|
+
console.log(JSON.stringify({
|
|
853
|
+
...entry,
|
|
854
|
+
timestamp: new Date().toISOString(),
|
|
855
|
+
component: 'amped-defi-swap'
|
|
856
|
+
}, (k, v) => typeof v === 'bigint' ? v.toString() : v));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// ============================================================================
|
|
860
|
+
// Tool Registration
|
|
861
|
+
// ============================================================================
|
|
862
|
+
|
|
863
|
+
export function registerSwapTools(agentTools: AgentTools): void {
|
|
864
|
+
// Register swap quote tool
|
|
865
|
+
agentTools.register({
|
|
866
|
+
name: 'amped_swap_quote',
|
|
867
|
+
summary: 'Get a swap quote for exact-in or exact-out swaps across chains',
|
|
868
|
+
description: 'Retrieves a quote for swapping tokens across chains using the SODAX swap protocol. ' +
|
|
869
|
+
'Supports both exact input (specify input amount, get output estimate) and ' +
|
|
870
|
+
'exact output (specify desired output, get required input) modes.',
|
|
871
|
+
schema: SwapQuoteRequestSchema,
|
|
872
|
+
handler: handleSwapQuote
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
// Register swap execute tool
|
|
876
|
+
agentTools.register({
|
|
877
|
+
name: 'amped_swap_execute',
|
|
878
|
+
summary: 'Execute a swap with policy enforcement and allowance handling',
|
|
879
|
+
description: 'Executes a swap using a previously obtained quote. ' +
|
|
880
|
+
'Performs policy checks, validates allowances, approves tokens if needed, ' +
|
|
881
|
+
'and executes the swap transaction. Returns transaction hashes and intent status.',
|
|
882
|
+
schema: SwapExecuteParamsSchema,
|
|
883
|
+
handler: handleSwapExecute
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
// Register swap status tool
|
|
887
|
+
agentTools.register({
|
|
888
|
+
name: 'amped_swap_status',
|
|
889
|
+
summary: 'Check the status of a swap intent or transaction',
|
|
890
|
+
description: 'Polls the status of a swap by intent hash or transaction hash. ' +
|
|
891
|
+
'Returns current status, fill amount, error details if failed, and timing information.',
|
|
892
|
+
schema: SwapStatusParamsSchema,
|
|
893
|
+
handler: handleSwapStatus
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
// Register swap cancel tool
|
|
897
|
+
agentTools.register({
|
|
898
|
+
name: 'amped_swap_cancel',
|
|
899
|
+
summary: 'Cancel an active swap intent',
|
|
900
|
+
description: 'Cancels a pending swap intent on the source chain. ' +
|
|
901
|
+
'Requires the intent details and source chain ID. Returns cancellation transaction hash.',
|
|
902
|
+
schema: SwapCancelParamsSchema,
|
|
903
|
+
handler: handleSwapCancel
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Silence unused variable warnings for result schemas (used for documentation)
|
|
908
|
+
void SwapExecuteResultSchema;
|
|
909
|
+
void SwapStatusResultSchema;
|
|
910
|
+
void SwapCancelResultSchema;
|
|
911
|
+
|
|
912
|
+
// Export schemas with consistent naming
|
|
913
|
+
export {
|
|
914
|
+
SwapQuoteRequestSchema as SwapQuoteSchema,
|
|
915
|
+
SwapExecuteParamsSchema as SwapExecuteSchema,
|
|
916
|
+
SwapStatusParamsSchema as SwapStatusSchema,
|
|
917
|
+
SwapCancelParamsSchema as SwapCancelSchema,
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
// Export handlers
|
|
921
|
+
export {
|
|
922
|
+
handleSwapQuote,
|
|
923
|
+
handleSwapExecute,
|
|
924
|
+
handleSwapStatus,
|
|
925
|
+
handleSwapCancel,
|
|
926
|
+
};
|