@agentlayer.tech/wallet 0.1.37 → 0.1.44
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/.claude-plugin/marketplace.json +17 -0
- package/.openclaw/extensions/agent-wallet/openclaw.plugin.json +1 -1
- package/.openclaw/extensions/agent-wallet/package.json +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +14 -0
- package/VERSION +1 -1
- package/agent-wallet/agent_wallet/__init__.py +1 -1
- package/agent-wallet/agent_wallet/evm_user_wallets.py +114 -6
- package/agent-wallet/agent_wallet/openclaw_adapter.py +331 -0
- package/agent-wallet/agent_wallet/openclaw_cli.py +22 -0
- package/agent-wallet/agent_wallet/providers/wdk_evm_local.py +2 -0
- package/agent-wallet/agent_wallet/telemetry.py +306 -0
- package/agent-wallet/agent_wallet/wallet_layer/base.py +32 -0
- package/agent-wallet/agent_wallet/wallet_layer/wdk_evm.py +156 -0
- package/agent-wallet/openclaw.plugin.json +1 -1
- package/agent-wallet/pyproject.toml +1 -1
- package/agent-wallet/scripts/bootstrap_openclaw_evm.py +20 -2
- package/claude-code/plugins/agent-wallet/.claude-plugin/plugin.json +1 -1
- package/claude-code/plugins/agent-wallet/README.md +30 -1
- package/claude-code/plugins/agent-wallet/commands/wallet-setup.md +24 -0
- package/claude-code/plugins/agent-wallet/hooks/hooks.json +16 -0
- package/claude-code/plugins/agent-wallet/scripts/bootstrap_backend.sh +136 -0
- package/claude-code/plugins/agent-wallet/scripts/run_mcp.sh +6 -1
- package/codex/plugins/agent-wallet/.codex-plugin/plugin.json +1 -1
- package/codex/plugins/agent-wallet/scripts/run_mcp.sh +5 -0
- package/hermes/plugins/agent_wallet/plugin.yaml +1 -1
- package/hermes/plugins/agent_wallet/tools.py +3 -0
- package/package.json +2 -1
- package/wdk-btc-wallet/package.json +1 -1
- package/wdk-evm-wallet/README.md +25 -0
- package/wdk-evm-wallet/package.json +4 -2
- package/wdk-evm-wallet/src/config.js +47 -0
- package/wdk-evm-wallet/src/server.js +25 -3
- package/wdk-evm-wallet/src/wdk_evm_wallet.js +545 -0
|
@@ -19,6 +19,13 @@ const LIFI_SOLANA_NATIVE_TOKEN_ADDRESS = "11111111111111111111111111111111";
|
|
|
19
19
|
const DEFAULT_SWAP_SLIPPAGE_BPS = 100;
|
|
20
20
|
const DEFAULT_LIFI_SLIPPAGE = 0.005;
|
|
21
21
|
const ALWAYS_DENIED_LIFI_BRIDGES = ["mayan"];
|
|
22
|
+
const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
|
|
23
|
+
const UNISWAP_SUPPORTED_CHAIN_IDS = { ethereum: 1, base: 8453 };
|
|
24
|
+
// Universal Router v2.0 allow-list (defense-in-depth: /swap response `to` must match).
|
|
25
|
+
const UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK = {
|
|
26
|
+
ethereum: "0x66a9893cc07d91d95644aedd05d03f95e1dba8af",
|
|
27
|
+
base: "0x6ff5693b99212da76ad316178a184ab56d299b43",
|
|
28
|
+
};
|
|
22
29
|
const AAVE_RAY = 10n ** 27n;
|
|
23
30
|
const LIDO_STETH_DECIMALS = 18;
|
|
24
31
|
const LIDO_MIN_STETH_WITHDRAWAL_AMOUNT = 100n;
|
|
@@ -296,6 +303,28 @@ function parseLifiSlippage(value, fallback = DEFAULT_LIFI_SLIPPAGE) {
|
|
|
296
303
|
return parsed;
|
|
297
304
|
}
|
|
298
305
|
|
|
306
|
+
function normalizeUniswapTokenAddress(value, fieldName) {
|
|
307
|
+
return normalizeEvmTokenAddressAllowingNative(value, fieldName);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function assertUniswapSupportedNetwork(network) {
|
|
311
|
+
const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[network];
|
|
312
|
+
if (!chainId) {
|
|
313
|
+
throw new Error(
|
|
314
|
+
"Uniswap Trading API swaps are currently supported only on ethereum and base mainnet."
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return chainId;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function uniswapSlippagePercentFromBps(bps) {
|
|
321
|
+
const parsed = Number(bps);
|
|
322
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 5000) {
|
|
323
|
+
throw new Error("slippageBps must be an integer between 0 and 5000.");
|
|
324
|
+
}
|
|
325
|
+
return parsed / 100;
|
|
326
|
+
}
|
|
327
|
+
|
|
299
328
|
function normalizeBridgeList(value, fieldName) {
|
|
300
329
|
if (value === undefined || value === null || value === "") {
|
|
301
330
|
return null;
|
|
@@ -393,6 +422,37 @@ function normalizeX402ExactTypedData({ domain, types, primaryType, message }, ru
|
|
|
393
422
|
};
|
|
394
423
|
}
|
|
395
424
|
|
|
425
|
+
function normalizeUniswapPermitData(permitData, runtimeConfig) {
|
|
426
|
+
const data = assertPlainObject(permitData, "permitData");
|
|
427
|
+
const domain = assertPlainObject(data.domain, "permitData.domain");
|
|
428
|
+
const domainChainId = assertNonNegativeInteger(domain.chainId, "permitData.domain.chainId");
|
|
429
|
+
if (domainChainId !== runtimeConfig.chainId) {
|
|
430
|
+
throw new Error("permitData.domain.chainId must match the active network chain id.");
|
|
431
|
+
}
|
|
432
|
+
const typesObject = assertPlainObject(data.types, "permitData.types");
|
|
433
|
+
const normalizedTypes = {};
|
|
434
|
+
for (const [typeName, fields] of Object.entries(typesObject)) {
|
|
435
|
+
if (typeName === "EIP712Domain") {
|
|
436
|
+
continue; // ethers infers the domain type; including it throws.
|
|
437
|
+
}
|
|
438
|
+
if (!Array.isArray(fields) || fields.length === 0) {
|
|
439
|
+
throw new Error(`permitData.types.${typeName} must be a non-empty array.`);
|
|
440
|
+
}
|
|
441
|
+
normalizedTypes[typeName] = fields.map((field, index) => {
|
|
442
|
+
const normalizedField = assertPlainObject(field, `permitData.types.${typeName}[${index}]`);
|
|
443
|
+
return {
|
|
444
|
+
name: assertNonEmptyString(normalizedField.name, `permitData.types.${typeName}[${index}].name`),
|
|
445
|
+
type: assertNonEmptyString(normalizedField.type, `permitData.types.${typeName}[${index}].type`),
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
if (Object.keys(normalizedTypes).length === 0) {
|
|
450
|
+
throw new Error("permitData.types must contain at least one non-domain type.");
|
|
451
|
+
}
|
|
452
|
+
const message = assertPlainObject(data.values, "permitData.values");
|
|
453
|
+
return { domain, types: normalizedTypes, message };
|
|
454
|
+
}
|
|
455
|
+
|
|
396
456
|
function buildSwapRequest({ tokenIn, tokenOut, tokenInAmount }) {
|
|
397
457
|
const swapRequest = {
|
|
398
458
|
tokenIn: normalizeVeloraTokenAddress(tokenIn, "tokenIn"),
|
|
@@ -403,6 +463,17 @@ function buildSwapRequest({ tokenIn, tokenOut, tokenInAmount }) {
|
|
|
403
463
|
return swapRequest;
|
|
404
464
|
}
|
|
405
465
|
|
|
466
|
+
function buildUniswapSwapRequest({ tokenIn, tokenOut, tokenInAmount, slippageBps }) {
|
|
467
|
+
const swapRequest = {
|
|
468
|
+
tokenIn: normalizeUniswapTokenAddress(tokenIn, "tokenIn"),
|
|
469
|
+
tokenOut: normalizeUniswapTokenAddress(tokenOut, "tokenOut"),
|
|
470
|
+
tokenInAmount: assertPositiveBigIntString(tokenInAmount, "tokenInAmount"),
|
|
471
|
+
slippagePercent: uniswapSlippagePercentFromBps(slippageBps),
|
|
472
|
+
};
|
|
473
|
+
assertDistinctAddresses(swapRequest.tokenIn, "tokenIn", swapRequest.tokenOut, "tokenOut");
|
|
474
|
+
return swapRequest;
|
|
475
|
+
}
|
|
476
|
+
|
|
406
477
|
function buildLifiEvmSwapRequest({
|
|
407
478
|
tokenIn,
|
|
408
479
|
destinationChain,
|
|
@@ -2261,6 +2332,254 @@ export class WdkEvmWalletService {
|
|
|
2261
2332
|
});
|
|
2262
2333
|
}
|
|
2263
2334
|
|
|
2335
|
+
async quoteUniswapSwap({
|
|
2336
|
+
seedPhrase,
|
|
2337
|
+
address,
|
|
2338
|
+
tokenIn,
|
|
2339
|
+
tokenOut,
|
|
2340
|
+
tokenInAmount,
|
|
2341
|
+
slippageBps,
|
|
2342
|
+
accountIndex = 0,
|
|
2343
|
+
network,
|
|
2344
|
+
}) {
|
|
2345
|
+
return this.#withReadableAccount(
|
|
2346
|
+
{ seedPhrase, address, accountIndex, network },
|
|
2347
|
+
async (account, runtimeConfig) => {
|
|
2348
|
+
assertUniswapSupportedNetwork(runtimeConfig.network);
|
|
2349
|
+
const swapRequest = buildUniswapSwapRequest({
|
|
2350
|
+
tokenIn,
|
|
2351
|
+
tokenOut,
|
|
2352
|
+
tokenInAmount,
|
|
2353
|
+
slippageBps:
|
|
2354
|
+
slippageBps === undefined || slippageBps === null
|
|
2355
|
+
? this.config.uniswapDefaultSlippageBps
|
|
2356
|
+
: slippageBps,
|
|
2357
|
+
});
|
|
2358
|
+
const swapperAddress = await account.getAddress();
|
|
2359
|
+
const plan = await this.#buildUniswapSwapPlan({
|
|
2360
|
+
account,
|
|
2361
|
+
runtimeConfig,
|
|
2362
|
+
address: swapperAddress,
|
|
2363
|
+
swapRequest,
|
|
2364
|
+
});
|
|
2365
|
+
return this.#formatUniswapSwapResponse({
|
|
2366
|
+
runtimeConfig,
|
|
2367
|
+
accountIndex,
|
|
2368
|
+
address: swapperAddress,
|
|
2369
|
+
swapRequest,
|
|
2370
|
+
plan,
|
|
2371
|
+
});
|
|
2372
|
+
}
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
async sendUniswapSwap({
|
|
2377
|
+
seedPhrase,
|
|
2378
|
+
tokenIn,
|
|
2379
|
+
tokenOut,
|
|
2380
|
+
tokenInAmount,
|
|
2381
|
+
slippageBps,
|
|
2382
|
+
accountIndex = 0,
|
|
2383
|
+
network,
|
|
2384
|
+
expectedQuoteFingerprint = null,
|
|
2385
|
+
minimumTokenOutAmount = null,
|
|
2386
|
+
}) {
|
|
2387
|
+
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2388
|
+
assertUniswapSupportedNetwork(runtimeConfig.network);
|
|
2389
|
+
const swapRequest = buildUniswapSwapRequest({
|
|
2390
|
+
tokenIn,
|
|
2391
|
+
tokenOut,
|
|
2392
|
+
tokenInAmount,
|
|
2393
|
+
slippageBps:
|
|
2394
|
+
slippageBps === undefined || slippageBps === null
|
|
2395
|
+
? this.config.uniswapDefaultSlippageBps
|
|
2396
|
+
: slippageBps,
|
|
2397
|
+
});
|
|
2398
|
+
const normalizedExpectedQuoteFingerprint =
|
|
2399
|
+
typeof expectedQuoteFingerprint === "string" && expectedQuoteFingerprint.trim()
|
|
2400
|
+
? expectedQuoteFingerprint.trim()
|
|
2401
|
+
: null;
|
|
2402
|
+
const requestedMinimumTokenOutAmount =
|
|
2403
|
+
minimumTokenOutAmount !== null && minimumTokenOutAmount !== undefined
|
|
2404
|
+
? assertPositiveBigIntString(minimumTokenOutAmount, "minimumTokenOutAmount")
|
|
2405
|
+
: null;
|
|
2406
|
+
const address = await account.getAddress();
|
|
2407
|
+
let initialPlan = await this.#buildUniswapSwapPlan({
|
|
2408
|
+
account,
|
|
2409
|
+
runtimeConfig,
|
|
2410
|
+
address,
|
|
2411
|
+
swapRequest,
|
|
2412
|
+
});
|
|
2413
|
+
this.#assertExpectedSwapFingerprint(
|
|
2414
|
+
normalizedExpectedQuoteFingerprint,
|
|
2415
|
+
initialPlan.quoteFingerprint
|
|
2416
|
+
);
|
|
2417
|
+
this.#assertMinimumSwapOutput(
|
|
2418
|
+
requestedMinimumTokenOutAmount,
|
|
2419
|
+
initialPlan.minimumTokenOutAmount,
|
|
2420
|
+
initialPlan.tokenOutAmount
|
|
2421
|
+
);
|
|
2422
|
+
|
|
2423
|
+
const approvalExecution = await this.#executeSwapApprovalsIfNeeded({
|
|
2424
|
+
account,
|
|
2425
|
+
runtimeConfig,
|
|
2426
|
+
swapRequest: { tokenIn: swapRequest.tokenIn },
|
|
2427
|
+
plan: initialPlan,
|
|
2428
|
+
});
|
|
2429
|
+
|
|
2430
|
+
let finalPlan = initialPlan;
|
|
2431
|
+
try {
|
|
2432
|
+
if (approvalExecution.performed) {
|
|
2433
|
+
// Re-fetch after approval to obtain fresh permitData (the deadline/nonce are short-lived).
|
|
2434
|
+
finalPlan = await this.#buildUniswapSwapPlan({
|
|
2435
|
+
account,
|
|
2436
|
+
runtimeConfig,
|
|
2437
|
+
address,
|
|
2438
|
+
swapRequest,
|
|
2439
|
+
});
|
|
2440
|
+
this.#assertExpectedSwapFingerprint(
|
|
2441
|
+
normalizedExpectedQuoteFingerprint,
|
|
2442
|
+
finalPlan.quoteFingerprint
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
this.#assertMinimumSwapOutput(
|
|
2446
|
+
requestedMinimumTokenOutAmount,
|
|
2447
|
+
finalPlan.minimumTokenOutAmount,
|
|
2448
|
+
finalPlan.tokenOutAmount
|
|
2449
|
+
);
|
|
2450
|
+
|
|
2451
|
+
const allowanceReadUncertain =
|
|
2452
|
+
approvalExecution.performed && finalPlan.allowanceReadError !== null;
|
|
2453
|
+
if (finalPlan.approval.required && !allowanceReadUncertain) {
|
|
2454
|
+
throw createTaggedError(
|
|
2455
|
+
"Uniswap swap still requires token approval after the approval step completed.",
|
|
2456
|
+
"swap_approval_required",
|
|
2457
|
+
{
|
|
2458
|
+
spender: finalPlan.spender,
|
|
2459
|
+
requiredAllowance: finalPlan.tokenInAmount.toString(),
|
|
2460
|
+
currentAllowance: finalPlan.currentAllowance.toString(),
|
|
2461
|
+
}
|
|
2462
|
+
);
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
const signature =
|
|
2466
|
+
finalPlan.isNativeTokenIn || !finalPlan.permitData
|
|
2467
|
+
? null
|
|
2468
|
+
: await this.#signUniswapPermit(account, finalPlan.permitData, runtimeConfig);
|
|
2469
|
+
|
|
2470
|
+
const swapTx = await this.#fetchUniswapSwapCalldata({
|
|
2471
|
+
runtimeConfig,
|
|
2472
|
+
quoteResponse: finalPlan.quoteResponse,
|
|
2473
|
+
permitData: finalPlan.permitData,
|
|
2474
|
+
signature,
|
|
2475
|
+
});
|
|
2476
|
+
|
|
2477
|
+
const simulation = await this.#simulatePreparedTransaction({
|
|
2478
|
+
runtimeConfig,
|
|
2479
|
+
from: address,
|
|
2480
|
+
tx: swapTx,
|
|
2481
|
+
});
|
|
2482
|
+
this.#assertSimulationSucceeded(simulation);
|
|
2483
|
+
|
|
2484
|
+
const { hash } = await account.sendTransaction(swapTx);
|
|
2485
|
+
const result = {
|
|
2486
|
+
hash,
|
|
2487
|
+
approvalFee: approvalExecution.totalFee.toString(),
|
|
2488
|
+
tokenInAmount: finalPlan.tokenInAmount.toString(),
|
|
2489
|
+
tokenOutAmount: finalPlan.tokenOutAmount.toString(),
|
|
2490
|
+
...(approvalExecution.approveHash ? { approveHash: approvalExecution.approveHash } : {}),
|
|
2491
|
+
...(approvalExecution.resetAllowanceHash
|
|
2492
|
+
? { resetAllowanceHash: approvalExecution.resetAllowanceHash }
|
|
2493
|
+
: {}),
|
|
2494
|
+
};
|
|
2495
|
+
return {
|
|
2496
|
+
...(await this.#formatUniswapSwapResponse({
|
|
2497
|
+
runtimeConfig,
|
|
2498
|
+
accountIndex,
|
|
2499
|
+
address,
|
|
2500
|
+
swapRequest,
|
|
2501
|
+
plan: {
|
|
2502
|
+
...finalPlan,
|
|
2503
|
+
approval: {
|
|
2504
|
+
...finalPlan.approval,
|
|
2505
|
+
estimatedFee: approvalExecution.totalFee,
|
|
2506
|
+
},
|
|
2507
|
+
},
|
|
2508
|
+
})),
|
|
2509
|
+
simulation,
|
|
2510
|
+
swapTransaction: { to: swapTx.to, value: swapTx.value.toString(), dataHash: sha256Hex(swapTx.data) },
|
|
2511
|
+
result,
|
|
2512
|
+
};
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
const cleanup = await this.#restoreAllowanceAfterFailedSwap({
|
|
2515
|
+
account,
|
|
2516
|
+
runtimeConfig,
|
|
2517
|
+
tokenAddress: swapRequest.tokenIn,
|
|
2518
|
+
spender: initialPlan.spender,
|
|
2519
|
+
originalAllowance: initialPlan.currentAllowance,
|
|
2520
|
+
approvalExecution,
|
|
2521
|
+
});
|
|
2522
|
+
this.#throwSwapFailureWithCleanup(error, cleanup);
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
async #getUniswapTokenMetadata(runtimeConfig, tokenAddress) {
|
|
2528
|
+
if (isZeroAddress(tokenAddress)) {
|
|
2529
|
+
return {
|
|
2530
|
+
address: ZERO_ADDRESS,
|
|
2531
|
+
name: runtimeConfig.nativeSymbol === "ETH" ? "Ether" : runtimeConfig.nativeSymbol,
|
|
2532
|
+
symbol: runtimeConfig.nativeSymbol,
|
|
2533
|
+
decimals: 18,
|
|
2534
|
+
verified: true,
|
|
2535
|
+
source: "native-asset",
|
|
2536
|
+
};
|
|
2537
|
+
}
|
|
2538
|
+
return this.#getTokenMetadata(runtimeConfig, tokenAddress);
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
async #formatUniswapSwapResponse({ runtimeConfig, accountIndex, address, swapRequest, plan }) {
|
|
2542
|
+
const [tokenInMetadata, tokenOutMetadata] = await Promise.all([
|
|
2543
|
+
this.#getUniswapTokenMetadata(runtimeConfig, swapRequest.tokenIn),
|
|
2544
|
+
this.#getUniswapTokenMetadata(runtimeConfig, swapRequest.tokenOut),
|
|
2545
|
+
]);
|
|
2546
|
+
return {
|
|
2547
|
+
network: runtimeConfig.network,
|
|
2548
|
+
chainId: runtimeConfig.chainId,
|
|
2549
|
+
accountIndex,
|
|
2550
|
+
address,
|
|
2551
|
+
protocol: "uniswap",
|
|
2552
|
+
executionSupported: true,
|
|
2553
|
+
routing: "CLASSIC",
|
|
2554
|
+
swapRequest: {
|
|
2555
|
+
tokenIn: swapRequest.tokenIn,
|
|
2556
|
+
tokenOut: swapRequest.tokenOut,
|
|
2557
|
+
tokenInAmount: swapRequest.tokenInAmount.toString(),
|
|
2558
|
+
},
|
|
2559
|
+
tokenInMetadata,
|
|
2560
|
+
tokenOutMetadata,
|
|
2561
|
+
inputAmountFormatted: formatUnits(swapRequest.tokenInAmount, tokenInMetadata.decimals),
|
|
2562
|
+
outputAmountFormatted: formatUnits(plan.tokenOutAmount, tokenOutMetadata.decimals),
|
|
2563
|
+
quoteFingerprint: plan.quoteFingerprint,
|
|
2564
|
+
slippageBps: plan.slippageBps,
|
|
2565
|
+
minimumOutputAmountRaw: plan.minimumTokenOutAmount.toString(),
|
|
2566
|
+
permitRequired: plan.permitData !== null,
|
|
2567
|
+
gasFeeWei: plan.gasFee,
|
|
2568
|
+
gasFeeUSD: plan.gasFeeUSD,
|
|
2569
|
+
estimatedApprovalFeeWei: plan.approval.estimatedFee.toString(),
|
|
2570
|
+
allowance: {
|
|
2571
|
+
spender: plan.spender,
|
|
2572
|
+
currentAllowance: plan.currentAllowance.toString(),
|
|
2573
|
+
requiredAllowance: plan.tokenInAmount.toString(),
|
|
2574
|
+
approvalRequired: plan.approval.required,
|
|
2575
|
+
approvalSequence: plan.approval.steps,
|
|
2576
|
+
readError: plan.allowanceReadError,
|
|
2577
|
+
},
|
|
2578
|
+
router: plan.router,
|
|
2579
|
+
source: "uniswap-trading-api",
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2264
2583
|
async quoteNativeTransfer({ seedPhrase, to, value, accountIndex = 0, network }) {
|
|
2265
2584
|
return this.#withAccount({ seedPhrase, accountIndex, network }, async (account, runtimeConfig) => {
|
|
2266
2585
|
const tx = {
|
|
@@ -2940,6 +3259,223 @@ export class WdkEvmWalletService {
|
|
|
2940
3259
|
};
|
|
2941
3260
|
}
|
|
2942
3261
|
|
|
3262
|
+
async #uniswapTradingApiRequest(pathname, body) {
|
|
3263
|
+
const headers = {
|
|
3264
|
+
"Content-Type": "application/json",
|
|
3265
|
+
Accept: "application/json",
|
|
3266
|
+
"x-universal-router-version": this.config.uniswapRouterVersion,
|
|
3267
|
+
};
|
|
3268
|
+
if (this.config.uniswapViaGateway) {
|
|
3269
|
+
// The provider gateway holds the Uniswap key and injects x-api-key upstream;
|
|
3270
|
+
// we authenticate to it with the shared gateway bearer (same token used for
|
|
3271
|
+
// EVM RPC routing), so no Uniswap key needs to live on this machine.
|
|
3272
|
+
const token = String(this.config.providerGatewayToken || "").trim();
|
|
3273
|
+
if (token) {
|
|
3274
|
+
headers.Authorization = `Bearer ${token}`;
|
|
3275
|
+
}
|
|
3276
|
+
} else {
|
|
3277
|
+
if (!this.config.uniswapApiKey) {
|
|
3278
|
+
throw createTaggedError(
|
|
3279
|
+
"UNISWAP_API_KEY is not configured. Set it, or route Uniswap through the provider gateway, to use Uniswap Trading API swaps.",
|
|
3280
|
+
"uniswap_api_key_missing",
|
|
3281
|
+
{ provider: "uniswap" }
|
|
3282
|
+
);
|
|
3283
|
+
}
|
|
3284
|
+
headers["x-api-key"] = this.config.uniswapApiKey;
|
|
3285
|
+
}
|
|
3286
|
+
const base = String(this.config.uniswapTradingApiBaseUrl).replace(/\/+$/, "");
|
|
3287
|
+
let response;
|
|
3288
|
+
try {
|
|
3289
|
+
response = await fetch(`${base}${pathname}`, {
|
|
3290
|
+
method: "POST",
|
|
3291
|
+
headers,
|
|
3292
|
+
body: JSON.stringify(body),
|
|
3293
|
+
});
|
|
3294
|
+
} catch (error) {
|
|
3295
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3296
|
+
throw createTaggedError(`Uniswap Trading API unavailable: ${message}`, "network_unavailable", {
|
|
3297
|
+
provider: "uniswap",
|
|
3298
|
+
pathname,
|
|
3299
|
+
});
|
|
3300
|
+
}
|
|
3301
|
+
let payload;
|
|
3302
|
+
try {
|
|
3303
|
+
payload = await response.json();
|
|
3304
|
+
} catch {
|
|
3305
|
+
payload = null;
|
|
3306
|
+
}
|
|
3307
|
+
if (!response.ok) {
|
|
3308
|
+
const message =
|
|
3309
|
+
payload?.detail ||
|
|
3310
|
+
payload?.message ||
|
|
3311
|
+
payload?.error ||
|
|
3312
|
+
`Uniswap Trading API ${pathname} failed with HTTP ${response.status}.`;
|
|
3313
|
+
throw createTaggedError(String(message), "network_unavailable", {
|
|
3314
|
+
provider: "uniswap",
|
|
3315
|
+
pathname,
|
|
3316
|
+
httpStatus: response.status,
|
|
3317
|
+
});
|
|
3318
|
+
}
|
|
3319
|
+
if (!payload || typeof payload !== "object") {
|
|
3320
|
+
throw createTaggedError(
|
|
3321
|
+
`Uniswap Trading API ${pathname} returned an empty response.`,
|
|
3322
|
+
"network_unavailable",
|
|
3323
|
+
{ provider: "uniswap", pathname }
|
|
3324
|
+
);
|
|
3325
|
+
}
|
|
3326
|
+
return payload;
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
async #fetchUniswapQuote({ runtimeConfig, address, swapRequest }) {
|
|
3330
|
+
const chainId = UNISWAP_SUPPORTED_CHAIN_IDS[runtimeConfig.network];
|
|
3331
|
+
const payload = await this.#uniswapTradingApiRequest("/quote", {
|
|
3332
|
+
swapper: address,
|
|
3333
|
+
tokenIn: swapRequest.tokenIn,
|
|
3334
|
+
tokenOut: swapRequest.tokenOut,
|
|
3335
|
+
tokenInChainId: chainId,
|
|
3336
|
+
tokenOutChainId: chainId,
|
|
3337
|
+
amount: swapRequest.tokenInAmount.toString(),
|
|
3338
|
+
type: "EXACT_INPUT",
|
|
3339
|
+
slippageTolerance: swapRequest.slippagePercent,
|
|
3340
|
+
// The live Trading API rejects routingPreference:"CLASSIC"; restricting
|
|
3341
|
+
// protocols to V2/V3/V4 is what excludes UniswapX and yields routing=CLASSIC.
|
|
3342
|
+
protocols: ["V2", "V3", "V4"],
|
|
3343
|
+
});
|
|
3344
|
+
const routing = String(payload.routing || "").toUpperCase();
|
|
3345
|
+
if (routing !== "CLASSIC") {
|
|
3346
|
+
throw createTaggedError(
|
|
3347
|
+
`Uniswap returned unsupported routing '${routing}'. Only CLASSIC is enabled in this runtime.`,
|
|
3348
|
+
"uniswap_unsupported_route",
|
|
3349
|
+
{ provider: "uniswap", routing }
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
if (!payload.quote || typeof payload.quote !== "object" || !payload.quote.output) {
|
|
3353
|
+
throw createTaggedError(
|
|
3354
|
+
"Uniswap quote response is missing CLASSIC quote/output fields.",
|
|
3355
|
+
"network_unavailable",
|
|
3356
|
+
{ provider: "uniswap" }
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
return payload;
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
async #buildUniswapSwapPlan({ account, runtimeConfig, address, swapRequest }) {
|
|
3363
|
+
const quoteResponse = await this.#fetchUniswapQuote({ runtimeConfig, address, swapRequest });
|
|
3364
|
+
const permitData = quoteResponse.permitData ?? null;
|
|
3365
|
+
const isNativeTokenIn = isZeroAddress(swapRequest.tokenIn);
|
|
3366
|
+
const spender = isNativeTokenIn ? null : PERMIT2_ADDRESS;
|
|
3367
|
+
const allowanceState = isNativeTokenIn
|
|
3368
|
+
? { currentAllowance: swapRequest.tokenInAmount, error: null }
|
|
3369
|
+
: await this.#getSwapAllowanceState({
|
|
3370
|
+
account,
|
|
3371
|
+
tokenAddress: swapRequest.tokenIn,
|
|
3372
|
+
spender,
|
|
3373
|
+
});
|
|
3374
|
+
const currentAllowance = allowanceState.currentAllowance;
|
|
3375
|
+
const approval = isNativeTokenIn
|
|
3376
|
+
? { required: false, estimatedFee: 0n, steps: [] }
|
|
3377
|
+
: await this.#buildSwapApprovalPlan({
|
|
3378
|
+
account,
|
|
3379
|
+
runtimeConfig,
|
|
3380
|
+
tokenAddress: swapRequest.tokenIn,
|
|
3381
|
+
spender,
|
|
3382
|
+
requiredAmount: swapRequest.tokenInAmount,
|
|
3383
|
+
currentAllowance,
|
|
3384
|
+
});
|
|
3385
|
+
const tokenOutAmount = BigInt(String(quoteResponse.quote.output.amount || "0"));
|
|
3386
|
+
const slippageBps = Math.round(swapRequest.slippagePercent * 100);
|
|
3387
|
+
// The Trading API returns the post-slippage floor directly; fall back to a
|
|
3388
|
+
// local computation only if the field is absent.
|
|
3389
|
+
const minimumTokenOutAmount =
|
|
3390
|
+
quoteResponse.quote.output.minimumAmount !== undefined &&
|
|
3391
|
+
quoteResponse.quote.output.minimumAmount !== null
|
|
3392
|
+
? BigInt(String(quoteResponse.quote.output.minimumAmount))
|
|
3393
|
+
: tokenOutAmount - (tokenOutAmount * BigInt(slippageBps)) / 10000n;
|
|
3394
|
+
const router = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
|
|
3395
|
+
// Bind only the stable swap *intent* (who/what/how-much/slippage/route), never
|
|
3396
|
+
// the live quoted output: the Trading API re-prices every block, so including
|
|
3397
|
+
// tokenOutAmount here made execute's re-quote fingerprint differ from preview's
|
|
3398
|
+
// and spuriously fail with "swap_quote_changed". Adverse price movement is
|
|
3399
|
+
// caught separately and tolerantly by the minimumTokenOutAmount check, matching
|
|
3400
|
+
// the velora swap contract (#buildVeloraSwapPlan).
|
|
3401
|
+
const quoteFingerprint = sha256Hex(
|
|
3402
|
+
JSON.stringify({
|
|
3403
|
+
chainId: runtimeConfig.chainId,
|
|
3404
|
+
network: runtimeConfig.network,
|
|
3405
|
+
from: address.toLowerCase(),
|
|
3406
|
+
router: router.toLowerCase(),
|
|
3407
|
+
spender: spender ? spender.toLowerCase() : null,
|
|
3408
|
+
tokenIn: swapRequest.tokenIn.toLowerCase(),
|
|
3409
|
+
tokenOut: swapRequest.tokenOut.toLowerCase(),
|
|
3410
|
+
tokenInAmount: swapRequest.tokenInAmount.toString(),
|
|
3411
|
+
slippageBps,
|
|
3412
|
+
routing: "CLASSIC",
|
|
3413
|
+
})
|
|
3414
|
+
);
|
|
3415
|
+
return {
|
|
3416
|
+
quoteResponse,
|
|
3417
|
+
permitData,
|
|
3418
|
+
isNativeTokenIn,
|
|
3419
|
+
spender,
|
|
3420
|
+
currentAllowance,
|
|
3421
|
+
allowanceReadError: allowanceState.error,
|
|
3422
|
+
approval,
|
|
3423
|
+
tokenInAmount: swapRequest.tokenInAmount,
|
|
3424
|
+
tokenOutAmount,
|
|
3425
|
+
minimumTokenOutAmount,
|
|
3426
|
+
slippageBps,
|
|
3427
|
+
quoteFingerprint,
|
|
3428
|
+
gasFee: quoteResponse.quote.gasFee ?? null,
|
|
3429
|
+
gasFeeUSD: quoteResponse.quote.gasFeeUSD ?? null,
|
|
3430
|
+
router,
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
async #signUniswapPermit(account, permitData, runtimeConfig) {
|
|
3435
|
+
const typed = normalizeUniswapPermitData(permitData, runtimeConfig);
|
|
3436
|
+
return account.signTypedData({
|
|
3437
|
+
domain: typed.domain,
|
|
3438
|
+
types: typed.types,
|
|
3439
|
+
message: typed.message,
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
async #fetchUniswapSwapCalldata({ runtimeConfig, quoteResponse, permitData, signature }) {
|
|
3444
|
+
const { permitData: _permitData, permitTransaction: _permitTransaction, ...cleanQuote } =
|
|
3445
|
+
quoteResponse;
|
|
3446
|
+
const body = { ...cleanQuote };
|
|
3447
|
+
// CLASSIC routing: the Universal Router needs permitData on-chain to verify the
|
|
3448
|
+
// Permit2 authorization, so signature and permitData are submitted together.
|
|
3449
|
+
if (signature && permitData) {
|
|
3450
|
+
body.signature = signature;
|
|
3451
|
+
body.permitData = permitData;
|
|
3452
|
+
}
|
|
3453
|
+
const payload = await this.#uniswapTradingApiRequest("/swap", body);
|
|
3454
|
+
const swap = payload.swap || {};
|
|
3455
|
+
const to = normalizeAddress(String(swap.to || ""), "swap.to");
|
|
3456
|
+
const expectedRouter = UNISWAP_UNIVERSAL_ROUTER_BY_NETWORK[runtimeConfig.network];
|
|
3457
|
+
if (to.toLowerCase() !== expectedRouter) {
|
|
3458
|
+
throw createTaggedError(
|
|
3459
|
+
"Uniswap /swap returned an unexpected target contract.",
|
|
3460
|
+
"uniswap_unexpected_router",
|
|
3461
|
+
{ provider: "uniswap", to: to.toLowerCase(), expected: expectedRouter }
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
const data = assertNonEmptyString(String(swap.data || ""), "swap.data");
|
|
3465
|
+
if (data === "0x") {
|
|
3466
|
+
throw createTaggedError(
|
|
3467
|
+
"Uniswap /swap returned empty calldata. The quote likely expired; generate a new preview.",
|
|
3468
|
+
"swap_quote_changed",
|
|
3469
|
+
{ provider: "uniswap" }
|
|
3470
|
+
);
|
|
3471
|
+
}
|
|
3472
|
+
return {
|
|
3473
|
+
to,
|
|
3474
|
+
data,
|
|
3475
|
+
value: parseHexOrDecimalBigInt(swap.value || "0", "swap.value"),
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
|
|
2943
3479
|
#assertExpectedSwapFingerprint(expectedQuoteFingerprint, actualQuoteFingerprint) {
|
|
2944
3480
|
if (!expectedQuoteFingerprint) {
|
|
2945
3481
|
return;
|
|
@@ -5102,3 +5638,12 @@ export class WdkEvmWalletService {
|
|
|
5102
5638
|
);
|
|
5103
5639
|
}
|
|
5104
5640
|
}
|
|
5641
|
+
|
|
5642
|
+
export const __testables = {
|
|
5643
|
+
PERMIT2_ADDRESS,
|
|
5644
|
+
UNISWAP_SUPPORTED_CHAIN_IDS,
|
|
5645
|
+
normalizeUniswapTokenAddress,
|
|
5646
|
+
assertUniswapSupportedNetwork,
|
|
5647
|
+
uniswapSlippagePercentFromBps,
|
|
5648
|
+
normalizeUniswapPermitData,
|
|
5649
|
+
};
|