From 0e05591809671d18b3387ee0376a12ef330aae34 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 20:22:34 +0200 Subject: [PATCH 01/10] feat: replace Relay with SushiSwap V3 for Gnosis-native token swaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add SushiSwapStampsRouter contract that swaps any Gnosis token → BZZ via SushiSwap V3 and atomically creates/tops up a Swarm stamp batch in a single transaction. Eliminates Relay dependency for same-chain swaps. - contracts/SushiSwapStampsRouter.sol: new router with single-hop, multi-hop (callback chaining), native xDAI support, and quote functions callable via eth_call - deploy/02_deploy_sushi_stamps_router.ts: deploy + auto-verify script (GnosisScan + Sourcify v2) - scripts/verify_router.ts, verify_registry.ts: standalone post-deploy verification scripts for both contracts - SushiQuotes.ts: frontend module for route discovery, quoting, and execution via the new router - constants.ts: add SUSHI_STAMPS_ROUTER_ADDRESS, SUSHI_FACTORY_ABI, SUSHI_STAMPS_ROUTER_ABI, GNOSIS_USDC_ADDRESS - SwapComponent.tsx: add Branch 2 (Gnosis + non-BZZ → SushiSwap); cross-chain traffic stays on Relay unchanged - hardhat.config.ts: enable viaIR and Sourcify v2 verification --- contracts/README.md | 135 ++++-- contracts/SushiSwapStampsRouter.sol | 585 +++++++++++++++++++++++ deploy/02_deploy_sushi_stamps_router.ts | 123 +++++ hardhat.config.ts | 7 + scripts/verify_registry.ts | 85 ++++ scripts/verify_router.ts | 114 +++++ src/app/components/SushiQuotes.ts | 598 ++++++++++++++++++++++++ src/app/components/SwapComponent.tsx | 204 ++++++-- src/app/components/constants.ts | 145 +++++- 9 files changed, 1918 insertions(+), 78 deletions(-) create mode 100644 contracts/SushiSwapStampsRouter.sol create mode 100644 deploy/02_deploy_sushi_stamps_router.ts create mode 100644 scripts/verify_registry.ts create mode 100644 scripts/verify_router.ts create mode 100644 src/app/components/SushiQuotes.ts diff --git a/contracts/README.md b/contracts/README.md index 4a485b5..4eae3d8 100644 --- a/contracts/README.md +++ b/contracts/README.md @@ -1,64 +1,145 @@ -# StampsRegistry Smart Contract +# Swarm Stamps Smart Contracts -This directory contains the StampsRegistry smart contract, which provides a registry for Swarm Postage Stamps. +This directory contains two smart contracts for purchasing Swarm Postage Stamps on Gnosis chain. -## Overview +--- -The StampsRegistry contract allows users to create and manage batches of stamps for the Swarm network. It serves as a registry that tracks ownership and provides methods for retrieving batch information. +## Contracts -## Deployment with Hardhat +### StampsRegistry + +A registry for Swarm Postage Stamps that wraps the core Swarm postage contract. Users interact with this contract to create or top up stamp batches using BZZ tokens they already hold. + +### SushiSwapStampsRouter + +Swaps **any Gnosis-chain token → BZZ** via SushiSwap V3 and atomically creates or tops up a Swarm stamp batch in a single transaction. Eliminates the need for Relay for same-chain swaps. + +**Features:** +- Single-hop swaps (e.g. USDC → BZZ) +- Multi-hop swaps (e.g. xDAI → WXDAI → USDC → BZZ) +- Native xDAI support (auto-wraps to WXDAI) +- Quote functions callable via `eth_call` (zero gas) +- Compatible with SushiSwap V3 (Uniswap V3 interface) + +**Gnosis addresses used by the router:** + +| Contract | Address | +|----------|---------| +| BZZ | `0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da` | +| WXDAI | `0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d` | +| SushiSwap V3 Factory | `0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef` | +| SushiSwap V3 QuoterV2 | `0xb1e835dc2785b52265711e17fccb0fd018226a6e` | +| BZZ/USDC pool | `0x6f30b7cf40cb423c1d23478a9855701ecf43931e` | + +--- + +## Deployment ### Prerequisites 1. Node.js and npm installed -2. Hardhat and required dependencies installed: +2. Hardhat dependencies: ```bash npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox @nomicfoundation/hardhat-verify @nomicfoundation/hardhat-ethers hardhat-deploy ethers@^6.0.0 dotenv ``` ### Environment Variables -Create a `.env.local` file in the project root with the following variables: +Create a `.env` file in the project root: ``` -# Hardhat Deployment -DEPLOYER_PRIVATE_KEY=your_private_key_here -GNOSIS_RPC_URL=https://gnosis-rpc.publicnode.com -GNOSIS_API_KEY=your_gnosisscan_api_key_here +# Deployer +WALLET_SECRET=your_private_key_here +GNOSIS_RPC_URL=https://rpc.gnosischain.com +MAINNET_ETHERSCAN_KEY=your_gnosisscan_api_key_here + +# StampsRegistry deployment SWARM_CONTRACT_ADDRESS=0x45a1502382541Cd610CC9068e88727426b696293 -``` -### Deployment Commands +# SushiSwapStampsRouter deployment (uses existing registry) +GNOSIS_STAMPS_REGISTRY=0x5EBfBeFB1E88391eFb022d5d33302f50a46bF4f3 +``` -To deploy to Gnosis Chain: +### Deploy StampsRegistry ```bash npx hardhat deploy --network gnosis --tags StampsRegistry ``` -The deployment script will: +### Deploy SushiSwapStampsRouter + +```bash +npx hardhat deploy --network gnosis --tags SushiSwapStampsRouter +``` -1. Deploy the StampsRegistry contract -2. Automatically verify the contract on GnosisScan (if API key is provided) +After deployment, add the router address to your `.env.local`: +``` +NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS= +``` ### Verification -If the automatic verification fails, you can manually verify the contract: +Both deploy scripts automatically attempt verification after deployment using two methods: + +| Method | Tool | API key needed | +|--------|------|---------------| +| **GnosisScan** (Etherscan-compatible) | `verify:verify` task | Yes (`MAINNET_ETHERSCAN_KEY`) | +| **Sourcify** (v2, Blockscout native) | `sourcify` task | No | + +#### Standalone verification scripts (run any time after deployment) ```bash -npx hardhat verify --network gnosis DEPLOYED_CONTRACT_ADDRESS SWARM_CONTRACT_ADDRESS +# Verify SushiSwapStampsRouter (uses address from deployments/ cache) +npx hardhat run scripts/verify_router.ts --network gnosis + +# Override address explicitly +ROUTER_ADDRESS=0x... npx hardhat run scripts/verify_router.ts --network gnosis + +# Verify StampsRegistry +npx hardhat run scripts/verify_registry.ts --network gnosis +REGISTRY_ADDRESS=0x... npx hardhat run scripts/verify_registry.ts --network gnosis ``` -## Contract Interaction +#### Manual one-liners + +```bash +# GnosisScan (Etherscan API) +npx hardhat verify --network gnosis
+ +# Sourcify (v2 – no API key) +npx hardhat sourcify --network gnosis --address
+``` -Once deployed, you can interact with the contract using the following functions: +Sourcify mirrors are picked up by GnosisScan/Blockscout automatically, so verifying on Sourcify alone is sufficient if you don't have a GnosisScan API key. -1. `createBatchRegistry`: Create a new batch of stamps -2. `getOwnerBatches`: Get all batches for a specific owner -3. `getOwnerBatchCount`: Get the count of batches for a specific owner -4. `getBatchPayer`: Get the payer address for a specific batch ID -5. `updateSwarmContract`: Update the Swarm contract address (admin only) +--- + +## Path Encoding + +The `SushiSwapStampsRouter` uses Uniswap V3-compatible **exact-output path encoding** (reversed token order): + +``` +single-hop: abi.encodePacked(BZZ, uint24(fee), tokenIn) // 43 bytes +two-hop: abi.encodePacked(BZZ, uint24(fee2), mid, uint24(fee1), tokenIn) // 66 bytes +``` + +In TypeScript (using viem): +```typescript +import { encodePacked } from 'viem'; + +// USDC → BZZ (single-hop, 1% fee) +const path = encodePacked( + ['address', 'uint24', 'address'], + [BZZ_ADDRESS, 10000, USDC_ADDRESS] +); + +// xDAI → WXDAI → USDC → BZZ (two-hop) +const path = encodePacked( + ['address', 'uint24', 'address', 'uint24', 'address'], + [BZZ_ADDRESS, fee2, USDC_ADDRESS, fee1, WXDAI_ADDRESS] +); +``` ## Notes -The terms "Batch" and "Stamps" are used interchangeably throughout the codebase. "Batch" refers to a collection of stamps created in a single transaction and is the terminology used in the Swarm protocol, while "Stamps" is a more user-friendly term used to describe the same concept. +The terms "Batch" and "Stamps" are used interchangeably. "Batch" is the Swarm protocol term; "Stamps" is the user-friendly term. diff --git a/contracts/SushiSwapStampsRouter.sol b/contracts/SushiSwapStampsRouter.sol new file mode 100644 index 0000000..d462492 --- /dev/null +++ b/contracts/SushiSwapStampsRouter.sol @@ -0,0 +1,585 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.23; + +/* + ███████╗██╗ ██╗███████╗██╗ ██╗██╗███████╗██╗ ██╗ █████╗ ██████╗ + ██╔════╝██║ ██║██╔════╝██║ ██║██║██╔════╝██║ ██║██╔══██╗██╔══██╗ + ███████╗██║ ██║███████╗███████║██║███████╗██║ █╗ ██║███████║██████╔╝ + ╚════██║██║ ██║╚════██║██╔══██║██║╚════██║██║███╗██║██╔══██║██╔═══╝ + ███████║╚██████╔╝███████║██║ ██║██║███████║╚███╔███╔╝██║ ██║██║ + ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ + + ███████╗████████╗ █████╗ ███╗ ███╗██████╗ ███████╗ + ██╔════╝╚══██╔══╝██╔══██╗████╗ ████║██╔══██╗██╔════╝ + ███████╗ ██║ ███████║██╔████╔██║██████╔╝███████╗ + ╚════██║ ██║ ██╔══██║██║╚██╔╝██║██╔═══╝ ╚════██║ + ███████║ ██║ ██║ ██║██║ ╚═╝ ██║██║ ███████║ + ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝ + + ██████╗ ██████╗ ██╗ ██╗████████╗███████╗██████╗ + ██╔══██╗██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██╔══██╗ + ██████╔╝██║ ██║██║ ██║ ██║ █████╗ ██████╔╝ + ██╔══██╗██║ ██║██║ ██║ ██║ ██╔══╝ ██╔══██╗ + ██║ ██║╚██████╔╝╚██████╔╝ ██║ ███████╗██║ ██║ + ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝ +*/ + +/** + * @title SushiSwapStampsRouter + * @notice Swaps any Gnosis-chain token to BZZ via SushiSwap V3 and atomically + * creates or tops up a Swarm postage-stamp batch in a single transaction. + * + * @dev Implements the Uniswap V3 callback interface (SushiSwap V3 is fully compatible). + * Supports both single-hop and multi-hop exact-output swaps via path encoding. + * + * Path encoding for exactOutput swaps (reversed token order): + * single-hop: BZZ ++ uint24(fee) ++ tokenIn (43 bytes) + * two-hop: BZZ ++ uint24(fee2) ++ mid ++ uint24(fee1) ++ tokenIn (66 bytes) + * + * Quote functions are non-view (Quoter simulates swaps internally) but are + * designed to be called via eth_call for gas-free estimation. + * + * Gnosis-chain addresses (hardcoded): + * BZZ = 0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da + * WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d + * Quoter = 0xb1e835dc2785b52265711e17fccb0fd018226a6e (SushiSwap V3 QuoterV2) + * Factory= 0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef (SushiSwap V3 Factory) + */ + +// ─── Interfaces ─────────────────────────────────────────────────────────────── + +interface IERC20 { + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} + +interface IWXDAI { + function deposit() external payable; + function withdraw(uint256 amount) external; + function approve(address spender, uint256 amount) external returns (bool); + function transfer(address recipient, uint256 amount) external returns (bool); + function transferFrom(address sender, address recipient, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); +} + +interface ISushiV3Pool { + function swap( + address recipient, + bool zeroForOne, + int256 amountSpecified, + uint160 sqrtPriceLimitX96, + bytes calldata data + ) external returns (int256 amount0, int256 amount1); + + function token0() external view returns (address); + function token1() external view returns (address); + function fee() external view returns (uint24); +} + +interface ISushiV3Factory { + function getPool( + address tokenA, + address tokenB, + uint24 fee + ) external view returns (address pool); +} + +interface IQuoterV2 { + struct QuoteExactOutputSingleParams { + address tokenIn; + address tokenOut; + uint256 amount; + uint24 fee; + uint160 sqrtPriceLimitX96; + } + + function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params) + external + returns ( + uint256 amountIn, + uint160 sqrtPriceX96After, + uint32 initializedTicksCrossed, + uint256 gasEstimate + ); + + function quoteExactOutput(bytes memory path, uint256 amountOut) + external + returns ( + uint256 amountIn, + uint160[] memory sqrtPriceX96AfterList, + uint32[] memory initializedTicksCrossedList, + uint256 gasEstimate + ); +} + +interface IStampsRegistry { + function createBatchRegistry( + address _owner, + address _nodeAddress, + uint256 _initialBalancePerChunk, + uint8 _depth, + uint8 _bucketDepth, + bytes32 _nonce, + bool _immutable + ) external; + + function topUpBatch(bytes32 _batchId, uint256 _topupAmountPerChunk) external; +} + +// ─── Router Contract ────────────────────────────────────────────────────────── + +contract SushiSwapStampsRouter { + + // ─── Constants ──────────────────────────────────────────────────────────── + + /// @notice BZZ token on Gnosis + address public constant BZZ = 0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da; + + /// @notice Wrapped xDAI on Gnosis + address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d; + + /// @notice SushiSwap V3 QuoterV2 on Gnosis + address public constant SUSHI_QUOTER = 0xb1E835Dc2785b52265711e17fCCb0fd018226a6e; + + /// @notice SushiSwap V3 Factory on Gnosis + address public constant SUSHI_FACTORY = 0xf78031CBCA409F2FB6876BDFDBc1b2df24cF9bEf; + + /// @notice Minimum sqrt price limit (used when selling token0 → token1, zeroForOne=true) + uint160 internal constant MIN_SQRT_RATIO = 4295128739; + + /// @notice Maximum sqrt price limit (used when selling token1 → token0, zeroForOne=false) + uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342; + + // Path encoding offsets (bytes): address=20, fee=3, nextOffset=23, popOffset=43 + uint256 private constant ADDR_SIZE = 20; + uint256 private constant FEE_SIZE = 3; + uint256 private constant NEXT_OFFSET = 23; // ADDR_SIZE + FEE_SIZE + uint256 private constant POP_OFFSET = 43; // NEXT_OFFSET + ADDR_SIZE + + // ─── Immutables ─────────────────────────────────────────────────────────── + + IStampsRegistry public immutable stampsRegistry; + + // ─── Events ─────────────────────────────────────────────────────────────── + + event BatchCreatedViaSwap( + bytes32 indexed batchId, + address indexed owner, + address tokenIn, + uint256 amountIn, + uint256 bzzAmount + ); + + event BatchToppedUpViaSwap( + bytes32 indexed batchId, + address tokenIn, + uint256 amountIn, + uint256 bzzAmount + ); + + // ─── Errors ─────────────────────────────────────────────────────────────── + + error InvalidCallback(); + error SlippageExceeded(uint256 required, uint256 maximum); + error InsufficientNativeValue(); + error NativeRefundFailed(); + error BzzTransferFailed(); + error BzzApproveFailed(); + error PoolNotFound(); + error InvalidPath(); + + // ─── Structs ────────────────────────────────────────────────────────────── + + struct CreateBatchParams { + address owner; + address nodeAddress; + uint256 initialBalancePerChunk; + uint8 depth; + uint8 bucketDepth; + bytes32 nonce; + bool immutable_; + } + + /// @dev Packed into the `data` argument of pool.swap(); threaded through callback chains. + struct SwapCallbackData { + bytes path; // remaining path in exactOutput encoding (BZZ-first) + address payer; // who pays the input token (address(this) for native swaps) + uint256 maxAmountIn; // slippage ceiling for the final (tokenIn) leg + } + + // ─── Constructor ────────────────────────────────────────────────────────── + + constructor(address _stampsRegistry) { + stampsRegistry = IStampsRegistry(_stampsRegistry); + } + + receive() external payable {} + + // ─── Quote Functions ────────────────────────────────────────────────────── + // These modify state internally (Quoter simulates swaps) but are designed to + // be called via eth_call for free gas-less estimation. + + /** + * @notice Quote: how many `tokenIn` are needed to get exactly `bzzAmountOut` BZZ + * via a single-hop pool. + * @param tokenIn Input token (use WXDAI for native xDAI quotes) + * @param fee Pool fee tier (e.g. 500, 3000, 10000) + * @param bzzAmountOut Exact BZZ amount wanted + * @return amountIn Input tokens required (before slippage) + */ + function quoteSingleHop( + address tokenIn, + uint24 fee, + uint256 bzzAmountOut + ) external returns (uint256 amountIn) { + (amountIn,,,) = IQuoterV2(SUSHI_QUOTER).quoteExactOutputSingle( + IQuoterV2.QuoteExactOutputSingleParams({ + tokenIn: tokenIn, + tokenOut: BZZ, + amount: bzzAmountOut, + fee: fee, + sqrtPriceLimitX96: 0 + }) + ); + } + + /** + * @notice Quote: how many input tokens are needed to get exactly `bzzAmountOut` BZZ + * via a multi-hop path. + * @param path Exact-output encoded path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn + * @param bzzAmountOut Exact BZZ amount wanted + * @return amountIn Input tokens required (before slippage) + */ + function quoteMultiHop( + bytes calldata path, + uint256 bzzAmountOut + ) external returns (uint256 amountIn) { + (amountIn,,,) = IQuoterV2(SUSHI_QUOTER).quoteExactOutput(path, bzzAmountOut); + } + + // ─── Create Batch ───────────────────────────────────────────────────────── + + /** + * @notice Swap `tokenIn` → BZZ via the given path and create a Swarm stamp batch. + * @dev `tokenIn` must be pre-approved to this contract for at least `maxAmountIn`. + * @param path Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn + * @param maxAmountIn Maximum tokenIn to spend (slippage protection) + * @param bzzAmountOut Exact BZZ needed (= swarmBatchTotal = initialBalancePerChunk × 2^depth) + * @param p Batch creation parameters + */ + function createBatch( + bytes calldata path, + uint256 maxAmountIn, + uint256 bzzAmountOut, + CreateBatchParams calldata p + ) external { + _swapExactOutput(path, msg.sender, maxAmountIn, bzzAmountOut); + bytes32 batchId = _approveBzzAndCreate(p, bzzAmountOut); + address tokenIn = _lastToken(path); + emit BatchCreatedViaSwap(batchId, p.owner, tokenIn, maxAmountIn, bzzAmountOut); + } + + /** + * @notice Swap native xDAI → BZZ and create a Swarm stamp batch. + * @dev Send msg.value ≥ maxAmountIn. Excess xDAI is refunded. + * @param path Exact-output path where the final token MUST be WXDAI: + * BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI + * @param maxAmountIn Maximum xDAI to spend + * @param bzzAmountOut Exact BZZ needed + * @param p Batch creation parameters + */ + function createBatchNative( + bytes calldata path, + uint256 maxAmountIn, + uint256 bzzAmountOut, + CreateBatchParams calldata p + ) external payable { + if (msg.value < maxAmountIn) revert InsufficientNativeValue(); + IWXDAI(WXDAI).deposit{value: maxAmountIn}(); + _swapExactOutput(path, address(this), maxAmountIn, bzzAmountOut); + bytes32 batchId = _approveBzzAndCreate(p, bzzAmountOut); + emit BatchCreatedViaSwap(batchId, p.owner, address(0), maxAmountIn, bzzAmountOut); + _refundNative(); + } + + // ─── Top Up Batch ───────────────────────────────────────────────────────── + + /** + * @notice Swap `tokenIn` → BZZ and top up an existing Swarm stamp batch. + * @dev `tokenIn` must be pre-approved to this contract for at least `maxAmountIn`. + * @param path Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn + * @param maxAmountIn Maximum tokenIn to spend + * @param bzzAmountOut Exact BZZ needed (= topupAmountPerChunk × 2^depth) + * @param batchId Batch to top up + * @param topupAmountPerChunk Per-chunk top-up amount (matches registry call) + */ + function topUp( + bytes calldata path, + uint256 maxAmountIn, + uint256 bzzAmountOut, + bytes32 batchId, + uint256 topupAmountPerChunk + ) external { + _swapExactOutput(path, msg.sender, maxAmountIn, bzzAmountOut); + _approveBzzAndTopUp(batchId, topupAmountPerChunk, bzzAmountOut); + address tokenIn = _lastToken(path); + emit BatchToppedUpViaSwap(batchId, tokenIn, maxAmountIn, bzzAmountOut); + } + + /** + * @notice Swap native xDAI → BZZ and top up an existing Swarm stamp batch. + * @dev Send msg.value ≥ maxAmountIn. Excess xDAI is refunded. + * @param path BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI + * @param maxAmountIn Maximum xDAI to spend + * @param bzzAmountOut Exact BZZ needed + * @param batchId Batch to top up + * @param topupAmountPerChunk Per-chunk top-up amount + */ + function topUpNative( + bytes calldata path, + uint256 maxAmountIn, + uint256 bzzAmountOut, + bytes32 batchId, + uint256 topupAmountPerChunk + ) external payable { + if (msg.value < maxAmountIn) revert InsufficientNativeValue(); + IWXDAI(WXDAI).deposit{value: maxAmountIn}(); + _swapExactOutput(path, address(this), maxAmountIn, bzzAmountOut); + _approveBzzAndTopUp(batchId, topupAmountPerChunk, bzzAmountOut); + emit BatchToppedUpViaSwap(batchId, address(0), maxAmountIn, bzzAmountOut); + _refundNative(); + } + + // ─── Uniswap V3 / SushiSwap V3 Swap Callback ───────────────────────────── + + /** + * @notice Called by a SushiSwap V3 pool during swap execution. + * @dev Implements the Uniswap V3 callback interface (SushiSwap V3 is compatible). + * For multi-hop swaps, this callback chains into the next pool swap before + * paying the current pool, routing tokens directly between pools. + */ + function uniswapV3SwapCallback( + int256 amount0Delta, + int256 amount1Delta, + bytes calldata data + ) external { + require(amount0Delta > 0 || amount1Delta > 0, "Zero deltas"); + + SwapCallbackData memory cb = abi.decode(data, (SwapCallbackData)); + + // Decode the first pool in the path to verify the caller is legitimate. + (address tokenOut, uint24 fee, address tokenIn) = _decodeFirstPool(cb.path); + address expectedPool = ISushiV3Factory(SUSHI_FACTORY).getPool(tokenOut, tokenIn, fee); + if (msg.sender != expectedPool) revert InvalidCallback(); + + // Determine which token we owe to the calling pool and how much. + // The positive delta is what the pool expects us to pay. + (address tokenOwed, uint256 amountOwed) = amount0Delta > 0 + ? (ISushiV3Pool(msg.sender).token0(), uint256(amount0Delta)) + : (ISushiV3Pool(msg.sender).token1(), uint256(amount1Delta)); + + if (_hasMultiplePools(cb.path)) { + // Multi-hop: continue to next pool. Skip the first token from path to get + // the remaining sub-path: mid ++ fee ++ ... ++ tokenIn + bytes memory remainingPath = _skipToken(cb.path); + + // Decode the next pool info from remaining path. + (address nextTokenOut, uint24 nextFee, address nextTokenIn) = _decodeFirstPool(remainingPath); + address nextPool = ISushiV3Factory(SUSHI_FACTORY).getPool(nextTokenOut, nextTokenIn, nextFee); + if (nextPool == address(0)) revert PoolNotFound(); + + // Swap in the next pool, sending output directly to msg.sender (current pool) + // so it receives the tokens it needs without going through this contract. + bool zeroForOne = nextTokenIn < nextTokenOut; + ISushiV3Pool(nextPool).swap( + msg.sender, // recipient = current pool (gets tokenOwed directly) + zeroForOne, + -int256(amountOwed), // exact output = amountOwed + zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, + abi.encode(SwapCallbackData({ + path: remainingPath, + payer: cb.payer, + maxAmountIn: cb.maxAmountIn + })) + ); + } else { + // Final hop: pay tokenOwed from the original payer. + if (amountOwed > cb.maxAmountIn) { + revert SlippageExceeded(amountOwed, cb.maxAmountIn); + } + + if (cb.payer == address(this)) { + // Native xDAI flow: we already hold WXDAI from the deposit. + if (!IERC20(tokenOwed).transfer(msg.sender, amountOwed)) { + revert BzzTransferFailed(); + } + } else { + // ERC20 flow: pull from user who pre-approved this contract. + if (!IERC20(tokenOwed).transferFrom(cb.payer, msg.sender, amountOwed)) { + revert BzzTransferFailed(); + } + } + } + } + + // ─── Internal Helpers ───────────────────────────────────────────────────── + + /** + * @dev Execute an exact-output swap for `bzzAmountOut` BZZ using the given path. + * The path is in exactOutput encoding: BZZ ++ fee ++ [...] ++ tokenIn. + * BZZ lands in address(this) after the swap completes. + */ + function _swapExactOutput( + bytes memory path, + address payer, + uint256 maxAmountIn, + uint256 bzzAmountOut + ) internal { + if (path.length < POP_OFFSET) revert InvalidPath(); + + // Decode the first (and for single-hop, only) pool in the path. + (address tokenOut, uint24 fee, address tokenIn) = _decodeFirstPool(path); + if (tokenOut != BZZ) revert InvalidPath(); + + address pool = ISushiV3Factory(SUSHI_FACTORY).getPool(tokenOut, tokenIn, fee); + if (pool == address(0)) revert PoolNotFound(); + + // zeroForOne: true if tokenIn is token0 (address < BZZ) + bool zeroForOne = tokenIn < tokenOut; + + // amountSpecified < 0 → exact output (we want exactly bzzAmountOut of BZZ) + ISushiV3Pool(pool).swap( + address(this), // receive BZZ here + zeroForOne, + -int256(bzzAmountOut), + zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1, + abi.encode(SwapCallbackData({ + path: path, + payer: payer, + maxAmountIn: maxAmountIn + })) + ); + } + + /** + * @dev Approve BZZ to the stamps registry and call createBatchRegistry. + * Returns the keccak256 batch ID consistent with the registry's derivation. + */ + function _approveBzzAndCreate( + CreateBatchParams memory p, + uint256 bzzAmountOut + ) internal returns (bytes32 batchId) { + if (!IERC20(BZZ).approve(address(stampsRegistry), bzzAmountOut)) { + revert BzzApproveFailed(); + } + + stampsRegistry.createBatchRegistry( + p.owner, + p.nodeAddress, + p.initialBalancePerChunk, + p.depth, + p.bucketDepth, + p.nonce, + p.immutable_ + ); + + // Registry derives batchId as keccak256(abi.encode(registry, nonce)). + batchId = keccak256(abi.encode(address(stampsRegistry), p.nonce)); + } + + /** + * @dev Approve BZZ to the stamps registry and call topUpBatch. + */ + function _approveBzzAndTopUp( + bytes32 batchId, + uint256 topupAmountPerChunk, + uint256 bzzAmountOut + ) internal { + if (!IERC20(BZZ).approve(address(stampsRegistry), bzzAmountOut)) { + revert BzzApproveFailed(); + } + stampsRegistry.topUpBatch(batchId, topupAmountPerChunk); + } + + /** + * @dev Unwrap any remaining WXDAI and refund all native xDAI to msg.sender. + */ + function _refundNative() internal { + uint256 wxdaiBalance = IERC20(WXDAI).balanceOf(address(this)); + if (wxdaiBalance > 0) { + IWXDAI(WXDAI).withdraw(wxdaiBalance); + } + uint256 nativeBalance = address(this).balance; + if (nativeBalance > 0) { + (bool ok,) = msg.sender.call{value: nativeBalance}(""); + if (!ok) revert NativeRefundFailed(); + } + } + + // ─── Path Utilities ─────────────────────────────────────────────────────── + + /** + * @dev Returns true if the path encodes more than one pool (length > 43 bytes). + */ + function _hasMultiplePools(bytes memory path) internal pure returns (bool) { + return path.length > POP_OFFSET; + } + + /** + * @dev Decodes the first pool segment from the path: + * tokenA (20 bytes) ++ fee (3 bytes) ++ tokenB (20 bytes) + */ + function _decodeFirstPool(bytes memory path) + internal + pure + returns (address tokenA, uint24 fee, address tokenB) + { + tokenA = _toAddress(path, 0); + fee = _toUint24(path, ADDR_SIZE); + tokenB = _toAddress(path, NEXT_OFFSET); + } + + /** + * @dev Returns the path with the first token removed (skips ADDR_SIZE + FEE_SIZE bytes). + * Used to advance through multi-hop paths in the callback. + */ + function _skipToken(bytes memory path) internal pure returns (bytes memory skipped) { + uint256 newLen = path.length - NEXT_OFFSET; + skipped = new bytes(newLen); + // Copy from offset NEXT_OFFSET onward + for (uint256 i = 0; i < newLen; i++) { + skipped[i] = path[i + NEXT_OFFSET]; + } + } + + /** + * @dev Extracts the last 20-byte address from the path (the tokenIn address). + */ + function _lastToken(bytes memory path) internal pure returns (address token) { + uint256 offset = path.length - ADDR_SIZE; + token = _toAddress(path, offset); + } + + /** + * @dev Reads a 20-byte address from `data` at `offset` using assembly. + * The address occupies bytes [offset, offset+20) and is right-aligned + * by shifting the 32-byte word 96 bits right. + */ + function _toAddress(bytes memory data, uint256 offset) internal pure returns (address addr) { + assembly { + addr := shr(96, mload(add(add(data, 0x20), offset))) + } + } + + /** + * @dev Reads a 3-byte uint24 from `data` at `offset` using assembly. + * Shifts the 32-byte word 232 bits right to extract the top 3 bytes. + */ + function _toUint24(bytes memory data, uint256 offset) internal pure returns (uint24 result) { + assembly { + result := shr(232, mload(add(add(data, 0x20), offset))) + } + } +} diff --git a/deploy/02_deploy_sushi_stamps_router.ts b/deploy/02_deploy_sushi_stamps_router.ts new file mode 100644 index 0000000..bebc743 --- /dev/null +++ b/deploy/02_deploy_sushi_stamps_router.ts @@ -0,0 +1,123 @@ +import { HardhatRuntimeEnvironment } from 'hardhat/types'; +import { DeployFunction } from 'hardhat-deploy/types'; + +/** + * Deploys the SushiSwapStampsRouter contract on Gnosis Chain. + * + * The router swaps any Gnosis-chain token → BZZ via SushiSwap V3 and atomically + * creates or tops up a Swarm postage-stamp batch in a single transaction. + * + * Prerequisites + * ───────────── + * Set these in your .env (or .env.local) before running: + * + * WALLET_SECRET Private key of the deployer account + * GNOSIS_RPC_URL Gnosis RPC (e.g. https://rpc.gnosischain.com) + * GNOSIS_STAMPS_REGISTRY Address of the deployed StampsRegistry contract + * Defaults to: 0x5EBfBeFB1E88391eFb022d5d33302f50a46bF4f3 + * MAINNET_ETHERSCAN_KEY GnosisScan API key for contract verification + * + * Deploy command + * ────────────── + * npx hardhat deploy --network gnosis --tags SushiSwapStampsRouter + * + * Manual verification (if auto-verify fails) + * ──────────────────────────────────────────── + * npx hardhat verify --network gnosis + */ + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployments, getNamedAccounts, network } = hre; + const { deploy, log } = deployments; + const { deployer } = await getNamedAccounts(); + + log('─────────────────────────────────────────────────────────────────'); + log('Deploying SushiSwapStampsRouter …'); + + // The existing StampsRegistry that the router will call after swapping BZZ. + // Must be the same registry the frontend uses (GNOSIS_CUSTOM_REGISTRY_ADDRESS). + const stampsRegistryAddress = + process.env.GNOSIS_STAMPS_REGISTRY || + '0x5EBfBeFB1E88391eFb022d5d33302f50a46bF4f3'; + + log(` StampsRegistry → ${stampsRegistryAddress}`); + log(` Deployer → ${deployer}`); + log(` Network → ${network.name} (chainId: ${network.config.chainId ?? 'unknown'})`); + + const router = await deploy('SushiSwapStampsRouter', { + from: deployer, + args: [stampsRegistryAddress], + log: true, + waitConfirmations: network.name === 'hardhat' ? 1 : 5, + }); + + log(`SushiSwapStampsRouter deployed at → ${router.address}`); + + // ── Contract Verification ─────────────────────────────────────────────────── + + if (network.name !== 'hardhat' && network.name !== 'localhost') { + // ── 1. Etherscan / GnosisScan API verification ──────────────────────────── + log('Verifying on GnosisScan (Etherscan API) …'); + try { + await hre.run('verify:verify', { + address: router.address, + constructorArguments: [stampsRegistryAddress], + contract: 'contracts/SushiSwapStampsRouter.sol:SushiSwapStampsRouter', + }); + log('✅ GnosisScan verification successful'); + } catch (error: any) { + if (error?.message?.toLowerCase().includes('already verified')) { + log('ℹ️ Already verified on GnosisScan'); + } else { + log('⚠️ GnosisScan verification failed – will try Sourcify next'); + log(` Manual retry: npx hardhat verify --network gnosis ${router.address} ${stampsRegistryAddress}`); + log(` Error: ${error?.message ?? error}`); + } + } + + // ── 2. Sourcify verification (v2 – no API key required) ────────────────── + log('Verifying on Sourcify (v2) …'); + try { + await hre.run('sourcify', { + address: router.address, + constructorArguments: [stampsRegistryAddress], + }); + log('✅ Sourcify verification successful'); + log(` View: https://repo.sourcify.dev/contracts/full_match/100/${router.address}/`); + } catch (error: any) { + if ( + error?.message?.toLowerCase().includes('already verified') || + error?.message?.toLowerCase().includes('already full match') + ) { + log('ℹ️ Already verified on Sourcify'); + } else { + log('⚠️ Sourcify verification failed'); + log(` Manual retry: npx hardhat sourcify --network gnosis --address ${router.address}`); + log(` Error: ${error?.message ?? error}`); + } + } + } + + // ── Post-deploy summary ───────────────────────────────────────────────────── + + log('─────────────────────────────────────────────────────────────────'); + log('Deployment summary:'); + log(` SushiSwapStampsRouter : ${router.address}`); + log(` StampsRegistry : ${stampsRegistryAddress}`); + log(''); + log('Add this to your .env.local / environment:'); + log(` NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS=${router.address}`); + log(''); + log('Known Gnosis SushiSwap V3 addresses (for reference):'); + log(' Factory : 0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef'); + log(' Quoter : 0xb1e835dc2785b52265711e17fccb0fd018226a6e'); + log(' BZZ/USDC pool: 0x6f30b7cf40cb423c1d23478a9855701ecf43931e'); + log('─────────────────────────────────────────────────────────────────'); +}; + +export default func; +func.tags = ['SushiSwapStampsRouter', 'all']; + +// This deploy script depends on StampsRegistry already being deployed. +// If you want automatic ordering, uncomment the next line: +// func.dependencies = ['StampsRegistry']; diff --git a/hardhat.config.ts b/hardhat.config.ts index b3cb655..4dcaddb 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -20,6 +20,7 @@ const config: HardhatUserConfig = { enabled: true, runs: 200, }, + viaIR: true, }, }, networks: { @@ -53,6 +54,12 @@ const config: HardhatUserConfig = { }, ], }, + + // Sourcify verification (v2 - supported by GnosisScan / Blockscout natively) + // No API key required. Verifies on https://sourcify.dev and mirrors to GnosisScan. + sourcify: { + enabled: true, + }, paths: { sources: "./contracts", tests: "./test", diff --git a/scripts/verify_registry.ts b/scripts/verify_registry.ts new file mode 100644 index 0000000..b503ab8 --- /dev/null +++ b/scripts/verify_registry.ts @@ -0,0 +1,85 @@ +/** + * Standalone verification script for StampsRegistry on Gnosis chain. + * + * Usage + * ───── + * npx hardhat run scripts/verify_registry.ts --network gnosis + * + * # Or pass the address explicitly: + * REGISTRY_ADDRESS=0xYourAddress npx hardhat run scripts/verify_registry.ts --network gnosis + */ + +import { run, deployments } from 'hardhat'; +import * as dotenv from 'dotenv'; +dotenv.config({ path: '.env' }); + +async function main() { + let registryAddress: string | undefined = process.env.REGISTRY_ADDRESS; + + if (!registryAddress) { + try { + const deployment = await deployments.get('StampsRegistry'); + registryAddress = deployment.address; + console.log(`📦 Loaded address from deployments cache: ${registryAddress}`); + } catch { + console.error( + '❌ No REGISTRY_ADDRESS env var set and no deployment found.\n' + + ' Set REGISTRY_ADDRESS=0x... or run the deploy script first.' + ); + process.exit(1); + } + } + + const swarmContractAddress = + process.env.SWARM_CONTRACT_ADDRESS || + '0x45a1502382541Cd610CC9068e88727426b696293'; + + console.log('\n══════════════════════════════════════════════════════════'); + console.log('StampsRegistry Verification'); + console.log('══════════════════════════════════════════════════════════'); + console.log(`Registry address : ${registryAddress}`); + console.log(`SwarmContract : ${swarmContractAddress}`); + console.log('══════════════════════════════════════════════════════════\n'); + + // ── GnosisScan ──────────────────────────────────────────────────────────── + console.log('▶ Step 1 – GnosisScan (Etherscan API) …'); + try { + await run('verify:verify', { + address: registryAddress, + constructorArguments: [swarmContractAddress], + contract: 'contracts/StampsRegistry.sol:StampsRegistry', + }); + console.log(`✅ GnosisScan verified: https://gnosisscan.io/address/${registryAddress}#code\n`); + } catch (err: any) { + const msg = err?.message ?? String(err); + if (msg.toLowerCase().includes('already verified')) { + console.log('ℹ️ Already verified on GnosisScan.\n'); + } else { + console.warn(`⚠️ Failed: ${msg}\n`); + } + } + + // ── Sourcify ────────────────────────────────────────────────────────────── + console.log('▶ Step 2 – Sourcify (v2) …'); + try { + await run('sourcify', { + address: registryAddress, + constructorArguments: [swarmContractAddress], + }); + console.log('✅ Sourcify verified!\n'); + } catch (err: any) { + const msg = err?.message ?? String(err); + if (msg.toLowerCase().includes('already') || msg.toLowerCase().includes('full match')) { + console.log('ℹ️ Already verified on Sourcify.\n'); + } else { + console.warn(`⚠️ Failed: ${msg}\n`); + } + } + + console.log('Done.'); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/scripts/verify_router.ts b/scripts/verify_router.ts new file mode 100644 index 0000000..16e0efb --- /dev/null +++ b/scripts/verify_router.ts @@ -0,0 +1,114 @@ +/** + * Standalone verification script for SushiSwapStampsRouter on Gnosis chain. + * + * Tries both verification methods supported by @nomicfoundation/hardhat-verify v2: + * 1. GnosisScan (Etherscan-compatible API) – requires MAINNET_ETHERSCAN_KEY in .env + * 2. Sourcify – no API key needed + * + * Usage + * ───── + * # From the project root: + * npx hardhat run scripts/verify_router.ts --network gnosis + * + * # Or pass the address via env variable (overrides the deployments/ cache): + * ROUTER_ADDRESS=0xYourDeployedAddress npx hardhat run scripts/verify_router.ts --network gnosis + * + * Requirements + * ──────────── + * WALLET_SECRET – deployer private key (for reading deployments) + * GNOSIS_RPC_URL – Gnosis RPC endpoint + * MAINNET_ETHERSCAN_KEY – GnosisScan API key (for Etherscan-style verification) + * GNOSIS_STAMPS_REGISTRY – StampsRegistry address (constructor arg; default shown below) + */ + +import { run, deployments } from 'hardhat'; +import * as dotenv from 'dotenv'; +dotenv.config({ path: '.env' }); + +async function main() { + // ── Resolve router address ──────────────────────────────────────────────── + let routerAddress: string | undefined = process.env.ROUTER_ADDRESS; + + if (!routerAddress) { + try { + const deployment = await deployments.get('SushiSwapStampsRouter'); + routerAddress = deployment.address; + console.log(`📦 Loaded address from deployments cache: ${routerAddress}`); + } catch { + console.error( + '❌ No ROUTER_ADDRESS env var set and no deployment found in deployments/ cache.\n' + + ' Run the deploy script first, or set ROUTER_ADDRESS=0x...' + ); + process.exit(1); + } + } + + // ── Resolve constructor arg ─────────────────────────────────────────────── + const stampsRegistry = + process.env.GNOSIS_STAMPS_REGISTRY || + '0x5EBfBeFB1E88391eFb022d5d33302f50a46bF4f3'; + + console.log('\n══════════════════════════════════════════════════════════'); + console.log('SushiSwapStampsRouter Verification'); + console.log('══════════════════════════════════════════════════════════'); + console.log(`Router address : ${routerAddress}`); + console.log(`StampsRegistry : ${stampsRegistry}`); + console.log(`Network : gnosis (chainId 100)`); + console.log('══════════════════════════════════════════════════════════\n'); + + // ── 1. GnosisScan / Etherscan API verification ──────────────────────────── + console.log('▶ Step 1 – GnosisScan (Etherscan API) verification …'); + try { + await run('verify:verify', { + address: routerAddress, + constructorArguments: [stampsRegistry], + contract: 'contracts/SushiSwapStampsRouter.sol:SushiSwapStampsRouter', + }); + console.log(`✅ GnosisScan verified: https://gnosisscan.io/address/${routerAddress}#code\n`); + } catch (err: any) { + const msg: string = err?.message ?? String(err); + if (msg.toLowerCase().includes('already verified')) { + console.log(`ℹ️ Already verified on GnosisScan.\n`); + } else { + console.warn(`⚠️ GnosisScan verification failed:\n ${msg}\n`); + console.log(' Manual command:'); + console.log( + ` npx hardhat verify --network gnosis ${routerAddress} ${stampsRegistry}\n` + ); + } + } + + // ── 2. Sourcify verification (no API key, v2) ───────────────────────────── + console.log('▶ Step 2 – Sourcify (v2) verification …'); + try { + await run('sourcify', { + address: routerAddress, + constructorArguments: [stampsRegistry], + }); + console.log('✅ Sourcify verified!'); + console.log( + ` View: https://repo.sourcify.dev/contracts/full_match/100/${routerAddress}/\n` + ); + } catch (err: any) { + const msg: string = err?.message ?? String(err); + if ( + msg.toLowerCase().includes('already verified') || + msg.toLowerCase().includes('already full match') + ) { + console.log('ℹ️ Already verified on Sourcify.\n'); + } else { + console.warn(`⚠️ Sourcify verification failed:\n ${msg}\n`); + console.log(' Manual command:'); + console.log(` npx hardhat sourcify --network gnosis --address ${routerAddress}\n`); + } + } + + console.log('══════════════════════════════════════════════════════════'); + console.log('Verification complete.'); + console.log('══════════════════════════════════════════════════════════'); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/app/components/SushiQuotes.ts b/src/app/components/SushiQuotes.ts new file mode 100644 index 0000000..d252eff --- /dev/null +++ b/src/app/components/SushiQuotes.ts @@ -0,0 +1,598 @@ +import { parseAbi, encodePacked, getAddress, parseUnits } from 'viem'; +import { + GNOSIS_BZZ_ADDRESS, + GNOSIS_WXDAI_ADDRESS, + SUSHI_STAMPS_ROUTER_ADDRESS, + SUSHI_STAMPS_ROUTER_ABI, + SUSHI_FACTORY_ADDRESS, + SUSHI_FACTORY_ABI, + BZZ_USDC_POOL_ADDRESS, + GNOSIS_USDC_ADDRESS, + DEFAULT_SLIPPAGE, + TRANSACTION_TIMEOUT_MS, +} from './constants'; +import { getGnosisPublicClient, performWithRetry } from './utils'; +import { getPollingInterval } from '@/app/wagmi'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +export interface SushiRouteInfo { + /** Encoded path for exactOutput: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn */ + path: `0x${string}`; + /** Whether tokenIn is native xDAI (uses createBatchNative / topUpNative) */ + isNative: boolean; + /** Pool fee tier(s) used in the path */ + fees: number[]; + /** Human-readable route description */ + description: string; +} + +export interface SushiQuoteResult { + /** Amount of input tokens required (including slippage buffer) */ + maxAmountIn: bigint; + /** Exact amount without slippage (from Quoter) */ + amountInBeforeSlippage: bigint; + /** Approximate USD cost (amountIn × tokenPriceUsd) */ + totalAmountUSD: number; + /** The route used */ + route: SushiRouteInfo; + /** tokenIn address (address(0) for native xDAI) */ + tokenIn: string; + /** tokenIn symbol for display */ + tokenInSymbol: string; + /** tokenIn decimals */ + tokenInDecimals: number; +} + +export interface SushiQuoteParams { + /** Hex address of the input token (address(0) or '0x0' for native xDAI) */ + fromToken: string; + /** Exact BZZ amount needed (= swarmBatchTotal) */ + bzzAmount: string; + /** Slippage as percentage (e.g. 5 = 5%) */ + slippagePercent?: number; + /** Token symbol for display */ + tokenSymbol?: string; + /** Token decimals */ + tokenDecimals?: number; + /** Token USD price (used to compute totalAmountUSD) */ + tokenPriceUsd?: number; +} + +export interface SushiExecuteParams extends SushiQuoteParams { + /** Caller's wallet address */ + address: string; + /** swarm config used to build stamp params */ + swarmConfig: any; + /** Batch ID to top up (undefined = create new batch) */ + topUpBatchId?: string; + /** Node address for new batches */ + nodeAddress: string; + /** wagmi walletClient */ + walletClient: any; + /** wagmi publicClient */ + publicClient: any; + /** Pre-computed quote (if already fetched) */ + quote?: SushiQuoteResult; + /** Callback for status updates */ + setStatusMessage: (status: any) => void; + onTransactionConfirmed?: () => void; +} + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; + +/** + * Common fee tiers to try when auto-discovering pools (SushiSwap V3). + * Order matters — try the most common first. + */ +const FEE_TIERS = [10000, 3000, 500, 100] as const; + +/** + * Known direct BZZ pools on Gnosis (token → pool, fee). + * These are cached to avoid repeated factory lookups. + */ +const KNOWN_BZZ_POOLS: Record = { + [GNOSIS_USDC_ADDRESS.toLowerCase()]: { pool: BZZ_USDC_POOL_ADDRESS, fee: 10000 }, +}; + +// ─── Pool Discovery ────────────────────────────────────────────────────────── + +/** + * Queries the SushiSwap V3 factory for a direct BZZ pool for `tokenIn`. + * Returns the pool address and fee tier, or null if none exists. + */ +async function findDirectBzzPool( + tokenIn: string +): Promise<{ pool: string; fee: number } | null> { + const normalised = tokenIn.toLowerCase(); + + // Check the cached known pools first. + if (KNOWN_BZZ_POOLS[normalised]) { + return KNOWN_BZZ_POOLS[normalised]; + } + + const { client } = getGnosisPublicClient(); + + for (const fee of FEE_TIERS) { + try { + const pool = await client.readContract({ + address: SUSHI_FACTORY_ADDRESS as `0x${string}`, + abi: SUSHI_FACTORY_ABI, + functionName: 'getPool', + args: [tokenIn as `0x${string}`, GNOSIS_BZZ_ADDRESS as `0x${string}`, fee], + }); + + if (pool && pool !== ZERO_ADDRESS) { + const result = { pool: pool as string, fee }; + // Cache for subsequent calls. + KNOWN_BZZ_POOLS[normalised] = result; + return result; + } + } catch { + // Try next fee tier. + } + } + + return null; +} + +/** + * Queries the SushiSwap V3 factory for a WXDAI/USDC pool (needed for xDAI two-hop routing). + */ +async function findWxdaiUsdcPool(): Promise<{ pool: string; fee: number } | null> { + const { client } = getGnosisPublicClient(); + + for (const fee of FEE_TIERS) { + try { + const pool = await client.readContract({ + address: SUSHI_FACTORY_ADDRESS as `0x${string}`, + abi: SUSHI_FACTORY_ABI, + functionName: 'getPool', + args: [ + GNOSIS_WXDAI_ADDRESS as `0x${string}`, + GNOSIS_USDC_ADDRESS as `0x${string}`, + fee, + ], + }); + + if (pool && pool !== ZERO_ADDRESS) { + return { pool: pool as string, fee }; + } + } catch { + // Try next fee tier. + } + } + + return null; +} + +// ─── Path Encoding ──────────────────────────────────────────────────────────── + +/** + * Encodes a single-hop exactOutput path: BZZ ++ fee ++ tokenIn. + */ +function encodeSingleHopPath(tokenIn: string, fee: number): `0x${string}` { + return encodePacked( + ['address', 'uint24', 'address'], + [GNOSIS_BZZ_ADDRESS as `0x${string}`, fee, tokenIn as `0x${string}`] + ); +} + +/** + * Encodes a two-hop exactOutput path: BZZ ++ fee2 ++ mid ++ fee1 ++ tokenIn. + */ +function encodeTwoHopPath( + tokenIn: string, + fee1: number, + mid: string, + fee2: number +): `0x${string}` { + return encodePacked( + ['address', 'uint24', 'address', 'uint24', 'address'], + [ + GNOSIS_BZZ_ADDRESS as `0x${string}`, + fee2, + mid as `0x${string}`, + fee1, + tokenIn as `0x${string}`, + ] + ); +} + +// ─── Route Resolution ───────────────────────────────────────────────────────── + +/** + * Determines the best swap route from `fromToken` to BZZ. + * Tries direct pools first, then routes through USDC or WXDAI. + */ +export async function findSushiRoute(fromToken: string): Promise { + const isNativeXdai = + fromToken === ZERO_ADDRESS || + fromToken === '0x0' || + fromToken.toLowerCase() === ZERO_ADDRESS; + + // For native xDAI, use WXDAI as the effective input token. + const effectiveTokenIn = isNativeXdai ? GNOSIS_WXDAI_ADDRESS : fromToken; + + // ── Case 1: direct BZZ pool ───────────────────────────────────────────────── + const directPool = await findDirectBzzPool(effectiveTokenIn); + if (directPool) { + const path = encodeSingleHopPath(effectiveTokenIn, directPool.fee); + return { + path, + isNative: isNativeXdai, + fees: [directPool.fee], + description: isNativeXdai + ? `xDAI → WXDAI → BZZ (single-hop, ${directPool.fee / 100}% fee)` + : `Direct → BZZ (${directPool.fee / 100}% fee)`, + }; + } + + // ── Case 2: two-hop via USDC (tokenIn → USDC → BZZ) ──────────────────────── + const bzzUsdcInfo = KNOWN_BZZ_POOLS[GNOSIS_USDC_ADDRESS.toLowerCase()] ?? { + pool: BZZ_USDC_POOL_ADDRESS, + fee: 10000, + }; + + if ( + effectiveTokenIn.toLowerCase() !== GNOSIS_USDC_ADDRESS.toLowerCase() + ) { + // Find tokenIn → USDC pool. + const { client } = getGnosisPublicClient(); + for (const fee of FEE_TIERS) { + try { + const pool = await client.readContract({ + address: SUSHI_FACTORY_ADDRESS as `0x${string}`, + abi: SUSHI_FACTORY_ABI, + functionName: 'getPool', + args: [ + effectiveTokenIn as `0x${string}`, + GNOSIS_USDC_ADDRESS as `0x${string}`, + fee, + ], + }); + + if (pool && pool !== ZERO_ADDRESS) { + const path = encodeTwoHopPath( + effectiveTokenIn, + fee, // fee for tokenIn→USDC + GNOSIS_USDC_ADDRESS, + bzzUsdcInfo.fee // fee for USDC→BZZ + ); + return { + path, + isNative: isNativeXdai, + fees: [fee, bzzUsdcInfo.fee], + description: isNativeXdai + ? `xDAI → WXDAI → USDC → BZZ (${fee / 100}% + ${bzzUsdcInfo.fee / 100}%)` + : `tokenIn → USDC → BZZ (${fee / 100}% + ${bzzUsdcInfo.fee / 100}%)`, + }; + } + } catch { + // Try next fee tier. + } + } + } + + // ── Case 3: two-hop via WXDAI (tokenIn → WXDAI → BZZ) ───────────────────── + const wxdaiBzzPool = await findDirectBzzPool(GNOSIS_WXDAI_ADDRESS); + if (wxdaiBzzPool && effectiveTokenIn.toLowerCase() !== GNOSIS_WXDAI_ADDRESS.toLowerCase()) { + const wxdaiUsdcPool = await findWxdaiUsdcPool(); + if (wxdaiUsdcPool) { + const path = encodeTwoHopPath( + effectiveTokenIn, + wxdaiUsdcPool.fee, + GNOSIS_WXDAI_ADDRESS, + wxdaiBzzPool.fee + ); + return { + path, + isNative: false, + fees: [wxdaiUsdcPool.fee, wxdaiBzzPool.fee], + description: `tokenIn → WXDAI → BZZ (${wxdaiUsdcPool.fee / 100}% + ${wxdaiBzzPool.fee / 100}%)`, + }; + } + } + + return null; +} + +// ─── Quote ──────────────────────────────────────────────────────────────────── + +/** + * Gets a quote for buying `bzzAmount` BZZ by swapping `fromToken` on Gnosis + * via the SushiSwapStampsRouter contract. Uses eth_call so no gas is consumed. + */ +export const getSushiQuote = async (params: SushiQuoteParams): Promise => { + const { + fromToken, + bzzAmount, + slippagePercent = DEFAULT_SLIPPAGE, + tokenSymbol = 'Token', + tokenDecimals = 18, + tokenPriceUsd = 0, + } = params; + + console.log('🍣 Getting SushiSwap quote for BZZ purchase…', { + fromToken, + bzzAmount, + slippagePercent, + }); + + const route = await performWithRetry( + () => findSushiRoute(fromToken), + 'findSushiRoute' + ); + + if (!route) { + throw new Error( + `No SushiSwap route found from ${tokenSymbol} to BZZ on Gnosis. ` + + 'Try using USDC or xDAI instead.' + ); + } + + console.log('🍣 Route found:', route.description); + + const { client } = getGnosisPublicClient(); + + // Call quoteSingleHop or quoteMultiHop depending on number of hops. + // Both are non-view but designed for eth_call (simulate = true). + const isSingleHop = route.fees.length === 1; + + let amountInBeforeSlippage: bigint; + + if (isSingleHop) { + const effectiveTokenIn = route.isNative ? GNOSIS_WXDAI_ADDRESS : fromToken; + const result = await client.simulateContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'quoteSingleHop', + args: [ + effectiveTokenIn as `0x${string}`, + route.fees[0], + BigInt(bzzAmount), + ], + }); + amountInBeforeSlippage = result.result as bigint; + } else { + const result = await client.simulateContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'quoteMultiHop', + args: [route.path, BigInt(bzzAmount)], + }); + amountInBeforeSlippage = result.result as bigint; + } + + // Apply slippage buffer. + const slippageBps = BigInt(Math.round(slippagePercent * 100)); // percent → bps + const maxAmountIn = + (amountInBeforeSlippage * (10000n + slippageBps)) / 10000n; + + // Compute approximate USD cost. + let totalAmountUSD = 0; + if (tokenPriceUsd > 0) { + const amountInFormatted = + Number(amountInBeforeSlippage) / 10 ** tokenDecimals; + totalAmountUSD = amountInFormatted * tokenPriceUsd; + } + + console.log('✅ SushiSwap quote:', { + route: route.description, + amountInBeforeSlippage: amountInBeforeSlippage.toString(), + maxAmountIn: maxAmountIn.toString(), + totalAmountUSD, + }); + + return { + maxAmountIn, + amountInBeforeSlippage, + totalAmountUSD, + route, + tokenIn: route.isNative ? ZERO_ADDRESS : fromToken, + tokenInSymbol: tokenSymbol, + tokenInDecimals: tokenDecimals, + }; +}; + +// ─── Allowance Check ────────────────────────────────────────────────────────── + +/** + * Checks whether the user has approved enough `tokenIn` to the router. + */ +export const checkRouterAllowance = async ( + userAddress: string, + tokenIn: string, + requiredAmount: bigint +): Promise => { + if (tokenIn === ZERO_ADDRESS || tokenIn === '0x0') { + return true; // Native xDAI doesn't need approval. + } + + try { + const { client } = getGnosisPublicClient(); + const allowance = await client.readContract({ + address: tokenIn as `0x${string}`, + abi: parseAbi([ + 'function allowance(address owner, address spender) external view returns (uint256)', + ]), + functionName: 'allowance', + args: [userAddress as `0x${string}`, SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`], + }); + + return BigInt(allowance.toString()) >= requiredAmount; + } catch (error) { + console.warn('⚠️ Could not check router allowance, will include approval:', error); + return false; + } +}; + +// ─── Execute ────────────────────────────────────────────────────────────────── + +/** + * Executes a SushiSwap token → BZZ → stamp purchase (create or top-up) in a + * single on-chain transaction via the SushiSwapStampsRouter contract. + */ +export const executeSushiSwap = async (params: SushiExecuteParams): Promise => { + const { + fromToken, + bzzAmount, + slippagePercent = DEFAULT_SLIPPAGE, + address, + swarmConfig, + topUpBatchId, + nodeAddress, + walletClient, + publicClient, + setStatusMessage, + onTransactionConfirmed, + } = params; + + console.log('🍣 Executing SushiSwap stamp purchase…'); + + // ── 1. Get / reuse quote ─────────────────────────────────────────────────── + setStatusMessage({ step: 'Quoting', message: 'Getting SushiSwap quote…' }); + + const quote = params.quote ?? (await getSushiQuote({ + fromToken, + bzzAmount, + slippagePercent, + tokenSymbol: params.tokenSymbol, + tokenDecimals: params.tokenDecimals, + tokenPriceUsd: params.tokenPriceUsd, + })); + + const { maxAmountIn, route } = quote; + const isNative = route.isNative; + const isTopUp = Boolean(topUpBatchId); + + // ── 2. Approve router (ERC20 only) ──────────────────────────────────────── + if (!isNative) { + const tokenIn = isNative ? GNOSIS_WXDAI_ADDRESS : fromToken; + const hasAllowance = await checkRouterAllowance(address, tokenIn, maxAmountIn); + + if (!hasAllowance) { + setStatusMessage({ step: 'Approval', message: 'Approving token for router…' }); + console.log('🔐 Approving router to spend token…', { tokenIn, maxAmountIn: maxAmountIn.toString() }); + + const MAX_UINT256 = + 115792089237316195423570985008687907853269984665640564039457584007913129639935n; + + const approveTxHash = await walletClient.writeContract({ + address: tokenIn as `0x${string}`, + abi: parseAbi(['function approve(address spender, uint256 amount) external returns (bool)']), + functionName: 'approve', + args: [SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, MAX_UINT256], + }); + + console.log('📝 Approval tx:', approveTxHash); + setStatusMessage({ step: 'Approval', message: 'Waiting for approval confirmation…' }); + + const approveReceipt = await publicClient.waitForTransactionReceipt({ + hash: approveTxHash, + timeout: TRANSACTION_TIMEOUT_MS, + pollingInterval: getPollingInterval(100), // Gnosis chainId = 100 + }); + + if (approveReceipt.status !== 'success') { + throw new Error('Token approval transaction failed'); + } + console.log('✅ Router approved'); + } else { + console.log('✅ Router already approved, skipping approval'); + } + } + + // ── 3. Build stamp registry params ──────────────────────────────────────── + const bzzAmountBigInt = BigInt(bzzAmount); + + setStatusMessage({ + step: 'Swapping', + message: isTopUp ? 'Topping up stamp via SushiSwap…' : 'Buying stamp via SushiSwap…', + }); + + // ── 4. Build and send the router transaction ─────────────────────────────── + let txHash: `0x${string}`; + + if (isTopUp && topUpBatchId) { + // Top-up branch + const topupAmountPerChunk = BigInt(swarmConfig.swarmBatchInitialBalance); + + if (isNative) { + txHash = await walletClient.writeContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'topUpNative', + args: [route.path, maxAmountIn, bzzAmountBigInt, topUpBatchId as `0x${string}`, topupAmountPerChunk], + value: maxAmountIn, + }); + } else { + txHash = await walletClient.writeContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'topUp', + args: [ + route.path, + maxAmountIn, + bzzAmountBigInt, + topUpBatchId as `0x${string}`, + topupAmountPerChunk, + ], + }); + } + } else { + // Create-batch branch + const batchParams = { + owner: address as `0x${string}`, + nodeAddress: nodeAddress as `0x${string}`, + initialBalancePerChunk: BigInt(swarmConfig.swarmBatchInitialBalance), + depth: Number(swarmConfig.swarmBatchDepth), + bucketDepth: Number(swarmConfig.swarmBatchBucketDepth), + nonce: swarmConfig.swarmBatchNonce as `0x${string}`, + immutable_: Boolean(swarmConfig.swarmBatchImmutable), + }; + + if (isNative) { + txHash = await walletClient.writeContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'createBatchNative', + args: [route.path, maxAmountIn, bzzAmountBigInt, batchParams], + value: maxAmountIn, + }); + } else { + txHash = await walletClient.writeContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'createBatch', + args: [route.path, maxAmountIn, bzzAmountBigInt, batchParams], + }); + } + } + + console.log('📝 SushiSwap router tx:', txHash); + + setStatusMessage({ + step: 'Confirming', + message: 'Waiting for transaction confirmation…', + }); + + // ── 5. Wait for confirmation ─────────────────────────────────────────────── + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + timeout: TRANSACTION_TIMEOUT_MS, + pollingInterval: getPollingInterval(100), // Gnosis chainId = 100 + }); + + if (receipt.status !== 'success') { + throw new Error('SushiSwap stamp transaction failed on-chain'); + } + + console.log('✅ SushiSwap stamp transaction confirmed:', txHash); + + if (onTransactionConfirmed) { + onTransactionConfirmed(); + } +}; diff --git a/src/app/components/SwapComponent.tsx b/src/app/components/SwapComponent.tsx index 36e78d3..2166946 100644 --- a/src/app/components/SwapComponent.tsx +++ b/src/app/components/SwapComponent.tsx @@ -22,6 +22,7 @@ import { GNOSIS_DESTINATION_TOKEN, TIME_OPTIONS, GNOSIS_CUSTOM_REGISTRY_ADDRESS, + SUSHI_STAMPS_ROUTER_ADDRESS, DEFAULT_BEE_API_URL, DEFAULT_SLIPPAGE, MIN_TOKEN_BALANCE_USD, @@ -65,6 +66,7 @@ import { RelayQuoteResponse, parseRelayError, } from './RelayQuotes'; +import { getSushiQuote, executeSushiSwap, SushiQuoteResult } from './SushiQuotes'; import { handleFileUpload as uploadFile, handleMultiFileUpload, @@ -357,7 +359,7 @@ const SwapComponent: React.FC = () => { try { const bzzAmount = calculateTotalAmount().toString(); - console.log('🔍 Relay price estimation:', { + console.log('🔍 Price estimation:', { bzzAmount: formatUnits(BigInt(bzzAmount), 16), selectedDays, stampSize: @@ -366,19 +368,57 @@ const SwapComponent: React.FC = () => { fromToken, }); - // Use the new Relay system for price estimation - const relayQuoteResult = await getRelaySwapQuotes({ - selectedChainId, - fromToken, - address, - bzzAmount, - nodeAddress, - swarmConfig, - topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, - setEstimatedTime: () => {}, // Don't override estimated time during price estimation - isForEstimation: true, - slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, - }); + // ── Gnosis + non-BZZ token: use SushiSwapStampsRouter for quote ────────── + const isGnosisNonBzz = + selectedChainId === ChainId.DAI && + fromToken && + getAddress(fromToken) !== getAddress(GNOSIS_BZZ_ADDRESS) && + SUSHI_STAMPS_ROUTER_ADDRESS !== ''; + + let totalAmountUSD: number; + + if (isGnosisNonBzz) { + console.log('🍣 Using SushiSwap quote for Gnosis token…'); + + const tokenPriceUsd = selectedTokenInfo + ? Number(selectedTokenInfo.priceUSD) + : 0; + const tokenDecimals = selectedTokenInfo?.decimals ?? 18; + const tokenSymbol = selectedTokenInfo?.symbol ?? 'Token'; + + const sushiQuote = await getSushiQuote({ + fromToken, + bzzAmount, + slippagePercent: useCustomSlippage ? customSlippagePercent : DEFAULT_SLIPPAGE, + tokenSymbol, + tokenDecimals, + tokenPriceUsd, + }); + + if (abortSignal.aborted) return; + + totalAmountUSD = sushiQuote.totalAmountUSD; + console.log(`💰 SushiSwap quote: $${totalAmountUSD.toFixed(2)}`); + } else { + // ── All other cases: Relay (same-chain BZZ or cross-chain) ────────────── + const relayQuoteResult = await getRelaySwapQuotes({ + selectedChainId, + fromToken, + address, + bzzAmount, + nodeAddress, + swarmConfig, + topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, + setEstimatedTime: () => {}, + isForEstimation: true, + slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, + }); + + if (abortSignal.aborted) return; + + totalAmountUSD = relayQuoteResult.totalAmountUSD; + console.log(`💰 Relay price estimation complete: $${totalAmountUSD.toFixed(2)}`); + } // If operation was aborted, don't continue if (abortSignal.aborted) { @@ -386,10 +426,7 @@ const SwapComponent: React.FC = () => { return; } - console.log( - `💰 Relay price estimation complete: $${relayQuoteResult.totalAmountUSD.toFixed(2)}` - ); - setTotalUsdAmount(relayQuoteResult.totalAmountUSD.toString()); + setTotalUsdAmount(totalAmountUSD.toString()); // Check if user has enough funds if (selectedTokenInfo) { @@ -398,10 +435,10 @@ const SwapComponent: React.FC = () => { Number(selectedTokenInfo.priceUSD); console.log('User token balance in USD:', tokenBalanceInUsd); - console.log('Required amount in USD:', relayQuoteResult.totalAmountUSD); + console.log('Required amount in USD:', totalAmountUSD); // Set insufficient funds flag if cost exceeds available balance - setInsufficientFunds(relayQuoteResult.totalAmountUSD > tokenBalanceInUsd); + setInsufficientFunds(totalAmountUSD > tokenBalanceInUsd); } } catch (error) { // Only update error state if not aborted @@ -1053,20 +1090,109 @@ const SwapComponent: React.FC = () => { message: 'Calculating amounts...', }); - // Deciding if we are buying stamps directly or swaping/bridging + // ── Branch 1: Gnosis + BZZ direct → no swap needed ───────────────────── if ( selectedChainId !== null && selectedChainId === ChainId.DAI && getAddress(fromToken) === getAddress(GNOSIS_BZZ_ADDRESS) ) { await handleDirectBzzTransactions(updatedConfig); + } else if ( + // ── Branch 2: Gnosis + other token → SushiSwapStampsRouter ──────────── + selectedChainId !== null && + selectedChainId === ChainId.DAI && + SUSHI_STAMPS_ROUTER_ADDRESS !== '' + ) { + const tokenPriceUsd = selectedToken ? Number(selectedTokenInfo?.priceUSD ?? 0) : 0; + const tokenDecimals = selectedTokenInfo?.decimals ?? 18; + const tokenSymbol = selectedTokenInfo?.symbol ?? 'Token'; + + await executeSushiSwap({ + fromToken, + bzzAmount: updatedConfig.swarmBatchTotal, + slippagePercent: useCustomSlippage ? customSlippagePercent : DEFAULT_SLIPPAGE, + address, + swarmConfig: updatedConfig, + topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, + nodeAddress, + walletClient, + publicClient, + tokenSymbol, + tokenDecimals, + tokenPriceUsd, + setStatusMessage, + onTransactionConfirmed: () => { + console.log('🍣 SushiSwap stamp transaction confirmed!'); + }, + }); + + console.log('🎉 SushiSwap stamp purchase completed successfully!'); + + // Reset timer when done + resetTimer(); + + // Handle post-swap completion flow (same as original LiFi implementation) + try { + if (isTopUp && topUpBatchId) { + console.log('Successfully topped up batch ID:', topUpBatchId); + setPostageBatchId(topUpBatchId); + + // Set top-up completion info + setTopUpCompleted(true); + setTopUpInfo({ + batchId: topUpBatchId, + days: selectedDays || 0, + cost: totalUsdAmount || '0', + }); + + setStatusMessage({ + step: 'Complete', + message: 'Batch Topped Up Successfully', + isSuccess: true, + }); + + // Update upload history with new expiry date immediately + if (address && selectedDays) { + updateHistoryAfterTopUp(topUpBatchId, selectedDays, address); + } + } else { + // Calculate the batch ID for new batch creation + const calculatedBatchId = readBatchId( + updatedConfig.swarmBatchNonce, + GNOSIS_CUSTOM_REGISTRY_ADDRESS + ); + + console.log('Batch created successfully with ID:', calculatedBatchId); + setPostageBatchId(calculatedBatchId); + + setStatusMessage({ + step: 'Complete', + message: 'Storage Bought Successfully', + isSuccess: true, + warning: + 'Note: It takes approximately 1-2 minutes for new storage to become accessible on the network. Please wait before uploading.', + }); + + // Transition to upload step - this was missing! + setIsNewStampCreated(true); + setUploadStep('ready'); + } + } catch (error) { + console.error('Failed to process batch completion:', error); + setStatusMessage({ + step: 'Error', + message: 'Failed to process batch completion', + error: error instanceof Error ? error.message : 'Unknown error', + isError: true, + }); + } } else { + // ── Branch 3: cross-chain → Relay ────────────────────────────────────── setStatusMessage({ step: 'Quoting', message: 'Getting quote...', }); - // Use the new Relay system for execution (don't set timer yet) const relayQuoteResult = await getRelaySwapQuotes({ selectedChainId, fromToken, @@ -1075,7 +1201,7 @@ const SwapComponent: React.FC = () => { nodeAddress, swarmConfig: updatedConfig, topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, - setEstimatedTime: () => {}, // Don't set timer during quote - will be set after confirmation + setEstimatedTime: () => {}, isForEstimation: false, slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, }); @@ -1086,78 +1212,58 @@ const SwapComponent: React.FC = () => { estimatedTime: relayQuoteResult.estimatedTime, }); - console.log( - '⏱️ Timer will be set to:', - relayQuoteResult.estimatedTime, - 'seconds after transaction confirmation' - ); - - // Set initial status (timer will start after transaction confirmation) setStatusMessage({ step: 'Preparing', - message: `Preparing cross-chain swap...`, + message: 'Preparing cross-chain swap...', }); - // Execute Relay steps with timer callback await executeRelaySteps( relayQuoteResult.relayQuoteResponse, walletClient, publicClient, setStatusMessage, () => { - // Start timer after transaction confirmation console.log( - '🚀 Transaction confirmed! Starting timer with duration:', + '🚀 Transaction confirmed! Starting timer:', relayQuoteResult.estimatedTime, 'seconds' ); setEstimatedTime(relayQuoteResult.estimatedTime); setStatusMessage({ step: 'Relay', - message: `Executing cross-chain swap...`, + message: 'Executing cross-chain swap...', }); } ); console.log('🎉 Relay swap completed successfully!'); - - // Reset timer when done resetTimer(); - // Handle post-swap completion flow (same as original LiFi implementation) try { if (isTopUp && topUpBatchId) { console.log('Successfully topped up batch ID:', topUpBatchId); setPostageBatchId(topUpBatchId); - - // Set top-up completion info setTopUpCompleted(true); setTopUpInfo({ batchId: topUpBatchId, days: selectedDays || 0, cost: totalUsdAmount || '0', }); - setStatusMessage({ step: 'Complete', message: 'Batch Topped Up Successfully', isSuccess: true, }); - - // Update upload history with new expiry date immediately if (address && selectedDays) { updateHistoryAfterTopUp(topUpBatchId, selectedDays, address); } } else { - // Calculate the batch ID for new batch creation const calculatedBatchId = readBatchId( updatedConfig.swarmBatchNonce, GNOSIS_CUSTOM_REGISTRY_ADDRESS ); - console.log('Batch created successfully with ID:', calculatedBatchId); setPostageBatchId(calculatedBatchId); - setStatusMessage({ step: 'Complete', message: 'Storage Bought Successfully', @@ -1165,13 +1271,11 @@ const SwapComponent: React.FC = () => { warning: 'Note: It takes approximately 1-2 minutes for new storage to become accessible on the network. Please wait before uploading.', }); - - // Transition to upload step - this was missing! setIsNewStampCreated(true); setUploadStep('ready'); } } catch (error) { - console.error('Failed to process batch completion:', error); + console.error('Failed to process Relay batch completion:', error); setStatusMessage({ step: 'Error', message: 'Failed to process batch completion', @@ -1183,7 +1287,7 @@ const SwapComponent: React.FC = () => { } catch (error) { console.error('An error occurred:', error); - // Parse Relay-specific errors for better user experience + // Parse errors for better user experience const { userMessage, errorCode } = parseRelayError(error); // Log detailed error information for debugging diff --git a/src/app/components/constants.ts b/src/app/components/constants.ts index 554196d..4f65a56 100644 --- a/src/app/components/constants.ts +++ b/src/app/components/constants.ts @@ -256,10 +256,153 @@ export const V3_POOL_ABI = [ }, ] as const; -// Sushiswap V3 Pool address for BZZ/WXDAI on Gnosis +// Sushiswap V3 Pool address for BZZ/USDC on Gnosis (also used for price display) export const BZZ_USDC_POOL_ADDRESS = process.env.NEXT_PUBLIC_BZZ_USDC_POOL_ADDRESS || '0x6f30b7cf40cb423c1d23478a9855701ecf43931e'; +// USDC token on Gnosis (bridged) +export const GNOSIS_USDC_ADDRESS = + process.env.NEXT_PUBLIC_GNOSIS_USDC_ADDRESS || '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83'; + +// ─── SushiSwap V3 on Gnosis ──────────────────────────────────────────────── + +/** SushiSwap V3 Factory on Gnosis – used for pool discovery */ +export const SUSHI_FACTORY_ADDRESS = + process.env.NEXT_PUBLIC_SUSHI_FACTORY_ADDRESS || '0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef'; + +/** SushiSwap V3 QuoterV2 on Gnosis – used for exact-output price estimation */ +export const SUSHI_QUOTER_ADDRESS = + process.env.NEXT_PUBLIC_SUSHI_QUOTER_ADDRESS || '0xb1e835dc2785b52265711e17fccb0fd018226a6e'; + +/** + * SushiSwapStampsRouter – our deployed router that swaps any Gnosis token → BZZ + * and atomically creates / tops up a Swarm stamp in a single transaction. + * Set NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS in .env after deploying. + */ +export const SUSHI_STAMPS_ROUTER_ADDRESS = + process.env.NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS || ''; + +/** Minimal ABI for the SushiSwap V3 Factory – only what we need for pool discovery */ +export const SUSHI_FACTORY_ABI = [ + { + inputs: [ + { internalType: 'address', name: 'tokenA', type: 'address' }, + { internalType: 'address', name: 'tokenB', type: 'address' }, + { internalType: 'uint24', name: 'fee', type: 'uint24' }, + ], + name: 'getPool', + outputs: [{ internalType: 'address', name: 'pool', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +] as const; + +/** Full ABI for the SushiSwapStampsRouter contract */ +export const SUSHI_STAMPS_ROUTER_ABI = [ + // ── Quote functions (call via eth_call / simulateContract) ───────────────── + { + inputs: [ + { internalType: 'address', name: 'tokenIn', type: 'address' }, + { internalType: 'uint24', name: 'fee', type: 'uint24' }, + { internalType: 'uint256', name: 'bzzAmountOut', type: 'uint256' }, + ], + name: 'quoteSingleHop', + outputs: [{ internalType: 'uint256', name: 'amountIn', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { internalType: 'bytes', name: 'path', type: 'bytes' }, + { internalType: 'uint256', name: 'bzzAmountOut', type: 'uint256' }, + ], + name: 'quoteMultiHop', + outputs: [{ internalType: 'uint256', name: 'amountIn', type: 'uint256' }], + stateMutability: 'nonpayable', + type: 'function', + }, + // ── Create batch (ERC20 input) ───────────────────────────────────────────── + { + inputs: [ + { internalType: 'bytes', name: 'path', type: 'bytes' }, + { internalType: 'uint256', name: 'maxAmountIn', type: 'uint256' }, + { internalType: 'uint256', name: 'bzzAmountOut', type: 'uint256' }, + { + components: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'nodeAddress', type: 'address' }, + { internalType: 'uint256', name: 'initialBalancePerChunk', type: 'uint256' }, + { internalType: 'uint8', name: 'depth', type: 'uint8' }, + { internalType: 'uint8', name: 'bucketDepth', type: 'uint8' }, + { internalType: 'bytes32', name: 'nonce', type: 'bytes32' }, + { internalType: 'bool', name: 'immutable_', type: 'bool' }, + ], + internalType: 'struct SushiSwapStampsRouter.CreateBatchParams', + name: 'p', + type: 'tuple', + }, + ], + name: 'createBatch', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + // ── Create batch (native xDAI input) ────────────────────────────────────── + { + inputs: [ + { internalType: 'bytes', name: 'path', type: 'bytes' }, + { internalType: 'uint256', name: 'maxAmountIn', type: 'uint256' }, + { internalType: 'uint256', name: 'bzzAmountOut', type: 'uint256' }, + { + components: [ + { internalType: 'address', name: 'owner', type: 'address' }, + { internalType: 'address', name: 'nodeAddress', type: 'address' }, + { internalType: 'uint256', name: 'initialBalancePerChunk', type: 'uint256' }, + { internalType: 'uint8', name: 'depth', type: 'uint8' }, + { internalType: 'uint8', name: 'bucketDepth', type: 'uint8' }, + { internalType: 'bytes32', name: 'nonce', type: 'bytes32' }, + { internalType: 'bool', name: 'immutable_', type: 'bool' }, + ], + internalType: 'struct SushiSwapStampsRouter.CreateBatchParams', + name: 'p', + type: 'tuple', + }, + ], + name: 'createBatchNative', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + // ── Top up (ERC20 input) ─────────────────────────────────────────────────── + { + inputs: [ + { internalType: 'bytes', name: 'path', type: 'bytes' }, + { internalType: 'uint256', name: 'maxAmountIn', type: 'uint256' }, + { internalType: 'uint256', name: 'bzzAmountOut', type: 'uint256' }, + { internalType: 'bytes32', name: 'batchId', type: 'bytes32' }, + { internalType: 'uint256', name: 'topupAmountPerChunk', type: 'uint256' }, + ], + name: 'topUp', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + // ── Top up (native xDAI input) ───────────────────────────────────────────── + { + inputs: [ + { internalType: 'bytes', name: 'path', type: 'bytes' }, + { internalType: 'uint256', name: 'maxAmountIn', type: 'uint256' }, + { internalType: 'uint256', name: 'bzzAmountOut', type: 'uint256' }, + { internalType: 'bytes32', name: 'batchId', type: 'bytes32' }, + { internalType: 'uint256', name: 'topupAmountPerChunk', type: 'uint256' }, + ], + name: 'topUpNative', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, +] as const; + /** * Note on naming convention: The terms "Batch" and "Stamps" are used interchangeably throughout the codebase. * "Batch" refers to a collection of stamps created in a single transaction and is the terminology used in the From 6a32fc94b373082c58eaa8ef90ec087ea6dee9eb Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 20:25:06 +0200 Subject: [PATCH 02/10] fix: read actual pool fee from contract instead of hardcoding for USDC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BZZ/USDC pool fee was hardcoded as 10000 (1%) which caused quoteSingleHop to revert because the Quoter looks up the pool via factory.getPool(tokenIn, BZZ, fee) — wrong fee = pool not found. - Read pool.fee() directly from the known pool address at runtime - Keep the pool address constant (BZZ_USDC_POOL_ADDRESS) but never assume the fee tier - Fall back to factory scan if the known address doesn't respond - Reorder FEE_TIERS to try 3000/500 before 10000 (more common tiers first) - Fix fee display: divide by 10000 not 100 (10000 bps = 1%, not 100%) --- src/app/components/SushiQuotes.ts | 63 ++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/src/app/components/SushiQuotes.ts b/src/app/components/SushiQuotes.ts index d252eff..c3d3ec8 100644 --- a/src/app/components/SushiQuotes.ts +++ b/src/app/components/SushiQuotes.ts @@ -87,34 +87,64 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000'; * Common fee tiers to try when auto-discovering pools (SushiSwap V3). * Order matters — try the most common first. */ -const FEE_TIERS = [10000, 3000, 500, 100] as const; +const FEE_TIERS = [3000, 500, 10000, 100] as const; + +/** Minimal ABI for reading the fee tier directly from a V3 pool contract. */ +const POOL_FEE_ABI = parseAbi(['function fee() external view returns (uint24)']); /** - * Known direct BZZ pools on Gnosis (token → pool, fee). - * These are cached to avoid repeated factory lookups. + * Known pool addresses for tokens that have a direct BZZ pool. + * We store only the address — the fee is read from the pool contract at + * runtime so we never hardcode an assumption about the fee tier. */ -const KNOWN_BZZ_POOLS: Record = { - [GNOSIS_USDC_ADDRESS.toLowerCase()]: { pool: BZZ_USDC_POOL_ADDRESS, fee: 10000 }, +const KNOWN_BZZ_POOL_ADDRESSES: Record = { + [GNOSIS_USDC_ADDRESS.toLowerCase()]: BZZ_USDC_POOL_ADDRESS, }; +/** Runtime cache: normalised token address → { pool, fee } */ +const bzzPoolCache: Record = {}; + // ─── Pool Discovery ────────────────────────────────────────────────────────── /** - * Queries the SushiSwap V3 factory for a direct BZZ pool for `tokenIn`. - * Returns the pool address and fee tier, or null if none exists. + * Finds a direct BZZ pool for `tokenIn` on SushiSwap V3 (Gnosis). + * + * Strategy: + * 1. Return the runtime cache if already resolved. + * 2. If a known pool address exists for this token, read its fee() directly + * from the pool contract — avoids assuming the wrong fee tier. + * 3. Fall back to scanning factory.getPool() across all common fee tiers. */ async function findDirectBzzPool( tokenIn: string ): Promise<{ pool: string; fee: number } | null> { const normalised = tokenIn.toLowerCase(); - // Check the cached known pools first. - if (KNOWN_BZZ_POOLS[normalised]) { - return KNOWN_BZZ_POOLS[normalised]; + if (bzzPoolCache[normalised]) { + return bzzPoolCache[normalised]; } const { client } = getGnosisPublicClient(); + // ── Strategy 1: known pool address → read actual fee from contract ───────── + const knownPoolAddress = KNOWN_BZZ_POOL_ADDRESSES[normalised]; + if (knownPoolAddress && knownPoolAddress !== ZERO_ADDRESS) { + try { + const fee = await client.readContract({ + address: knownPoolAddress as `0x${string}`, + abi: POOL_FEE_ABI, + functionName: 'fee', + }); + const result = { pool: knownPoolAddress, fee: Number(fee) }; + bzzPoolCache[normalised] = result; + console.log(`🍣 Pool fee for ${tokenIn}: ${Number(fee)} (${Number(fee) / 10000}%)`); + return result; + } catch { + console.warn('⚠️ Could not read fee from known pool, falling back to factory scan'); + } + } + + // ── Strategy 2: scan factory for all fee tiers ──────────────────────────── for (const fee of FEE_TIERS) { try { const pool = await client.readContract({ @@ -126,8 +156,7 @@ async function findDirectBzzPool( if (pool && pool !== ZERO_ADDRESS) { const result = { pool: pool as string, fee }; - // Cache for subsequent calls. - KNOWN_BZZ_POOLS[normalised] = result; + bzzPoolCache[normalised] = result; return result; } } catch { @@ -225,8 +254,8 @@ export async function findSushiRoute(fromToken: string): Promise Date: Tue, 7 Apr 2026 20:44:13 +0200 Subject: [PATCH 03/10] fix: resolve KNOWN_BZZ_POOLS crash and broken multi-hop routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in findSushiRoute broke non-BZZ Gnosis token routing: 1. ReferenceError: KNOWN_BZZ_POOLS reference was missed when renaming to bzzPoolCache in the previous commit — crashed immediately for any two-hop route (e.g. GBPe). 2. Case 3 (tokenIn → WXDAI → BZZ) used findWxdaiUsdcPool() for the tokenIn→WXDAI leg, which looked up a WXDAI/USDC pool instead of a tokenIn/WXDAI pool — completely wrong fee and path for any token that isn't USDC. - Replace findWxdaiUsdcPool() with generic findPoolBetween(tokenA, tokenB) that scans all fee tiers for any pair, with pair-keyed caching - Case 2: use findPoolBetween(tokenIn, USDC) + findDirectBzzPool(USDC) in parallel — both legs discovered correctly - Case 3: use findPoolBetween(tokenIn, WXDAI) + findDirectBzzPool(WXDAI) in parallel — both legs discovered correctly - Tokens like GBPe that route through WXDAI or USDC now work properly --- src/app/components/SushiQuotes.ts | 106 ++++++++++++++---------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/src/app/components/SushiQuotes.ts b/src/app/components/SushiQuotes.ts index c3d3ec8..4a7cf5c 100644 --- a/src/app/components/SushiQuotes.ts +++ b/src/app/components/SushiQuotes.ts @@ -167,10 +167,23 @@ async function findDirectBzzPool( return null; } +/** Runtime cache for arbitrary token-pair pools (key = `tokenA:tokenB`). */ +const poolPairCache: Record = {}; + /** - * Queries the SushiSwap V3 factory for a WXDAI/USDC pool (needed for xDAI two-hop routing). + * Scans the SushiSwap V3 factory for any pool between `tokenA` and `tokenB` + * across all common fee tiers. Results are cached by normalised pair key. */ -async function findWxdaiUsdcPool(): Promise<{ pool: string; fee: number } | null> { +async function findPoolBetween( + tokenA: string, + tokenB: string +): Promise<{ pool: string; fee: number } | null> { + const key = [tokenA.toLowerCase(), tokenB.toLowerCase()].sort().join(':'); + + if (key in poolPairCache) { + return poolPairCache[key]; + } + const { client } = getGnosisPublicClient(); for (const fee of FEE_TIERS) { @@ -179,21 +192,20 @@ async function findWxdaiUsdcPool(): Promise<{ pool: string; fee: number } | null address: SUSHI_FACTORY_ADDRESS as `0x${string}`, abi: SUSHI_FACTORY_ABI, functionName: 'getPool', - args: [ - GNOSIS_WXDAI_ADDRESS as `0x${string}`, - GNOSIS_USDC_ADDRESS as `0x${string}`, - fee, - ], + args: [tokenA as `0x${string}`, tokenB as `0x${string}`, fee], }); if (pool && pool !== ZERO_ADDRESS) { - return { pool: pool as string, fee }; + const result = { pool: pool as string, fee }; + poolPairCache[key] = result; + return result; } } catch { // Try next fee tier. } } + poolPairCache[key] = null; return null; } @@ -260,67 +272,49 @@ export async function findSushiRoute(fromToken: string): Promise Date: Tue, 7 Apr 2026 20:48:31 +0200 Subject: [PATCH 04/10] fix: resolve ERR_UNKNOWN_FILE_EXTENSION for hardhat scripts on Node 22 The project tsconfig.json uses moduleResolution: bundler (Next.js), which ts-node can't handle, causing all `hardhat run scripts/*.ts` commands to fail with ERR_UNKNOWN_FILE_EXTENSION on Node 22. - Add tsconfig.hardhat.json with module: CommonJS / moduleResolution: node scoped to deploy/ and scripts/ only, leaving the Next.js tsconfig untouched - Set ts-node.project in package.json to point at the new tsconfig - Add npm scripts (verify:router, verify:registry, deploy:router) that set TS_NODE_PROJECT=tsconfig.hardhat.json automatically so plain `npm run verify:router` just works - Update script usage comments to show both the npm shortcut and the direct command with the required env var --- package.json | 9 ++++++++- scripts/verify_registry.ts | 10 +++++++--- scripts/verify_router.ts | 11 +++++++---- tsconfig.hardhat.json | 14 ++++++++++++++ 4 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 tsconfig.hardhat.json diff --git a/package.json b/package.json index f51a326..95b2eed 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,10 @@ "start": "next start", "lint": "next lint", "format": "prettier --write .", - "format:check": "prettier --check ." + "format:check": "prettier --check .", + "deploy:router": "TS_NODE_PROJECT=tsconfig.hardhat.json npx hardhat deploy --network gnosis --tags SushiSwapStampsRouter", + "verify:router": "TS_NODE_PROJECT=tsconfig.hardhat.json npx hardhat run scripts/verify_router.ts --network gnosis", + "verify:registry": "TS_NODE_PROJECT=tsconfig.hardhat.json npx hardhat run scripts/verify_registry.ts --network gnosis" }, "dependencies": { "@formbricks/js": "^4.1.0", @@ -43,6 +46,10 @@ "wagmi": "^2.12.17", "zod": "^3.25.76" }, + "ts-node": { + "project": "./tsconfig.hardhat.json", + "transpileOnly": true + }, "devDependencies": { "@nomicfoundation/hardhat-ethers": "^3.0.8", "@nomicfoundation/hardhat-toolbox": "^5.0.0", diff --git a/scripts/verify_registry.ts b/scripts/verify_registry.ts index b503ab8..0169491 100644 --- a/scripts/verify_registry.ts +++ b/scripts/verify_registry.ts @@ -3,10 +3,14 @@ * * Usage * ───── - * npx hardhat run scripts/verify_registry.ts --network gnosis + * # Convenience npm script (sets TS_NODE_PROJECT automatically): + * npm run verify:registry * - * # Or pass the address explicitly: - * REGISTRY_ADDRESS=0xYourAddress npx hardhat run scripts/verify_registry.ts --network gnosis + * # Or directly (TS_NODE_PROJECT required for Node 22 + Next.js tsconfig): + * TS_NODE_PROJECT=tsconfig.hardhat.json npx hardhat run scripts/verify_registry.ts --network gnosis + * + * # Pass the address explicitly: + * REGISTRY_ADDRESS=0xYourAddress npm run verify:registry */ import { run, deployments } from 'hardhat'; diff --git a/scripts/verify_router.ts b/scripts/verify_router.ts index 16e0efb..2140662 100644 --- a/scripts/verify_router.ts +++ b/scripts/verify_router.ts @@ -7,11 +7,14 @@ * * Usage * ───── - * # From the project root: - * npx hardhat run scripts/verify_router.ts --network gnosis + * # Convenience npm script (sets TS_NODE_PROJECT automatically): + * npm run verify:router * - * # Or pass the address via env variable (overrides the deployments/ cache): - * ROUTER_ADDRESS=0xYourDeployedAddress npx hardhat run scripts/verify_router.ts --network gnosis + * # Or directly (TS_NODE_PROJECT required for Node 22 + Next.js tsconfig): + * TS_NODE_PROJECT=tsconfig.hardhat.json npx hardhat run scripts/verify_router.ts --network gnosis + * + * # Pass the router address explicitly (overrides the deployments/ cache): + * ROUTER_ADDRESS=0xYourDeployedAddress npm run verify:router * * Requirements * ──────────── diff --git a/tsconfig.hardhat.json b/tsconfig.hardhat.json new file mode 100644 index 0000000..50c39e6 --- /dev/null +++ b/tsconfig.hardhat.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "outDir": "./dist-hardhat" + }, + "include": ["hardhat.config.ts", "deploy/**/*.ts", "scripts/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "src"] +} From 03368e27022f9df6eb0efb52ebbede1884312432 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 20:49:28 +0200 Subject: [PATCH 05/10] fix verify --- deployments/gnosis/SushiSwapStampsRouter.json | 609 ++++++++++++++++++ .../90fe721cd304da2d010b5da7732aee0a.json | 40 ++ hardhat.config.ts | 1 + 3 files changed, 650 insertions(+) create mode 100644 deployments/gnosis/SushiSwapStampsRouter.json create mode 100644 deployments/gnosis/solcInputs/90fe721cd304da2d010b5da7732aee0a.json diff --git a/deployments/gnosis/SushiSwapStampsRouter.json b/deployments/gnosis/SushiSwapStampsRouter.json new file mode 100644 index 0000000..25a72c1 --- /dev/null +++ b/deployments/gnosis/SushiSwapStampsRouter.json @@ -0,0 +1,609 @@ +{ + "address": "0x2a0a54368Bb6b0D8fa31568D092ffBDf350ab553", + "abi": [ + { + "inputs": [ + { + "internalType": "address", + "name": "_stampsRegistry", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "BzzApproveFailed", + "type": "error" + }, + { + "inputs": [], + "name": "BzzTransferFailed", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientNativeValue", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidCallback", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPath", + "type": "error" + }, + { + "inputs": [], + "name": "NativeRefundFailed", + "type": "error" + }, + { + "inputs": [], + "name": "PoolNotFound", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "required", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "maximum", + "type": "uint256" + } + ], + "name": "SlippageExceeded", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "batchId", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "bzzAmount", + "type": "uint256" + } + ], + "name": "BatchCreatedViaSwap", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "batchId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "bzzAmount", + "type": "uint256" + } + ], + "name": "BatchToppedUpViaSwap", + "type": "event" + }, + { + "inputs": [], + "name": "BZZ", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SUSHI_FACTORY", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "SUSHI_QUOTER", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "WXDAI", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bzzAmountOut", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "nodeAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialBalancePerChunk", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "depth", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "bucketDepth", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "nonce", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "immutable_", + "type": "bool" + } + ], + "internalType": "struct SushiSwapStampsRouter.CreateBatchParams", + "name": "p", + "type": "tuple" + } + ], + "name": "createBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bzzAmountOut", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "nodeAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "initialBalancePerChunk", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "depth", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "bucketDepth", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "nonce", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "immutable_", + "type": "bool" + } + ], + "internalType": "struct SushiSwapStampsRouter.CreateBatchParams", + "name": "p", + "type": "tuple" + } + ], + "name": "createBatchNative", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "bzzAmountOut", + "type": "uint256" + } + ], + "name": "quoteMultiHop", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "tokenIn", + "type": "address" + }, + { + "internalType": "uint24", + "name": "fee", + "type": "uint24" + }, + { + "internalType": "uint256", + "name": "bzzAmountOut", + "type": "uint256" + } + ], + "name": "quoteSingleHop", + "outputs": [ + { + "internalType": "uint256", + "name": "amountIn", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "stampsRegistry", + "outputs": [ + { + "internalType": "contract IStampsRegistry", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bzzAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "batchId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "topupAmountPerChunk", + "type": "uint256" + } + ], + "name": "topUp", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "path", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "maxAmountIn", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "bzzAmountOut", + "type": "uint256" + }, + { + "internalType": "bytes32", + "name": "batchId", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "topupAmountPerChunk", + "type": "uint256" + } + ], + "name": "topUpNative", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "int256", + "name": "amount0Delta", + "type": "int256" + }, + { + "internalType": "int256", + "name": "amount1Delta", + "type": "int256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "uniswapV3SwapCallback", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } + ], + "transactionHash": "0x1e89af5249b93f7491b74e4e7c88398c21738fc2abced8adf39f501def851115", + "receipt": { + "to": null, + "from": "0xb1C7F17Ed88189Abf269Bf68A3B2Ed83C5276aAe", + "contractAddress": "0x2a0a54368Bb6b0D8fa31568D092ffBDf350ab553", + "transactionIndex": 14, + "gasUsed": "1396096", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "blockHash": "0x96eb23ebc9b335635f99f29cb9e81f05a1a816f46b82fcdd09f618e8860e819a", + "transactionHash": "0x1e89af5249b93f7491b74e4e7c88398c21738fc2abced8adf39f501def851115", + "logs": [], + "blockNumber": 45555563, + "cumulativeGasUsed": "8074489", + "status": 1, + "byzantium": true + }, + "args": [ + "0x5EBfBeFB1E88391eFb022d5d33302f50a46bF4f3" + ], + "numDeployments": 1, + "solcInputHash": "90fe721cd304da2d010b5da7732aee0a", + "metadata": "{\"compiler\":{\"version\":\"0.8.23+commit.f704f362\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[{\"internalType\":\"address\",\"name\":\"_stampsRegistry\",\"type\":\"address\"}],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[],\"name\":\"BzzApproveFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"BzzTransferFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InsufficientNativeValue\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidCallback\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"InvalidPath\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"NativeRefundFailed\",\"type\":\"error\"},{\"inputs\":[],\"name\":\"PoolNotFound\",\"type\":\"error\"},{\"inputs\":[{\"internalType\":\"uint256\",\"name\":\"required\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"maximum\",\"type\":\"uint256\"}],\"name\":\"SlippageExceeded\",\"type\":\"error\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"batchId\",\"type\":\"bytes32\"},{\"indexed\":true,\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"tokenIn\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"bzzAmount\",\"type\":\"uint256\"}],\"name\":\"BatchCreatedViaSwap\",\"type\":\"event\"},{\"anonymous\":false,\"inputs\":[{\"indexed\":true,\"internalType\":\"bytes32\",\"name\":\"batchId\",\"type\":\"bytes32\"},{\"indexed\":false,\"internalType\":\"address\",\"name\":\"tokenIn\",\"type\":\"address\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"},{\"indexed\":false,\"internalType\":\"uint256\",\"name\":\"bzzAmount\",\"type\":\"uint256\"}],\"name\":\"BatchToppedUpViaSwap\",\"type\":\"event\"},{\"inputs\":[],\"name\":\"BZZ\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"SUSHI_FACTORY\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"SUSHI_QUOTER\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"WXDAI\",\"outputs\":[{\"internalType\":\"address\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"path\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"maxAmountIn\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"bzzAmountOut\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"nodeAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialBalancePerChunk\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"depth\",\"type\":\"uint8\"},{\"internalType\":\"uint8\",\"name\":\"bucketDepth\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"nonce\",\"type\":\"bytes32\"},{\"internalType\":\"bool\",\"name\":\"immutable_\",\"type\":\"bool\"}],\"internalType\":\"struct SushiSwapStampsRouter.CreateBatchParams\",\"name\":\"p\",\"type\":\"tuple\"}],\"name\":\"createBatch\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"path\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"maxAmountIn\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"bzzAmountOut\",\"type\":\"uint256\"},{\"components\":[{\"internalType\":\"address\",\"name\":\"owner\",\"type\":\"address\"},{\"internalType\":\"address\",\"name\":\"nodeAddress\",\"type\":\"address\"},{\"internalType\":\"uint256\",\"name\":\"initialBalancePerChunk\",\"type\":\"uint256\"},{\"internalType\":\"uint8\",\"name\":\"depth\",\"type\":\"uint8\"},{\"internalType\":\"uint8\",\"name\":\"bucketDepth\",\"type\":\"uint8\"},{\"internalType\":\"bytes32\",\"name\":\"nonce\",\"type\":\"bytes32\"},{\"internalType\":\"bool\",\"name\":\"immutable_\",\"type\":\"bool\"}],\"internalType\":\"struct SushiSwapStampsRouter.CreateBatchParams\",\"name\":\"p\",\"type\":\"tuple\"}],\"name\":\"createBatchNative\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"path\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"bzzAmountOut\",\"type\":\"uint256\"}],\"name\":\"quoteMultiHop\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address\",\"name\":\"tokenIn\",\"type\":\"address\"},{\"internalType\":\"uint24\",\"name\":\"fee\",\"type\":\"uint24\"},{\"internalType\":\"uint256\",\"name\":\"bzzAmountOut\",\"type\":\"uint256\"}],\"name\":\"quoteSingleHop\",\"outputs\":[{\"internalType\":\"uint256\",\"name\":\"amountIn\",\"type\":\"uint256\"}],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[],\"name\":\"stampsRegistry\",\"outputs\":[{\"internalType\":\"contract IStampsRegistry\",\"name\":\"\",\"type\":\"address\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"path\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"maxAmountIn\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"bzzAmountOut\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"batchId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"topupAmountPerChunk\",\"type\":\"uint256\"}],\"name\":\"topUp\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"bytes\",\"name\":\"path\",\"type\":\"bytes\"},{\"internalType\":\"uint256\",\"name\":\"maxAmountIn\",\"type\":\"uint256\"},{\"internalType\":\"uint256\",\"name\":\"bzzAmountOut\",\"type\":\"uint256\"},{\"internalType\":\"bytes32\",\"name\":\"batchId\",\"type\":\"bytes32\"},{\"internalType\":\"uint256\",\"name\":\"topupAmountPerChunk\",\"type\":\"uint256\"}],\"name\":\"topUpNative\",\"outputs\":[],\"stateMutability\":\"payable\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"int256\",\"name\":\"amount0Delta\",\"type\":\"int256\"},{\"internalType\":\"int256\",\"name\":\"amount1Delta\",\"type\":\"int256\"},{\"internalType\":\"bytes\",\"name\":\"data\",\"type\":\"bytes\"}],\"name\":\"uniswapV3SwapCallback\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"},{\"stateMutability\":\"payable\",\"type\":\"receive\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"createBatch(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))\":{\"details\":\"`tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.\",\"params\":{\"bzzAmountOut\":\"Exact BZZ needed (= swarmBatchTotal = initialBalancePerChunk \\u00d7 2^depth)\",\"maxAmountIn\":\"Maximum tokenIn to spend (slippage protection)\",\"p\":\"Batch creation parameters\",\"path\":\"Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\"}},\"createBatchNative(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))\":{\"details\":\"Send msg.value \\u2265 maxAmountIn. Excess xDAI is refunded.\",\"params\":{\"bzzAmountOut\":\"Exact BZZ needed\",\"maxAmountIn\":\"Maximum xDAI to spend\",\"p\":\"Batch creation parameters\",\"path\":\"Exact-output path where the final token MUST be WXDAI: BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI\"}},\"quoteMultiHop(bytes,uint256)\":{\"params\":{\"bzzAmountOut\":\"Exact BZZ amount wanted\",\"path\":\"Exact-output encoded path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\"},\"returns\":{\"amountIn\":\" Input tokens required (before slippage)\"}},\"quoteSingleHop(address,uint24,uint256)\":{\"params\":{\"bzzAmountOut\":\"Exact BZZ amount wanted\",\"fee\":\"Pool fee tier (e.g. 500, 3000, 10000)\",\"tokenIn\":\"Input token (use WXDAI for native xDAI quotes)\"},\"returns\":{\"amountIn\":\" Input tokens required (before slippage)\"}},\"topUp(bytes,uint256,uint256,bytes32,uint256)\":{\"details\":\"`tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.\",\"params\":{\"batchId\":\"Batch to top up\",\"bzzAmountOut\":\"Exact BZZ needed (= topupAmountPerChunk \\u00d7 2^depth)\",\"maxAmountIn\":\"Maximum tokenIn to spend\",\"path\":\"Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\",\"topupAmountPerChunk\":\"Per-chunk top-up amount (matches registry call)\"}},\"topUpNative(bytes,uint256,uint256,bytes32,uint256)\":{\"details\":\"Send msg.value \\u2265 maxAmountIn. Excess xDAI is refunded.\",\"params\":{\"batchId\":\"Batch to top up\",\"bzzAmountOut\":\"Exact BZZ needed\",\"maxAmountIn\":\"Maximum xDAI to spend\",\"path\":\"BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI\",\"topupAmountPerChunk\":\"Per-chunk top-up amount\"}},\"uniswapV3SwapCallback(int256,int256,bytes)\":{\"details\":\"Implements the Uniswap V3 callback interface (SushiSwap V3 is compatible). For multi-hop swaps, this callback chains into the next pool swap before paying the current pool, routing tokens directly between pools.\"}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"BZZ()\":{\"notice\":\"BZZ token on Gnosis\"},\"SUSHI_FACTORY()\":{\"notice\":\"SushiSwap V3 Factory on Gnosis\"},\"SUSHI_QUOTER()\":{\"notice\":\"SushiSwap V3 QuoterV2 on Gnosis\"},\"WXDAI()\":{\"notice\":\"Wrapped xDAI on Gnosis\"},\"createBatch(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))\":{\"notice\":\"Swap `tokenIn` \\u2192 BZZ via the given path and create a Swarm stamp batch.\"},\"createBatchNative(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))\":{\"notice\":\"Swap native xDAI \\u2192 BZZ and create a Swarm stamp batch.\"},\"quoteMultiHop(bytes,uint256)\":{\"notice\":\"Quote: how many input tokens are needed to get exactly `bzzAmountOut` BZZ via a multi-hop path.\"},\"quoteSingleHop(address,uint24,uint256)\":{\"notice\":\"Quote: how many `tokenIn` are needed to get exactly `bzzAmountOut` BZZ via a single-hop pool.\"},\"topUp(bytes,uint256,uint256,bytes32,uint256)\":{\"notice\":\"Swap `tokenIn` \\u2192 BZZ and top up an existing Swarm stamp batch.\"},\"topUpNative(bytes,uint256,uint256,bytes32,uint256)\":{\"notice\":\"Swap native xDAI \\u2192 BZZ and top up an existing Swarm stamp batch.\"},\"uniswapV3SwapCallback(int256,int256,bytes)\":{\"notice\":\"Called by a SushiSwap V3 pool during swap execution.\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/SushiSwapStampsRouter.sol\":\"SushiSwapStampsRouter\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"bytecodeHash\":\"ipfs\",\"useLiteralContent\":true},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[],\"viaIR\":true},\"sources\":{\"contracts/SushiSwapStampsRouter.sol\":{\"content\":\"// SPDX-License-Identifier: MIT\\npragma solidity ^0.8.23;\\n\\n/*\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2557\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\n \\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2550\\u255d\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2550\\u255d\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2550\\u255d\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2551 \\u2588\\u2557 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d\\n \\u255a\\u2550\\u2550\\u2550\\u2550\\u2588\\u2588\\u2551\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u255a\\u2550\\u2550\\u2550\\u2550\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\u255a\\u2550\\u2550\\u2550\\u2550\\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u255d\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\u255a\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\u255a\\u2588\\u2588\\u2588\\u2554\\u2588\\u2588\\u2588\\u2554\\u255d\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\n \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d\\u255a\\u2550\\u255d \\u255a\\u2550\\u255d\\u255a\\u2550\\u255d\\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u2550\\u255d\\u255a\\u2550\\u2550\\u255d \\u255a\\u2550\\u255d \\u255a\\u2550\\u255d\\u255a\\u2550\\u255d\\n\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\n \\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2550\\u255d\\u255a\\u2550\\u2550\\u2588\\u2588\\u2554\\u2550\\u2550\\u255d\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2550\\u255d\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2588\\u2588\\u2588\\u2588\\u2554\\u2588\\u2588\\u2551\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\n \\u255a\\u2550\\u2550\\u2550\\u2550\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2551\\u2588\\u2588\\u2551\\u255a\\u2588\\u2588\\u2554\\u255d\\u2588\\u2588\\u2551\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2588\\u2588\\u2551\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551 \\u255a\\u2550\\u255d \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2551\\n \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u255d \\u255a\\u2550\\u255d \\u255a\\u2550\\u255d\\u255a\\u2550\\u255d \\u255a\\u2550\\u255d\\u255a\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d\\n\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2557 \\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\n \\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2588\\u2588\\u2557\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u255a\\u2550\\u2550\\u2588\\u2588\\u2554\\u2550\\u2550\\u255d\\u2588\\u2588\\u2554\\u2550\\u2550\\u2550\\u2550\\u255d\\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\n \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2557 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d\\n \\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2554\\u2550\\u2550\\u255d \\u2588\\u2588\\u2554\\u2550\\u2550\\u2588\\u2588\\u2557\\n \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\u255a\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d\\u255a\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2554\\u255d \\u2588\\u2588\\u2551 \\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2588\\u2557\\u2588\\u2588\\u2551 \\u2588\\u2588\\u2551\\n \\u255a\\u2550\\u255d \\u255a\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d \\u255a\\u2550\\u255d \\u255a\\u2550\\u2550\\u2550\\u2550\\u2550\\u2550\\u255d\\u255a\\u2550\\u255d \\u255a\\u2550\\u255d\\n*/\\n\\n/**\\n * @title SushiSwapStampsRouter\\n * @notice Swaps any Gnosis-chain token to BZZ via SushiSwap V3 and atomically\\n * creates or tops up a Swarm postage-stamp batch in a single transaction.\\n *\\n * @dev Implements the Uniswap V3 callback interface (SushiSwap V3 is fully compatible).\\n * Supports both single-hop and multi-hop exact-output swaps via path encoding.\\n *\\n * Path encoding for exactOutput swaps (reversed token order):\\n * single-hop: BZZ ++ uint24(fee) ++ tokenIn (43 bytes)\\n * two-hop: BZZ ++ uint24(fee2) ++ mid ++ uint24(fee1) ++ tokenIn (66 bytes)\\n *\\n * Quote functions are non-view (Quoter simulates swaps internally) but are\\n * designed to be called via eth_call for gas-free estimation.\\n *\\n * Gnosis-chain addresses (hardcoded):\\n * BZZ = 0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da\\n * WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d\\n * Quoter = 0xb1e835dc2785b52265711e17fccb0fd018226a6e (SushiSwap V3 QuoterV2)\\n * Factory= 0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef (SushiSwap V3 Factory)\\n */\\n\\n// \\u2500\\u2500\\u2500 Interfaces \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\ninterface IERC20 {\\n function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);\\n function transfer(address recipient, uint256 amount) external returns (bool);\\n function approve(address spender, uint256 amount) external returns (bool);\\n function balanceOf(address account) external view returns (uint256);\\n}\\n\\ninterface IWXDAI {\\n function deposit() external payable;\\n function withdraw(uint256 amount) external;\\n function approve(address spender, uint256 amount) external returns (bool);\\n function transfer(address recipient, uint256 amount) external returns (bool);\\n function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);\\n function balanceOf(address account) external view returns (uint256);\\n}\\n\\ninterface ISushiV3Pool {\\n function swap(\\n address recipient,\\n bool zeroForOne,\\n int256 amountSpecified,\\n uint160 sqrtPriceLimitX96,\\n bytes calldata data\\n ) external returns (int256 amount0, int256 amount1);\\n\\n function token0() external view returns (address);\\n function token1() external view returns (address);\\n function fee() external view returns (uint24);\\n}\\n\\ninterface ISushiV3Factory {\\n function getPool(\\n address tokenA,\\n address tokenB,\\n uint24 fee\\n ) external view returns (address pool);\\n}\\n\\ninterface IQuoterV2 {\\n struct QuoteExactOutputSingleParams {\\n address tokenIn;\\n address tokenOut;\\n uint256 amount;\\n uint24 fee;\\n uint160 sqrtPriceLimitX96;\\n }\\n\\n function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params)\\n external\\n returns (\\n uint256 amountIn,\\n uint160 sqrtPriceX96After,\\n uint32 initializedTicksCrossed,\\n uint256 gasEstimate\\n );\\n\\n function quoteExactOutput(bytes memory path, uint256 amountOut)\\n external\\n returns (\\n uint256 amountIn,\\n uint160[] memory sqrtPriceX96AfterList,\\n uint32[] memory initializedTicksCrossedList,\\n uint256 gasEstimate\\n );\\n}\\n\\ninterface IStampsRegistry {\\n function createBatchRegistry(\\n address _owner,\\n address _nodeAddress,\\n uint256 _initialBalancePerChunk,\\n uint8 _depth,\\n uint8 _bucketDepth,\\n bytes32 _nonce,\\n bool _immutable\\n ) external;\\n\\n function topUpBatch(bytes32 _batchId, uint256 _topupAmountPerChunk) external;\\n}\\n\\n// \\u2500\\u2500\\u2500 Router Contract \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\ncontract SushiSwapStampsRouter {\\n\\n // \\u2500\\u2500\\u2500 Constants \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n /// @notice BZZ token on Gnosis\\n address public constant BZZ = 0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da;\\n\\n /// @notice Wrapped xDAI on Gnosis\\n address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d;\\n\\n /// @notice SushiSwap V3 QuoterV2 on Gnosis\\n address public constant SUSHI_QUOTER = 0xb1E835Dc2785b52265711e17fCCb0fd018226a6e;\\n\\n /// @notice SushiSwap V3 Factory on Gnosis\\n address public constant SUSHI_FACTORY = 0xf78031CBCA409F2FB6876BDFDBc1b2df24cF9bEf;\\n\\n /// @notice Minimum sqrt price limit (used when selling token0 \\u2192 token1, zeroForOne=true)\\n uint160 internal constant MIN_SQRT_RATIO = 4295128739;\\n\\n /// @notice Maximum sqrt price limit (used when selling token1 \\u2192 token0, zeroForOne=false)\\n uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;\\n\\n // Path encoding offsets (bytes): address=20, fee=3, nextOffset=23, popOffset=43\\n uint256 private constant ADDR_SIZE = 20;\\n uint256 private constant FEE_SIZE = 3;\\n uint256 private constant NEXT_OFFSET = 23; // ADDR_SIZE + FEE_SIZE\\n uint256 private constant POP_OFFSET = 43; // NEXT_OFFSET + ADDR_SIZE\\n\\n // \\u2500\\u2500\\u2500 Immutables \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n IStampsRegistry public immutable stampsRegistry;\\n\\n // \\u2500\\u2500\\u2500 Events \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n event BatchCreatedViaSwap(\\n bytes32 indexed batchId,\\n address indexed owner,\\n address tokenIn,\\n uint256 amountIn,\\n uint256 bzzAmount\\n );\\n\\n event BatchToppedUpViaSwap(\\n bytes32 indexed batchId,\\n address tokenIn,\\n uint256 amountIn,\\n uint256 bzzAmount\\n );\\n\\n // \\u2500\\u2500\\u2500 Errors \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n error InvalidCallback();\\n error SlippageExceeded(uint256 required, uint256 maximum);\\n error InsufficientNativeValue();\\n error NativeRefundFailed();\\n error BzzTransferFailed();\\n error BzzApproveFailed();\\n error PoolNotFound();\\n error InvalidPath();\\n\\n // \\u2500\\u2500\\u2500 Structs \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n struct CreateBatchParams {\\n address owner;\\n address nodeAddress;\\n uint256 initialBalancePerChunk;\\n uint8 depth;\\n uint8 bucketDepth;\\n bytes32 nonce;\\n bool immutable_;\\n }\\n\\n /// @dev Packed into the `data` argument of pool.swap(); threaded through callback chains.\\n struct SwapCallbackData {\\n bytes path; // remaining path in exactOutput encoding (BZZ-first)\\n address payer; // who pays the input token (address(this) for native swaps)\\n uint256 maxAmountIn; // slippage ceiling for the final (tokenIn) leg\\n }\\n\\n // \\u2500\\u2500\\u2500 Constructor \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n constructor(address _stampsRegistry) {\\n stampsRegistry = IStampsRegistry(_stampsRegistry);\\n }\\n\\n receive() external payable {}\\n\\n // \\u2500\\u2500\\u2500 Quote Functions \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n // These modify state internally (Quoter simulates swaps) but are designed to\\n // be called via eth_call for free gas-less estimation.\\n\\n /**\\n * @notice Quote: how many `tokenIn` are needed to get exactly `bzzAmountOut` BZZ\\n * via a single-hop pool.\\n * @param tokenIn Input token (use WXDAI for native xDAI quotes)\\n * @param fee Pool fee tier (e.g. 500, 3000, 10000)\\n * @param bzzAmountOut Exact BZZ amount wanted\\n * @return amountIn Input tokens required (before slippage)\\n */\\n function quoteSingleHop(\\n address tokenIn,\\n uint24 fee,\\n uint256 bzzAmountOut\\n ) external returns (uint256 amountIn) {\\n (amountIn,,,) = IQuoterV2(SUSHI_QUOTER).quoteExactOutputSingle(\\n IQuoterV2.QuoteExactOutputSingleParams({\\n tokenIn: tokenIn,\\n tokenOut: BZZ,\\n amount: bzzAmountOut,\\n fee: fee,\\n sqrtPriceLimitX96: 0\\n })\\n );\\n }\\n\\n /**\\n * @notice Quote: how many input tokens are needed to get exactly `bzzAmountOut` BZZ\\n * via a multi-hop path.\\n * @param path Exact-output encoded path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\\n * @param bzzAmountOut Exact BZZ amount wanted\\n * @return amountIn Input tokens required (before slippage)\\n */\\n function quoteMultiHop(\\n bytes calldata path,\\n uint256 bzzAmountOut\\n ) external returns (uint256 amountIn) {\\n (amountIn,,,) = IQuoterV2(SUSHI_QUOTER).quoteExactOutput(path, bzzAmountOut);\\n }\\n\\n // \\u2500\\u2500\\u2500 Create Batch \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n /**\\n * @notice Swap `tokenIn` \\u2192 BZZ via the given path and create a Swarm stamp batch.\\n * @dev `tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.\\n * @param path Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\\n * @param maxAmountIn Maximum tokenIn to spend (slippage protection)\\n * @param bzzAmountOut Exact BZZ needed (= swarmBatchTotal = initialBalancePerChunk \\u00d7 2^depth)\\n * @param p Batch creation parameters\\n */\\n function createBatch(\\n bytes calldata path,\\n uint256 maxAmountIn,\\n uint256 bzzAmountOut,\\n CreateBatchParams calldata p\\n ) external {\\n _swapExactOutput(path, msg.sender, maxAmountIn, bzzAmountOut);\\n bytes32 batchId = _approveBzzAndCreate(p, bzzAmountOut);\\n address tokenIn = _lastToken(path);\\n emit BatchCreatedViaSwap(batchId, p.owner, tokenIn, maxAmountIn, bzzAmountOut);\\n }\\n\\n /**\\n * @notice Swap native xDAI \\u2192 BZZ and create a Swarm stamp batch.\\n * @dev Send msg.value \\u2265 maxAmountIn. Excess xDAI is refunded.\\n * @param path Exact-output path where the final token MUST be WXDAI:\\n * BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI\\n * @param maxAmountIn Maximum xDAI to spend\\n * @param bzzAmountOut Exact BZZ needed\\n * @param p Batch creation parameters\\n */\\n function createBatchNative(\\n bytes calldata path,\\n uint256 maxAmountIn,\\n uint256 bzzAmountOut,\\n CreateBatchParams calldata p\\n ) external payable {\\n if (msg.value < maxAmountIn) revert InsufficientNativeValue();\\n IWXDAI(WXDAI).deposit{value: maxAmountIn}();\\n _swapExactOutput(path, address(this), maxAmountIn, bzzAmountOut);\\n bytes32 batchId = _approveBzzAndCreate(p, bzzAmountOut);\\n emit BatchCreatedViaSwap(batchId, p.owner, address(0), maxAmountIn, bzzAmountOut);\\n _refundNative();\\n }\\n\\n // \\u2500\\u2500\\u2500 Top Up Batch \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n /**\\n * @notice Swap `tokenIn` \\u2192 BZZ and top up an existing Swarm stamp batch.\\n * @dev `tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.\\n * @param path Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\\n * @param maxAmountIn Maximum tokenIn to spend\\n * @param bzzAmountOut Exact BZZ needed (= topupAmountPerChunk \\u00d7 2^depth)\\n * @param batchId Batch to top up\\n * @param topupAmountPerChunk Per-chunk top-up amount (matches registry call)\\n */\\n function topUp(\\n bytes calldata path,\\n uint256 maxAmountIn,\\n uint256 bzzAmountOut,\\n bytes32 batchId,\\n uint256 topupAmountPerChunk\\n ) external {\\n _swapExactOutput(path, msg.sender, maxAmountIn, bzzAmountOut);\\n _approveBzzAndTopUp(batchId, topupAmountPerChunk, bzzAmountOut);\\n address tokenIn = _lastToken(path);\\n emit BatchToppedUpViaSwap(batchId, tokenIn, maxAmountIn, bzzAmountOut);\\n }\\n\\n /**\\n * @notice Swap native xDAI \\u2192 BZZ and top up an existing Swarm stamp batch.\\n * @dev Send msg.value \\u2265 maxAmountIn. Excess xDAI is refunded.\\n * @param path BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI\\n * @param maxAmountIn Maximum xDAI to spend\\n * @param bzzAmountOut Exact BZZ needed\\n * @param batchId Batch to top up\\n * @param topupAmountPerChunk Per-chunk top-up amount\\n */\\n function topUpNative(\\n bytes calldata path,\\n uint256 maxAmountIn,\\n uint256 bzzAmountOut,\\n bytes32 batchId,\\n uint256 topupAmountPerChunk\\n ) external payable {\\n if (msg.value < maxAmountIn) revert InsufficientNativeValue();\\n IWXDAI(WXDAI).deposit{value: maxAmountIn}();\\n _swapExactOutput(path, address(this), maxAmountIn, bzzAmountOut);\\n _approveBzzAndTopUp(batchId, topupAmountPerChunk, bzzAmountOut);\\n emit BatchToppedUpViaSwap(batchId, address(0), maxAmountIn, bzzAmountOut);\\n _refundNative();\\n }\\n\\n // \\u2500\\u2500\\u2500 Uniswap V3 / SushiSwap V3 Swap Callback \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n /**\\n * @notice Called by a SushiSwap V3 pool during swap execution.\\n * @dev Implements the Uniswap V3 callback interface (SushiSwap V3 is compatible).\\n * For multi-hop swaps, this callback chains into the next pool swap before\\n * paying the current pool, routing tokens directly between pools.\\n */\\n function uniswapV3SwapCallback(\\n int256 amount0Delta,\\n int256 amount1Delta,\\n bytes calldata data\\n ) external {\\n require(amount0Delta > 0 || amount1Delta > 0, \\\"Zero deltas\\\");\\n\\n SwapCallbackData memory cb = abi.decode(data, (SwapCallbackData));\\n\\n // Decode the first pool in the path to verify the caller is legitimate.\\n (address tokenOut, uint24 fee, address tokenIn) = _decodeFirstPool(cb.path);\\n address expectedPool = ISushiV3Factory(SUSHI_FACTORY).getPool(tokenOut, tokenIn, fee);\\n if (msg.sender != expectedPool) revert InvalidCallback();\\n\\n // Determine which token we owe to the calling pool and how much.\\n // The positive delta is what the pool expects us to pay.\\n (address tokenOwed, uint256 amountOwed) = amount0Delta > 0\\n ? (ISushiV3Pool(msg.sender).token0(), uint256(amount0Delta))\\n : (ISushiV3Pool(msg.sender).token1(), uint256(amount1Delta));\\n\\n if (_hasMultiplePools(cb.path)) {\\n // Multi-hop: continue to next pool. Skip the first token from path to get\\n // the remaining sub-path: mid ++ fee ++ ... ++ tokenIn\\n bytes memory remainingPath = _skipToken(cb.path);\\n\\n // Decode the next pool info from remaining path.\\n (address nextTokenOut, uint24 nextFee, address nextTokenIn) = _decodeFirstPool(remainingPath);\\n address nextPool = ISushiV3Factory(SUSHI_FACTORY).getPool(nextTokenOut, nextTokenIn, nextFee);\\n if (nextPool == address(0)) revert PoolNotFound();\\n\\n // Swap in the next pool, sending output directly to msg.sender (current pool)\\n // so it receives the tokens it needs without going through this contract.\\n bool zeroForOne = nextTokenIn < nextTokenOut;\\n ISushiV3Pool(nextPool).swap(\\n msg.sender, // recipient = current pool (gets tokenOwed directly)\\n zeroForOne,\\n -int256(amountOwed), // exact output = amountOwed\\n zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,\\n abi.encode(SwapCallbackData({\\n path: remainingPath,\\n payer: cb.payer,\\n maxAmountIn: cb.maxAmountIn\\n }))\\n );\\n } else {\\n // Final hop: pay tokenOwed from the original payer.\\n if (amountOwed > cb.maxAmountIn) {\\n revert SlippageExceeded(amountOwed, cb.maxAmountIn);\\n }\\n\\n if (cb.payer == address(this)) {\\n // Native xDAI flow: we already hold WXDAI from the deposit.\\n if (!IERC20(tokenOwed).transfer(msg.sender, amountOwed)) {\\n revert BzzTransferFailed();\\n }\\n } else {\\n // ERC20 flow: pull from user who pre-approved this contract.\\n if (!IERC20(tokenOwed).transferFrom(cb.payer, msg.sender, amountOwed)) {\\n revert BzzTransferFailed();\\n }\\n }\\n }\\n }\\n\\n // \\u2500\\u2500\\u2500 Internal Helpers \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n /**\\n * @dev Execute an exact-output swap for `bzzAmountOut` BZZ using the given path.\\n * The path is in exactOutput encoding: BZZ ++ fee ++ [...] ++ tokenIn.\\n * BZZ lands in address(this) after the swap completes.\\n */\\n function _swapExactOutput(\\n bytes memory path,\\n address payer,\\n uint256 maxAmountIn,\\n uint256 bzzAmountOut\\n ) internal {\\n if (path.length < POP_OFFSET) revert InvalidPath();\\n\\n // Decode the first (and for single-hop, only) pool in the path.\\n (address tokenOut, uint24 fee, address tokenIn) = _decodeFirstPool(path);\\n if (tokenOut != BZZ) revert InvalidPath();\\n\\n address pool = ISushiV3Factory(SUSHI_FACTORY).getPool(tokenOut, tokenIn, fee);\\n if (pool == address(0)) revert PoolNotFound();\\n\\n // zeroForOne: true if tokenIn is token0 (address < BZZ)\\n bool zeroForOne = tokenIn < tokenOut;\\n\\n // amountSpecified < 0 \\u2192 exact output (we want exactly bzzAmountOut of BZZ)\\n ISushiV3Pool(pool).swap(\\n address(this), // receive BZZ here\\n zeroForOne,\\n -int256(bzzAmountOut),\\n zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,\\n abi.encode(SwapCallbackData({\\n path: path,\\n payer: payer,\\n maxAmountIn: maxAmountIn\\n }))\\n );\\n }\\n\\n /**\\n * @dev Approve BZZ to the stamps registry and call createBatchRegistry.\\n * Returns the keccak256 batch ID consistent with the registry's derivation.\\n */\\n function _approveBzzAndCreate(\\n CreateBatchParams memory p,\\n uint256 bzzAmountOut\\n ) internal returns (bytes32 batchId) {\\n if (!IERC20(BZZ).approve(address(stampsRegistry), bzzAmountOut)) {\\n revert BzzApproveFailed();\\n }\\n\\n stampsRegistry.createBatchRegistry(\\n p.owner,\\n p.nodeAddress,\\n p.initialBalancePerChunk,\\n p.depth,\\n p.bucketDepth,\\n p.nonce,\\n p.immutable_\\n );\\n\\n // Registry derives batchId as keccak256(abi.encode(registry, nonce)).\\n batchId = keccak256(abi.encode(address(stampsRegistry), p.nonce));\\n }\\n\\n /**\\n * @dev Approve BZZ to the stamps registry and call topUpBatch.\\n */\\n function _approveBzzAndTopUp(\\n bytes32 batchId,\\n uint256 topupAmountPerChunk,\\n uint256 bzzAmountOut\\n ) internal {\\n if (!IERC20(BZZ).approve(address(stampsRegistry), bzzAmountOut)) {\\n revert BzzApproveFailed();\\n }\\n stampsRegistry.topUpBatch(batchId, topupAmountPerChunk);\\n }\\n\\n /**\\n * @dev Unwrap any remaining WXDAI and refund all native xDAI to msg.sender.\\n */\\n function _refundNative() internal {\\n uint256 wxdaiBalance = IERC20(WXDAI).balanceOf(address(this));\\n if (wxdaiBalance > 0) {\\n IWXDAI(WXDAI).withdraw(wxdaiBalance);\\n }\\n uint256 nativeBalance = address(this).balance;\\n if (nativeBalance > 0) {\\n (bool ok,) = msg.sender.call{value: nativeBalance}(\\\"\\\");\\n if (!ok) revert NativeRefundFailed();\\n }\\n }\\n\\n // \\u2500\\u2500\\u2500 Path Utilities \\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\u2500\\n\\n /**\\n * @dev Returns true if the path encodes more than one pool (length > 43 bytes).\\n */\\n function _hasMultiplePools(bytes memory path) internal pure returns (bool) {\\n return path.length > POP_OFFSET;\\n }\\n\\n /**\\n * @dev Decodes the first pool segment from the path:\\n * tokenA (20 bytes) ++ fee (3 bytes) ++ tokenB (20 bytes)\\n */\\n function _decodeFirstPool(bytes memory path)\\n internal\\n pure\\n returns (address tokenA, uint24 fee, address tokenB)\\n {\\n tokenA = _toAddress(path, 0);\\n fee = _toUint24(path, ADDR_SIZE);\\n tokenB = _toAddress(path, NEXT_OFFSET);\\n }\\n\\n /**\\n * @dev Returns the path with the first token removed (skips ADDR_SIZE + FEE_SIZE bytes).\\n * Used to advance through multi-hop paths in the callback.\\n */\\n function _skipToken(bytes memory path) internal pure returns (bytes memory skipped) {\\n uint256 newLen = path.length - NEXT_OFFSET;\\n skipped = new bytes(newLen);\\n // Copy from offset NEXT_OFFSET onward\\n for (uint256 i = 0; i < newLen; i++) {\\n skipped[i] = path[i + NEXT_OFFSET];\\n }\\n }\\n\\n /**\\n * @dev Extracts the last 20-byte address from the path (the tokenIn address).\\n */\\n function _lastToken(bytes memory path) internal pure returns (address token) {\\n uint256 offset = path.length - ADDR_SIZE;\\n token = _toAddress(path, offset);\\n }\\n\\n /**\\n * @dev Reads a 20-byte address from `data` at `offset` using assembly.\\n * The address occupies bytes [offset, offset+20) and is right-aligned\\n * by shifting the 32-byte word 96 bits right.\\n */\\n function _toAddress(bytes memory data, uint256 offset) internal pure returns (address addr) {\\n assembly {\\n addr := shr(96, mload(add(add(data, 0x20), offset)))\\n }\\n }\\n\\n /**\\n * @dev Reads a 3-byte uint24 from `data` at `offset` using assembly.\\n * Shifts the 32-byte word 232 bits right to extract the top 3 bytes.\\n */\\n function _toUint24(bytes memory data, uint256 offset) internal pure returns (uint24 result) {\\n assembly {\\n result := shr(232, mload(add(add(data, 0x20), offset)))\\n }\\n }\\n}\\n\",\"keccak256\":\"0x3ea468d65d7478e465dcbccb2363f6a16739508c6f2da61939432b8b5424b910\",\"license\":\"MIT\"}},\"version\":1}", + "bytecode": "0x60a03461007a57601f6118d738819003918201601f19168301916001600160401b0383118484101761007f5780849260209460405283398101031261007a57516001600160a01b0381169081900361007a57608052604051611841908161009682396080518181816105de0152818161137d01526115d00152f35b600080fd5b634e487b7160e01b600052604160045260246000fdfe6080604052600436101561001b575b361561001957600080fd5b005b60003560e01c80630e70205d146100db5780630f43a7df146100d65780632a426602146100d15780632cb1dd93146100cc578063348fdcc4146100c7578063604a52fb146100c25780638acc27b9146100bd578063a7c086be146100b8578063b753bb47146100b3578063b972d70d146100ae578063eaa4f401146100a95763fa461e330361000e576107d6565b6107a7565b6106db565b6106ac565b61060d565b6105c8565b610599565b6104b1565b610390565b610237565b61018a565b6100f0565b60009103126100eb57565b600080fd5b346100eb5760003660031901126100eb57602060405173dbf3ea6f5bee45c02255b2c26a16f300502f68da8152f35b9181601f840112156100eb578235916001600160401b0383116100eb57602083818601950101116100eb57565b60a06003198201126100eb57600435906001600160401b0382116100eb576101769160040161011f565b909160243590604435906064359060843590565b346100eb576101ec7fc65f0fa5466fb164f60e47c03216e4cae4cf47cb57de7d15f9cbdd413028ace66101f36101f86102216101c53661014c565b91839894926101e682809a98969d949d336101e136898b610d4c565b61118c565b8b611366565b3691610d4c565b611466565b604080516001600160a01b03909216825260208201949094529283019190915281906060820190565b0390a2005b6001600160a01b038116036100eb57565b346100eb5760603660031901126100eb5760043561025481610226565b60243562ffffff811681036100eb57610317916102b5608092610287610278610d03565b6001600160a01b039094168452565b73dbf3ea6f5bee45c02255b2c26a16f300502f68da6020840152604435604084015262ffffff166060830152565b60008183015260408051635e90b82560e11b815282516001600160a01b039081166004830152602084015181166024830152918301516044820152606083015162ffffff166064820152608090920151166084820152918290819060a4820190565b0381600073b1e835dc2785b52265711e17fccb0fd018226a6e5af1801561038b5761035591600091610359575b506040519081529081906020820190565b0390f35b61037b915060803d608011610384575b6103738183610ce2565b810190610d94565b50505038610344565b503d610369565b610dc6565b6103993661014c565b90949383929334106104585773e91d153e0b41518a2ce8dd3d7944fa863463a97d91823b156100eb5760008493600460405180968193630d0e30db60e41b83525af1801561038b57858581946101e17fc65f0fa5466fb164f60e47c03216e4cae4cf47cb57de7d15f9cbdd413028ace69a6104279861042196610449575b5030923691610d4c565b86611366565b60408051600081526020810192909252810191909152606090a26100196114bd565b61045290610caf565b38610417565b604051631ac4c73760e11b8152600490fd5b6101406003198201126100eb576004356001600160401b0381116100eb57816104959160040161011f565b929092916024359160e06044359260631901126100eb57606490565b6104ba3661046a565b9291938434106104585773e91d153e0b41518a2ce8dd3d7944fa863463a97d90813b156100eb5760008692600460405180958193630d0e30db60e41b83525af191821561038b577f1ec8178b486abb6332d45b8822f76f29e264fc077dc6ae1d5db41f9a1f2be645946101e18593899361053c96610586575030923691610d4c565b61054f8161054a3686610df5565b6115b9565b926001600160a01b039061056290610e86565b604080516000815260208101979097528601929092521692606090a36100196114bd565b8061059361045292610caf565b806100e0565b346100eb5760003660031901126100eb57602060405173e91d153e0b41518a2ce8dd3d7944fa863463a97d8152f35b346100eb5760003660031901126100eb576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346100eb576106a76106737f1ec8178b486abb6332d45b8822f76f29e264fc077dc6ae1d5db41f9a1f2be6456101f36106453661046a565b9792969194909361065d8587336101e1368d87610d4c565b61066b8561054a368c610df5565b973691610d4c565b95359161067f83610226565b604080516001600160a01b039889168152602081019590955284015294169381906060820190565b0390a3005b346100eb5760003660031901126100eb57602060405173f78031cbca409f2fb6876bdfdbc1b2df24cf9bef8152f35b346100eb5760403660031901126100eb576004356001600160401b0381116100eb57600061070f606492369060040161011f565b9283916040519485938492632f80bb1d60e01b84526040600485015281604485015284840137848382840101526024356024830152601f801991011681010301818373b1e835dc2785b52265711e17fccb0fd018226a6e5af1801561038b576103559160009161078a57506040519081529081906020820190565b61037b91503d806000833e61079f8183610ce2565b810190610f13565b346100eb5760003660031901126100eb57602060405173b1e835dc2785b52265711e17fccb0fd018226a6e8152f35b346100eb5760603660031901126100eb576004803590602435604435916001600160401b0383116100eb5761081161089f933690830161011f565b92909461083760009687831395868015610c90575b61082f90610fb9565b810190610ff3565b926108598451602081015160601c916037603483015160e81c92015160601c90565b909691946040978851998a91630b4c774160e11b9485845260209988850191939262ffffff90604092606085019660018060a01b03809216865216602085015216910152565b0392868a73f78031cbca409f2fb6876bdfdbc1b2df24cf9bef9581875afa998a1561038b578b9a610c71575b506001600160a01b03998a163303610c615715610c1a57508651630dfe168160e01b815285818581335afa90811561038b578a91610bfd575b505b865151602b1015610ace57508697869786610925610982989951611781565b9461094686602081015160601c916037603483015160e81c92015160601c90565b94519687526001600160a01b038083168a8901908152908616602082015262ffffff909116604082015290999395938492918391829160600190565b03915afa801561038b5782918c91610aa1575b5016958615610a915792610a1c6109bc8b999897969484610a38958f9816911610966110af565b978615610a76576401000276a49a5b81810151908b0151906109fb906001600160a01b03166109e9610d22565b9586526001600160a01b031683860152565b8a840152610a0e8a519384928301611105565b03601f198101835282610ce2565b8751630251596160e31b81529889978896879533908701611153565b03925af1801561038b57610a4b57505080f35b81610a6a92903d10610a6f575b610a628183610ce2565b81019061113d565b505080f35b503d610a58565b73fffd8963efd1fc6a506488495d951d5263988d259a6109cb565b89516301dbb3ff60e61b81528590fd5b610ac19150893d8b11610ac7575b610ab98183610ce2565b81019061106f565b38610995565b503d610aaf565b93929794958092508791500151808311610bd9575084015184929190879089906001600160a01b03168087163003610b815750875163a9059cbb60e01b8152338a820190815260208101949094529586949385935083906040010393165af191821561038b578592610b54575b505015610b4757505080f35b516304aa965560e41b8152fd5b610b739250803d10610b7a575b610b6b8183610ce2565b810190611084565b3880610b3b565b503d610b61565b88516323b872dd60e01b81526001600160a01b0390911692810192835233602084015260408301939093529194859392849290919083906060010393165af191821561038b578592610b5457505015610b4757505080f35b86516371c4efed60e01b815280890184815260208101929092529081906040010390fd5b610c149150863d8811610ac757610ab98183610ce2565b38610904565b875163d21220a760e01b815290945085818581335afa90811561038b578a91610c44575b50610906565b610c5b9150863d8811610ac757610ab98183610ce2565b38610c3e565b885163f7a632f560e01b81528590fd5b610c89919a50873d8911610ac757610ab98183610ce2565b98386108cb565b50888513610826565b634e487b7160e01b600052604160045260246000fd5b6001600160401b038111610cc257604052565b610c99565b606081019081106001600160401b03821117610cc257604052565b90601f801991011681019081106001600160401b03821117610cc257604052565b6040519060a082018281106001600160401b03821117610cc257604052565b60405190610d2f82610cc7565b565b6001600160401b038111610cc257601f01601f191660200190565b929192610d5882610d31565b91610d666040519384610ce2565b8294818452818301116100eb578281602093846000960137010152565b519063ffffffff821682036100eb57565b91908260809103126100eb578151916020810151610db181610226565b916060610dc060408401610d83565b92015190565b6040513d6000823e3d90fd5b359060ff821682036100eb57565b801515036100eb57565b3590610d2f82610de0565b91908260e09103126100eb5760405160e081018181106001600160401b03821117610cc25760405260c0610e818183958035610e3081610226565b85526020810135610e4081610226565b602086015260408101356040860152610e5b60608201610dd2565b6060860152610e6c60808201610dd2565b608086015260a081013560a086015201610dea565b910152565b35610e9081610226565b90565b6001600160401b038111610cc25760051b60200190565b9080601f830112156100eb57815190602091610ec581610e93565b93610ed36040519586610ce2565b81855260208086019260051b8201019283116100eb57602001905b828210610efc575050505090565b838091610f0884610d83565b815201910190610eee565b6080818303126100eb5780519260209283830151936001600160401b03948581116100eb5784019082601f830112156100eb57815191610f5283610e93565b92610f606040519485610ce2565b808452828085019160051b830101918583116100eb578301905b828210610fa057505050509360408401519081116100eb57606091610dc0918501610eaa565b8380918351610fae81610226565b815201910190610f7a565b15610fc057565b60405162461bcd60e51b815260206004820152600b60248201526a5a65726f2064656c74617360a81b6044820152606490fd5b906020828203126100eb5781356001600160401b03928382116100eb57016060818303126100eb576040519261102884610cc7565b81359081116100eb57810182601f820112156100eb5760409281602061105093359101610d4c565b8352602081013561106081610226565b60208401520135604082015290565b908160209103126100eb5751610e9081610226565b908160209103126100eb5751610e9081610de0565b634e487b7160e01b600052601160045260246000fd5b600160ff1b81146110c05760000390565b611099565b919082519283825260005b8481106110f1575050826000602080949584010152601f8019910116010190565b6020818301810151848301820152016110d0565b6020815260606040611122845183602086015260808501906110c5565b60208501516001600160a01b03168483015293015191015290565b91908260409103126100eb576020825192015190565b6001600160a01b039182168152911515602083015260408201929092529116606082015260a060808201819052610e90929101906110c5565b929091602b845110611354576111b884602081015160601c916037603483015160e81c92015160601c90565b9195919391906001600160a01b038088169173dbf3ea6f5bee45c02255b2c26a16f300502f68d91983016113545760408051630b4c774160e11b81526001600160a01b039a8b16600482015299881660248b015262ffffff9190911660448a01529760208160648173f78031cbca409f2fb6876bdfdbc1b2df24cf9bef5afa801561038b578291600091611335575b5016958615611324576112ac97959361126c6000948b9997946112ba941610936110af565b948385146113065761129a6401000276a4985b611287610d22565b9384526001600160a01b03166020840152565b88820152875198899160208301611105565b03601f198101895288610ce2565b6112da865197889687958694630251596160e31b86523060048701611153565b03925af1801561038b576112ec575050565b8161130292903d10610a6f57610a628183610ce2565b5050565b61129a73fffd8963efd1fc6a506488495d951d5263988d259861127f565b88516301dbb3ff60e61b8152600490fd5b61134e915060203d602011610ac757610ab98183610ce2565b38611247565b6040516320db826760e01b8152600490fd5b60405163095ea7b360e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482018190526024820194909452909392600092916020816044818773dbf3ea6f5bee45c02255b2c26a16f300502f68da5af190811561038b578491611447575b501561143557803b1561143157604051631549361960e01b8152600481019590955260248501919091529192918290604490829084905af1801561038b576114245750565b80610593610d2f92610caf565b8280fd5b604051632546bed360e01b8152600490fd5b611460915060203d602011610b7a57610b6b8183610ce2565b386113df565b8051806013198101116110c05701600c015160601c90565b908160209103126100eb575190565b3d156114b8573d9061149e82610d31565b916114ac6040519384610ce2565b82523d6000602084013e565b606090565b6040516370a0823160e01b815230600482015273e91d153e0b41518a2ce8dd3d7944fa863463a97d602082602481845afa91821561038b57600092611588575b508161153a575b5050478061150f5750565b600080808093335af161152061148d565b501561152857565b6040516308520d7160e41b8152600490fd5b803b156100eb57604051632e1a7d4d60e01b815260048101929092526000908290602490829084905af1801561038b57611575575b80611504565b8061059361158292610caf565b3861156f565b6115ab91925060203d6020116115b2575b6115a38183610ce2565b81019061147e565b90386114fd565b503d611599565b60405163095ea7b360e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482018190526024820193909352600091906020816044818673dbf3ea6f5bee45c02255b2c26a16f300502f68da5af190811561038b57839161173b575b50156114355780516001600160a01b031660208201519092906001600160a01b0316604083015193611665606085015160ff1690565b608085015160ff169460a081019561168260c08851930151151590565b92893b15611737576040516317f8c86b60e11b81526001600160a01b039586166004820152959094166024860152604485019790975260ff928316606485015291909516608483015260a482015292151560c48401528260e48183875af190811561038b57610a0e9261171e92611724575b5051604080516001600160a01b039095166020860190815290850191909152929182906060820190565b51902090565b8061059361173192610caf565b386116f4565b8680fd5b611754915060203d602011610b7a57610b6b8183610ce2565b3861162f565b90815181101561176b570160200190565b634e487b7160e01b600052603260045260246000fd5b8051601619810192919083116110c05761179a83610d31565b926117a86040519485610ce2565b808452601f196117b782610d31565b013660208601378360009260005b8381106117d3575050505050565b60178101908181116110c0576001916001600160f81b0319906117f6908561175a565b5116861a611804828661175a565b53016117c556fea2646970667358221220c1bcb94f145b5031b5241b079ab267fc10582dc6d84e331e4ed3116a4e43b44b64736f6c63430008170033", + "deployedBytecode": "0x6080604052600436101561001b575b361561001957600080fd5b005b60003560e01c80630e70205d146100db5780630f43a7df146100d65780632a426602146100d15780632cb1dd93146100cc578063348fdcc4146100c7578063604a52fb146100c25780638acc27b9146100bd578063a7c086be146100b8578063b753bb47146100b3578063b972d70d146100ae578063eaa4f401146100a95763fa461e330361000e576107d6565b6107a7565b6106db565b6106ac565b61060d565b6105c8565b610599565b6104b1565b610390565b610237565b61018a565b6100f0565b60009103126100eb57565b600080fd5b346100eb5760003660031901126100eb57602060405173dbf3ea6f5bee45c02255b2c26a16f300502f68da8152f35b9181601f840112156100eb578235916001600160401b0383116100eb57602083818601950101116100eb57565b60a06003198201126100eb57600435906001600160401b0382116100eb576101769160040161011f565b909160243590604435906064359060843590565b346100eb576101ec7fc65f0fa5466fb164f60e47c03216e4cae4cf47cb57de7d15f9cbdd413028ace66101f36101f86102216101c53661014c565b91839894926101e682809a98969d949d336101e136898b610d4c565b61118c565b8b611366565b3691610d4c565b611466565b604080516001600160a01b03909216825260208201949094529283019190915281906060820190565b0390a2005b6001600160a01b038116036100eb57565b346100eb5760603660031901126100eb5760043561025481610226565b60243562ffffff811681036100eb57610317916102b5608092610287610278610d03565b6001600160a01b039094168452565b73dbf3ea6f5bee45c02255b2c26a16f300502f68da6020840152604435604084015262ffffff166060830152565b60008183015260408051635e90b82560e11b815282516001600160a01b039081166004830152602084015181166024830152918301516044820152606083015162ffffff166064820152608090920151166084820152918290819060a4820190565b0381600073b1e835dc2785b52265711e17fccb0fd018226a6e5af1801561038b5761035591600091610359575b506040519081529081906020820190565b0390f35b61037b915060803d608011610384575b6103738183610ce2565b810190610d94565b50505038610344565b503d610369565b610dc6565b6103993661014c565b90949383929334106104585773e91d153e0b41518a2ce8dd3d7944fa863463a97d91823b156100eb5760008493600460405180968193630d0e30db60e41b83525af1801561038b57858581946101e17fc65f0fa5466fb164f60e47c03216e4cae4cf47cb57de7d15f9cbdd413028ace69a6104279861042196610449575b5030923691610d4c565b86611366565b60408051600081526020810192909252810191909152606090a26100196114bd565b61045290610caf565b38610417565b604051631ac4c73760e11b8152600490fd5b6101406003198201126100eb576004356001600160401b0381116100eb57816104959160040161011f565b929092916024359160e06044359260631901126100eb57606490565b6104ba3661046a565b9291938434106104585773e91d153e0b41518a2ce8dd3d7944fa863463a97d90813b156100eb5760008692600460405180958193630d0e30db60e41b83525af191821561038b577f1ec8178b486abb6332d45b8822f76f29e264fc077dc6ae1d5db41f9a1f2be645946101e18593899361053c96610586575030923691610d4c565b61054f8161054a3686610df5565b6115b9565b926001600160a01b039061056290610e86565b604080516000815260208101979097528601929092521692606090a36100196114bd565b8061059361045292610caf565b806100e0565b346100eb5760003660031901126100eb57602060405173e91d153e0b41518a2ce8dd3d7944fa863463a97d8152f35b346100eb5760003660031901126100eb576040517f00000000000000000000000000000000000000000000000000000000000000006001600160a01b03168152602090f35b346100eb576106a76106737f1ec8178b486abb6332d45b8822f76f29e264fc077dc6ae1d5db41f9a1f2be6456101f36106453661046a565b9792969194909361065d8587336101e1368d87610d4c565b61066b8561054a368c610df5565b973691610d4c565b95359161067f83610226565b604080516001600160a01b039889168152602081019590955284015294169381906060820190565b0390a3005b346100eb5760003660031901126100eb57602060405173f78031cbca409f2fb6876bdfdbc1b2df24cf9bef8152f35b346100eb5760403660031901126100eb576004356001600160401b0381116100eb57600061070f606492369060040161011f565b9283916040519485938492632f80bb1d60e01b84526040600485015281604485015284840137848382840101526024356024830152601f801991011681010301818373b1e835dc2785b52265711e17fccb0fd018226a6e5af1801561038b576103559160009161078a57506040519081529081906020820190565b61037b91503d806000833e61079f8183610ce2565b810190610f13565b346100eb5760003660031901126100eb57602060405173b1e835dc2785b52265711e17fccb0fd018226a6e8152f35b346100eb5760603660031901126100eb576004803590602435604435916001600160401b0383116100eb5761081161089f933690830161011f565b92909461083760009687831395868015610c90575b61082f90610fb9565b810190610ff3565b926108598451602081015160601c916037603483015160e81c92015160601c90565b909691946040978851998a91630b4c774160e11b9485845260209988850191939262ffffff90604092606085019660018060a01b03809216865216602085015216910152565b0392868a73f78031cbca409f2fb6876bdfdbc1b2df24cf9bef9581875afa998a1561038b578b9a610c71575b506001600160a01b03998a163303610c615715610c1a57508651630dfe168160e01b815285818581335afa90811561038b578a91610bfd575b505b865151602b1015610ace57508697869786610925610982989951611781565b9461094686602081015160601c916037603483015160e81c92015160601c90565b94519687526001600160a01b038083168a8901908152908616602082015262ffffff909116604082015290999395938492918391829160600190565b03915afa801561038b5782918c91610aa1575b5016958615610a915792610a1c6109bc8b999897969484610a38958f9816911610966110af565b978615610a76576401000276a49a5b81810151908b0151906109fb906001600160a01b03166109e9610d22565b9586526001600160a01b031683860152565b8a840152610a0e8a519384928301611105565b03601f198101835282610ce2565b8751630251596160e31b81529889978896879533908701611153565b03925af1801561038b57610a4b57505080f35b81610a6a92903d10610a6f575b610a628183610ce2565b81019061113d565b505080f35b503d610a58565b73fffd8963efd1fc6a506488495d951d5263988d259a6109cb565b89516301dbb3ff60e61b81528590fd5b610ac19150893d8b11610ac7575b610ab98183610ce2565b81019061106f565b38610995565b503d610aaf565b93929794958092508791500151808311610bd9575084015184929190879089906001600160a01b03168087163003610b815750875163a9059cbb60e01b8152338a820190815260208101949094529586949385935083906040010393165af191821561038b578592610b54575b505015610b4757505080f35b516304aa965560e41b8152fd5b610b739250803d10610b7a575b610b6b8183610ce2565b810190611084565b3880610b3b565b503d610b61565b88516323b872dd60e01b81526001600160a01b0390911692810192835233602084015260408301939093529194859392849290919083906060010393165af191821561038b578592610b5457505015610b4757505080f35b86516371c4efed60e01b815280890184815260208101929092529081906040010390fd5b610c149150863d8811610ac757610ab98183610ce2565b38610904565b875163d21220a760e01b815290945085818581335afa90811561038b578a91610c44575b50610906565b610c5b9150863d8811610ac757610ab98183610ce2565b38610c3e565b885163f7a632f560e01b81528590fd5b610c89919a50873d8911610ac757610ab98183610ce2565b98386108cb565b50888513610826565b634e487b7160e01b600052604160045260246000fd5b6001600160401b038111610cc257604052565b610c99565b606081019081106001600160401b03821117610cc257604052565b90601f801991011681019081106001600160401b03821117610cc257604052565b6040519060a082018281106001600160401b03821117610cc257604052565b60405190610d2f82610cc7565b565b6001600160401b038111610cc257601f01601f191660200190565b929192610d5882610d31565b91610d666040519384610ce2565b8294818452818301116100eb578281602093846000960137010152565b519063ffffffff821682036100eb57565b91908260809103126100eb578151916020810151610db181610226565b916060610dc060408401610d83565b92015190565b6040513d6000823e3d90fd5b359060ff821682036100eb57565b801515036100eb57565b3590610d2f82610de0565b91908260e09103126100eb5760405160e081018181106001600160401b03821117610cc25760405260c0610e818183958035610e3081610226565b85526020810135610e4081610226565b602086015260408101356040860152610e5b60608201610dd2565b6060860152610e6c60808201610dd2565b608086015260a081013560a086015201610dea565b910152565b35610e9081610226565b90565b6001600160401b038111610cc25760051b60200190565b9080601f830112156100eb57815190602091610ec581610e93565b93610ed36040519586610ce2565b81855260208086019260051b8201019283116100eb57602001905b828210610efc575050505090565b838091610f0884610d83565b815201910190610eee565b6080818303126100eb5780519260209283830151936001600160401b03948581116100eb5784019082601f830112156100eb57815191610f5283610e93565b92610f606040519485610ce2565b808452828085019160051b830101918583116100eb578301905b828210610fa057505050509360408401519081116100eb57606091610dc0918501610eaa565b8380918351610fae81610226565b815201910190610f7a565b15610fc057565b60405162461bcd60e51b815260206004820152600b60248201526a5a65726f2064656c74617360a81b6044820152606490fd5b906020828203126100eb5781356001600160401b03928382116100eb57016060818303126100eb576040519261102884610cc7565b81359081116100eb57810182601f820112156100eb5760409281602061105093359101610d4c565b8352602081013561106081610226565b60208401520135604082015290565b908160209103126100eb5751610e9081610226565b908160209103126100eb5751610e9081610de0565b634e487b7160e01b600052601160045260246000fd5b600160ff1b81146110c05760000390565b611099565b919082519283825260005b8481106110f1575050826000602080949584010152601f8019910116010190565b6020818301810151848301820152016110d0565b6020815260606040611122845183602086015260808501906110c5565b60208501516001600160a01b03168483015293015191015290565b91908260409103126100eb576020825192015190565b6001600160a01b039182168152911515602083015260408201929092529116606082015260a060808201819052610e90929101906110c5565b929091602b845110611354576111b884602081015160601c916037603483015160e81c92015160601c90565b9195919391906001600160a01b038088169173dbf3ea6f5bee45c02255b2c26a16f300502f68d91983016113545760408051630b4c774160e11b81526001600160a01b039a8b16600482015299881660248b015262ffffff9190911660448a01529760208160648173f78031cbca409f2fb6876bdfdbc1b2df24cf9bef5afa801561038b578291600091611335575b5016958615611324576112ac97959361126c6000948b9997946112ba941610936110af565b948385146113065761129a6401000276a4985b611287610d22565b9384526001600160a01b03166020840152565b88820152875198899160208301611105565b03601f198101895288610ce2565b6112da865197889687958694630251596160e31b86523060048701611153565b03925af1801561038b576112ec575050565b8161130292903d10610a6f57610a628183610ce2565b5050565b61129a73fffd8963efd1fc6a506488495d951d5263988d259861127f565b88516301dbb3ff60e61b8152600490fd5b61134e915060203d602011610ac757610ab98183610ce2565b38611247565b6040516320db826760e01b8152600490fd5b60405163095ea7b360e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482018190526024820194909452909392600092916020816044818773dbf3ea6f5bee45c02255b2c26a16f300502f68da5af190811561038b578491611447575b501561143557803b1561143157604051631549361960e01b8152600481019590955260248501919091529192918290604490829084905af1801561038b576114245750565b80610593610d2f92610caf565b8280fd5b604051632546bed360e01b8152600490fd5b611460915060203d602011610b7a57610b6b8183610ce2565b386113df565b8051806013198101116110c05701600c015160601c90565b908160209103126100eb575190565b3d156114b8573d9061149e82610d31565b916114ac6040519384610ce2565b82523d6000602084013e565b606090565b6040516370a0823160e01b815230600482015273e91d153e0b41518a2ce8dd3d7944fa863463a97d602082602481845afa91821561038b57600092611588575b508161153a575b5050478061150f5750565b600080808093335af161152061148d565b501561152857565b6040516308520d7160e41b8152600490fd5b803b156100eb57604051632e1a7d4d60e01b815260048101929092526000908290602490829084905af1801561038b57611575575b80611504565b8061059361158292610caf565b3861156f565b6115ab91925060203d6020116115b2575b6115a38183610ce2565b81019061147e565b90386114fd565b503d611599565b60405163095ea7b360e01b81526001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016600482018190526024820193909352600091906020816044818673dbf3ea6f5bee45c02255b2c26a16f300502f68da5af190811561038b57839161173b575b50156114355780516001600160a01b031660208201519092906001600160a01b0316604083015193611665606085015160ff1690565b608085015160ff169460a081019561168260c08851930151151590565b92893b15611737576040516317f8c86b60e11b81526001600160a01b039586166004820152959094166024860152604485019790975260ff928316606485015291909516608483015260a482015292151560c48401528260e48183875af190811561038b57610a0e9261171e92611724575b5051604080516001600160a01b039095166020860190815290850191909152929182906060820190565b51902090565b8061059361173192610caf565b386116f4565b8680fd5b611754915060203d602011610b7a57610b6b8183610ce2565b3861162f565b90815181101561176b570160200190565b634e487b7160e01b600052603260045260246000fd5b8051601619810192919083116110c05761179a83610d31565b926117a86040519485610ce2565b808452601f196117b782610d31565b013660208601378360009260005b8381106117d3575050505050565b60178101908181116110c0576001916001600160f81b0319906117f6908561175a565b5116861a611804828661175a565b53016117c556fea2646970667358221220c1bcb94f145b5031b5241b079ab267fc10582dc6d84e331e4ed3116a4e43b44b64736f6c63430008170033", + "devdoc": { + "kind": "dev", + "methods": { + "createBatch(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))": { + "details": "`tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.", + "params": { + "bzzAmountOut": "Exact BZZ needed (= swarmBatchTotal = initialBalancePerChunk × 2^depth)", + "maxAmountIn": "Maximum tokenIn to spend (slippage protection)", + "p": "Batch creation parameters", + "path": "Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn" + } + }, + "createBatchNative(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))": { + "details": "Send msg.value ≥ maxAmountIn. Excess xDAI is refunded.", + "params": { + "bzzAmountOut": "Exact BZZ needed", + "maxAmountIn": "Maximum xDAI to spend", + "p": "Batch creation parameters", + "path": "Exact-output path where the final token MUST be WXDAI: BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI" + } + }, + "quoteMultiHop(bytes,uint256)": { + "params": { + "bzzAmountOut": "Exact BZZ amount wanted", + "path": "Exact-output encoded path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn" + }, + "returns": { + "amountIn": " Input tokens required (before slippage)" + } + }, + "quoteSingleHop(address,uint24,uint256)": { + "params": { + "bzzAmountOut": "Exact BZZ amount wanted", + "fee": "Pool fee tier (e.g. 500, 3000, 10000)", + "tokenIn": "Input token (use WXDAI for native xDAI quotes)" + }, + "returns": { + "amountIn": " Input tokens required (before slippage)" + } + }, + "topUp(bytes,uint256,uint256,bytes32,uint256)": { + "details": "`tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.", + "params": { + "batchId": "Batch to top up", + "bzzAmountOut": "Exact BZZ needed (= topupAmountPerChunk × 2^depth)", + "maxAmountIn": "Maximum tokenIn to spend", + "path": "Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn", + "topupAmountPerChunk": "Per-chunk top-up amount (matches registry call)" + } + }, + "topUpNative(bytes,uint256,uint256,bytes32,uint256)": { + "details": "Send msg.value ≥ maxAmountIn. Excess xDAI is refunded.", + "params": { + "batchId": "Batch to top up", + "bzzAmountOut": "Exact BZZ needed", + "maxAmountIn": "Maximum xDAI to spend", + "path": "BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI", + "topupAmountPerChunk": "Per-chunk top-up amount" + } + }, + "uniswapV3SwapCallback(int256,int256,bytes)": { + "details": "Implements the Uniswap V3 callback interface (SushiSwap V3 is compatible). For multi-hop swaps, this callback chains into the next pool swap before paying the current pool, routing tokens directly between pools." + } + }, + "version": 1 + }, + "userdoc": { + "kind": "user", + "methods": { + "BZZ()": { + "notice": "BZZ token on Gnosis" + }, + "SUSHI_FACTORY()": { + "notice": "SushiSwap V3 Factory on Gnosis" + }, + "SUSHI_QUOTER()": { + "notice": "SushiSwap V3 QuoterV2 on Gnosis" + }, + "WXDAI()": { + "notice": "Wrapped xDAI on Gnosis" + }, + "createBatch(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))": { + "notice": "Swap `tokenIn` → BZZ via the given path and create a Swarm stamp batch." + }, + "createBatchNative(bytes,uint256,uint256,(address,address,uint256,uint8,uint8,bytes32,bool))": { + "notice": "Swap native xDAI → BZZ and create a Swarm stamp batch." + }, + "quoteMultiHop(bytes,uint256)": { + "notice": "Quote: how many input tokens are needed to get exactly `bzzAmountOut` BZZ via a multi-hop path." + }, + "quoteSingleHop(address,uint24,uint256)": { + "notice": "Quote: how many `tokenIn` are needed to get exactly `bzzAmountOut` BZZ via a single-hop pool." + }, + "topUp(bytes,uint256,uint256,bytes32,uint256)": { + "notice": "Swap `tokenIn` → BZZ and top up an existing Swarm stamp batch." + }, + "topUpNative(bytes,uint256,uint256,bytes32,uint256)": { + "notice": "Swap native xDAI → BZZ and top up an existing Swarm stamp batch." + }, + "uniswapV3SwapCallback(int256,int256,bytes)": { + "notice": "Called by a SushiSwap V3 pool during swap execution." + } + }, + "version": 1 + }, + "storageLayout": { + "storage": [], + "types": null + } +} \ No newline at end of file diff --git a/deployments/gnosis/solcInputs/90fe721cd304da2d010b5da7732aee0a.json b/deployments/gnosis/solcInputs/90fe721cd304da2d010b5da7732aee0a.json new file mode 100644 index 0000000..0f9fd47 --- /dev/null +++ b/deployments/gnosis/solcInputs/90fe721cd304da2d010b5da7732aee0a.json @@ -0,0 +1,40 @@ +{ + "language": "Solidity", + "sources": { + "contracts/StampsRegistry.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\n/*\n ███████╗████████╗ █████╗ ███╗ ███╗██████╗ ███████╗\n ██╔════╝╚══██╔══╝██╔══██╗████╗ ████║██╔══██╗██╔════╝\n ███████╗ ██║ ███████║██╔████╔██║██████╔╝███████╗\n ╚════██║ ██║ ██╔══██║██║╚██╔╝██║██╔═══╝ ╚════██║\n ███████║ ██║ ██║ ██║██║ ╚═╝ ██║██║ ███████║\n ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝\n \n ██████╗ ███████╗ ██████╗ ██╗███████╗████████╗██████╗ ██╗ ██╗\n ██╔══██╗██╔════╝██╔════╝ ██║██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝\n ██████╔╝█████╗ ██║ ███╗██║███████╗ ██║ ██████╔╝ ╚████╔╝ \n ██╔══██╗██╔══╝ ██║ ██║██║╚════██║ ██║ ██╔══██╗ ╚██╔╝ \n ██║ ██║███████╗╚██████╔╝██║███████║ ██║ ██║ ██║ ██║ \n ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ \n*/\n\n/**\n * @title StampsRegistry\n * @notice A registry for Swarm Postage Stamps\n * @dev Note on naming convention: The terms \"Batch\" and \"Stamps\" are used interchangeably throughout the codebase.\n * \"Batch\" refers to a collection of stamps created in a single transaction and is the terminology used in the\n * Swarm protocol. \"Stamps\" is a more user-friendly term used to describe the same concept.\n * For example: \"BatchCreated\" event, but \"StampsRegistry\" contract.\n */\n\ninterface ISwarmContract {\n function createBatch(\n address _owner,\n uint256 _initialBalancePerChunk,\n uint8 _depth,\n uint8 _bucketDepth,\n bytes32 _nonce,\n bool _immutable\n ) external;\n\n function topUp(bytes32 _batchId, uint256 _topupAmountPerChunk) external;\n \n function increaseDepth(bytes32 _batchId, uint8 _newDepth) external;\n\n function currentTotalOutPayment() external view returns (uint256);\n \n function remainingBalance(bytes32 _batchId) external view returns (uint256);\n \n function batchOwner(bytes32 _batchId) external view returns (address);\n \n function batchDepth(bytes32 _batchId) external view returns (uint8);\n \n function batchBucketDepth(bytes32 _batchId) external view returns (uint8);\n \n function batchImmutableFlag(bytes32 _batchId) external view returns (bool);\n \n function batchNormalisedBalance(bytes32 _batchId) external view returns (uint256);\n \n function batchLastUpdatedBlockNumber(bytes32 _batchId) external view returns (uint256);\n}\n\ninterface IERC20 {\n function transferFrom(\n address sender,\n address recipient,\n uint256 amount\n ) external returns (bool);\n\n function approve(address spender, uint256 amount) external returns (bool);\n}\n\ncontract StampsRegistry {\n // State variables\n ISwarmContract public swarmStampContract;\n IERC20 public constant BZZ_TOKEN =\n IERC20(0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da);\n mapping(bytes32 => address) public batchPayers;\n address public admin;\n \n // New data structure to store batch information by owner\n struct BatchInfo {\n bytes32 batchId;\n uint256 totalAmount;\n uint256 normalisedBalance;\n address nodeAddress;\n address payer;\n uint8 depth;\n uint8 bucketDepth;\n bool immutable_;\n uint256 timestamp;\n }\n \n // Mapping from owner address to array of BatchInfo\n mapping(address => BatchInfo[]) public ownerBatches;\n \n // Events\n event BatchCreated(\n bytes32 indexed batchId,\n uint256 totalAmount,\n uint256 normalisedBalance,\n address indexed owner,\n address indexed payer,\n uint8 depth,\n uint8 bucketDepth,\n bool immutable_\n );\n\n event BatchTopUp(\n bytes32 indexed batchId,\n uint256 totalAmount,\n uint256 topupAmountPerChunk,\n address indexed owner\n );\n\n event BatchDepthIncrease(\n bytes32 indexed batchId,\n uint8 newDepth,\n uint256 newNormalisedBalance,\n address indexed owner\n );\n\n event BatchMigrated(\n bytes32 indexed batchId,\n uint256 totalAmount,\n uint256 normalisedBalance,\n address indexed owner,\n address indexed payer,\n uint8 depth,\n uint8 bucketDepth,\n bool immutable_\n );\n event SwarmContractUpdated(address oldAddress, address newAddress);\n event AdminTransferred(address oldAdmin, address newAdmin);\n\n // Custom errors\n error TransferFailed();\n error ApprovalFailed();\n\n // Modifiers\n modifier onlyAdmin() {\n require(msg.sender == admin, \"Only admin can call this function\");\n _;\n }\n\n constructor(address _swarmContractAddress) {\n swarmStampContract = ISwarmContract(_swarmContractAddress);\n admin = msg.sender;\n }\n\n ////////////////////////////////////////\n // SETTERS //\n ////////////////////////////////////////\n\n /**\n * @notice Transfer admin rights to a new address\n * @param _newAdmin The address of the new admin\n */\n function transferAdmin(address _newAdmin) external onlyAdmin {\n require(_newAdmin != address(0), \"New admin cannot be the zero address\");\n address oldAdmin = admin;\n admin = _newAdmin;\n emit AdminTransferred(oldAdmin, _newAdmin);\n }\n\n /**\n * @notice Updates the swarm contract address\n * @param _newSwarmContractAddress New address of the swarm contract\n */\n function updateSwarmContract(\n address _newSwarmContractAddress\n ) external onlyAdmin {\n address oldAddress = address(swarmStampContract);\n swarmStampContract = ISwarmContract(_newSwarmContractAddress);\n emit SwarmContractUpdated(oldAddress, _newSwarmContractAddress);\n }\n\n /**\n * @notice Migrate batch data from old contract without performing token transfers\n * @param _owner Address that owns the batch\n * @param _batchId Batch ID from the old contract\n * @param _totalAmount Total amount from the old batch\n * @param _normalisedBalance Normalised balance from the old batch\n * @param _nodeAddress Node address from the old batch\n * @param _depth Depth from the old batch\n * @param _bucketDepth Bucket depth from the old batch\n * @param _immutable Immutable flag from the old batch\n * @param _timestamp Original timestamp from the old batch\n */\n function migrateBatchRegistry(\n address _owner,\n bytes32 _batchId,\n uint256 _totalAmount,\n uint256 _normalisedBalance,\n address _nodeAddress,\n uint8 _depth,\n uint8 _bucketDepth,\n bool _immutable,\n uint256 _timestamp\n ) external onlyAdmin {\n // Store the payer information\n batchPayers[_batchId] = _owner;\n \n // Store batch information in the owner's batches array\n ownerBatches[_owner].push(BatchInfo({\n batchId: _batchId,\n totalAmount: _totalAmount,\n normalisedBalance: _normalisedBalance,\n nodeAddress: _nodeAddress,\n payer: _owner,\n depth: _depth,\n bucketDepth: _bucketDepth,\n immutable_: _immutable,\n timestamp: _timestamp\n }));\n\n // Emit the batch migration event\n emit BatchMigrated(\n _batchId,\n _totalAmount,\n _normalisedBalance,\n _nodeAddress,\n _owner,\n _depth,\n _bucketDepth,\n _immutable\n );\n }\n\n /**\n * @notice Creates a new batch and registers the payer\n * @param _owner Address that pays for the batch, but not the owner of the batch\n * @param _nodeAddress Address of the node that will own the batch\n * @param _initialBalancePerChunk Initial balance per chunk\n * @param _depth Depth of the batch\n * @param _bucketDepth Bucket depth\n * @param _nonce Unique nonce for the batch\n * @param _immutable Whether the batch is immutable\n */\n function createBatchRegistry(\n address _owner,\n address _nodeAddress,\n uint256 _initialBalancePerChunk,\n uint8 _depth,\n uint8 _bucketDepth,\n bytes32 _nonce,\n bool _immutable\n ) external {\n // Calculate total amount\n uint256 totalAmount = _initialBalancePerChunk * (1 << _depth);\n\n // Transfer BZZ tokens from sender to this contract\n if (!BZZ_TOKEN.transferFrom(msg.sender, address(this), totalAmount)) {\n revert TransferFailed();\n }\n\n // Approve swarmStampContract to spend the BZZ tokens\n if (!BZZ_TOKEN.approve(address(swarmStampContract), totalAmount)) {\n revert ApprovalFailed();\n }\n\n // Call the original swarm contract with nodeAddress as owner\n swarmStampContract.createBatch(\n _nodeAddress,\n _initialBalancePerChunk,\n _depth,\n _bucketDepth,\n _nonce,\n _immutable\n );\n\n // Calculate batchId as bytes32\n bytes32 batchId = keccak256(abi.encode(address(this), _nonce));\n\n // Store the payer information\n batchPayers[batchId] = _owner;\n\n // Get normalized balance\n uint256 normalisedBalance = swarmStampContract\n .currentTotalOutPayment() + _initialBalancePerChunk;\n \n // Store batch information in the owner's batches array\n ownerBatches[_owner].push(BatchInfo({\n batchId: batchId,\n totalAmount: totalAmount,\n normalisedBalance: normalisedBalance,\n nodeAddress: _nodeAddress,\n payer: _owner,\n depth: _depth,\n bucketDepth: _bucketDepth,\n immutable_: _immutable,\n timestamp: block.timestamp\n }));\n\n // Emit the batch creation event\n emit BatchCreated(\n batchId,\n totalAmount,\n normalisedBalance,\n _nodeAddress,\n _owner,\n _depth,\n _bucketDepth,\n _immutable\n );\n }\n\n /**\n * @notice Top up an existing batch\n * @param _batchId The id of the batch to top up\n * @param _topupAmountPerChunk The amount of additional tokens to add per chunk\n */\n function topUpBatch(\n bytes32 _batchId,\n uint256 _topupAmountPerChunk\n ) external {\n // Find the batch info in owner's batches\n address owner = batchPayers[_batchId];\n require(owner != address(0), \"Batch does not exist in registry\");\n \n // Find the batch to get its depth for total amount calculation\n uint8 depth;\n uint256 currentNormalisedBalance;\n for (uint i = 0; i < ownerBatches[owner].length; i++) {\n if (ownerBatches[owner][i].batchId == _batchId) {\n depth = ownerBatches[owner][i].depth;\n currentNormalisedBalance = ownerBatches[owner][i].normalisedBalance;\n break;\n }\n }\n \n // Calculate total amount to be topped up\n uint256 totalAmount = _topupAmountPerChunk * (1 << depth);\n \n // Transfer BZZ tokens from sender to this contract\n if (!BZZ_TOKEN.transferFrom(msg.sender, address(this), totalAmount)) {\n revert TransferFailed();\n }\n \n // Approve swarmStampContract to spend the BZZ tokens\n if (!BZZ_TOKEN.approve(address(swarmStampContract), totalAmount)) {\n revert ApprovalFailed();\n }\n \n // Call the topUp function on the swarm contract\n swarmStampContract.topUp(_batchId, _topupAmountPerChunk);\n \n // Update the batch info in the registry\n for (uint i = 0; i < ownerBatches[owner].length; i++) {\n if (ownerBatches[owner][i].batchId == _batchId) {\n ownerBatches[owner][i].normalisedBalance = currentNormalisedBalance + _topupAmountPerChunk;\n break;\n }\n }\n \n // Emit the batch top up event\n emit BatchTopUp(\n _batchId,\n totalAmount,\n _topupAmountPerChunk,\n owner\n );\n }\n\n /**\n * @notice Increase the depth of an existing batch\n * @param _batchId The id of the batch to increase depth\n * @param _newDepth The new depth for the batch (must be greater than current depth)\n */\n function increaseBatchDepth(bytes32 _batchId, uint8 _newDepth) external {\n // Find the batch info in owner's batches\n address owner = batchPayers[_batchId];\n require(owner != address(0), \"Batch does not exist in registry\");\n \n // Verify that msg.sender is the owner of the batch in the registry\n require(owner == msg.sender, \"Only the batch owner can increase depth\");\n \n // Find the batch to get its current depth and index\n uint8 currentDepth;\n uint256 batchIndex;\n bool foundBatch = false;\n \n for (uint i = 0; i < ownerBatches[owner].length; i++) {\n if (ownerBatches[owner][i].batchId == _batchId) {\n currentDepth = ownerBatches[owner][i].depth;\n batchIndex = i;\n foundBatch = true;\n break;\n }\n }\n \n require(foundBatch, \"Batch not found in owner's batches\");\n require(_newDepth > currentDepth, \"New depth must be greater than current depth\");\n \n // Call increaseDepth on the swarm contract\n swarmStampContract.increaseDepth(_batchId, _newDepth);\n \n // Get the updated values directly from the swarm contract\n uint8 updatedDepth = swarmStampContract.batchDepth(_batchId);\n uint256 newNormalisedBalance = swarmStampContract.batchNormalisedBalance(_batchId);\n \n // Update the batch in registry with values from the swarm contract\n ownerBatches[owner][batchIndex].depth = updatedDepth;\n ownerBatches[owner][batchIndex].normalisedBalance = newNormalisedBalance;\n \n // Emit batch depth increase event\n emit BatchDepthIncrease(\n _batchId,\n updatedDepth,\n newNormalisedBalance,\n owner\n );\n }\n\n ////////////////////////////////////////\n // GETTERS //\n ////////////////////////////////////////\n\n /**\n * @notice Get the payer address for a specific batch ID\n * @param _batchId The ID of the batch\n * @return The address of the payer\n */\n function getBatchPayer(bytes32 _batchId) external view returns (address) {\n return batchPayers[_batchId];\n }\n \n /**\n * @notice Get all batches for a specific owner\n * @param _owner The address of the owner\n * @return Array of BatchInfo for the owner\n */\n function getOwnerBatches(address _owner) external view returns (BatchInfo[] memory) {\n return ownerBatches[_owner];\n }\n \n /**\n * @notice Get the number of batches for a specific owner\n * @param _owner The address of the owner\n * @return The number of batches\n */\n function getOwnerBatchCount(address _owner) external view returns (uint256) {\n return ownerBatches[_owner].length;\n }\n}\n" + }, + "contracts/SushiSwapStampsRouter.sol": { + "content": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.23;\n\n/*\n ███████╗██╗ ██╗███████╗██╗ ██╗██╗███████╗██╗ ██╗ █████╗ ██████╗\n ██╔════╝██║ ██║██╔════╝██║ ██║██║██╔════╝██║ ██║██╔══██╗██╔══██╗\n ███████╗██║ ██║███████╗███████║██║███████╗██║ █╗ ██║███████║██████╔╝\n ╚════██║██║ ██║╚════██║██╔══██║██║╚════██║██║███╗██║██╔══██║██╔═══╝\n ███████║╚██████╔╝███████║██║ ██║██║███████║╚███╔███╔╝██║ ██║██║\n ╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝\n\n ███████╗████████╗ █████╗ ███╗ ███╗██████╗ ███████╗\n ██╔════╝╚══██╔══╝██╔══██╗████╗ ████║██╔══██╗██╔════╝\n ███████╗ ██║ ███████║██╔████╔██║██████╔╝███████╗\n ╚════██║ ██║ ██╔══██║██║╚██╔╝██║██╔═══╝ ╚════██║\n ███████║ ██║ ██║ ██║██║ ╚═╝ ██║██║ ███████║\n ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚══════╝\n\n ██████╗ ██████╗ ██╗ ██╗████████╗███████╗██████╗\n ██╔══██╗██╔═══██╗██║ ██║╚══██╔══╝██╔════╝██╔══██╗\n ██████╔╝██║ ██║██║ ██║ ██║ █████╗ ██████╔╝\n ██╔══██╗██║ ██║██║ ██║ ██║ ██╔══╝ ██╔══██╗\n ██║ ██║╚██████╔╝╚██████╔╝ ██║ ███████╗██║ ██║\n ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝\n*/\n\n/**\n * @title SushiSwapStampsRouter\n * @notice Swaps any Gnosis-chain token to BZZ via SushiSwap V3 and atomically\n * creates or tops up a Swarm postage-stamp batch in a single transaction.\n *\n * @dev Implements the Uniswap V3 callback interface (SushiSwap V3 is fully compatible).\n * Supports both single-hop and multi-hop exact-output swaps via path encoding.\n *\n * Path encoding for exactOutput swaps (reversed token order):\n * single-hop: BZZ ++ uint24(fee) ++ tokenIn (43 bytes)\n * two-hop: BZZ ++ uint24(fee2) ++ mid ++ uint24(fee1) ++ tokenIn (66 bytes)\n *\n * Quote functions are non-view (Quoter simulates swaps internally) but are\n * designed to be called via eth_call for gas-free estimation.\n *\n * Gnosis-chain addresses (hardcoded):\n * BZZ = 0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da\n * WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d\n * Quoter = 0xb1e835dc2785b52265711e17fccb0fd018226a6e (SushiSwap V3 QuoterV2)\n * Factory= 0xf78031cbca409f2fb6876bdfdbc1b2df24cf9bef (SushiSwap V3 Factory)\n */\n\n// ─── Interfaces ───────────────────────────────────────────────────────────────\n\ninterface IERC20 {\n function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);\n function transfer(address recipient, uint256 amount) external returns (bool);\n function approve(address spender, uint256 amount) external returns (bool);\n function balanceOf(address account) external view returns (uint256);\n}\n\ninterface IWXDAI {\n function deposit() external payable;\n function withdraw(uint256 amount) external;\n function approve(address spender, uint256 amount) external returns (bool);\n function transfer(address recipient, uint256 amount) external returns (bool);\n function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);\n function balanceOf(address account) external view returns (uint256);\n}\n\ninterface ISushiV3Pool {\n function swap(\n address recipient,\n bool zeroForOne,\n int256 amountSpecified,\n uint160 sqrtPriceLimitX96,\n bytes calldata data\n ) external returns (int256 amount0, int256 amount1);\n\n function token0() external view returns (address);\n function token1() external view returns (address);\n function fee() external view returns (uint24);\n}\n\ninterface ISushiV3Factory {\n function getPool(\n address tokenA,\n address tokenB,\n uint24 fee\n ) external view returns (address pool);\n}\n\ninterface IQuoterV2 {\n struct QuoteExactOutputSingleParams {\n address tokenIn;\n address tokenOut;\n uint256 amount;\n uint24 fee;\n uint160 sqrtPriceLimitX96;\n }\n\n function quoteExactOutputSingle(QuoteExactOutputSingleParams memory params)\n external\n returns (\n uint256 amountIn,\n uint160 sqrtPriceX96After,\n uint32 initializedTicksCrossed,\n uint256 gasEstimate\n );\n\n function quoteExactOutput(bytes memory path, uint256 amountOut)\n external\n returns (\n uint256 amountIn,\n uint160[] memory sqrtPriceX96AfterList,\n uint32[] memory initializedTicksCrossedList,\n uint256 gasEstimate\n );\n}\n\ninterface IStampsRegistry {\n function createBatchRegistry(\n address _owner,\n address _nodeAddress,\n uint256 _initialBalancePerChunk,\n uint8 _depth,\n uint8 _bucketDepth,\n bytes32 _nonce,\n bool _immutable\n ) external;\n\n function topUpBatch(bytes32 _batchId, uint256 _topupAmountPerChunk) external;\n}\n\n// ─── Router Contract ──────────────────────────────────────────────────────────\n\ncontract SushiSwapStampsRouter {\n\n // ─── Constants ────────────────────────────────────────────────────────────\n\n /// @notice BZZ token on Gnosis\n address public constant BZZ = 0xdBF3Ea6F5beE45c02255B2c26a16F300502F68da;\n\n /// @notice Wrapped xDAI on Gnosis\n address public constant WXDAI = 0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d;\n\n /// @notice SushiSwap V3 QuoterV2 on Gnosis\n address public constant SUSHI_QUOTER = 0xb1E835Dc2785b52265711e17fCCb0fd018226a6e;\n\n /// @notice SushiSwap V3 Factory on Gnosis\n address public constant SUSHI_FACTORY = 0xf78031CBCA409F2FB6876BDFDBc1b2df24cF9bEf;\n\n /// @notice Minimum sqrt price limit (used when selling token0 → token1, zeroForOne=true)\n uint160 internal constant MIN_SQRT_RATIO = 4295128739;\n\n /// @notice Maximum sqrt price limit (used when selling token1 → token0, zeroForOne=false)\n uint160 internal constant MAX_SQRT_RATIO = 1461446703485210103287273052203988822378723970342;\n\n // Path encoding offsets (bytes): address=20, fee=3, nextOffset=23, popOffset=43\n uint256 private constant ADDR_SIZE = 20;\n uint256 private constant FEE_SIZE = 3;\n uint256 private constant NEXT_OFFSET = 23; // ADDR_SIZE + FEE_SIZE\n uint256 private constant POP_OFFSET = 43; // NEXT_OFFSET + ADDR_SIZE\n\n // ─── Immutables ───────────────────────────────────────────────────────────\n\n IStampsRegistry public immutable stampsRegistry;\n\n // ─── Events ───────────────────────────────────────────────────────────────\n\n event BatchCreatedViaSwap(\n bytes32 indexed batchId,\n address indexed owner,\n address tokenIn,\n uint256 amountIn,\n uint256 bzzAmount\n );\n\n event BatchToppedUpViaSwap(\n bytes32 indexed batchId,\n address tokenIn,\n uint256 amountIn,\n uint256 bzzAmount\n );\n\n // ─── Errors ───────────────────────────────────────────────────────────────\n\n error InvalidCallback();\n error SlippageExceeded(uint256 required, uint256 maximum);\n error InsufficientNativeValue();\n error NativeRefundFailed();\n error BzzTransferFailed();\n error BzzApproveFailed();\n error PoolNotFound();\n error InvalidPath();\n\n // ─── Structs ──────────────────────────────────────────────────────────────\n\n struct CreateBatchParams {\n address owner;\n address nodeAddress;\n uint256 initialBalancePerChunk;\n uint8 depth;\n uint8 bucketDepth;\n bytes32 nonce;\n bool immutable_;\n }\n\n /// @dev Packed into the `data` argument of pool.swap(); threaded through callback chains.\n struct SwapCallbackData {\n bytes path; // remaining path in exactOutput encoding (BZZ-first)\n address payer; // who pays the input token (address(this) for native swaps)\n uint256 maxAmountIn; // slippage ceiling for the final (tokenIn) leg\n }\n\n // ─── Constructor ──────────────────────────────────────────────────────────\n\n constructor(address _stampsRegistry) {\n stampsRegistry = IStampsRegistry(_stampsRegistry);\n }\n\n receive() external payable {}\n\n // ─── Quote Functions ──────────────────────────────────────────────────────\n // These modify state internally (Quoter simulates swaps) but are designed to\n // be called via eth_call for free gas-less estimation.\n\n /**\n * @notice Quote: how many `tokenIn` are needed to get exactly `bzzAmountOut` BZZ\n * via a single-hop pool.\n * @param tokenIn Input token (use WXDAI for native xDAI quotes)\n * @param fee Pool fee tier (e.g. 500, 3000, 10000)\n * @param bzzAmountOut Exact BZZ amount wanted\n * @return amountIn Input tokens required (before slippage)\n */\n function quoteSingleHop(\n address tokenIn,\n uint24 fee,\n uint256 bzzAmountOut\n ) external returns (uint256 amountIn) {\n (amountIn,,,) = IQuoterV2(SUSHI_QUOTER).quoteExactOutputSingle(\n IQuoterV2.QuoteExactOutputSingleParams({\n tokenIn: tokenIn,\n tokenOut: BZZ,\n amount: bzzAmountOut,\n fee: fee,\n sqrtPriceLimitX96: 0\n })\n );\n }\n\n /**\n * @notice Quote: how many input tokens are needed to get exactly `bzzAmountOut` BZZ\n * via a multi-hop path.\n * @param path Exact-output encoded path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\n * @param bzzAmountOut Exact BZZ amount wanted\n * @return amountIn Input tokens required (before slippage)\n */\n function quoteMultiHop(\n bytes calldata path,\n uint256 bzzAmountOut\n ) external returns (uint256 amountIn) {\n (amountIn,,,) = IQuoterV2(SUSHI_QUOTER).quoteExactOutput(path, bzzAmountOut);\n }\n\n // ─── Create Batch ─────────────────────────────────────────────────────────\n\n /**\n * @notice Swap `tokenIn` → BZZ via the given path and create a Swarm stamp batch.\n * @dev `tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.\n * @param path Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\n * @param maxAmountIn Maximum tokenIn to spend (slippage protection)\n * @param bzzAmountOut Exact BZZ needed (= swarmBatchTotal = initialBalancePerChunk × 2^depth)\n * @param p Batch creation parameters\n */\n function createBatch(\n bytes calldata path,\n uint256 maxAmountIn,\n uint256 bzzAmountOut,\n CreateBatchParams calldata p\n ) external {\n _swapExactOutput(path, msg.sender, maxAmountIn, bzzAmountOut);\n bytes32 batchId = _approveBzzAndCreate(p, bzzAmountOut);\n address tokenIn = _lastToken(path);\n emit BatchCreatedViaSwap(batchId, p.owner, tokenIn, maxAmountIn, bzzAmountOut);\n }\n\n /**\n * @notice Swap native xDAI → BZZ and create a Swarm stamp batch.\n * @dev Send msg.value ≥ maxAmountIn. Excess xDAI is refunded.\n * @param path Exact-output path where the final token MUST be WXDAI:\n * BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI\n * @param maxAmountIn Maximum xDAI to spend\n * @param bzzAmountOut Exact BZZ needed\n * @param p Batch creation parameters\n */\n function createBatchNative(\n bytes calldata path,\n uint256 maxAmountIn,\n uint256 bzzAmountOut,\n CreateBatchParams calldata p\n ) external payable {\n if (msg.value < maxAmountIn) revert InsufficientNativeValue();\n IWXDAI(WXDAI).deposit{value: maxAmountIn}();\n _swapExactOutput(path, address(this), maxAmountIn, bzzAmountOut);\n bytes32 batchId = _approveBzzAndCreate(p, bzzAmountOut);\n emit BatchCreatedViaSwap(batchId, p.owner, address(0), maxAmountIn, bzzAmountOut);\n _refundNative();\n }\n\n // ─── Top Up Batch ─────────────────────────────────────────────────────────\n\n /**\n * @notice Swap `tokenIn` → BZZ and top up an existing Swarm stamp batch.\n * @dev `tokenIn` must be pre-approved to this contract for at least `maxAmountIn`.\n * @param path Exact-output path: BZZ ++ fee ++ [mid ++ fee]* ++ tokenIn\n * @param maxAmountIn Maximum tokenIn to spend\n * @param bzzAmountOut Exact BZZ needed (= topupAmountPerChunk × 2^depth)\n * @param batchId Batch to top up\n * @param topupAmountPerChunk Per-chunk top-up amount (matches registry call)\n */\n function topUp(\n bytes calldata path,\n uint256 maxAmountIn,\n uint256 bzzAmountOut,\n bytes32 batchId,\n uint256 topupAmountPerChunk\n ) external {\n _swapExactOutput(path, msg.sender, maxAmountIn, bzzAmountOut);\n _approveBzzAndTopUp(batchId, topupAmountPerChunk, bzzAmountOut);\n address tokenIn = _lastToken(path);\n emit BatchToppedUpViaSwap(batchId, tokenIn, maxAmountIn, bzzAmountOut);\n }\n\n /**\n * @notice Swap native xDAI → BZZ and top up an existing Swarm stamp batch.\n * @dev Send msg.value ≥ maxAmountIn. Excess xDAI is refunded.\n * @param path BZZ ++ fee ++ [mid ++ fee]* ++ WXDAI\n * @param maxAmountIn Maximum xDAI to spend\n * @param bzzAmountOut Exact BZZ needed\n * @param batchId Batch to top up\n * @param topupAmountPerChunk Per-chunk top-up amount\n */\n function topUpNative(\n bytes calldata path,\n uint256 maxAmountIn,\n uint256 bzzAmountOut,\n bytes32 batchId,\n uint256 topupAmountPerChunk\n ) external payable {\n if (msg.value < maxAmountIn) revert InsufficientNativeValue();\n IWXDAI(WXDAI).deposit{value: maxAmountIn}();\n _swapExactOutput(path, address(this), maxAmountIn, bzzAmountOut);\n _approveBzzAndTopUp(batchId, topupAmountPerChunk, bzzAmountOut);\n emit BatchToppedUpViaSwap(batchId, address(0), maxAmountIn, bzzAmountOut);\n _refundNative();\n }\n\n // ─── Uniswap V3 / SushiSwap V3 Swap Callback ─────────────────────────────\n\n /**\n * @notice Called by a SushiSwap V3 pool during swap execution.\n * @dev Implements the Uniswap V3 callback interface (SushiSwap V3 is compatible).\n * For multi-hop swaps, this callback chains into the next pool swap before\n * paying the current pool, routing tokens directly between pools.\n */\n function uniswapV3SwapCallback(\n int256 amount0Delta,\n int256 amount1Delta,\n bytes calldata data\n ) external {\n require(amount0Delta > 0 || amount1Delta > 0, \"Zero deltas\");\n\n SwapCallbackData memory cb = abi.decode(data, (SwapCallbackData));\n\n // Decode the first pool in the path to verify the caller is legitimate.\n (address tokenOut, uint24 fee, address tokenIn) = _decodeFirstPool(cb.path);\n address expectedPool = ISushiV3Factory(SUSHI_FACTORY).getPool(tokenOut, tokenIn, fee);\n if (msg.sender != expectedPool) revert InvalidCallback();\n\n // Determine which token we owe to the calling pool and how much.\n // The positive delta is what the pool expects us to pay.\n (address tokenOwed, uint256 amountOwed) = amount0Delta > 0\n ? (ISushiV3Pool(msg.sender).token0(), uint256(amount0Delta))\n : (ISushiV3Pool(msg.sender).token1(), uint256(amount1Delta));\n\n if (_hasMultiplePools(cb.path)) {\n // Multi-hop: continue to next pool. Skip the first token from path to get\n // the remaining sub-path: mid ++ fee ++ ... ++ tokenIn\n bytes memory remainingPath = _skipToken(cb.path);\n\n // Decode the next pool info from remaining path.\n (address nextTokenOut, uint24 nextFee, address nextTokenIn) = _decodeFirstPool(remainingPath);\n address nextPool = ISushiV3Factory(SUSHI_FACTORY).getPool(nextTokenOut, nextTokenIn, nextFee);\n if (nextPool == address(0)) revert PoolNotFound();\n\n // Swap in the next pool, sending output directly to msg.sender (current pool)\n // so it receives the tokens it needs without going through this contract.\n bool zeroForOne = nextTokenIn < nextTokenOut;\n ISushiV3Pool(nextPool).swap(\n msg.sender, // recipient = current pool (gets tokenOwed directly)\n zeroForOne,\n -int256(amountOwed), // exact output = amountOwed\n zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,\n abi.encode(SwapCallbackData({\n path: remainingPath,\n payer: cb.payer,\n maxAmountIn: cb.maxAmountIn\n }))\n );\n } else {\n // Final hop: pay tokenOwed from the original payer.\n if (amountOwed > cb.maxAmountIn) {\n revert SlippageExceeded(amountOwed, cb.maxAmountIn);\n }\n\n if (cb.payer == address(this)) {\n // Native xDAI flow: we already hold WXDAI from the deposit.\n if (!IERC20(tokenOwed).transfer(msg.sender, amountOwed)) {\n revert BzzTransferFailed();\n }\n } else {\n // ERC20 flow: pull from user who pre-approved this contract.\n if (!IERC20(tokenOwed).transferFrom(cb.payer, msg.sender, amountOwed)) {\n revert BzzTransferFailed();\n }\n }\n }\n }\n\n // ─── Internal Helpers ─────────────────────────────────────────────────────\n\n /**\n * @dev Execute an exact-output swap for `bzzAmountOut` BZZ using the given path.\n * The path is in exactOutput encoding: BZZ ++ fee ++ [...] ++ tokenIn.\n * BZZ lands in address(this) after the swap completes.\n */\n function _swapExactOutput(\n bytes memory path,\n address payer,\n uint256 maxAmountIn,\n uint256 bzzAmountOut\n ) internal {\n if (path.length < POP_OFFSET) revert InvalidPath();\n\n // Decode the first (and for single-hop, only) pool in the path.\n (address tokenOut, uint24 fee, address tokenIn) = _decodeFirstPool(path);\n if (tokenOut != BZZ) revert InvalidPath();\n\n address pool = ISushiV3Factory(SUSHI_FACTORY).getPool(tokenOut, tokenIn, fee);\n if (pool == address(0)) revert PoolNotFound();\n\n // zeroForOne: true if tokenIn is token0 (address < BZZ)\n bool zeroForOne = tokenIn < tokenOut;\n\n // amountSpecified < 0 → exact output (we want exactly bzzAmountOut of BZZ)\n ISushiV3Pool(pool).swap(\n address(this), // receive BZZ here\n zeroForOne,\n -int256(bzzAmountOut),\n zeroForOne ? MIN_SQRT_RATIO + 1 : MAX_SQRT_RATIO - 1,\n abi.encode(SwapCallbackData({\n path: path,\n payer: payer,\n maxAmountIn: maxAmountIn\n }))\n );\n }\n\n /**\n * @dev Approve BZZ to the stamps registry and call createBatchRegistry.\n * Returns the keccak256 batch ID consistent with the registry's derivation.\n */\n function _approveBzzAndCreate(\n CreateBatchParams memory p,\n uint256 bzzAmountOut\n ) internal returns (bytes32 batchId) {\n if (!IERC20(BZZ).approve(address(stampsRegistry), bzzAmountOut)) {\n revert BzzApproveFailed();\n }\n\n stampsRegistry.createBatchRegistry(\n p.owner,\n p.nodeAddress,\n p.initialBalancePerChunk,\n p.depth,\n p.bucketDepth,\n p.nonce,\n p.immutable_\n );\n\n // Registry derives batchId as keccak256(abi.encode(registry, nonce)).\n batchId = keccak256(abi.encode(address(stampsRegistry), p.nonce));\n }\n\n /**\n * @dev Approve BZZ to the stamps registry and call topUpBatch.\n */\n function _approveBzzAndTopUp(\n bytes32 batchId,\n uint256 topupAmountPerChunk,\n uint256 bzzAmountOut\n ) internal {\n if (!IERC20(BZZ).approve(address(stampsRegistry), bzzAmountOut)) {\n revert BzzApproveFailed();\n }\n stampsRegistry.topUpBatch(batchId, topupAmountPerChunk);\n }\n\n /**\n * @dev Unwrap any remaining WXDAI and refund all native xDAI to msg.sender.\n */\n function _refundNative() internal {\n uint256 wxdaiBalance = IERC20(WXDAI).balanceOf(address(this));\n if (wxdaiBalance > 0) {\n IWXDAI(WXDAI).withdraw(wxdaiBalance);\n }\n uint256 nativeBalance = address(this).balance;\n if (nativeBalance > 0) {\n (bool ok,) = msg.sender.call{value: nativeBalance}(\"\");\n if (!ok) revert NativeRefundFailed();\n }\n }\n\n // ─── Path Utilities ───────────────────────────────────────────────────────\n\n /**\n * @dev Returns true if the path encodes more than one pool (length > 43 bytes).\n */\n function _hasMultiplePools(bytes memory path) internal pure returns (bool) {\n return path.length > POP_OFFSET;\n }\n\n /**\n * @dev Decodes the first pool segment from the path:\n * tokenA (20 bytes) ++ fee (3 bytes) ++ tokenB (20 bytes)\n */\n function _decodeFirstPool(bytes memory path)\n internal\n pure\n returns (address tokenA, uint24 fee, address tokenB)\n {\n tokenA = _toAddress(path, 0);\n fee = _toUint24(path, ADDR_SIZE);\n tokenB = _toAddress(path, NEXT_OFFSET);\n }\n\n /**\n * @dev Returns the path with the first token removed (skips ADDR_SIZE + FEE_SIZE bytes).\n * Used to advance through multi-hop paths in the callback.\n */\n function _skipToken(bytes memory path) internal pure returns (bytes memory skipped) {\n uint256 newLen = path.length - NEXT_OFFSET;\n skipped = new bytes(newLen);\n // Copy from offset NEXT_OFFSET onward\n for (uint256 i = 0; i < newLen; i++) {\n skipped[i] = path[i + NEXT_OFFSET];\n }\n }\n\n /**\n * @dev Extracts the last 20-byte address from the path (the tokenIn address).\n */\n function _lastToken(bytes memory path) internal pure returns (address token) {\n uint256 offset = path.length - ADDR_SIZE;\n token = _toAddress(path, offset);\n }\n\n /**\n * @dev Reads a 20-byte address from `data` at `offset` using assembly.\n * The address occupies bytes [offset, offset+20) and is right-aligned\n * by shifting the 32-byte word 96 bits right.\n */\n function _toAddress(bytes memory data, uint256 offset) internal pure returns (address addr) {\n assembly {\n addr := shr(96, mload(add(add(data, 0x20), offset)))\n }\n }\n\n /**\n * @dev Reads a 3-byte uint24 from `data` at `offset` using assembly.\n * Shifts the 32-byte word 232 bits right to extract the top 3 bytes.\n */\n function _toUint24(bytes memory data, uint256 offset) internal pure returns (uint24 result) {\n assembly {\n result := shr(232, mload(add(add(data, 0x20), offset)))\n }\n }\n}\n" + } + }, + "settings": { + "optimizer": { + "enabled": true, + "runs": 200 + }, + "viaIR": true, + "evmVersion": "paris", + "outputSelection": { + "*": { + "*": [ + "abi", + "evm.bytecode", + "evm.deployedBytecode", + "evm.methodIdentifiers", + "metadata", + "devdoc", + "userdoc", + "storageLayout", + "evm.gasEstimates" + ], + "": [ + "ast" + ] + } + }, + "metadata": { + "useLiteralContent": true + } + } +} \ No newline at end of file diff --git a/hardhat.config.ts b/hardhat.config.ts index 4dcaddb..5b4ae9c 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -7,6 +7,7 @@ import "hardhat-deploy"; import * as dotenv from "dotenv"; dotenv.config({ path: ".env" }); + // Get environment variables or use defaults const PRIVATE_KEY = process.env.WALLET_SECRET || "0x0000000000000000000000000000000000000000000000000000000000000000"; const GNOSIS_RPC_URL = process.env.GNOSIS_RPC_URL || "https://gnosis-rpc.publicnode.com"; From 89a03ec213fcb3539756e6ea00af5089eaa9d704 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 21:30:20 +0200 Subject: [PATCH 06/10] fix(gnosis): gate spend tokens on real Sushi routes and pool liquidity - Filter Gnosis "from" tokens with findSushiRoute after LiFi balances; clear selection when nothing spendable remains. - Require V3 pool liquidity() > 0 in discovery so empty pools (e.g. COW/USDC) are not treated as routable; Quoter no longer reverts on those paths. - Add findSushiRoutes and try each candidate in getSushiQuote until a quote succeeds. - Export gnosisFromTokenCanReachBzz (BZZ direct or Sushi path to BZZ). --- src/app/components/SushiQuotes.ts | 182 ++++++++++++++++++++---------- src/app/components/TokenUtils.ts | 56 ++++++++- 2 files changed, 172 insertions(+), 66 deletions(-) diff --git a/src/app/components/SushiQuotes.ts b/src/app/components/SushiQuotes.ts index 4a7cf5c..b576c72 100644 --- a/src/app/components/SushiQuotes.ts +++ b/src/app/components/SushiQuotes.ts @@ -92,6 +92,23 @@ const FEE_TIERS = [3000, 500, 10000, 100] as const; /** Minimal ABI for reading the fee tier directly from a V3 pool contract. */ const POOL_FEE_ABI = parseAbi(['function fee() external view returns (uint24)']); +/** V3 pool liquidity; pools can be deployed with no active LPs — Quoter then reverts. */ +const POOL_LIQUIDITY_ABI = parseAbi(['function liquidity() external view returns (uint128)']); + +async function v3PoolHasLiquidity(poolAddress: string): Promise { + try { + const { client } = getGnosisPublicClient(); + const liq = await client.readContract({ + address: poolAddress as `0x${string}`, + abi: POOL_LIQUIDITY_ABI, + functionName: 'liquidity', + }); + return liq > 0n; + } catch { + return false; + } +} + /** * Known pool addresses for tokens that have a direct BZZ pool. * We store only the address — the fee is read from the pool contract at @@ -130,17 +147,21 @@ async function findDirectBzzPool( const knownPoolAddress = KNOWN_BZZ_POOL_ADDRESSES[normalised]; if (knownPoolAddress && knownPoolAddress !== ZERO_ADDRESS) { try { - const fee = await client.readContract({ - address: knownPoolAddress as `0x${string}`, - abi: POOL_FEE_ABI, - functionName: 'fee', - }); - const result = { pool: knownPoolAddress, fee: Number(fee) }; - bzzPoolCache[normalised] = result; - console.log(`🍣 Pool fee for ${tokenIn}: ${Number(fee)} (${Number(fee) / 10000}%)`); - return result; + if (!(await v3PoolHasLiquidity(knownPoolAddress))) { + console.warn('⚠️ Known BZZ pool has zero liquidity, falling back to factory scan'); + } else { + const fee = await client.readContract({ + address: knownPoolAddress as `0x${string}`, + abi: POOL_FEE_ABI, + functionName: 'fee', + }); + const result = { pool: knownPoolAddress, fee: Number(fee) }; + bzzPoolCache[normalised] = result; + console.log(`🍣 Pool fee for ${tokenIn}: ${Number(fee)} (${Number(fee) / 10000}%)`); + return result; + } } catch { - console.warn('⚠️ Could not read fee from known pool, falling back to factory scan'); + console.warn('⚠️ Could not read known BZZ pool, falling back to factory scan'); } } @@ -154,7 +175,7 @@ async function findDirectBzzPool( args: [tokenIn as `0x${string}`, GNOSIS_BZZ_ADDRESS as `0x${string}`, fee], }); - if (pool && pool !== ZERO_ADDRESS) { + if (pool && pool !== ZERO_ADDRESS && (await v3PoolHasLiquidity(pool as string))) { const result = { pool: pool as string, fee }; bzzPoolCache[normalised] = result; return result; @@ -195,7 +216,7 @@ async function findPoolBetween( args: [tokenA as `0x${string}`, tokenB as `0x${string}`, fee], }); - if (pool && pool !== ZERO_ADDRESS) { + if (pool && pool !== ZERO_ADDRESS && (await v3PoolHasLiquidity(pool as string))) { const result = { pool: pool as string, fee }; poolPairCache[key] = result; return result; @@ -245,33 +266,31 @@ function encodeTwoHopPath( // ─── Route Resolution ───────────────────────────────────────────────────────── /** - * Determines the best swap route from `fromToken` to BZZ. - * Tries direct pools first, then routes through USDC or WXDAI. + * All structurally valid routes (pools exist and have non-zero liquidity), best first. */ -export async function findSushiRoute(fromToken: string): Promise { +export async function findSushiRoutes(fromToken: string): Promise { + const routes: SushiRouteInfo[] = []; + const isNativeXdai = fromToken === ZERO_ADDRESS || fromToken === '0x0' || fromToken.toLowerCase() === ZERO_ADDRESS; - // For native xDAI, use WXDAI as the effective input token. const effectiveTokenIn = isNativeXdai ? GNOSIS_WXDAI_ADDRESS : fromToken; - // ── Case 1: direct BZZ pool ───────────────────────────────────────────────── const directPool = await findDirectBzzPool(effectiveTokenIn); if (directPool) { const path = encodeSingleHopPath(effectiveTokenIn, directPool.fee); - return { + routes.push({ path, isNative: isNativeXdai, fees: [directPool.fee], description: isNativeXdai ? `xDAI → WXDAI → BZZ (single-hop, ${directPool.fee / 10000}% fee)` : `Direct → BZZ (${directPool.fee / 10000}% fee)`, - }; + }); } - // ── Case 2: two-hop via USDC (tokenIn → USDC → BZZ) ──────────────────────── if (effectiveTokenIn.toLowerCase() !== GNOSIS_USDC_ADDRESS.toLowerCase()) { const [tokenInUsdcPool, usdcBzzPool] = await Promise.all([ findPoolBetween(effectiveTokenIn, GNOSIS_USDC_ADDRESS), @@ -285,18 +304,17 @@ export async function findSushiRoute(fromToken: string): Promise { + const routes = await findSushiRoutes(fromToken); + return routes[0] ?? null; +} + +/** + * True if this Gnosis "from" token can fund stamps: BZZ (direct) or any token + * {@link findSushiRoute} can swap to BZZ via SushiSwap V3. + */ +export async function gnosisFromTokenCanReachBzz(fromToken: string): Promise { + const lower = fromToken.toLowerCase(); + if (lower === ZERO_ADDRESS || lower === '0x0') { + return (await findSushiRoute(fromToken)) !== null; + } + try { + if (getAddress(fromToken).toLowerCase() === getAddress(GNOSIS_BZZ_ADDRESS).toLowerCase()) { + return true; + } + } catch { + return false; + } + return (await findSushiRoute(fromToken)) !== null; } // ─── Quote ──────────────────────────────────────────────────────────────────── @@ -344,49 +391,60 @@ export const getSushiQuote = async (params: SushiQuoteParams): Promise findSushiRoute(fromToken), - 'findSushiRoute' - ); + const routes = await performWithRetry(() => findSushiRoutes(fromToken), 'findSushiRoutes'); - if (!route) { + if (routes.length === 0) { throw new Error( `No SushiSwap route found from ${tokenSymbol} to BZZ on Gnosis. ` + 'Try using USDC or xDAI instead.' ); } - console.log('🍣 Route found:', route.description); - const { client } = getGnosisPublicClient(); - // Call quoteSingleHop or quoteMultiHop depending on number of hops. - // Both are non-view but designed for eth_call (simulate = true). - const isSingleHop = route.fees.length === 1; - - let amountInBeforeSlippage: bigint; - - if (isSingleHop) { - const effectiveTokenIn = route.isNative ? GNOSIS_WXDAI_ADDRESS : fromToken; - const result = await client.simulateContract({ - address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, - abi: SUSHI_STAMPS_ROUTER_ABI, - functionName: 'quoteSingleHop', - args: [ - effectiveTokenIn as `0x${string}`, - route.fees[0], - BigInt(bzzAmount), - ], - }); - amountInBeforeSlippage = result.result as bigint; - } else { - const result = await client.simulateContract({ - address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, - abi: SUSHI_STAMPS_ROUTER_ABI, - functionName: 'quoteMultiHop', - args: [route.path, BigInt(bzzAmount)], - }); - amountInBeforeSlippage = result.result as bigint; + let amountInBeforeSlippage: bigint | undefined; + let route: SushiRouteInfo | undefined; + let lastQuoteError: unknown; + + for (const candidate of routes) { + const isSingleHop = candidate.fees.length === 1; + try { + if (isSingleHop) { + const effectiveTokenIn = candidate.isNative ? GNOSIS_WXDAI_ADDRESS : fromToken; + const result = await client.simulateContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'quoteSingleHop', + args: [ + effectiveTokenIn as `0x${string}`, + candidate.fees[0], + BigInt(bzzAmount), + ], + }); + amountInBeforeSlippage = result.result as bigint; + } else { + const result = await client.simulateContract({ + address: SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'quoteMultiHop', + args: [candidate.path, BigInt(bzzAmount)], + }); + amountInBeforeSlippage = result.result as bigint; + } + route = candidate; + console.log('🍣 Route quoted:', route.description); + break; + } catch (e) { + lastQuoteError = e; + console.warn('🍣 Quote failed for route, trying next…', candidate.description, e); + } + } + + if (route === undefined || amountInBeforeSlippage === undefined) { + throw new Error( + `Could not get a SushiSwap quote from ${tokenSymbol} to BZZ (tried ${routes.length} on-chain route(s)). ` + + 'Try another token or a smaller stamp size.' + ); } // Apply slippage buffer. diff --git a/src/app/components/TokenUtils.ts b/src/app/components/TokenUtils.ts index 91d90f8..14fc041 100644 --- a/src/app/components/TokenUtils.ts +++ b/src/app/components/TokenUtils.ts @@ -1,8 +1,35 @@ -import { ChainType, getTokenBalancesByChain, getTokens, TokensResponse } from '@lifi/sdk'; +import { + ChainId, + ChainType, + getTokenBalancesByChain, + getTokens, + TokensResponse, +} from '@lifi/sdk'; import { useState, useCallback } from 'react'; import { formatUnits } from 'viem'; +import { gnosisFromTokenCanReachBzz } from './SushiQuotes'; import { performWithRetry, toChecksumAddress } from './utils'; +/** Parallel RPC checks for Gnosis route gating (each token runs multiple reads). */ +const GNOSIS_ROUTE_CHECK_CONCURRENCY = 4; + +async function filterGnosisBalancesToSushiRoutable( + tokensWithPositiveBalance: T[] +): Promise { + if (tokensWithPositiveBalance.length === 0) { + return []; + } + const kept: T[] = []; + for (let i = 0; i < tokensWithPositiveBalance.length; i += GNOSIS_ROUTE_CHECK_CONCURRENCY) { + const chunk = tokensWithPositiveBalance.slice(i, i + GNOSIS_ROUTE_CHECK_CONCURRENCY); + const flags = await Promise.all( + chunk.map(async t => ((await gnosisFromTokenCanReachBzz(t.address)) ? t : null)) + ); + kept.push(...(flags.filter(Boolean) as T[])); + } + return kept; +} + // List of popular tokens to prioritize when wallet is not connected const POPULAR_TOKENS = [ 'ETH', @@ -118,11 +145,29 @@ export const useTokenManagement = ( } ); console.log('Token balances:', balances); - setTokenBalances(balances); + + let effectiveBalances = balances; + if (currentChainId === ChainId.DAI && balances?.[currentChainId]) { + const chainList = balances[currentChainId]; + const positive = chainList.filter(t => (t?.amount ?? 0n) > 0n); + if (positive.length > 0) { + const routable = await filterGnosisBalancesToSushiRoutable(positive); + const allow = new Set(routable.map(t => t.address.toLowerCase())); + const filteredChain = chainList.filter( + t => (t?.amount ?? 0n) === 0n || allow.has(t.address.toLowerCase()) + ); + effectiveBalances = { ...balances, [currentChainId]: filteredChain }; + console.log( + `🍣 Gnosis: ${positive.length} token(s) with balance → ${routable.length} with Sushi route to BZZ` + ); + } + } + + setTokenBalances(effectiveBalances); // Find tokens with balance - if (balances?.[currentChainId]) { - const tokensWithBalance = balances[currentChainId] + if (effectiveBalances?.[currentChainId]) { + const tokensWithBalance = effectiveBalances[currentChainId] .filter(t => (t?.amount ?? 0n) > 0n) .sort((a, b) => { const aUsdValue = @@ -154,6 +199,9 @@ export const useTokenManagement = ( setFromToken(checksumAddress); setSelectedTokenInfo(uniqueTokensWithBalance[0]); } + } else { + setFromToken(''); + setSelectedTokenInfo(null); } } } else { From e40af11c9961587555f8b41d6a9f7343efe33852 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 22:35:32 +0200 Subject: [PATCH 07/10] add sushi router to constants --- src/app/components/constants.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/components/constants.ts b/src/app/components/constants.ts index 4f65a56..210a1d1 100644 --- a/src/app/components/constants.ts +++ b/src/app/components/constants.ts @@ -277,10 +277,11 @@ export const SUSHI_QUOTER_ADDRESS = /** * SushiSwapStampsRouter – our deployed router that swaps any Gnosis token → BZZ * and atomically creates / tops up a Swarm stamp in a single transaction. - * Set NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS in .env after deploying. + * Override with NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS when using another deployment. */ export const SUSHI_STAMPS_ROUTER_ADDRESS = - process.env.NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS || ''; + process.env.NEXT_PUBLIC_SUSHI_STAMPS_ROUTER_ADDRESS || + '0x2a0a54368Bb6b0D8fa31568D092ffBDf350ab553'; /** Minimal ABI for the SushiSwap V3 Factory – only what we need for pool discovery */ export const SUSHI_FACTORY_ABI = [ From 0ef785ff92952b48bd20c6c1ced1405897c554fd Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 22:53:29 +0200 Subject: [PATCH 08/10] =?UTF-8?q?feat:=20cross-chain=20stamp=20purchase=20?= =?UTF-8?q?via=20Relay=E2=86=92USDC=E2=86=92SushiRouter=E2=86=92BZZ?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Relay has reliable routes to USDC on every major chain but poor BZZ routes. The existing SushiSwapStampsRouter already handles any Gnosis token→BZZ→stamp atomically. New flow for cross-chain users: 1. Relay bridges source token → USDC on Gnosis (EXACT_OUTPUT) 2. Relay's Multicaller executes two destination txs atomically: a. USDC.approve(sushiRouter, maxUsdcIn) b. sushiRouter.createBatch / topUp(path, maxUsdcIn, bzzOut, ...) 3. SushiRouter swaps USDC → BZZ via SushiSwap V3 and calls registry No new contract needed — the deployed SushiSwapStampsRouter already supports any ERC-20 payer (Relay's Multicaller in this case). Changes: - constants.ts: add RELAY_BRIDGE_TOKEN_ON_GNOSIS (default USDC), RELAY_BRIDGE_TOKEN_DECIMALS and RELAY_BRIDGE_TOKEN_SYMBOL — all overridable via env vars for future flexibility. - RelayQuotes.ts: add getRelayCrossChainWithSushiQuote() that chains a Sushi quote (USDC→BZZ) with a Relay EXACT_OUTPUT quote (source→USDC) and builds the two-tx calldata bundle for Relay. - SwapComponent.tsx: price estimation and handleSwap Branch 3 now use the new USDC+Sushi path when the router is deployed; legacy Relay→BZZ path kept as fallback when router address is empty. --- src/app/components/RelayQuotes.ts | 162 +++++++++++++++++++++++++++ src/app/components/SwapComponent.tsx | 108 ++++++++++++++---- src/app/components/constants.ts | 19 ++++ 3 files changed, 267 insertions(+), 22 deletions(-) diff --git a/src/app/components/RelayQuotes.ts b/src/app/components/RelayQuotes.ts index 066018e..fd15dce 100644 --- a/src/app/components/RelayQuotes.ts +++ b/src/app/components/RelayQuotes.ts @@ -9,8 +9,14 @@ import { RELAY_STATUS_CHECK_INTERVAL_MS, RELAY_STATUS_MAX_ATTEMPTS, TRANSACTION_TIMEOUT_MS, + RELAY_BRIDGE_TOKEN_ON_GNOSIS, + RELAY_BRIDGE_TOKEN_DECIMALS, + RELAY_BRIDGE_TOKEN_SYMBOL, + SUSHI_STAMPS_ROUTER_ADDRESS, + SUSHI_STAMPS_ROUTER_ABI, } from './constants'; import { performWithRetry, getGnosisPublicClient } from './utils'; +import { getSushiQuote } from './SushiQuotes'; import { getPollingInterval } from '@/app/wagmi'; // Relay API Error Codes and Messages @@ -833,6 +839,162 @@ const monitorRelayStatus = async ( throw new Error(`Operation timed out for step ${stepId} after ${maxAttempts} attempts`); }; +/** + * Cross-chain flow: bridge source token → RELAY_BRIDGE_TOKEN (USDC) on Gnosis via Relay, + * then atomically swap USDC → BZZ and create/top-up a Swarm stamp via SushiSwapStampsRouter. + * + * Why: Relay has reliable routes to USDC on every major chain; routing directly to BZZ is + * fragile. The existing SushiSwapStampsRouter already handles USDC → BZZ → stamp on-chain. + * + * Relay delivers USDC to its Multicaller which executes two destination txs atomically: + * 1. USDC.approve(sushiRouter, maxUsdcIn) + * 2. sushiRouter.createBatch / topUp(path, maxUsdcIn, bzzOut, params) + */ +export const getRelayCrossChainWithSushiQuote = async ({ + selectedChainId, + fromToken, + address, + bzzAmount, + nodeAddress, + swarmConfig, + topUpBatchId, + setEstimatedTime, + isForEstimation = false, + slippagePercent, +}: RelayQuoteParams) => { + console.log( + `🌉 Cross-chain via USDC+Sushi – ${isForEstimation ? 'estimating' : 'executing'}…` + ); + + // ── Step 1: Sushi quote for bridge token → BZZ ────────────────────────────── + const sushiQuote = await getSushiQuote({ + fromToken: RELAY_BRIDGE_TOKEN_ON_GNOSIS, + bzzAmount, + slippagePercent: slippagePercent ?? DEFAULT_SLIPPAGE, + tokenSymbol: RELAY_BRIDGE_TOKEN_SYMBOL, + tokenDecimals: RELAY_BRIDGE_TOKEN_DECIMALS, + tokenPriceUsd: 1.0, // USDC ≈ $1 – used only for the internal USD display of the Sushi leg + }); + + const maxBridgeTokenIn = sushiQuote.maxAmountIn; // includes slippage buffer + + console.log(`🍣 Sushi quote: need ${maxBridgeTokenIn.toString()} ${RELAY_BRIDGE_TOKEN_SYMBOL} for ${bzzAmount} BZZ`); + + // ── Step 2: Encode SushiRouter calldata ───────────────────────────────────── + let routerCallData: string; + if (topUpBatchId) { + routerCallData = encodeFunctionData({ + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'topUp', + args: [ + sushiQuote.route.path, + maxBridgeTokenIn, + BigInt(bzzAmount), + topUpBatchId as `0x${string}`, + swarmConfig.swarmBatchInitialBalance, + ], + }); + } else { + routerCallData = encodeFunctionData({ + abi: SUSHI_STAMPS_ROUTER_ABI, + functionName: 'createBatch', + args: [ + sushiQuote.route.path, + maxBridgeTokenIn, + BigInt(bzzAmount), + { + owner: address as `0x${string}`, + nodeAddress: nodeAddress as `0x${string}`, + initialBalancePerChunk: swarmConfig.swarmBatchInitialBalance, + depth: swarmConfig.swarmBatchDepth, + bucketDepth: swarmConfig.swarmBatchBucketDepth, + nonce: swarmConfig.swarmBatchNonce, + immutable_: swarmConfig.swarmBatchImmutable, + }, + ], + }); + } + + // ── Step 3: Encode ERC-20 approval (exact amount; Multicaller is the caller) ─ + const approvalData = encodeFunctionData({ + abi: parseAbi(['function approve(address spender, uint256 amount) external returns (bool)']), + functionName: 'approve', + args: [SUSHI_STAMPS_ROUTER_ADDRESS as `0x${string}`, maxBridgeTokenIn], + }); + + const txs = [ + { to: RELAY_BRIDGE_TOKEN_ON_GNOSIS, value: '0', data: approvalData }, + { to: SUSHI_STAMPS_ROUTER_ADDRESS, value: '0', data: routerCallData }, + ]; + + // ── Step 4: Gas top-up on Gnosis ───────────────────────────────────────────── + const hasEnoughGas = await checkGnosisGasBalance(address); + const shouldTopupGas = !hasEnoughGas; + console.log(`⛽ Gas top-up: ${shouldTopupGas ? 'ENABLED' : 'DISABLED'}`); + + // ── Step 5: Relay quote to bridge source token → USDC on Gnosis ───────────── + const relayQuoteRequest: RelayQuoteRequest = { + user: address, + recipient: address, + originChainId: selectedChainId, + destinationChainId: ChainId.DAI, + originCurrency: fromToken, + destinationCurrency: RELAY_BRIDGE_TOKEN_ON_GNOSIS, + amount: maxBridgeTokenIn.toString(), + tradeType: 'EXACT_OUTPUT', // deliver exactly maxBridgeTokenIn USDC + txs, + slippageTolerance: Math.round((slippagePercent ?? DEFAULT_SLIPPAGE) * 100).toString(), + refundOnOrigin: true, + topupGas: shouldTopupGas, + ...(shouldTopupGas && { topupGasAmount: GAS_TOPUP_AMOUNT_USD }), + }; + + const relayQuoteResponse = await performWithRetry( + async () => { + console.log('🌐 Relay cross-chain USDC quote request:', relayQuoteRequest); + const response = await fetch('https://api.relay.link/quote', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(relayQuoteRequest), + }); + if (!response.ok) { + const errorText = await response.text(); + const { userMessage, errorCode } = parseRelayError(errorText); + const err = new Error(userMessage); + (err as any).relayErrorCode = errorCode; + (err as any).originalError = errorText; + throw err; + } + const data = await response.json(); + console.log('✅ Relay USDC quote response:', data); + return data as RelayQuoteResponse; + }, + `getRelayCrossChainWithSushiQuote-${isForEstimation ? 'estimation' : 'execution'}`, + undefined, + isForEstimation ? 3 : 5, + 500 + ); + + const totalAmountUSD = Number(relayQuoteResponse.details.currencyIn.amountUsd || 0); + + if (setEstimatedTime && relayQuoteResponse.details.timeEstimate) { + setEstimatedTime(relayQuoteResponse.details.timeEstimate); + } + + console.log( + `✅ Cross-chain USDC+Sushi quote: $${totalAmountUSD.toFixed(2)}, steps: ${relayQuoteResponse.steps.length}` + ); + + return { + totalAmountUSD, + relayQuoteResponse, + steps: relayQuoteResponse.steps, + estimatedTime: relayQuoteResponse.details.timeEstimate, + isGnosisOnly: false, + selectedChainId, + }; +}; + /** * Unified function that replaces the old modular quote system * Uses Relay API for both price estimation and execution diff --git a/src/app/components/SwapComponent.tsx b/src/app/components/SwapComponent.tsx index 2166946..73f42c1 100644 --- a/src/app/components/SwapComponent.tsx +++ b/src/app/components/SwapComponent.tsx @@ -62,6 +62,7 @@ import { useTimer } from './TimerUtils'; // Note: LiFi quote functions removed - now using Relay API via RelayQuotes.ts import { getRelaySwapQuotes, + getRelayCrossChainWithSushiQuote, executeRelaySteps, RelayQuoteResponse, parseRelayError, @@ -375,6 +376,11 @@ const SwapComponent: React.FC = () => { getAddress(fromToken) !== getAddress(GNOSIS_BZZ_ADDRESS) && SUSHI_STAMPS_ROUTER_ADDRESS !== ''; + // ── Cross-chain + router deployed: Relay → USDC → Sushi → BZZ → stamp ── + const isCrossChainWithSushi = + selectedChainId !== ChainId.DAI && + SUSHI_STAMPS_ROUTER_ADDRESS !== ''; + let totalAmountUSD: number; if (isGnosisNonBzz) { @@ -399,8 +405,29 @@ const SwapComponent: React.FC = () => { totalAmountUSD = sushiQuote.totalAmountUSD; console.log(`💰 SushiSwap quote: $${totalAmountUSD.toFixed(2)}`); + } else if (isCrossChainWithSushi) { + // ── Cross-chain: Relay bridges to USDC, SushiRouter swaps → BZZ → stamp + console.log('🌉 Using Relay→USDC→Sushi quote for cross-chain…'); + + const crossChainResult = await getRelayCrossChainWithSushiQuote({ + selectedChainId, + fromToken, + address, + bzzAmount, + nodeAddress, + swarmConfig, + topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, + setEstimatedTime: () => {}, + isForEstimation: true, + slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, + }); + + if (abortSignal.aborted) return; + + totalAmountUSD = crossChainResult.totalAmountUSD; + console.log(`💰 Cross-chain USDC+Sushi estimate: $${totalAmountUSD.toFixed(2)}`); } else { - // ── All other cases: Relay (same-chain BZZ or cross-chain) ────────────── + // ── Gnosis + BZZ direct (or fallback): use Relay ─────────────────────── const relayQuoteResult = await getRelaySwapQuotes({ selectedChainId, fromToken, @@ -1187,30 +1214,67 @@ const SwapComponent: React.FC = () => { }); } } else { - // ── Branch 3: cross-chain → Relay ────────────────────────────────────── + // ── Branch 3: cross-chain ─────────────────────────────────────────────── + // Prefer Relay → USDC → SushiRouter → BZZ → stamp (better routing). + // Fall back to legacy Relay → BZZ path if router is not deployed. + const useSushiBridge = SUSHI_STAMPS_ROUTER_ADDRESS !== ''; + setStatusMessage({ step: 'Quoting', message: 'Getting quote...', }); - const relayQuoteResult = await getRelaySwapQuotes({ - selectedChainId, - fromToken, - address, - bzzAmount: updatedConfig.swarmBatchTotal, - nodeAddress, - swarmConfig: updatedConfig, - topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, - setEstimatedTime: () => {}, - isForEstimation: false, - slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, - }); + let crossChainRelayResponse: RelayQuoteResponse; + let crossChainEstimatedTime: number; - console.log('✅ Relay execution quotes ready:', { - totalUSD: `$${relayQuoteResult.totalAmountUSD.toFixed(2)}`, - steps: relayQuoteResult.steps.length, - estimatedTime: relayQuoteResult.estimatedTime, - }); + if (useSushiBridge) { + console.log('🌉 Cross-chain via Relay→USDC→SushiRouter→BZZ→stamp…'); + + const result = await getRelayCrossChainWithSushiQuote({ + selectedChainId, + fromToken, + address, + bzzAmount: updatedConfig.swarmBatchTotal, + nodeAddress, + swarmConfig: updatedConfig, + topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, + setEstimatedTime: () => {}, + isForEstimation: false, + slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, + }); + + crossChainRelayResponse = result.relayQuoteResponse; + crossChainEstimatedTime = result.estimatedTime; + + console.log('✅ Cross-chain USDC+Sushi quotes ready:', { + totalUSD: `$${result.totalAmountUSD.toFixed(2)}`, + steps: result.steps.length, + estimatedTime: result.estimatedTime, + }); + } else { + // Legacy: Relay bridges directly to BZZ + const relayQuoteResult = await getRelaySwapQuotes({ + selectedChainId, + fromToken, + address, + bzzAmount: updatedConfig.swarmBatchTotal, + nodeAddress, + swarmConfig: updatedConfig, + topUpBatchId: isTopUp ? topUpBatchId || undefined : undefined, + setEstimatedTime: () => {}, + isForEstimation: false, + slippagePercent: useCustomSlippage ? customSlippagePercent : undefined, + }); + + crossChainRelayResponse = relayQuoteResult.relayQuoteResponse; + crossChainEstimatedTime = relayQuoteResult.estimatedTime; + + console.log('✅ Relay execution quotes ready:', { + totalUSD: `$${relayQuoteResult.totalAmountUSD.toFixed(2)}`, + steps: relayQuoteResult.steps.length, + estimatedTime: relayQuoteResult.estimatedTime, + }); + } setStatusMessage({ step: 'Preparing', @@ -1218,17 +1282,17 @@ const SwapComponent: React.FC = () => { }); await executeRelaySteps( - relayQuoteResult.relayQuoteResponse, + crossChainRelayResponse, walletClient, publicClient, setStatusMessage, () => { console.log( '🚀 Transaction confirmed! Starting timer:', - relayQuoteResult.estimatedTime, + crossChainEstimatedTime, 'seconds' ); - setEstimatedTime(relayQuoteResult.estimatedTime); + setEstimatedTime(crossChainEstimatedTime); setStatusMessage({ step: 'Relay', message: 'Executing cross-chain swap...', diff --git a/src/app/components/constants.ts b/src/app/components/constants.ts index 210a1d1..49a83ac 100644 --- a/src/app/components/constants.ts +++ b/src/app/components/constants.ts @@ -264,6 +264,25 @@ export const BZZ_USDC_POOL_ADDRESS = export const GNOSIS_USDC_ADDRESS = process.env.NEXT_PUBLIC_GNOSIS_USDC_ADDRESS || '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83'; +/** + * Intermediate token Relay bridges to on Gnosis for cross-chain stamp purchases. + * Relay has excellent routes to USDC on most chains; the SushiSwapStampsRouter then + * swaps this token → BZZ and creates the stamp atomically on Gnosis. + * Override with NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_ON_GNOSIS to use a different token + * (must have a working Sushi V3 route to BZZ on Gnosis). + */ +export const RELAY_BRIDGE_TOKEN_ON_GNOSIS = + process.env.NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_ON_GNOSIS || GNOSIS_USDC_ADDRESS; + +/** Decimals of {@link RELAY_BRIDGE_TOKEN_ON_GNOSIS}. Override when changing the bridge token. */ +export const RELAY_BRIDGE_TOKEN_DECIMALS = Number( + process.env.NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_DECIMALS ?? '6' +); + +/** Symbol of {@link RELAY_BRIDGE_TOKEN_ON_GNOSIS} (used for display/logging). */ +export const RELAY_BRIDGE_TOKEN_SYMBOL = + process.env.NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_SYMBOL || 'USDC'; + // ─── SushiSwap V3 on Gnosis ──────────────────────────────────────────────── /** SushiSwap V3 Factory on Gnosis – used for pool discovery */ From 8ea3f46be53c3b05e85dd4c36cc4271b3133fe24 Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 23:17:38 +0200 Subject: [PATCH 09/10] fix: use Relay's native Circle USDC address on Gnosis for cross-chain bridging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RELAY_BRIDGE_TOKEN_ON_GNOSIS was pointing at the old Ethereum-bridged USDC (0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83) which Relay cannot route to. Relay only has solver liquidity for the native Circle USDC on Gnosis (0x2a22f9c3b484c3629090feed35f17ff8f88f76f0). The SushiSwapStampsRouter still works because there is a Sushi V3 fee-100 pool between the two USDC tokens (liq ~2.4T) and the existing findSushiRoute logic already checks token → GNOSIS_USDC_ADDRESS (bridged) → BZZ, producing the two-hop path: native USDC → bridged USDC → BZZ. Verified manually: Relay accepts the small failing amount ($1.85) with the correct address from Base, Ethereum, Arbitrum, Optimism, and Polygon. --- src/app/components/constants.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/app/components/constants.ts b/src/app/components/constants.ts index 49a83ac..d5d2ddc 100644 --- a/src/app/components/constants.ts +++ b/src/app/components/constants.ts @@ -266,13 +266,20 @@ export const GNOSIS_USDC_ADDRESS = /** * Intermediate token Relay bridges to on Gnosis for cross-chain stamp purchases. - * Relay has excellent routes to USDC on most chains; the SushiSwapStampsRouter then - * swaps this token → BZZ and creates the stamp atomically on Gnosis. - * Override with NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_ON_GNOSIS to use a different token + * Relay has excellent routes to this token on most chains; the SushiSwapStampsRouter + * then swaps it → BZZ and creates the stamp atomically on Gnosis. + * + * IMPORTANT: This is Circle's native USDC on Gnosis (0x2a22…), NOT the older Ethereum- + * bridged USDC (0xDDAf…). Relay only supports routing to the native address. + * The SushiSwapStampsRouter handles the two-hop: native USDC → bridged USDC → BZZ, + * using the fee-100 Sushi V3 pool between the two USDC tokens. + * + * Override with NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_ON_GNOSIS when using a different token * (must have a working Sushi V3 route to BZZ on Gnosis). */ export const RELAY_BRIDGE_TOKEN_ON_GNOSIS = - process.env.NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_ON_GNOSIS || GNOSIS_USDC_ADDRESS; + process.env.NEXT_PUBLIC_RELAY_BRIDGE_TOKEN_ON_GNOSIS || + '0x2a22f9c3b484c3629090feed35f17ff8f88f76f0'; // native Circle USDC on Gnosis /** Decimals of {@link RELAY_BRIDGE_TOKEN_ON_GNOSIS}. Override when changing the bridge token. */ export const RELAY_BRIDGE_TOKEN_DECIMALS = Number( From 6f00777b005fa9907f1bf0a5476f67c20c081a0c Mon Sep 17 00:00:00 2001 From: Cardinal Date: Tue, 7 Apr 2026 23:35:42 +0200 Subject: [PATCH 10/10] fix timers --- src/app/components/RelayQuotes.ts | 4 ++-- src/app/components/TimerUtils.ts | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/components/RelayQuotes.ts b/src/app/components/RelayQuotes.ts index fd15dce..175cc1d 100644 --- a/src/app/components/RelayQuotes.ts +++ b/src/app/components/RelayQuotes.ts @@ -530,7 +530,7 @@ export const getRelayQuote = async ({ // Step 6: Set estimated time if provided if (setEstimatedTime && relayQuoteResponse.details.timeEstimate) { - setEstimatedTime(relayQuoteResponse.details.timeEstimate); + setEstimatedTime(Math.ceil(relayQuoteResponse.details.timeEstimate)); } console.log( @@ -978,7 +978,7 @@ export const getRelayCrossChainWithSushiQuote = async ({ const totalAmountUSD = Number(relayQuoteResponse.details.currencyIn.amountUsd || 0); if (setEstimatedTime && relayQuoteResponse.details.timeEstimate) { - setEstimatedTime(relayQuoteResponse.details.timeEstimate); + setEstimatedTime(Math.ceil(relayQuoteResponse.details.timeEstimate)); } console.log( diff --git a/src/app/components/TimerUtils.ts b/src/app/components/TimerUtils.ts index 3ae9a18..11adec1 100644 --- a/src/app/components/TimerUtils.ts +++ b/src/app/components/TimerUtils.ts @@ -20,9 +20,10 @@ export const useTimer = (statusMessage: ExecutionStatus) => { * @returns Formatted time string (e.g., "2:45") */ const formatTime = (seconds: number): string => { - if (seconds <= 0) return '0:00'; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = seconds % 60; + const s = Math.ceil(seconds); + if (s <= 0) return '0:00'; + const minutes = Math.floor(s / 60); + const remainingSeconds = s % 60; return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; };