diff --git a/apps/node/HOOKS_USAGE_GUIDE.md b/apps/node/HOOKS_USAGE_GUIDE.md new file mode 100644 index 000000000..112b9df86 --- /dev/null +++ b/apps/node/HOOKS_USAGE_GUIDE.md @@ -0,0 +1,224 @@ +# Intent Hooks SDK Usage Guide + +This guide provides comprehensive instructions for using the Intent Hooks SDK and the `intent-hooks.ts` CLI script to interact with the sodax protocol on Sonic Mainnet. + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Reference Addresses](#reference-addresses) +3. [Setup & Configuration](#setup--configuration) +4. [CLI Commands Guide](#cli-commands-guide) + - [General](#general) + - [Credit Hook (Limit Orders)](#credit-hook-limit-orders) + - [Leverage Hook](#leverage-hook) + - [Deleverage Hook](#deleverage-hook) + - [Debt Side Leverage Hook](#debt-side-leverage-hook) + - [Liquidation Hook](#liquidation-hook) +5. [Common Workflows](#common-workflows) +6. [Troubleshooting](#troubleshooting) + +## Prerequisites + +1. **Environment Variable**: Set `EVM_PRIVATE_KEY` in your `.env` file (must be a funded wallet on Sonic Mainnet). +2. **Tokens**: Ensure you have sufficient balances of `sodaUSDC` and `sodaETH`. +3. **Node.js**: Ensure Node.js and `pnpm` are installed. + +## Reference Addresses + +### Tokens (Sonic Mainnet - Chain ID 146) + +| Token | Symbol | Address | Decimals | Usage | +|-------|--------|---------|----------|-------| +| **sodaETH** | sodaETH | `0x4effB5813271699683C25c734F4daBc45B363709` | 18 | Collateral | +| **sodaUSDC** | sodaUSDC | `0xAbbb91c0617090F0028BDC27597Cd0D038F3A833` | 18 | Debt | + +### Contract Addresses + +| Contract | Address | +|----------|---------| +| **Intents Contract** | `0x6382D6ccD780758C5e8A6123c33ee8F4472F96ef` | +| **Credit Hook** | `0xe2A8E6023eB4C88c51472c8eB1332b87Dd09d8f7` | +| **Leverage Hook** | `0xB0E2ee3C1dA131d4004f0b8cc2ca159FaA129B86` | +| **Debt Side Leverage Hook** | `0x34aFac3b87c5585942D74a1F12eA13a33821D4bd` | +| **Deleverage Hook** | `0x5fF9c34f1734c2B62c53231E6923D0967F95a8A3` | +| **Liquidation Hook** | `0x9e6D9D2D9c900Be023d839910855A864eDE3ABBD` | + +You can also view these addresses by running: +```bash +pnpm tsx src/intent-hooks.ts showAddresses +``` + +## Setup & Configuration + +Verify your setup by checking token balances: + +```bash +pnpm tsx src/intent-hooks.ts checkBalances +``` + +**Note**: If the balance shows 0 but is visible on sodax.com/swap, tokens might be in a smart contract wallet. + +### Address Normalization +The SDK automatically handles address normalization (converting to 20 bytes), so you can safely paste addresses from block explorers. + +### Amount Units +All amounts must be specified in **wei** (18 decimals). +- 1.0 Token = `1000000000000000000` +- 0.1 Token = `100000000000000000` + +## Intent Types & Purposes + +Understanding which hook to use and how they interact with your money market position: + +### 1. Credit Hook (Limit Orders) +**Purpose**: Perform limit orders or spot swaps using your credit line or wallet balance. +- **Mechanism**: You **delegate credit** (or approve token spending) to the Hook contract. The hook uses this to borrow the "Debt Asset" from your available credit line (or pull from wallet). +- **Function**: The hook pays the "Debt Asset" to the solver, who in exchange provides the "Target Asset" to you. +- **Use Case**: swapping assets without upfront capital (using credit), or paying off debts with other assets. + +### 2. Leverage Hook +**Purpose**: Open or increase a leveraged position in a single transaction. +- **Mechanism**: **Prerequisite: You must have an existing supply position.** You **delegate credit** to the Hook contract. The hook borrows the "Debt Asset" on your behalf against your existing collateral. +- **Function**: The hook borrows the asset, sends it to the solver, and the solver swaps it for "Collateral Asset". The hook then deposits this new collateral back into your position, effectively increasing your exposure. +- **Use Case**: Longing an asset with leverage (e.g., Borrow USDC -> Buy ETH -> Supply ETH). + +### 3. Deleverage Hook +**Purpose**: Reduce leverage or close a position to avoid liquidation or take profits. +- **Mechanism**: You **approve the Hook contract to spend your aTokens** (your receipt tokens for supplied collateral). +- **Function**: The hook uses this approval to **withdraw** "Collateral Asset" from your position. It sends this collateral to the solver, who pays "Debt Asset" in return. The hook immediately uses this "Debt Asset" to **repay** your loan in the money market. +- **Use Case**: Reducing risk, taking profits, or avoiding liquidation (e.g., Withdraw ETH -> Sell for USDC -> Repay USDC Debt). + +### 4. Debt Side Leverage Hook +**Purpose**: Advanced leverage management, often for specific debt strategies. +- **Mechanism**: Similar to the Leverage Hook, relies on **credit delegation**. You delegate borrowing power to the Hook. +- **Function**: The hook borrows "Debt Asset" on your behalf (can be combined with user-provided capital). The solver swaps this for "Collateral Asset", which is deposited into your position. This is distinct from standard leverage in how it handles the initial capital source and accounting. +- **Use Case**: Advanced yield farming or maximizing borrowing power against specific collateral. + +### 5. Liquidation Hook +**Purpose**: Protocol health maintenance (for Liquidators). +- **Mechanism**: Does not require user delegation. The protocol allows liquidation of positions with Health Factor < 1.0. +- **Function**: The hook (called by a liquidator) pays "Debt Asset" to repay the user's bad debt. In return, the hook **seizes** a portion of the user's "Collateral Asset" (plus a liquidation bonus) and sends it to the solver/liquidator. +- **Use Case**: Running a liquidation bot to earn profits and secure the protocol. + +## CLI Commands Guide + +### General + +| Command | Description | +|---------|-------------| +| `showAddresses` | Show all hook contract addresses | +| `checkBalances` | Check sodaUSDC and sodaETH balances | +| `getIntentState ` | Check the state of an intent | +| `cancelIntentFromTx ` | Cancel an intent using its transaction hash | + +### Credit Hook (Limit Orders) + +**Create Credit Intent:** +```bash +pnpm tsx src/intent-hooks.ts createCreditIntentWithPrerequisites \ + \ + \ + \ + \ + 0 # deadline (0 = none) +``` +*Auto-approves credit delegation.* + +**Fill Credit Intent (for Solvers):** +```bash +pnpm tsx src/intent-hooks.ts fillIntentWithData +``` + +### Leverage Hook + +**Create Leverage Intent:** +```bash +pnpm tsx src/intent-hooks.ts createLeverageIntentWithPrerequisites \ + \ + \ + \ + \ + 0 +``` +- `inputToken` (Solver Provides): debtAsset +- `outputToken` (Solver Receives): collateralAsset + +### Deleverage Hook + +**Create Deleverage Intent:** +```bash +pnpm tsx src/intent-hooks.ts createDeleverageIntentWithPrerequisites \ + \ + \ + \ + \ + 0 +``` +- `inputToken` (Hook Withdraws): collateralAsset +- `outputToken` (Hook Repays): debtAsset (Solver provides this) + +### Debt Side Leverage Hook + +**Check Status:** +```bash +pnpm tsx src/intent-hooks.ts checkDebtSideLeverageStatus +``` + +**Create Intent:** +```bash +pnpm tsx src/intent-hooks.ts createDebtSideLeverageIntentWithPrerequisites \ + \ + \ + \ + \ + \ + 0 +``` + +### Liquidation Hook + +**Check Opportunity:** +```bash +pnpm tsx src/intent-hooks.ts checkLiquidation +``` + +**Create Intent:** +```bash +pnpm tsx src/intent-hooks.ts createLiquidationIntent \ + \ + \ + \ + \ + \ + 0 +``` + +## Common Workflows + +### Complete Leverage & Deleverage Test Loop +This workflow tests the entire cycle: creating a leverage position, filling it, then unwinding it with deleverage. + +```bash +pnpm tsx src/intent-hooks.ts leverageAndDeleverage \ + 0x4effB5813271699683C25c734F4daBc45B363709 \ # sodaETH + 0xAbbb91c0617090F0028BDC27597Cd0D038F3A833 \ # sodaUSDC + \ + +``` + +**Steps Performed:** +1. Creates leverage intent (auto-approves delegation). +2. Fills leverage intent (you act as solver). +3. Verifies position. +4. Creates deleverage intent (auto-approves aToken). +5. Fills deleverage intent. + +## Troubleshooting + +| Error | Cause | Solution | +|-------|-------|----------| +| `ERC20InsufficientBalance` | Low token balance | Ensure you have enough `sodaETH`/`sodaUSDC`. Solvers need `sodaETH` inventory. | +| `IntentAlreadyExists` | Duplicate intent ID | Intent ID is random but based on params; try again or cancel existing. | +| `PartialFillNotAllowed` | Wrong fill amounts | Use exact `inputAmount` and `minOutputAmount` when filling. | +| `ERC20InsufficientAllowance` | Missing approval | Use the `...WithPrerequisites` commands to auto-approve. | +| `IntentNotFound` | Hash mismatch | Verify intent data and address normalization. | diff --git a/apps/node/package.json b/apps/node/package.json index ddf4e6781..87440b73a 100644 --- a/apps/node/package.json +++ b/apps/node/package.json @@ -16,6 +16,7 @@ "backend-api-test": "pnpm run build && node dist/backend-api.test.js", "solana": "pnpm run build && node dist/solana.js", "swap": "pnpm run build && node dist/swap.js", + "intent-hooks": "pnpm run build && node dist/intent-hooks.js", "example": "pnpm run build && node dist/examples/injective/index.js", "build": "tsc", "checkTs": "tsc --noEmit", diff --git a/apps/node/src/intent-hooks.ts b/apps/node/src/intent-hooks.ts new file mode 100644 index 000000000..716ebf01e --- /dev/null +++ b/apps/node/src/intent-hooks.ts @@ -0,0 +1,1795 @@ +// apps/node/src/intent-hooks.ts +import 'dotenv/config'; +import { createPublicClient, createWalletClient, http, type Address, type Hex, erc20Abi, formatUnits } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { sonic } from 'viem/chains'; +import { HooksService, HookType } from '@sodax/sdk'; +import { + SONIC_MAINNET_CHAIN_ID, + type HubChainId, + type HookIntent, + getSolverConfig, + getMoneyMarketConfig, + getHooksConfig, +} from '@sodax/types'; +import { poolAbi } from '@sodax/sdk'; + +// Load configuration from environment +const privateKey = process.env.EVM_PRIVATE_KEY; + +if (!privateKey) { + throw new Error('EVM_PRIVATE_KEY environment variable is required'); +} + +// Ensure private key has 0x prefix +const formattedPrivateKey = privateKey.startsWith('0x') ? (privateKey as Hex) : (`0x${privateKey}` as Hex); + +// Constants +const HUB_CHAIN_ID: HubChainId = SONIC_MAINNET_CHAIN_ID; +const HUB_RPC_URL = 'https://rpc.soniclabs.com'; + +// Create viem clients +const account = privateKeyToAccount(formattedPrivateKey); + +const publicClient = createPublicClient({ + chain: sonic, + transport: http(HUB_RPC_URL), +}); + +const walletClient = createWalletClient({ + account, + chain: sonic, + transport: http(HUB_RPC_URL), +}); + +// Initialize HooksService +const hooksService = new HooksService({ + publicClient, + walletClient, + chainId: HUB_CHAIN_ID, +}); + +// Helper to get user address +async function getUserAddress(): Promise
{ + const addresses = await walletClient.getAddresses(); + if (!addresses[0]) { + throw new Error('No wallet address available'); + } + return addresses[0]; +} + +function normalizeAddress(addr: string): Hex { + const hex = addr.startsWith('0x') ? addr : `0x${addr}`; + const bytes = (hex.length - 2) / 2; + if (bytes === 32) { + // Extract last 20 bytes (40 hex chars) from 32-byte ABI-encoded value + return `0x${hex.slice(-40)}` as Hex; + } + if (bytes === 20) { + return hex.toLowerCase() as Hex; + } + if (bytes > 20) { + // If longer, extract last 20 bytes + return `0x${hex.slice(-40)}` as Hex; + } + // If shorter, it's invalid for EVM addresses, but use as-is + return hex.toLowerCase() as Hex; +} + +// === CREDIT HOOK FUNCTIONS === + +/** + * Check credit delegation status for a specific hook + */ +async function checkCreditDelegation(debtAsset: Address, hookType: HookType): Promise { + console.log(`\n[checkCreditDelegation] Checking delegation for ${hookType} hook...`); + const userAddress = await getUserAddress(); + console.log(`[checkCreditDelegation] User: ${userAddress}`); + console.log(`[checkCreditDelegation] Debt Asset: ${debtAsset}`); + + const result = await hooksService.getCreditDelegationStatus(debtAsset, userAddress, hookType); + + if (result.ok) { + console.log(`[checkCreditDelegation] Delegated: ${result.value.delegated}`); + console.log(`[checkCreditDelegation] Allowance: ${result.value.allowance}`); + } else { + console.error('[checkCreditDelegation] Error:', result.error); + } +} + +/** + * Approve credit delegation for a specific hook + */ +async function approveDelegation(debtAsset: Address, amount: string, hookType: HookType): Promise { + console.log(`\n[approveDelegation] Approving delegation for ${hookType} hook...`); + console.log(`[approveDelegation] Debt Asset: ${debtAsset}`); + console.log(`[approveDelegation] Amount: ${amount}`); + + const result = await hooksService.approveCreditDelegation(debtAsset, amount, hookType); + + if (result.ok) { + console.log('[approveDelegation] Approved!'); + console.log(`[approveDelegation] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[approveDelegation] Error:', result.error); + } +} + +/** + * Approve token spending for a specific hook + */ +async function approveTokenSpending(tokenAddress: Address, amount: string, hookType: HookType): Promise { + console.log(`\n[approveToken] Approving token for ${hookType} hook...`); + console.log(`[approveToken] Token: ${tokenAddress}`); + console.log(`[approveToken] Amount: ${amount}`); + + const result = await hooksService.approveToken(tokenAddress, amount, hookType); + + if (result.ok) { + console.log('[approveToken] Approved!'); + console.log(`[approveToken] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[approveToken] Error:', result.error); + } +} + +/** + * Approve aToken spending for a specific hook + */ +async function approveATokenSpending(underlyingAsset: Address, amount: string, hookType: HookType): Promise { + console.log(`\n[approveAToken] Approving aToken for ${hookType} hook...`); + console.log(`[approveAToken] Underlying Asset: ${underlyingAsset}`); + console.log(`[approveAToken] Amount: ${amount}`); + + const result = await hooksService.approveAToken(underlyingAsset, amount, hookType); + + if (result.ok) { + console.log('[approveAToken] Approved!'); + console.log(`[approveAToken] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[approveAToken] Error:', result.error); + } +} + +/** + * Create a credit intent (limit order) + */ +async function createCreditIntent( + debtAsset: Address, + targetAsset: Address, + maxPayment: string, + minReceive: string, + deadline?: string, +): Promise { + console.log('\n[createCreditIntent] Creating credit intent (limit order)...'); + console.log(`[createCreditIntent] Debt Asset: ${debtAsset}`); + console.log(`[createCreditIntent] Target Asset: ${targetAsset}`); + console.log(`[createCreditIntent] Max Payment: ${maxPayment}`); + console.log(`[createCreditIntent] Min Receive: ${minReceive}`); + + const result = await hooksService.createCreditIntent( + { + debtAsset, + targetAsset, + maxPayment, + minReceive, + deadline, + }, + 146, // Sonic mainnet chain ID + ); + + if (result.ok) { + console.log('[createCreditIntent] Intent created!'); + console.log(`[createCreditIntent] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[createCreditIntent] Error:', result.error); + } +} + +/** + * Create a credit intent with automatic prerequisite handling + */ +async function createCreditIntentWithPrerequisites( + debtAsset: Address, + targetAsset: Address, + maxPayment: string, + minReceive: string, + deadline?: string, + checkOnly?: boolean, +): Promise { + console.log('\n[createCreditIntentWithPrerequisites] Creating credit intent with prerequisites...'); + console.log(`[createCreditIntentWithPrerequisites] Debt Asset: ${debtAsset}`); + console.log(`[createCreditIntentWithPrerequisites] Target Asset: ${targetAsset}`); + console.log(`[createCreditIntentWithPrerequisites] Max Payment: ${maxPayment}`); + console.log(`[createCreditIntentWithPrerequisites] Min Receive: ${minReceive}`); + if (checkOnly) { + console.log('[createCreditIntentWithPrerequisites] Check-only mode: will not create intent'); + } + + const result = await hooksService.createCreditIntentWithPrerequisites( + { + debtAsset, + targetAsset, + maxPayment, + minReceive, + deadline, + }, + 146, + { checkOnly: checkOnly === true }, + ); + + if (result.ok) { + console.log('[createCreditIntentWithPrerequisites] Prerequisites:'); + console.log(` - Credit Delegation Approved: ${result.value.prerequisites.creditDelegationApproved}`); + if (!checkOnly) { + console.log('[createCreditIntentWithPrerequisites] Intent created!'); + console.log(`[createCreditIntentWithPrerequisites] Tx Hash: ${result.value.txHash}`); + } + } else { + console.error('[createCreditIntentWithPrerequisites] Error:', result.error); + } +} + +// === LEVERAGE HOOK FUNCTIONS === + +/** + * Create a leverage intent + */ +async function createLeverageIntent( + collateralAsset: Address, + debtAsset: Address, + collateralAmount: string, + borrowAmount: string, + deadline?: string, +): Promise { + console.log('\n[createLeverageIntent] Creating leverage intent...'); + console.log(`[createLeverageIntent] Collateral Asset: ${collateralAsset}`); + console.log(`[createLeverageIntent] Debt Asset: ${debtAsset}`); + console.log(`[createLeverageIntent] Collateral Amount: ${collateralAmount}`); + console.log(`[createLeverageIntent] Borrow Amount: ${borrowAmount}`); + + const result = await hooksService.createLeverageIntent( + { + collateralAsset, + debtAsset, + collateralAmount, + borrowAmount, + deadline, + }, + 146, + ); + + if (result.ok) { + console.log('[createLeverageIntent] Intent created!'); + console.log(`[createLeverageIntent] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[createLeverageIntent] Error:', result.error); + } +} + +/** + * Create a leverage intent with automatic prerequisite handling + */ +async function createLeverageIntentWithPrerequisites( + collateralAsset: Address, + debtAsset: Address, + collateralAmount: string, + borrowAmount: string, + deadline?: string, + checkOnly?: boolean, + feeAmount?: string, + feeReceiver?: Address, +): Promise { + console.log('\n[createLeverageIntentWithPrerequisites] Creating leverage intent with prerequisites...'); + console.log(`[createLeverageIntentWithPrerequisites] Collateral Asset: ${collateralAsset}`); + console.log(`[createLeverageIntentWithPrerequisites] Debt Asset: ${debtAsset}`); + console.log(`[createLeverageIntentWithPrerequisites] Collateral Amount: ${collateralAmount}`); + console.log(`[createLeverageIntentWithPrerequisites] Borrow Amount: ${borrowAmount}`); + if (feeAmount && feeReceiver) { + console.log(`[createLeverageIntentWithPrerequisites] Fee Amount: ${feeAmount}`); + console.log(`[createLeverageIntentWithPrerequisites] Fee Receiver: ${feeReceiver}`); + } + if (checkOnly) { + console.log('[createLeverageIntentWithPrerequisites] Check-only mode: will not create intent'); + } + + const result = await hooksService.createLeverageIntentWithPrerequisites( + { + collateralAsset, + debtAsset, + collateralAmount, + borrowAmount, + deadline, + feeAmount, + feeReceiver, + }, + 146, + { checkOnly: checkOnly === true }, + ); + + if (result.ok) { + console.log('[createLeverageIntentWithPrerequisites] Prerequisites:'); + console.log(` - Credit Delegation Approved: ${result.value.prerequisites.creditDelegationApproved}`); + if (!checkOnly) { + console.log('[createLeverageIntentWithPrerequisites] Intent created!'); + console.log(`[createLeverageIntentWithPrerequisites] Tx Hash: ${result.value.txHash}`); + } + } else { + console.error('[createLeverageIntentWithPrerequisites] Error:', result.error); + } +} + +// === DEBT SIDE LEVERAGE HOOK FUNCTIONS === + +/** + * Check debt side leverage status + */ +async function checkDebtSideLeverageStatus(debtAsset: Address): Promise { + console.log('\n[checkDebtSideLeverageStatus] Checking status...'); + const userAddress = await getUserAddress(); + console.log(`[checkDebtSideLeverageStatus] User: ${userAddress}`); + console.log(`[checkDebtSideLeverageStatus] Debt Asset: ${debtAsset}`); + + const result = await hooksService.getDebtSideLeverageStatus(debtAsset, userAddress); + + if (result.ok) { + console.log(`[checkDebtSideLeverageStatus] Token Allowance: ${result.value.tokenAllowance}`); + console.log(`[checkDebtSideLeverageStatus] Credit Delegation: ${result.value.creditDelegation}`); + console.log(`[checkDebtSideLeverageStatus] Token Balance: ${result.value.tokenBalance}`); + console.log(`[checkDebtSideLeverageStatus] Is Ready: ${result.value.isReady ? ' Yes' : ' No'}`); + } else { + console.error('[checkDebtSideLeverageStatus] Error:', result.error); + } +} + +/** + * Create a debt side leverage intent + */ +async function createDebtSideLeverageIntent( + collateralAsset: Address, + debtAsset: Address, + collateralAmount: string, + userProvidedAmount: string, + totalBorrowAmount: string, + deadline?: string, +): Promise { + console.log('\n[createDebtSideLeverageIntent] Creating debt side leverage intent...'); + console.log(`[createDebtSideLeverageIntent] Collateral Asset: ${collateralAsset}`); + console.log(`[createDebtSideLeverageIntent] Debt Asset: ${debtAsset}`); + console.log(`[createDebtSideLeverageIntent] Collateral Amount: ${collateralAmount}`); + console.log(`[createDebtSideLeverageIntent] User Provided Amount: ${userProvidedAmount}`); + console.log(`[createDebtSideLeverageIntent] Total Borrow Amount: ${totalBorrowAmount}`); + + const result = await hooksService.createDebtSideLeverageIntent( + { + collateralAsset, + debtAsset, + collateralAmount, + userProvidedAmount, + totalBorrowAmount, + deadline, + }, + 146, + ); + + if (result.ok) { + console.log('[createDebtSideLeverageIntent] Intent created!'); + console.log(`[createDebtSideLeverageIntent] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[createDebtSideLeverageIntent] Error:', result.error); + } +} + +/** + * Create a debt side leverage intent with automatic prerequisite handling + */ +async function createDebtSideLeverageIntentWithPrerequisites( + collateralAsset: Address, + debtAsset: Address, + collateralAmount: string, + userProvidedAmount: string, + totalBorrowAmount: string, + deadline?: string, + checkOnly?: boolean, +): Promise { + console.log( + '\n[createDebtSideLeverageIntentWithPrerequisites] Creating debt side leverage intent with prerequisites...', + ); + console.log(`[createDebtSideLeverageIntentWithPrerequisites] Collateral Asset: ${collateralAsset}`); + console.log(`[createDebtSideLeverageIntentWithPrerequisites] Debt Asset: ${debtAsset}`); + console.log(`[createDebtSideLeverageIntentWithPrerequisites] Collateral Amount: ${collateralAmount}`); + console.log(`[createDebtSideLeverageIntentWithPrerequisites] User Provided Amount: ${userProvidedAmount}`); + console.log(`[createDebtSideLeverageIntentWithPrerequisites] Total Borrow Amount: ${totalBorrowAmount}`); + if (checkOnly) { + console.log('[createDebtSideLeverageIntentWithPrerequisites] Check-only mode: will not create intent'); + } + + const result = await hooksService.createDebtSideLeverageIntentWithPrerequisites( + { + collateralAsset, + debtAsset, + collateralAmount, + userProvidedAmount, + totalBorrowAmount, + deadline, + }, + 146, + { checkOnly: checkOnly === true }, + ); + + if (result.ok) { + console.log('[createDebtSideLeverageIntentWithPrerequisites] Prerequisites:'); + console.log(` - Credit Delegation Approved: ${result.value.prerequisites.creditDelegationApproved}`); + console.log(` - Token Approved: ${result.value.prerequisites.tokenApproved}`); + if (!checkOnly) { + console.log('[createDebtSideLeverageIntentWithPrerequisites] Intent created!'); + console.log(`[createDebtSideLeverageIntentWithPrerequisites] Tx Hash: ${result.value.txHash}`); + } + } else { + console.error('[createDebtSideLeverageIntentWithPrerequisites] Error:', result.error); + } +} + +// === DELEVERAGE HOOK FUNCTIONS === + +/** + * Check aToken approval info for deleverage + */ +async function checkATokenApprovalInfo( + collateralAsset: Address, + withdrawAmount: string, + feeAmount?: string, +): Promise { + console.log('\n[checkATokenApprovalInfo] Checking aToken approval info...'); + const userAddress = await getUserAddress(); + console.log(`[checkATokenApprovalInfo] User: ${userAddress}`); + console.log(`[checkATokenApprovalInfo] Collateral Asset: ${collateralAsset}`); + console.log(`[checkATokenApprovalInfo] Withdraw Amount: ${withdrawAmount}`); + + const result = await hooksService.getATokenApprovalInfo(collateralAsset, userAddress, withdrawAmount, feeAmount); + + if (result.ok) { + console.log(`[checkATokenApprovalInfo] aToken Address: ${result.value.aTokenAddress}`); + console.log(`[checkATokenApprovalInfo] aTokens Needed: ${result.value.aTokensNeeded}`); + console.log(`[checkATokenApprovalInfo] Current Allowance: ${result.value.currentAllowance}`); + console.log(`[checkATokenApprovalInfo] Is Approved: ${result.value.isApproved ? ' Yes' : ' No'}`); + } else { + console.error('[checkATokenApprovalInfo] Error:', result.error); + } +} + +/** + * Create a deleverage intent + */ +async function createDeleverageIntent( + collateralAsset: Address, + debtAsset: Address, + withdrawAmount: string, + repayAmount: string, + deadline?: string, +): Promise { + console.log('\n[createDeleverageIntent] Creating deleverage intent...'); + console.log(`[createDeleverageIntent] Collateral Asset: ${collateralAsset}`); + console.log(`[createDeleverageIntent] Debt Asset: ${debtAsset}`); + console.log(`[createDeleverageIntent] Withdraw Amount: ${withdrawAmount}`); + console.log(`[createDeleverageIntent] Repay Amount: ${repayAmount}`); + + const result = await hooksService.createDeleverageIntent( + { + collateralAsset, + debtAsset, + withdrawAmount, + repayAmount, + deadline, + }, + 146, + ); + + if (result.ok) { + console.log('[createDeleverageIntent] Intent created!'); + console.log(`[createDeleverageIntent] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[createDeleverageIntent] Error:', result.error); + } +} + +/** + * Create a deleverage intent with automatic prerequisite handling + */ +async function createDeleverageIntentWithPrerequisites( + collateralAsset: Address, + debtAsset: Address, + withdrawAmount: string, + repayAmount: string, + deadline?: string, + checkOnly?: boolean, +): Promise { + console.log('\n[createDeleverageIntentWithPrerequisites] Creating deleverage intent with prerequisites...'); + console.log(`[createDeleverageIntentWithPrerequisites] Collateral Asset: ${collateralAsset}`); + console.log(`[createDeleverageIntentWithPrerequisites] Debt Asset: ${debtAsset}`); + console.log(`[createDeleverageIntentWithPrerequisites] Withdraw Amount: ${withdrawAmount}`); + console.log(`[createDeleverageIntentWithPrerequisites] Repay Amount: ${repayAmount}`); + if (checkOnly) { + console.log('[createDeleverageIntentWithPrerequisites] Check-only mode: will not create intent'); + } + + const result = await hooksService.createDeleverageIntentWithPrerequisites( + { + collateralAsset, + debtAsset, + withdrawAmount, + repayAmount, + deadline, + }, + 146, + { checkOnly: checkOnly === true }, + ); + + if (result.ok) { + console.log('[createDeleverageIntentWithPrerequisites] Prerequisites:'); + console.log(` - aToken Approved: ${result.value.prerequisites.aTokenApproved}`); + if (!checkOnly) { + console.log('[createDeleverageIntentWithPrerequisites] Intent created!'); + console.log(`[createDeleverageIntentWithPrerequisites] Tx Hash: ${result.value.txHash}`); + } + } else { + console.error('[createDeleverageIntentWithPrerequisites] Error:', result.error); + } +} + +// === LIQUIDATION HOOK FUNCTIONS === + +/** + * Check if a user position is liquidatable + */ +async function checkLiquidationOpportunity(userToCheck: Address): Promise { + console.log('\n[checkLiquidationOpportunity] Checking liquidation opportunity...'); + console.log(`[checkLiquidationOpportunity] User to Check: ${userToCheck}`); + + const result = await hooksService.getLiquidationOpportunity(userToCheck); + + if (result.ok) { + console.log(`[checkLiquidationOpportunity] Health Factor: ${result.value.healthFactor}`); + console.log(`[checkLiquidationOpportunity] Is Liquidatable: ${result.value.isLiquidatable ? ' Yes' : ' No'}`); + console.log('[checkLiquidationOpportunity] Account Data:'); + console.log(` - Total Collateral: ${result.value.accountData.totalCollateralBase}`); + console.log(` - Total Debt: ${result.value.accountData.totalDebtBase}`); + console.log(` - Available Borrows: ${result.value.accountData.availableBorrowsBase}`); + console.log(` - LTV: ${result.value.accountData.ltv}`); + console.log(` - Liquidation Threshold: ${result.value.accountData.currentLiquidationThreshold}`); + } else { + console.error('[checkLiquidationOpportunity] Error:', result.error); + } +} + +/** + * Create a liquidation intent + */ +async function createLiquidationIntent( + collateralAsset: Address, + debtAsset: Address, + userToLiquidate: Address, + collateralAmount: string, + debtAmount: string, + deadline?: string, +): Promise { + console.log('\n[createLiquidationIntent] Creating liquidation intent...'); + console.log(`[createLiquidationIntent] Collateral Asset: ${collateralAsset}`); + console.log(`[createLiquidationIntent] Debt Asset: ${debtAsset}`); + console.log(`[createLiquidationIntent] User to Liquidate: ${userToLiquidate}`); + console.log(`[createLiquidationIntent] Collateral Amount: ${collateralAmount}`); + console.log(`[createLiquidationIntent] Debt Amount: ${debtAmount}`); + + const result = await hooksService.createLiquidationIntent( + { + collateralAsset, + debtAsset, + userToLiquidate, + collateralAmount, + debtAmount, + deadline, + }, + 146, + ); + + if (result.ok) { + console.log('[createLiquidationIntent] Intent created!'); + console.log(`[createLiquidationIntent] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[createLiquidationIntent] Error:', result.error); + } +} + +// === INTENT LIFECYCLE FUNCTIONS === + +/** + * Get intent hash from transaction hash by parsing IntentCreated event + */ +async function getIntentHashFromTx(txHash: Hex): Promise { + try { + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const solverConfig = getSolverConfig(HUB_CHAIN_ID); + const userAddress = await getUserAddress(); + + // Query IntentCreated events from the transaction's block + const blockNumber = receipt.blockNumber; + + // Get all IntentCreated events from the block + const logs = await publicClient.getLogs({ + address: solverConfig.intentsContract as Address, + event: { + type: 'event', + name: 'IntentCreated', + inputs: [ + { name: 'intentHash', type: 'bytes32', indexed: false }, + { + name: 'intent', + type: 'tuple', + components: [ + { name: 'intentId', type: 'uint256' }, + { name: 'creator', type: 'address' }, + { name: 'inputToken', type: 'address' }, + { name: 'outputToken', type: 'address' }, + { name: 'inputAmount', type: 'uint256' }, + { name: 'minOutputAmount', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'allowPartialFill', type: 'bool' }, + { name: 'srcChain', type: 'uint256' }, + { name: 'dstChain', type: 'uint256' }, + { name: 'srcAddress', type: 'bytes' }, + { name: 'dstAddress', type: 'bytes' }, + { name: 'solver', type: 'address' }, + { name: 'data', type: 'bytes' }, + ], + }, + ], + }, + fromBlock: blockNumber, + toBlock: blockNumber, + }); + + // Find the log where creator matches our address + const matchingLog = logs.find(log => { + const intent = log.args.intent; + return intent && intent.creator?.toLowerCase() === userAddress.toLowerCase(); + }); + + if (matchingLog && matchingLog.args.intentHash) { + return matchingLog.args.intentHash as Hex; + } + + return null; + } catch (error) { + console.error('[getIntentHashFromTx] Error:', error); + return null; + } +} + +/** + * Get intent state by hash + */ +async function getIntentState(intentHash: Hex): Promise { + console.log('\n[getIntentState] Querying intent state...'); + console.log(`[getIntentState] Intent Hash: ${intentHash}`); + + const result = await hooksService.getIntentState(intentHash); + + if (result.ok) { + console.log('[getIntentState] Intent State:'); + console.log(` - Exists: ${result.value.exists}`); + console.log(` - Remaining Input: ${result.value.remainingInput}`); + console.log(` - Received Output: ${result.value.receivedOutput}`); + console.log(` - Pending Payment: ${result.value.pendingPayment}`); + } else { + console.error('[getIntentState] Error:', result.error); + } +} + +/** + * Get pending intent state + */ +async function getPendingIntentState(intentHash: Hex): Promise { + console.log('\n[getPendingIntentState] Querying pending state...'); + console.log(`[getPendingIntentState] Intent Hash: ${intentHash}`); + + const result = await hooksService.getPendingIntentState(intentHash); + + if (result.ok) { + console.log('[getPendingIntentState] Pending State:'); + console.log(` - Pending Input: ${result.value.pendingInput}`); + console.log(` - Pending Output: ${result.value.pendingOutput}`); + } else { + console.error('[getPendingIntentState] Error:', result.error); + } +} + +/** + * Check if an intent is fillable + */ +async function checkFillable(intentHash: Hex): Promise { + console.log('\n[checkFillable] Checking if intent is fillable...'); + console.log(`[checkFillable] Intent Hash: ${intentHash}`); + + const result = await hooksService.isFillable(intentHash); + + if (result.ok) { + console.log(`[checkFillable] Is Fillable: ${result.value ? 'Yes' : 'No'}`); + } else { + console.error('[checkFillable] Error:', result.error); + } +} + +/** + * Check intent state before cancellation + */ +async function checkIntentStateBeforeCancel(intent: HookIntent): Promise { + console.log('\n[checkIntentStateBeforeCancel] Checking intent state...'); + + const intentHash = hooksService.computeIntentHash(intent); + console.log(`[checkIntentStateBeforeCancel] Intent Hash: ${intentHash}`); + + const stateResult = await hooksService.getIntentState(intentHash); + if (!stateResult.ok) { + console.error('[checkIntentStateBeforeCancel] Error getting intent state:', stateResult.error); + return; + } + + const { exists, remainingInput } = stateResult.value; + console.log(`[checkIntentStateBeforeCancel] Intent exists: ${exists}`); + console.log(`[checkIntentStateBeforeCancel] Remaining input: ${remainingInput}`); + + if (!exists) { + console.error('[checkIntentStateBeforeCancel] Intent does not exist!'); + console.error('[checkIntentStateBeforeCancel] This might mean:'); + console.error(' - The intent hash does not match (check address normalization)'); + console.error(' - The intent was never created'); + console.error(' - The intent parameters are incorrect'); + return; + } + + if (remainingInput === '0') { + console.error('[checkIntentStateBeforeCancel] Intent has no remaining input - cannot cancel!'); + console.error('[checkIntentStateBeforeCancel] The intent has already been fully filled or cancelled.'); + return; + } + + console.log('[checkIntentStateBeforeCancel] Intent can be cancelled āœ“'); +} + +/** + * Cancel an intent using intent data directly + */ +async function cancelIntentWithData(intentData: { + intentId: string; + creator: Address; + inputToken: Address; + outputToken: Address; + inputAmount: string; + minOutputAmount: string; + deadline: string; + allowPartialFill: boolean; + srcChain: string; + dstChain: string; + srcAddress: string; // hex string + dstAddress: string; // hex string + solver: Address; + data: string; // hex string +}): Promise { + console.log('\n[cancelIntentWithData] Cancelling intent with provided data...'); + console.log(`[cancelIntentWithData] Intent ID: ${intentData.intentId}`); + console.log(`[cancelIntentWithData] Creator: ${intentData.creator}`); + + // Ensure data is properly formatted as Hex + const dataHex = intentData.data.startsWith('0x') ? (intentData.data as Hex) : (`0x${intentData.data}` as Hex); + + const srcAddressHex = normalizeAddress(intentData.srcAddress); + const dstAddressHex = normalizeAddress(intentData.dstAddress); + + console.log(`[cancelIntentWithData] Data (hex): ${dataHex}`); + console.log(`[cancelIntentWithData] Data length: ${(dataHex.length - 2) / 2} bytes`); + console.log( + `[cancelIntentWithData] srcAddress (formatted): ${srcAddressHex} (${(srcAddressHex.length - 2) / 2} bytes)`, + ); + console.log( + `[cancelIntentWithData] dstAddress (formatted): ${dstAddressHex} (${(dstAddressHex.length - 2) / 2} bytes)`, + ); + + const intent: HookIntent = { + intentId: BigInt(intentData.intentId), + creator: intentData.creator, + inputToken: intentData.inputToken, + outputToken: intentData.outputToken, + inputAmount: BigInt(intentData.inputAmount), + minOutputAmount: BigInt(intentData.minOutputAmount), + deadline: BigInt(intentData.deadline), + allowPartialFill: intentData.allowPartialFill, + srcChain: BigInt(intentData.srcChain), + dstChain: BigInt(intentData.dstChain), + srcAddress: srcAddressHex, + dstAddress: dstAddressHex, + solver: intentData.solver, + data: dataHex, + }; + + // Check intent state before attempting cancellation + await checkIntentStateBeforeCancel(intent); + + const result = await hooksService.cancelIntent(intent); + + if (result.ok) { + console.log('[cancelIntentWithData] Intent cancelled!'); + console.log(`[cancelIntentWithData] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[cancelIntentWithData] Error:', result.error); + } +} + +/** + * Get full intent data from a transaction hash + */ +async function getIntentDataFromTx(txHash: Hex): Promise { + try { + const receipt = await publicClient.waitForTransactionReceipt({ + hash: txHash, + }); + const solverConfig = getSolverConfig(HUB_CHAIN_ID); + const userAddress = await getUserAddress(); + + // Query IntentCreated events from the transaction's block + const blockNumber = receipt.blockNumber; + + // Get all IntentCreated events from the block + const logs = await publicClient.getLogs({ + address: solverConfig.intentsContract as Address, + event: { + type: 'event', + name: 'IntentCreated', + inputs: [ + { name: 'intentHash', type: 'bytes32', indexed: false }, + { + name: 'intent', + type: 'tuple', + components: [ + { name: 'intentId', type: 'uint256' }, + { name: 'creator', type: 'address' }, + { name: 'inputToken', type: 'address' }, + { name: 'outputToken', type: 'address' }, + { name: 'inputAmount', type: 'uint256' }, + { name: 'minOutputAmount', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'allowPartialFill', type: 'bool' }, + { name: 'srcChain', type: 'uint256' }, + { name: 'dstChain', type: 'uint256' }, + { name: 'srcAddress', type: 'bytes' }, + { name: 'dstAddress', type: 'bytes' }, + { name: 'solver', type: 'address' }, + { name: 'data', type: 'bytes' }, + ], + }, + ], + }, + fromBlock: blockNumber, + toBlock: blockNumber, + }); + + // Find the log where creator matches our address + const matchingLog = logs.find(log => { + const intent = log.args.intent; + return intent && intent.creator?.toLowerCase() === userAddress.toLowerCase(); + }); + + if (matchingLog && matchingLog.args.intent) { + const intent = matchingLog.args.intent; + return { + intentId: intent.intentId, + creator: intent.creator as Address, + inputToken: intent.inputToken as Address, + outputToken: intent.outputToken as Address, + inputAmount: intent.inputAmount, + minOutputAmount: intent.minOutputAmount, + deadline: intent.deadline, + allowPartialFill: intent.allowPartialFill, + srcChain: intent.srcChain, + dstChain: intent.dstChain, + srcAddress: normalizeAddress(intent.srcAddress as string), + dstAddress: normalizeAddress(intent.dstAddress as string), + solver: intent.solver as Address, + data: intent.data as string, + }; + } + + return null; + } catch (error) { + console.error('[getIntentDataFromTx] Error:', error); + return null; + } +} + +/** + * Cancel an intent using transaction hash + */ +async function cancelIntentFromTx(txHash: Hex): Promise { + console.log('\n[cancelIntentFromTx] Getting intent data from transaction...'); + console.log(`[cancelIntentFromTx] Transaction Hash: ${txHash}`); + + const intentData = await getIntentDataFromTx(txHash); + if (!intentData) { + console.error('[cancelIntentFromTx] Failed to get intent data from transaction'); + return; + } + + console.log('[cancelIntentFromTx] Intent data retrieved, cancelling intent...'); + + await cancelIntentWithData({ + intentId: intentData.intentId.toString(), + creator: intentData.creator, + inputToken: intentData.inputToken, + outputToken: intentData.outputToken, + inputAmount: intentData.inputAmount.toString(), + minOutputAmount: intentData.minOutputAmount.toString(), + deadline: intentData.deadline.toString(), + allowPartialFill: intentData.allowPartialFill, + srcChain: intentData.srcChain.toString(), + dstChain: intentData.dstChain.toString(), + srcAddress: intentData.srcAddress, + dstAddress: intentData.dstAddress, + solver: intentData.solver, + data: intentData.data, + }); +} + +/** + * Ensure token is approved to Intents contract for filling + * The solver needs to approve inputToken to the Intents contract + */ +async function ensureTokenApprovedForFill(tokenAddress: Address, amount: string): Promise { + const userAddress = await getUserAddress(); + const solverConfig = getSolverConfig(HUB_CHAIN_ID); + const intentsContractAddress = solverConfig.intentsContract; + + try { + // Check balance first + const balance = await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [userAddress], + }); + + const currentAllowance = await publicClient.readContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: 'allowance', + args: [userAddress, intentsContractAddress], + }); + + const amountBigInt = BigInt(amount); + console.log( + `[ensureTokenApprovedForFill] Balance: ${balance.toString()}, Required: ${amountBigInt.toString()}, Allowance: ${currentAllowance.toString()}`, + ); + + if (balance < amountBigInt) { + throw new Error(`Insufficient balance: have ${balance.toString()}, need ${amountBigInt.toString()}`); + } + + if (currentAllowance < amountBigInt) { + console.log(`[ensureTokenApprovedForFill] Approving ${amount} tokens to Intents contract...`); + const hash = await walletClient.writeContract({ + address: tokenAddress, + abi: erc20Abi, + functionName: 'approve', + args: [intentsContractAddress, amountBigInt], + account: walletClient.account, + chain: walletClient.chain, + }); + await publicClient.waitForTransactionReceipt({ hash }); + console.log('[ensureTokenApprovedForFill] Token approved!'); + } else { + console.log('[ensureTokenApprovedForFill] Token already approved'); + } + } catch (error) { + console.error('[ensureTokenApprovedForFill] Error:', error); + throw error; + } +} + +/** + * Decode hook address from intent data + */ +function decodeHookAddressFromIntentData(data: string): Address | null { + try { + // Intent data format: 0x02 + ABI-encoded HookData + // HookData: {hook: address, data: bytes} + if (!data.startsWith('0x02')) { + return null; + } + // Skip 0x02 prefix (1 byte = 2 hex chars) + const hookDataBytes = data.slice(4); // Remove "0x02" + // ABI-encoded tuple: offset (32 bytes = 64 hex chars) + hook address (32 bytes = 64 hex chars, last 40 are the address) + // Hook address is at position: last 40 chars of the address field = hex chars 88-128 + const hookAddressHex = `0x${hookDataBytes.slice(88, 128)}`; // Extract 20 bytes (40 hex chars) + return hookAddressHex.toLowerCase() as Address; + } catch (error) { + console.error('[decodeHookAddressFromIntentData] Error:', error); + return null; + } +} + +/** + * Fill an intent (using user address as solver) + * Addresses are normalized to 20 bytes before passing to HooksService + * Automatically approves inputToken if needed + * For debt side leverage hook, also approves outputToken to Intents contract + */ +async function fillIntentWithData( + intent: HookIntent, + inputAmount: string, + outputAmount: string, + externalFillId?: string, +): Promise { + console.log('\n[fillIntentWithData] Filling intent...'); + console.log(`[fillIntentWithData] Input Amount: ${inputAmount}`); + console.log(`[fillIntentWithData] Output Amount: ${outputAmount}`); + + // Decode hook address from intent data to check if it's debt side leverage + const hookAddress = decodeHookAddressFromIntentData(intent.data); + const hooksConfig = getHooksConfig(HUB_CHAIN_ID); + const isDebtSideLeverage = hookAddress?.toLowerCase() === hooksConfig.debtSideLeverageHookAddress.toLowerCase(); + + // Ensure outputToken is approved (solver needs to provide outputToken) + console.log('[fillIntentWithData] Ensuring outputToken is approved...'); + await ensureTokenApprovedForFill(intent.outputToken, outputAmount); + + // Normalize addresses to 20 bytes before passing to HooksService + const normalizedIntent: HookIntent = { + ...intent, + srcAddress: normalizeAddress(intent.srcAddress), + dstAddress: normalizeAddress(intent.dstAddress), + }; + + console.log( + `[fillIntentWithData] srcAddress (normalized): ${ + normalizedIntent.srcAddress + } (${(normalizedIntent.srcAddress.length - 2) / 2} bytes)`, + ); + console.log( + `[fillIntentWithData] dstAddress (normalized): ${ + normalizedIntent.dstAddress + } (${(normalizedIntent.dstAddress.length - 2) / 2} bytes)`, + ); + + const result = await hooksService.fillIntent({ + intent: normalizedIntent, + inputAmount, + outputAmount, + externalFillId, + }); + + if (result.ok) { + console.log('[fillIntentWithData] Intent filled!'); + console.log(`[fillIntentWithData] Tx Hash: ${result.value.txHash}`); + } else { + console.error('[fillIntentWithData] Error:', result.error); + } +} + +/** + * Create leverage intent, fill it, then create deleverage intent + */ +async function leverageAndDeleverage( + collateralAsset: Address, + debtAsset: Address, + collateralAmount: string, + borrowAmount: string, + deadline?: string, +): Promise { + console.log('\n[leverageAndDeleverage] Starting leverage -> fill -> deleverage workflow...'); + console.log(`[leverageAndDeleverage] Collateral Asset: ${collateralAsset}`); + console.log(`[leverageAndDeleverage] Debt Asset: ${debtAsset}`); + console.log(`[leverageAndDeleverage] Collateral Amount: ${collateralAmount}`); + console.log(`[leverageAndDeleverage] Borrow Amount: ${borrowAmount}`); + + // Step 1: Create leverage intent with prerequisites + console.log('\n[leverageAndDeleverage] Step 1: Creating leverage intent...'); + const leverageResult = await hooksService.createLeverageIntentWithPrerequisites( + { + collateralAsset, + debtAsset, + collateralAmount, + borrowAmount, + deadline, + }, + 146, + { checkOnly: false }, + ); + + if (!leverageResult.ok) { + console.error('[leverageAndDeleverage] Failed to create leverage intent:', leverageResult.error); + return; + } + + const leverageTxHash = leverageResult.value.txHash; + console.log(`[leverageAndDeleverage] Leverage intent created! Tx Hash: ${leverageTxHash}`); + + // Step 2: Get full intent data from transaction + console.log('\n[leverageAndDeleverage] Step 2: Getting intent data from transaction...'); + await new Promise(resolve => setTimeout(resolve, 3000)); // Wait for transaction to be indexed + + const intentData = await getIntentDataFromTx(leverageTxHash as Hex); + if (!intentData) { + console.error('[leverageAndDeleverage] Failed to get intent data from transaction'); + return; + } + + console.log('[leverageAndDeleverage] Intent data retrieved successfully'); + + // Step 3: Fill the leverage intent (using user address as solver) + console.log('\n[leverageAndDeleverage] Step 3: Filling leverage intent...'); + // For leverage intent: + // - inputToken = debtAsset (sodaUSDC), inputAmount = borrowAmount + // - outputToken = collateralAsset (sodaETH), minOutputAmount = collateralAmount + // We need to fill with the exact amounts from the intent to avoid PartialFillNotAllowed + await fillIntentWithData(intentData, intentData.inputAmount.toString(), intentData.minOutputAmount.toString()); + + // Wait for fill to be processed + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Step 4: Check position after leverage + console.log('\n[leverageAndDeleverage] Step 4: Checking position after leverage...'); + const userAddress = await getUserAddress(); + await checkLiquidationOpportunity(userAddress); + + // Step 5: Create deleverage intent + // For deleverage: we withdraw collateral and repay debt + // withdrawAmount should be less than or equal to what we supplied + // repayAmount should be less than or equal to what we borrowed + console.log('\n[leverageAndDeleverage] Step 5: Creating deleverage intent...'); + const deleverageResult = await hooksService.createDeleverageIntentWithPrerequisites( + { + collateralAsset, + debtAsset, + withdrawAmount: collateralAmount, // Withdraw the same amount we supplied + repayAmount: borrowAmount, // Repay the same amount we borrowed + deadline, + }, + 146, + { checkOnly: false }, + ); + + if (!deleverageResult.ok) { + console.error('[leverageAndDeleverage] Failed to create deleverage intent:', deleverageResult.error); + return; + } + + const deleverageTxHash = deleverageResult.value.txHash; + console.log(`[leverageAndDeleverage] Deleverage intent created! Tx Hash: ${deleverageTxHash}`); + + // Step 6: Get deleverage intent data from transaction + console.log('\n[leverageAndDeleverage] Step 6: Getting deleverage intent data from transaction...'); + await new Promise(resolve => setTimeout(resolve, 3000)); // Wait for transaction to be indexed + + const deleverageIntentData = await getIntentDataFromTx(deleverageTxHash as Hex); + if (!deleverageIntentData) { + console.error('[leverageAndDeleverage] Failed to get deleverage intent data from transaction'); + return; + } + + console.log('[leverageAndDeleverage] Deleverage intent data retrieved successfully'); + + // Step 7: Fill the deleverage intent (using user address as solver) + console.log('\n[leverageAndDeleverage] Step 7: Filling deleverage intent...'); + // For deleverage intent: + // - inputToken = debtAsset (sodaUSDC), inputAmount = repayAmount + // - outputToken = collateralAsset (sodaETH), minOutputAmount = withdrawAmount + // We need to fill with the exact amounts from the intent + await fillIntentWithData( + deleverageIntentData, + deleverageIntentData.inputAmount.toString(), + deleverageIntentData.minOutputAmount.toString(), + ); + + // Wait for fill to be processed + await new Promise(resolve => setTimeout(resolve, 5000)); + + // Step 8: Check final position after deleverage + console.log('\n[leverageAndDeleverage] Step 8: Checking position after deleverage...'); + await checkLiquidationOpportunity(userAddress); + + console.log('\n[leverageAndDeleverage] Workflow completed!'); + console.log(`[leverageAndDeleverage] Leverage Intent Tx: ${leverageTxHash}`); + console.log('[leverageAndDeleverage] Leverage Fill Tx: (from fillIntentWithData)'); + console.log(`[leverageAndDeleverage] Deleverage Intent Tx: ${deleverageTxHash}`); + console.log('[leverageAndDeleverage] Deleverage Fill Tx: (from fillIntentWithData)'); +} + +// === UTILITY FUNCTIONS === + +/** + * Check balances for required test tokens (sodaUSDC and sodaETH) + * Note: If balance shows 0 but you see it on sodax.com/swap, it may be in a smart contract wallet + */ +async function checkRequiredTokenBalances(): Promise { + console.log('\n=== Checking Required Token Balances ==='); + const userAddress = await getUserAddress(); + console.log(`Wallet Address: ${userAddress}`); + console.log('Note: If balance shows 0 but visible on sodax.com/swap, it may be in a smart contract wallet\n'); + + // Token addresses (18 decimals for both) + const sodaUSDCAddress: Address = '0xAbbb91c0617090F0028BDC27597Cd0D038F3A833'; + const sodaETHAddress: Address = '0x4effB5813271699683C25c734F4daBc45B363709'; + + try { + // Check sodaUSDC balance + const sodaUSDCBalance = await publicClient.readContract({ + address: sodaUSDCAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [userAddress], + }); + + // Check sodaETH balance + const sodaETHBalance = await publicClient.readContract({ + address: sodaETHAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [userAddress], + }); + + const sodaUSDCFormatted = formatUnits(sodaUSDCBalance, 18); + const sodaETHFormatted = formatUnits(sodaETHBalance, 18); + + console.log('Token Balances (Direct Wallet):'); + console.log(` sodaUSDC: ${sodaUSDCFormatted} (raw: ${sodaUSDCBalance.toString()})`); + console.log(` sodaETH: ${sodaETHFormatted} (raw: ${sodaETHBalance.toString()})`); + + // Check if balances meet minimum requirements (lowered for testing) + console.log('\nBalance Status:'); + const minSodaUSDC = BigInt('1000000000000000000'); // 1 sodaUSDC + const minSodaETHRecommended = BigInt('10000000000000000'); // 0.01 sodaETH (recommended) + const minSodaETHMinimum = BigInt('1000000000000000'); // 0.001 sodaETH (absolute minimum for testing) + + if (sodaUSDCBalance >= minSodaUSDC) { + console.log(` sodaUSDC: Sufficient (minimum: 1.0, have: ${sodaUSDCFormatted})`); + } else { + console.log(` sodaUSDC: Low (minimum: 1.0, have: ${sodaUSDCFormatted})`); + } + + if (sodaETHBalance >= minSodaETHRecommended) { + console.log(` sodaETH: Sufficient (recommended: 0.01, have: ${sodaETHFormatted})`); + } else if (sodaETHBalance >= minSodaETHMinimum) { + console.log(` sodaETH: Low but testable (recommended: 0.01, minimum: 0.001, have: ${sodaETHFormatted})`); + console.log(' Note: You can test with this amount, but some operations may be limited'); + } else { + console.log(` sodaETH: Insufficient (minimum: 0.001, have: ${sodaETHFormatted})`); + console.log(' Please swap for at least 0.001 sodaETH (0.0015+ recommended)'); + } + + if (sodaETHBalance === 0n) { + console.log('\nšŸ’” Tip: If you see balance on sodax.com/swap but not here,'); + console.log(' the tokens may be in a smart contract wallet. Transactions may still work.'); + } + console.log(`\nšŸ’” Note: Using sodaETH address: ${sodaETHAddress}`); + } catch (error) { + console.error('[checkRequiredTokenBalances] Error:', error); + } +} + +/** + * Check aToken balance for a given underlying asset + */ +async function checkATokenBalance(underlyingAsset: Address): Promise { + console.log('\n[checkATokenBalance] Checking aToken balance...'); + console.log(`[checkATokenBalance] Underlying Asset: ${underlyingAsset}`); + + const userAddress = await getUserAddress(); + const moneyMarketConfig = getMoneyMarketConfig(HUB_CHAIN_ID); + const poolAddress = moneyMarketConfig.lendingPool; + + try { + // Get aToken address from pool + const reserveData = await publicClient.readContract({ + address: poolAddress, + abi: poolAbi, + functionName: 'getReserveData', + args: [underlyingAsset], + }); + + const aTokenAddress = reserveData.aTokenAddress; + console.log(`[checkATokenBalance] aToken Address: ${aTokenAddress}`); + + if (aTokenAddress === '0x0000000000000000000000000000000000000000') { + console.log('[checkATokenBalance] No aToken address found - asset may not be configured in the pool'); + return; + } + + // Check aToken balance + const aTokenBalance = await publicClient.readContract({ + address: aTokenAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [userAddress], + }); + + console.log(`[checkATokenBalance] aToken Balance: ${aTokenBalance.toString()}`); + console.log(`[checkATokenBalance] Formatted: ${formatUnits(aTokenBalance, 18)} aTokens`); + + // Get underlying balance from pool (what you can withdraw) + const accountData = await publicClient.readContract({ + address: poolAddress, + abi: poolAbi, + functionName: 'getUserAccountData', + args: [userAddress], + }); + + // getUserAccountData returns: [totalCollateralBase, totalDebtBase, availableBorrowsBase, currentLiquidationThreshold, ltv, healthFactor] + const [totalCollateralBase, totalDebtBase] = accountData; + console.log(`[checkATokenBalance] Total Collateral (base): ${totalCollateralBase.toString()}`); + console.log(`[checkATokenBalance] Total Debt (base): ${totalDebtBase.toString()}`); + } catch (error) { + console.error('[checkATokenBalance] Error:', error); + } +} + +/** + * Get hook address by type + */ +function showHookAddress(hookType: HookType): void { + const address = hooksService.getHookAddress(hookType); + console.log(`\n[getHookAddress] ${hookType} Hook Address: ${address}`); +} + +/** + * Show all hook addresses + */ +function showAllHookAddresses(): void { + console.log('\n=== Hook Contract Addresses ==='); + console.log(`Credit Hook: ${hooksService.getHookAddress(HookType.Credit)}`); + console.log(`Leverage Hook: ${hooksService.getHookAddress(HookType.Leverage)}`); + console.log(`DebtSideLeverage Hook: ${hooksService.getHookAddress(HookType.DebtSideLeverage)}`); + console.log(`Deleverage Hook: ${hooksService.getHookAddress(HookType.Deleverage)}`); + console.log(`Liquidation Hook: ${hooksService.getHookAddress(HookType.Liquidation)}`); +} + +// Helper to parse hook type from string +function parseHookType(hookTypeStr: string): HookType { + const hookTypeMap: Record = { + credit: HookType.Credit, + leverage: HookType.Leverage, + debtsideleverage: HookType.DebtSideLeverage, + deleverage: HookType.Deleverage, + liquidation: HookType.Liquidation, + }; + + const hookType = hookTypeMap[hookTypeStr.toLowerCase()]; + if (!hookType) { + throw new Error( + `Invalid hook type: ${hookTypeStr}. Valid types: credit, leverage, debtSideLeverage, deleverage, liquidation`, + ); + } + return hookType; +} + +// Main function +async function main(): Promise { + const functionName = process.argv[2]; + + try { + switch (functionName) { + // General commands + case 'showAddresses': + showAllHookAddresses(); + break; + + case 'checkBalances': + await checkRequiredTokenBalances(); + break; + + case 'getHookAddress': { + const hookType = parseHookType(process.argv[3]); + showHookAddress(hookType); + break; + } + + // Approval commands + case 'checkDelegation': { + const debtAsset = process.argv[3] as Address; + const hookType = parseHookType(process.argv[4]); + await checkCreditDelegation(debtAsset, hookType); + break; + } + + case 'approveDelegation': { + const debtAsset = process.argv[3] as Address; + const amount = process.argv[4]; + const hookType = parseHookType(process.argv[5]); + await approveDelegation(debtAsset, amount, hookType); + break; + } + + case 'approveToken': { + const tokenAddress = process.argv[3] as Address; + const amount = process.argv[4]; + const hookType = parseHookType(process.argv[5]); + await approveTokenSpending(tokenAddress, amount, hookType); + break; + } + + case 'approveAToken': { + const underlyingAsset = process.argv[3] as Address; + const amount = process.argv[4]; + const hookType = parseHookType(process.argv[5]); + await approveATokenSpending(underlyingAsset, amount, hookType); + break; + } + + // Credit Hook commands + case 'createCreditIntent': { + const debtAsset = process.argv[3] as Address; + const targetAsset = process.argv[4] as Address; + const maxPayment = process.argv[5]; + const minReceive = process.argv[6]; + const deadline = process.argv[7]; + await createCreditIntent(debtAsset, targetAsset, maxPayment, minReceive, deadline); + break; + } + + case 'createCreditIntentWithPrerequisites': { + const debtAsset = process.argv[3] as Address; + const targetAsset = process.argv[4] as Address; + const maxPayment = process.argv[5]; + const minReceive = process.argv[6]; + const deadline = process.argv[7]; + const checkOnly = process.argv[8] === 'true'; + await createCreditIntentWithPrerequisites(debtAsset, targetAsset, maxPayment, minReceive, deadline, checkOnly); + break; + } + + // Leverage Hook commands + case 'createLeverageIntent': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const collateralAmount = process.argv[5]; + const borrowAmount = process.argv[6]; + const deadline = process.argv[7]; + await createLeverageIntent(collateralAsset, debtAsset, collateralAmount, borrowAmount, deadline); + break; + } + + case 'createLeverageIntentWithPrerequisites': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const collateralAmount = process.argv[5]; + const borrowAmount = process.argv[6]; + const deadline = process.argv[7]; + const checkOnly = process.argv[8] === 'true'; + const feeAmount = process.argv[9]; + const feeReceiver = process.argv[10] as Address; + await createLeverageIntentWithPrerequisites( + collateralAsset, + debtAsset, + collateralAmount, + borrowAmount, + deadline, + checkOnly, + feeAmount, + feeReceiver, + ); + break; + } + + // Debt Side Leverage Hook commands + case 'checkDebtSideLeverageStatus': { + const debtAsset = process.argv[3] as Address; + await checkDebtSideLeverageStatus(debtAsset); + break; + } + + case 'createDebtSideLeverageIntent': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const collateralAmount = process.argv[5]; + const userProvidedAmount = process.argv[6]; + const totalBorrowAmount = process.argv[7]; + const deadline = process.argv[8]; + await createDebtSideLeverageIntent( + collateralAsset, + debtAsset, + collateralAmount, + userProvidedAmount, + totalBorrowAmount, + deadline, + ); + break; + } + + case 'createDebtSideLeverageIntentWithPrerequisites': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const collateralAmount = process.argv[5]; + const userProvidedAmount = process.argv[6]; + const totalBorrowAmount = process.argv[7]; + const deadline = process.argv[8]; + const checkOnly = process.argv[9] === 'true'; + await createDebtSideLeverageIntentWithPrerequisites( + collateralAsset, + debtAsset, + collateralAmount, + userProvidedAmount, + totalBorrowAmount, + deadline, + checkOnly, + ); + break; + } + + // Deleverage Hook commands + case 'checkATokenApproval': { + const collateralAsset = process.argv[3] as Address; + const withdrawAmount = process.argv[4]; + const feeAmount = process.argv[5]; + await checkATokenApprovalInfo(collateralAsset, withdrawAmount, feeAmount); + break; + } + + case 'createDeleverageIntent': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const withdrawAmount = process.argv[5]; + const repayAmount = process.argv[6]; + const deadline = process.argv[7]; + await createDeleverageIntent(collateralAsset, debtAsset, withdrawAmount, repayAmount, deadline); + break; + } + + case 'createDeleverageIntentWithPrerequisites': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const withdrawAmount = process.argv[5]; + const repayAmount = process.argv[6]; + const deadline = process.argv[7]; + const checkOnly = process.argv[8] === 'true'; + await createDeleverageIntentWithPrerequisites( + collateralAsset, + debtAsset, + withdrawAmount, + repayAmount, + deadline, + checkOnly, + ); + break; + } + + case 'leverageAndDeleverage': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const collateralAmount = process.argv[5]; + const borrowAmount = process.argv[6]; + const deadline = process.argv[7]; + await leverageAndDeleverage(collateralAsset, debtAsset, collateralAmount, borrowAmount, deadline); + break; + } + + // Liquidation Hook commands + case 'checkLiquidation': { + const userToCheck = process.argv[3] as Address; + await checkLiquidationOpportunity(userToCheck); + break; + } + + case 'createLiquidationIntent': { + const collateralAsset = process.argv[3] as Address; + const debtAsset = process.argv[4] as Address; + const userToLiquidate = process.argv[5] as Address; + const collateralAmount = process.argv[6]; + const debtAmount = process.argv[7]; + const deadline = process.argv[8]; + await createLiquidationIntent( + collateralAsset, + debtAsset, + userToLiquidate, + collateralAmount, + debtAmount, + deadline, + ); + break; + } + + case 'checkATokenBalance': { + const underlyingAsset = process.argv[3] as Address; + await checkATokenBalance(underlyingAsset); + break; + } + + // Intent Lifecycle commands + case 'getIntentHashFromTx': { + const txHash = process.argv[3] as Hex; + console.log('\n[getIntentHashFromTx] Getting intent hash from transaction...'); + console.log(`[getIntentHashFromTx] Transaction Hash: ${txHash}`); + const intentHash = await getIntentHashFromTx(txHash); + if (intentHash) { + console.log(`[getIntentHashFromTx] Intent Hash: ${intentHash}`); + } else { + console.error('[getIntentHashFromTx] Could not find intent hash in transaction'); + } + break; + } + + case 'getIntentState': { + const intentHash = process.argv[3] as Hex; + await getIntentState(intentHash); + break; + } + + case 'getPendingState': { + const intentHash = process.argv[3] as Hex; + await getPendingIntentState(intentHash); + break; + } + + case 'checkFillable': { + const intentHash = process.argv[3] as Hex; + await checkFillable(intentHash); + break; + } + + case 'fillIntentWithData': { + // Usage: fillIntentWithData [externalFillId] + const txHash = process.argv[3] as Hex; + const inputAmount = process.argv[4]; + const outputAmount = process.argv[5]; + const externalFillId = process.argv[6]; + + console.log('\n[fillIntentWithData] Getting intent data from transaction...'); + console.log(`[fillIntentWithData] Transaction Hash: ${txHash}`); + + const intentData = await getIntentDataFromTx(txHash); + if (!intentData) { + console.error('[fillIntentWithData] Failed to get intent data from transaction'); + break; + } + + await fillIntentWithData(intentData, inputAmount, outputAmount, externalFillId); + break; + } + + case 'cancelIntentFromTx': { + // Usage: cancelIntentFromTx + const txHash = process.argv[3] as Hex; + if (!txHash) { + console.error('[cancelIntentFromTx] Missing transaction hash'); + console.log('Usage: cancelIntentFromTx '); + break; + } + await cancelIntentFromTx(txHash); + break; + } + + case 'cancelIntentWithData': { + // Usage: cancelIntentWithData + const intentId = process.argv[3]; + const creator = process.argv[4] as Address; + const inputToken = process.argv[5] as Address; + const outputToken = process.argv[6] as Address; + const inputAmount = process.argv[7]; + const minOutputAmount = process.argv[8]; + const deadline = process.argv[9]; + const allowPartialFill = process.argv[10] === 'true'; + const srcChain = process.argv[11]; + const dstChain = process.argv[12]; + const srcAddress = process.argv[13] as string; + const dstAddress = process.argv[14] as string; + const solver = process.argv[15] as Address; + const data = process.argv[16] as Hex; + + if (!intentId || !creator || !inputToken || !outputToken) { + console.error('[cancelIntentWithData] Missing required parameters'); + console.log( + 'Usage: cancelIntentWithData ', + ); + break; + } + + await cancelIntentWithData({ + intentId, + creator, + inputToken, + outputToken, + inputAmount, + minOutputAmount, + deadline, + allowPartialFill, + srcChain, + dstChain, + srcAddress, + dstAddress, + solver, + data, + }); + break; + } + + default: + console.log('\n=== Intent Hooks CLI ===\n'); + console.log('Usage: pnpm intent-hooks [args...]\n'); + console.log('Commands:'); + console.log('\n General:'); + console.log(' showAddresses - Show all hook contract addresses'); + console.log(' checkBalances - Check bnUSD and WETH token balances'); + console.log(' getHookAddress - Get specific hook address'); + console.log('\n Approvals (hookType: credit|leverage|debtSideLeverage|deleverage|liquidation):'); + console.log(' checkDelegation - Check credit delegation status'); + console.log(' approveDelegation - Approve credit delegation'); + console.log(' approveToken - Approve token spending'); + console.log(' approveAToken - Approve aToken spending'); + console.log('\n Credit Hook (Limit Orders):'); + console.log(' createCreditIntent [deadline]'); + console.log( + ' createCreditIntentWithPrerequisites [deadline] [checkOnly] - Auto-handles approvals', + ); + console.log('\n Leverage Hook:'); + console.log( + ' createLeverageIntent [deadline]', + ); + console.log( + ' createLeverageIntentWithPrerequisites [deadline] [checkOnly] - Auto-handles approvals', + ); + console.log('\n Debt Side Leverage Hook:'); + console.log(' checkDebtSideLeverageStatus - Check readiness status'); + console.log( + ' createDebtSideLeverageIntent [deadline]', + ); + console.log( + ' createDebtSideLeverageIntentWithPrerequisites [deadline] [checkOnly] - Auto-handles approvals', + ); + console.log('\n Deleverage Hook:'); + console.log(' checkATokenApproval [feeAmount] - Check aToken approval'); + console.log( + ' createDeleverageIntent [deadline]', + ); + console.log( + ' createDeleverageIntentWithPrerequisites [deadline] [checkOnly] - Auto-handles approvals', + ); + console.log( + ' leverageAndDeleverage [deadline] - Create leverage intent, fill it, create deleverage intent, and fill it', + ); + console.log('\n Liquidation Hook:'); + console.log(' checkLiquidation - Check if user is liquidatable'); + console.log( + ' createLiquidationIntent [deadline]', + ); + console.log('\n Money Market Position Management:'); + console.log(' checkATokenBalance - Check aToken balance for an asset'); + console.log('\n Intent Lifecycle:'); + console.log(' getIntentHashFromTx - Get intent hash from transaction hash'); + console.log(' getIntentState - Get intent state by hash'); + console.log(' getPendingState - Get pending state (cross-chain)'); + console.log(' checkFillable - Check if intent can be filled'); + console.log( + ' fillIntentWithData [externalFillId] - Fill intent using data from transaction', + ); + console.log( + ' cancelIntentFromTx - Cancel intent using transaction hash', + ); + console.log( + ' cancelIntentWithData - Cancel intent with data', + ); + console.log('\nExamples:'); + console.log(' pnpm intent-hooks showAddresses'); + console.log(' pnpm intent-hooks checkBalances'); + console.log(' pnpm intent-hooks checkDelegation 0xUSDC... credit'); + console.log(' pnpm intent-hooks approveDelegation 0xUSDC... 1000000000 leverage'); + console.log(' pnpm intent-hooks createCreditIntent 0xUSDC... 0xWETH... 1500000000 1000000000000000000'); + console.log(' pnpm intent-hooks checkLiquidation 0xUserAddress...'); + console.log(' pnpm intent-hooks getIntentState 0xIntentHash...'); + console.log(' pnpm intent-hooks cancelIntentWithData '); + break; + } + } catch (error) { + console.error('\n Error:', error instanceof Error ? error.message : error); + process.exit(1); + } +} + +main(); diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index cde0b1ceb..07c824238 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -6,3 +6,4 @@ export * from './bridge/index.js'; export * from './staking/index.js'; export * from './migration/index.js'; export * from '@sodax/types'; +export * from './intentHooks/index.js'; diff --git a/packages/sdk/src/intentHooks/HooksService.test.ts b/packages/sdk/src/intentHooks/HooksService.test.ts new file mode 100644 index 000000000..ef7a89add --- /dev/null +++ b/packages/sdk/src/intentHooks/HooksService.test.ts @@ -0,0 +1,951 @@ +import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import { HooksService, HookType, type HooksServiceConstructorParams } from './HooksService.js'; +import type { PublicClient, WalletClient, Address } from 'viem'; +import { SONIC_MAINNET_CHAIN_ID, type HookIntent, type FillHookIntentParams } from '@sodax/types'; + +// Mock @sodax/types config functions +vi.mock('@sodax/types', async () => { + const actual = await vi.importActual('@sodax/types'); + return { + ...actual, + getSolverConfig: vi.fn().mockReturnValue({ + intentsContract: '0x6382D6ccD780758C5e8A6123c33ee8F4472F96ef', + solverApiEndpoint: 'https://api.sodax.com/v1/intent', + }), + getMoneyMarketConfig: vi.fn().mockReturnValue({ + lendingPool: '0x553434896D39F867761859D0FE7189d2Af70514E', + uiPoolDataProvider: '0xC04d746C38f1E51C8b3A3E2730250bbAC2F271bf', + poolAddressesProvider: '0x036aDe0aBAA4c82445Cb7597f2d6d6130C118c7b', + bnUSD: '0x94dC79ce9C515ba4AE4D195da8E6AB86c69BFc38', + bnUSDAToken: '0xa2cDA49735e42f0905496E40a66B3C5475Ed69dF', + bnUSDVault: '0xE801CA34E19aBCbFeA12025378D19c4FBE250131', + }), + getHooksConfig: vi.fn().mockReturnValue({ + creditHookAddress: '0x1111111111111111111111111111111111111111', + leverageHookAddress: '0x2222222222222222222222222222222222222222', + debtSideLeverageHookAddress: '0x3333333333333333333333333333333333333333', + deleverageHookAddress: '0x4444444444444444444444444444444444444444', + liquidationHookAddress: '0x5555555555555555555555555555555555555555', + }), + }; +}); + +// Mock hook addresses for test assertions +const mockHooksConfig = { + creditHookAddress: '0x1111111111111111111111111111111111111111', + leverageHookAddress: '0x2222222222222222222222222222222222222222', + debtSideLeverageHookAddress: '0x3333333333333333333333333333333333333333', + deleverageHookAddress: '0x4444444444444444444444444444444444444444', + liquidationHookAddress: '0x5555555555555555555555555555555555555555', +}; + +describe('HooksService', () => { + let hooksService: HooksService; + let mockPublicClient: PublicClient; + let mockWalletClient: WalletClient; + + const userAddress = '0x1234567890123456789012345678901234567890' as Address; + const debtAsset = '0x1111111111111111111111111111111111111111' as Address; + const collateralAsset = '0x2222222222222222222222222222222222222222' as Address; + const debtTokenAddress = '0xDebtToken1234567890123456789012345678901' as Address; + const aTokenAddress = '0xAToken123456789012345678901234567890123' as Address; + + const createMockPublicClient = (): PublicClient => { + return { + readContract: vi.fn(), + waitForTransactionReceipt: vi.fn().mockResolvedValue({ status: 'success' }), + getLogs: vi.fn(), + } as unknown as PublicClient; + }; + + const createMockWalletClient = (): WalletClient => { + return { + getAddresses: vi.fn().mockResolvedValue([userAddress]), + writeContract: vi + .fn() + .mockResolvedValue('0xTransactionHash123456789012345678901234567890123456789012345678901234'), + chain: { id: 146 }, + account: { + address: userAddress, + }, + } as unknown as WalletClient; + }; + + beforeEach(() => { + vi.clearAllMocks(); + mockPublicClient = createMockPublicClient(); + mockWalletClient = createMockWalletClient(); + + const params: HooksServiceConstructorParams = { + publicClient: mockPublicClient, + walletClient: mockWalletClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }; + + hooksService = new HooksService(params); + }); + + describe('Constructor', () => { + it('should create instance with publicClient only (read-only mode)', () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + expect(readOnlyService).toBeInstanceOf(HooksService); + }); + + it('should create instance with both publicClient and walletClient', () => { + expect(hooksService).toBeInstanceOf(HooksService); + }); + }); + + describe('getHookAddress', () => { + it('should return correct address for each hook type', () => { + expect(hooksService.getHookAddress(HookType.Credit)).toBe(mockHooksConfig.creditHookAddress); + expect(hooksService.getHookAddress(HookType.Leverage)).toBe(mockHooksConfig.leverageHookAddress); + expect(hooksService.getHookAddress(HookType.DebtSideLeverage)).toBe(mockHooksConfig.debtSideLeverageHookAddress); + expect(hooksService.getHookAddress(HookType.Deleverage)).toBe(mockHooksConfig.deleverageHookAddress); + expect(hooksService.getHookAddress(HookType.Liquidation)).toBe(mockHooksConfig.liquidationHookAddress); + }); + }); + + describe('Shared Approval Methods', () => { + describe('getCreditDelegationStatus', () => { + it('should return credit delegation status for Credit hook', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockReserveData) // getReserveData + .mockResolvedValueOnce(1000000n); // borrowAllowance + + const result = await hooksService.getCreditDelegationStatus(debtAsset, userAddress, HookType.Credit); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.delegated).toBe(true); + expect(result.value.allowance).toBe('1000000'); + } + }); + + it('should return credit delegation status for Leverage hook', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData).mockResolvedValueOnce(2000000n); + + const result = await hooksService.getCreditDelegationStatus(debtAsset, userAddress, HookType.Leverage); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.delegated).toBe(true); + expect(result.value.allowance).toBe('2000000'); + } + }); + + it('should return delegated: false when allowance is 0', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData).mockResolvedValueOnce(0n); + + const result = await hooksService.getCreditDelegationStatus(debtAsset, userAddress, HookType.Credit); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.delegated).toBe(false); + expect(result.value.allowance).toBe('0'); + } + }); + + it('should return error on failure', async () => { + (mockPublicClient.readContract as Mock).mockRejectedValueOnce(new Error('RPC error')); + + const result = await hooksService.getCreditDelegationStatus(debtAsset, userAddress, HookType.Credit); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toBeInstanceOf(Error); + } + }); + }); + + describe('approveCreditDelegation', () => { + it('should approve credit delegation for Credit hook', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData); + + const result = await hooksService.approveCreditDelegation(debtAsset, '1000000', HookType.Credit); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.approved).toBe(true); + expect(result.value.txHash).toBeDefined(); + } + }); + + it('should approve credit delegation for Leverage hook', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData); + + const result = await hooksService.approveCreditDelegation(debtAsset, '1000000', HookType.Leverage); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.approved).toBe(true); + } + }); + + it('should approve credit delegation for DebtSideLeverage hook', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData); + + const result = await hooksService.approveCreditDelegation(debtAsset, '1000000', HookType.DebtSideLeverage); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.approved).toBe(true); + } + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const result = await readOnlyService.approveCreditDelegation(debtAsset, '1000000', HookType.Credit); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toBe('Wallet client required'); + } + }); + }); + + describe('approveToken', () => { + it('should approve token for DebtSideLeverage hook', async () => { + const result = await hooksService.approveToken(debtAsset, '1000000', HookType.DebtSideLeverage); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.approved).toBe(true); + expect(result.value.txHash).toBeDefined(); + } + }); + + it('should approve token for any hook type', async () => { + const result = await hooksService.approveToken(collateralAsset, '2000000', HookType.Deleverage); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.approved).toBe(true); + } + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const result = await readOnlyService.approveToken(debtAsset, '1000000', HookType.DebtSideLeverage); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toBe('Wallet client required'); + } + }); + }); + + describe('approveAToken', () => { + it('should approve aToken for Deleverage hook', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData); + + const result = await hooksService.approveAToken(collateralAsset, '1000000', HookType.Deleverage); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.approved).toBe(true); + expect(result.value.txHash).toBeDefined(); + } + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const result = await readOnlyService.approveAToken(collateralAsset, '1000000', HookType.Deleverage); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toBe('Wallet client required'); + } + }); + }); + }); + + describe('Credit Hook', () => { + describe('createCreditIntent', () => { + it('should create credit intent successfully', async () => { + const params = { + debtAsset: debtAsset, + targetAsset: collateralAsset, + maxPayment: '1500000000', + minReceive: '1000000000000000000', + }; + + const result = await hooksService.createCreditIntent(params, 146); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.txHash).toBeDefined(); + } + expect(mockWalletClient.writeContract).toHaveBeenCalled(); + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const params = { + debtAsset: debtAsset, + targetAsset: collateralAsset, + maxPayment: '1500000000', + minReceive: '1000000000000000000', + }; + + const result = await readOnlyService.createCreditIntent(params, 146); + + expect(result.ok).toBe(false); + }); + }); + }); + + describe('Leverage Hook', () => { + describe('createLeverageIntent', () => { + it('should create leverage intent successfully', async () => { + const params = { + collateralAsset: collateralAsset, + debtAsset: debtAsset, + collateralAmount: '1000000000000000000', + borrowAmount: '1500000000', + }; + + const result = await hooksService.createLeverageIntent(params, 146); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.txHash).toBeDefined(); + } + }); + }); + }); + + describe('Debt Side Leverage Hook', () => { + describe('getDebtSideLeverageStatus', () => { + it('should return debt side leverage status successfully', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockReserveData) // getReserveData + .mockResolvedValueOnce(1000000n) // tokenAllowance + .mockResolvedValueOnce(2000000n) // creditDelegation + .mockResolvedValueOnce(500000n); // tokenBalance + + const result = await hooksService.getDebtSideLeverageStatus(debtAsset, userAddress); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.tokenAllowance).toBe('1000000'); + expect(result.value.creditDelegation).toBe('2000000'); + expect(result.value.tokenBalance).toBe('500000'); + expect(result.value.isReady).toBe(true); + } + }); + + it('should return isReady: false when any value is 0', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockReserveData) + .mockResolvedValueOnce(0n) // tokenAllowance = 0 + .mockResolvedValueOnce(2000000n) + .mockResolvedValueOnce(500000n); + + const result = await hooksService.getDebtSideLeverageStatus(debtAsset, userAddress); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.isReady).toBe(false); + } + }); + }); + + describe('createDebtSideLeverageIntent', () => { + it('should create debt side leverage intent successfully', async () => { + const params = { + collateralAsset: collateralAsset, + debtAsset: debtAsset, + collateralAmount: '1000000000000000000', + userProvidedAmount: '500000000', + totalBorrowAmount: '1500000000', + }; + + const result = await hooksService.createDebtSideLeverageIntent(params, 146); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.txHash).toBeDefined(); + } + }); + }); + }); + + describe('Deleverage Hook', () => { + describe('getATokenApprovalInfo', () => { + it('should return aToken approval info successfully', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockReserveData) // getReserveData + .mockResolvedValueOnce(2000000n); // allowance + + const result = await hooksService.getATokenApprovalInfo(collateralAsset, userAddress, '1000000', '100000'); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.aTokenAddress).toBe(aTokenAddress); + expect(result.value.aTokensNeeded).toBe('1100000'); // withdrawAmount + feeAmount + expect(result.value.currentAllowance).toBe('2000000'); + expect(result.value.isApproved).toBe(true); + } + }); + + it('should return isApproved: false when allowance is insufficient', async () => { + const mockReserveData = { + variableDebtTokenAddress: debtTokenAddress, + aTokenAddress: aTokenAddress, + }; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockReserveData).mockResolvedValueOnce(500000n); // allowance < aTokensNeeded + + const result = await hooksService.getATokenApprovalInfo(collateralAsset, userAddress, '1000000', '100000'); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.isApproved).toBe(false); + } + }); + }); + + describe('createDeleverageIntent', () => { + it('should create deleverage intent successfully', async () => { + const params = { + collateralAsset: collateralAsset, + debtAsset: debtAsset, + withdrawAmount: '500000000000000000', + repayAmount: '750000000', + }; + + const result = await hooksService.createDeleverageIntent(params, 146); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.txHash).toBeDefined(); + } + }); + }); + }); + + describe('Liquidation Hook', () => { + describe('getLiquidationOpportunity', () => { + it('should return liquidation opportunity for liquidatable position', async () => { + const mockAccountData = [ + 1000000n, // totalCollateralBase + 500000n, // totalDebtBase + 300000n, // availableBorrowsBase + 8000n, // currentLiquidationThreshold + 7500n, // ltv + 900000000000000000n, // healthFactor < 1e18 + ]; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockAccountData); + + const result = await hooksService.getLiquidationOpportunity(userAddress); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.isLiquidatable).toBe(true); + expect(result.value.healthFactor).toBe('900000000000000000'); + expect(result.value.accountData.totalCollateralBase).toBe('1000000'); + } + }); + + it('should return non-liquidatable for healthy position', async () => { + const mockAccountData = [ + 1000000n, + 500000n, + 300000n, + 8000n, + 7500n, + 1500000000000000000n, // healthFactor > 1e18 + ]; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockAccountData); + + const result = await hooksService.getLiquidationOpportunity(userAddress); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.isLiquidatable).toBe(false); + } + }); + }); + + describe('createLiquidationIntent', () => { + it('should create liquidation intent for liquidatable position', async () => { + const mockAccountData = [ + 1000000n, + 500000n, + 300000n, + 8000n, + 7500n, + 900000000000000000n, // healthFactor < 1e18 + ]; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockAccountData); + + const params = { + collateralAsset: collateralAsset, + debtAsset: debtAsset, + userToLiquidate: '0xLiquidatableUser12345678901234567890123' as Address, + collateralAmount: '100000000', + debtAmount: '10000000000', + }; + + const result = await hooksService.createLiquidationIntent(params, 146); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.txHash).toBeDefined(); + } + }); + + it('should return error when position is not liquidatable', async () => { + const mockAccountData = [ + 1000000n, + 500000n, + 300000n, + 8000n, + 7500n, + 1500000000000000000n, // healthFactor > 1e18 + ]; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockAccountData); + + const params = { + collateralAsset: collateralAsset, + debtAsset: debtAsset, + userToLiquidate: '0xHealthyUser123456789012345678901234567' as Address, + collateralAmount: '100000000', + debtAmount: '10000000000', + }; + + const result = await hooksService.createLiquidationIntent(params, 146); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toContain('Position is not liquidatable'); + } + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const params = { + collateralAsset: collateralAsset, + debtAsset: debtAsset, + userToLiquidate: userAddress, + collateralAmount: '100000000', + debtAmount: '10000000000', + }; + + const result = await readOnlyService.createLiquidationIntent(params, 146); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toBe('Wallet client required'); + } + }); + }); + }); + + describe('Intent Lifecycle', () => { + const intentHash = '0xIntentHash1234567890123456789012345678901234567890123456789012345678' as const; + const mockIntent: HookIntent = { + intentId: 1n, + creator: userAddress, + inputToken: debtAsset, + outputToken: collateralAsset, + inputAmount: 1000000n, + minOutputAmount: 500000n, + deadline: 9999999999n, + allowPartialFill: true, + srcChain: 146n, + dstChain: 146n, + srcAddress: '0x' as const, + dstAddress: '0x' as const, + solver: '0x0000000000000000000000000000000000000000' as Address, + data: '0x' as const, + }; + + describe('getIntentState', () => { + it('should return intent state successfully', async () => { + const mockState = [ + true, // exists + 800000n, // remainingInput + 200000n, // receivedOutput + 0n, // pendingPayment + ]; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockState); + + const result = await hooksService.getIntentState(intentHash); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.exists).toBe(true); + expect(result.value.remainingInput).toBe('800000'); + expect(result.value.receivedOutput).toBe('200000'); + expect(result.value.pendingPayment).toBe(0n); + } + }); + + it('should return error on contract read failure', async () => { + (mockPublicClient.readContract as Mock).mockRejectedValueOnce(new Error('RPC error')); + + const result = await hooksService.getIntentState(intentHash); + + expect(result.ok).toBe(false); + }); + }); + + describe('getPendingIntentState', () => { + it('should return pending intent state successfully', async () => { + const mockPendingState = [ + 100000n, // pendingInput + 50000n, // pendingOutput + ]; + + (mockPublicClient.readContract as Mock).mockResolvedValueOnce(mockPendingState); + + const result = await hooksService.getPendingIntentState(intentHash); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.pendingInput).toBe('100000'); + expect(result.value.pendingOutput).toBe('50000'); + } + }); + + it('should return error on contract read failure', async () => { + (mockPublicClient.readContract as Mock).mockRejectedValueOnce(new Error('RPC error')); + + const result = await hooksService.getPendingIntentState(intentHash); + + expect(result.ok).toBe(false); + }); + }); + + describe('isFillable', () => { + it('should return true when intent is fillable', async () => { + const mockState = [ + true, // exists + 800000n, // remainingInput + 200000n, // receivedOutput + 0n, // pendingPayment + ]; + + const mockPendingState = [ + 0n, // pendingInput + 0n, // pendingOutput + ]; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockState) + .mockResolvedValueOnce(mockPendingState); + + const result = await hooksService.isFillable(intentHash); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value).toBe(true); + } + }); + + it('should return false when intent does not exist', async () => { + const mockState = [ + false, // exists + 0n, // remainingInput + 0n, // receivedOutput + 0n, // pendingPayment + ]; + + const mockPendingState = [ + 0n, // pendingInput + 0n, // pendingOutput + ]; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockState) + .mockResolvedValueOnce(mockPendingState); + + const result = await hooksService.isFillable(intentHash); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value).toBe(false); + } + }); + + it('should return false when there is pending payment', async () => { + const mockState = [ + true, // exists + 800000n, // remainingInput + 200000n, // receivedOutput + 100000n, // pendingPayment + ]; + + const mockPendingState = [ + 0n, // pendingInput + 0n, // pendingOutput + ]; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockState) + .mockResolvedValueOnce(mockPendingState); + + const result = await hooksService.isFillable(intentHash); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value).toBe(false); + } + }); + + it('should return false when no available input', async () => { + const mockState = [ + true, // exists + 100000n, // remainingInput + 0n, // receivedOutput + 0n, // pendingPayment + ]; + + const mockPendingState = [ + 100000n, // pendingInput (All input is pending) + 0n, // pendingOutput + ]; + + (mockPublicClient.readContract as Mock) + .mockResolvedValueOnce(mockState) + .mockResolvedValueOnce(mockPendingState); + + const result = await hooksService.isFillable(intentHash); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value).toBe(false); + } + }); + + it('should return error when getIntentState fails', async () => { + (mockPublicClient.readContract as Mock).mockRejectedValueOnce(new Error('RPC error')); + + const result = await hooksService.isFillable(intentHash); + + expect(result.ok).toBe(false); + }); + }); + + describe('computeIntentHash', () => { + it('should compute intent hash correctly', () => { + const hash = hooksService.computeIntentHash(mockIntent); + + expect(hash).toBeDefined(); + expect(hash).toMatch(/^0x[a-fA-F0-9]{64}$/); + }); + + it('should produce consistent hash for same intent', () => { + const hash1 = hooksService.computeIntentHash(mockIntent); + const hash2 = hooksService.computeIntentHash(mockIntent); + + expect(hash1).toBe(hash2); + }); + + it('should produce different hash for different intents', () => { + const intent1 = { ...mockIntent, intentId: 1n }; + const intent2 = { ...mockIntent, intentId: 2n }; + + const hash1 = hooksService.computeIntentHash(intent1); + const hash2 = hooksService.computeIntentHash(intent2); + + expect(hash1).not.toBe(hash2); + }); + }); + + describe('cancelIntent', () => { + it('should cancel intent successfully', async () => { + const result = await hooksService.cancelIntent(mockIntent); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.cancelled).toBe(true); + expect(result.value.txHash).toBeDefined(); + } + expect(mockWalletClient.writeContract).toHaveBeenCalled(); + expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalled(); + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const result = await readOnlyService.cancelIntent(mockIntent); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toBe('Wallet client required'); + } + }); + + it('should handle transaction errors', async () => { + (mockWalletClient.writeContract as Mock).mockRejectedValueOnce(new Error('Transaction failed')); + + const result = await hooksService.cancelIntent(mockIntent); + + expect(result.ok).toBe(false); + }); + }); + + describe('fillIntent', () => { + const fillParams: FillHookIntentParams = { + intent: mockIntent, + inputAmount: '500000', + outputAmount: '250000', + }; + + it('should fill intent successfully', async () => { + const result = await hooksService.fillIntent(fillParams); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.filled).toBe(true); + expect(result.value.txHash).toBeDefined(); + } + expect(mockWalletClient.writeContract).toHaveBeenCalled(); + expect(mockPublicClient.waitForTransactionReceipt).toHaveBeenCalled(); + }); + + it('should fill intent with external fill ID', async () => { + const paramsWithFillId: FillHookIntentParams = { + ...fillParams, + externalFillId: '12345', + }; + + const result = await hooksService.fillIntent(paramsWithFillId); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.value.filled).toBe(true); + } + }); + + it('should include value when output token is native', async () => { + const nativeOutputIntent = { + ...mockIntent, + outputToken: '0x0000000000000000000000000000000000000000' as Address, + }; + + const params: FillHookIntentParams = { + intent: nativeOutputIntent, + inputAmount: '500000', + outputAmount: '250000', + }; + + const result = await hooksService.fillIntent(params); + + expect(result.ok).toBe(true); + expect(mockWalletClient.writeContract).toHaveBeenCalledWith( + expect.objectContaining({ + value: 250000n, + }), + ); + }); + + it('should return error when walletClient is not provided', async () => { + const readOnlyService = new HooksService({ + publicClient: mockPublicClient, + chainId: SONIC_MAINNET_CHAIN_ID, + }); + + const result = await readOnlyService.fillIntent(fillParams); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect((result.error as Error).message).toBe('Wallet client required'); + } + }); + + it('should handle transaction errors', async () => { + (mockWalletClient.writeContract as Mock).mockRejectedValueOnce(new Error('Transaction failed')); + + const result = await hooksService.fillIntent(fillParams); + + expect(result.ok).toBe(false); + }); + }); + }); +}); diff --git a/packages/sdk/src/intentHooks/HooksService.ts b/packages/sdk/src/intentHooks/HooksService.ts new file mode 100644 index 000000000..bc406cc43 --- /dev/null +++ b/packages/sdk/src/intentHooks/HooksService.ts @@ -0,0 +1,1559 @@ +import type { PublicClient, WalletClient, Hex, Address as ViemAddress } from 'viem'; +import { encodeAbiParameters, encodePacked, keccak256 } from 'viem'; +import { + getSolverConfig, + getMoneyMarketConfig, + getHooksConfig, + type Address, + type HooksConfig, + type CreditHookParams, + type CreditDelegationStatus, + type LeverageHookParams, + type DebtSideLeverageHookParams, + type DebtSideLeverageStatus, + type DeleverageHookParams, + type ATokenApprovalInfo, + type LiquidationHookParams, + type LiquidationOpportunity, + type IntentCreationResult, + type ApprovalResult, + type HubChainId, + type HookIntent, + type HookIntentState, + type HookPendingIntentState, + type FillHookIntentParams, + type CancelIntentResult, + type FillIntentResult, +} from '@sodax/types'; +import { erc20Abi } from '../shared/abis/erc20.abi.js'; +import { poolAbi } from '../shared/abis/pool.abi.js'; +import { variableDebtTokenAbi } from '../shared/abis/variableDebtToken.abi.js'; +import { IntentsAbi } from '../shared/abis/intents.abi.js'; +import { randomUint256 } from '../shared/utils/shared-utils.js'; +import type { Result } from '../shared/types.js'; + +const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const; + +/** + * Hook types for credit delegation and token approvals + */ +export enum HookType { + Credit = 'credit', + Leverage = 'leverage', + DebtSideLeverage = 'debtSideLeverage', + Deleverage = 'deleverage', + Liquidation = 'liquidation', +} + +export type HooksServiceConstructorParams = { + publicClient: PublicClient; + walletClient?: WalletClient; + chainId: HubChainId; +}; + +/** + * Helper function to get the wallet address from a WalletClient + */ +async function getWalletAddress(walletClient: WalletClient): Promise { + const addresses = await walletClient.getAddresses(); + const address = addresses[0]; + if (!address) { + throw new Error('No wallet address available'); + } + return address; +} + +export class HooksService { + private readonly publicClient: PublicClient; + private readonly walletClient?: WalletClient; + private readonly hooksConfig: HooksConfig; + private readonly intentsAddress: Address; + private readonly poolAddress: Address; + + constructor({ publicClient, walletClient, chainId }: HooksServiceConstructorParams) { + this.publicClient = publicClient; + this.walletClient = walletClient; + + const solverConfig = getSolverConfig(chainId); + const moneyMarketConfig = getMoneyMarketConfig(chainId); + this.hooksConfig = getHooksConfig(chainId); + this.intentsAddress = solverConfig.intentsContract; + this.poolAddress = moneyMarketConfig.lendingPool; + } + + // === SHARED APPROVAL METHODS === + + /** + * Get the hook address for a given hook type + * @param hookType - The type of hook + * @returns The hook contract address + */ + getHookAddress(hookType: HookType): Address { + const hookAddressMap: Record = { + [HookType.Credit]: this.hooksConfig.creditHookAddress, + [HookType.Leverage]: this.hooksConfig.leverageHookAddress, + [HookType.DebtSideLeverage]: this.hooksConfig.debtSideLeverageHookAddress, + [HookType.Deleverage]: this.hooksConfig.deleverageHookAddress, + [HookType.Liquidation]: this.hooksConfig.liquidationHookAddress, + }; + return hookAddressMap[hookType]; + } + + /** + * Get credit delegation status for a debt asset to a specific hook + * @param debtAsset - The address of the debt asset + * @param userAddress - The user's wallet address + * @param hookType - The hook type to check delegation for + * @returns Credit delegation status with allowance information + */ + async getCreditDelegationStatus( + debtAsset: Address, + userAddress: Address, + hookType: HookType, + ): Promise> { + try { + const debtTokenAddress = await this.getVariableDebtToken(debtAsset); + + // If variable debt token is zero address, the asset is not initialized as a reserve + if (debtTokenAddress === ZERO_ADDRESS) { + return { + ok: false, + error: new Error(`Asset ${debtAsset} is not initialized as a reserve in the lending pool`), + }; + } + + const hookAddress = this.getHookAddress(hookType); + + const allowance = await this.publicClient.readContract({ + address: debtTokenAddress, + abi: variableDebtTokenAbi, + functionName: 'borrowAllowance', + args: [userAddress, hookAddress], + }); + + return { + ok: true, + value: { + delegated: allowance > 0n, + allowance: allowance.toString(), + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Approve credit delegation for a specific hook + * @param debtAsset - The address of the debt asset + * @param amount - The amount to approve for delegation + * @param hookType - The hook type to approve delegation for + * @returns Approval result with transaction hash + */ + async approveCreditDelegation( + debtAsset: Address, + amount: string, + hookType: HookType, + ): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const debtTokenAddress = await this.getVariableDebtToken(debtAsset); + const hookAddress = this.getHookAddress(hookType); + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + const hash = await this.walletClient.writeContract({ + address: debtTokenAddress, + abi: variableDebtTokenAbi, + functionName: 'approveDelegation', + args: [hookAddress, BigInt(amount)], + chain: this.walletClient.chain, + account: this.walletClient.account, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash, approved: true }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Approve token spending for a specific hook + * @param tokenAddress - The address of the token to approve + * @param amount - The amount to approve + * @param hookType - The hook type to approve spending for + * @returns Approval result with transaction hash + */ + async approveToken(tokenAddress: Address, amount: string, hookType: HookType): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const hookAddress = this.getHookAddress(hookType); + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + + const hash = await this.walletClient.writeContract({ + address: tokenAddress as ViemAddress, + abi: erc20Abi, + functionName: 'approve', + args: [hookAddress as ViemAddress, BigInt(amount)], + account: this.walletClient.account, + chain: this.walletClient.chain, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash, approved: true }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Approve aToken spending for a specific hook (resolves aToken address from underlying asset) + * @param underlyingAsset - The address of the underlying collateral asset + * @param amount - The amount to approve + * @param hookType - The hook type to approve spending for + * @returns Approval result with transaction hash + */ + async approveAToken(underlyingAsset: Address, amount: string, hookType: HookType): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const aTokenAddress = await this.getAToken(underlyingAsset); + const hookAddress = this.getHookAddress(hookType); + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + + const hash = await this.walletClient.writeContract({ + address: aTokenAddress, + abi: erc20Abi, + functionName: 'approve', + args: [hookAddress as ViemAddress, BigInt(amount)], + account: this.walletClient.account, + chain: this.walletClient.chain, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash, approved: true }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + // === CREDIT HOOK (Limit Orders) === + + /** + * Create a credit intent (limit order) + * @param params - Credit hook parameters + * - solver: Optional specific solver address. If not provided or address(0), any solver can fill the intent. + * @param chainId - The chain ID + * @returns Intent creation result with transaction hash + */ + async createCreditIntent(params: CreditHookParams, chainId: number): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const intentData = this.encodeIntentData( + this.hooksConfig.creditHookAddress, + '0x', + params.feeReceiver, + params.feeAmount, + ); + + const deadlineValue = params.deadline && params.deadline !== '' ? params.deadline : '0'; + const intent = { + intentId: randomUint256(), + creator: userAddress, + inputToken: params.debtAsset as ViemAddress, + outputToken: params.targetAsset as ViemAddress, + inputAmount: BigInt(params.maxPayment), + minOutputAmount: BigInt(params.minReceive), + deadline: BigInt(deadlineValue), + allowPartialFill: false, + srcChain: BigInt(chainId), + dstChain: BigInt(chainId), + srcAddress: userAddress.toLowerCase() as Hex, + dstAddress: userAddress.toLowerCase() as Hex, + solver: (params.solver || ZERO_ADDRESS) as ViemAddress, // Use provided solver or zero address (any solver) + data: intentData, + }; + + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'createIntent', + args: [intent], + chain: this.walletClient.chain, + account: this.walletClient.account, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + // === LEVERAGE HOOK === + + /** + * Create a leverage intent + * @param params - Leverage hook parameters + * @param chainId - The chain ID + * @returns Intent creation result with transaction hash + */ + async createLeverageIntent(params: LeverageHookParams, chainId: number): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const intentData = this.encodeIntentData( + this.hooksConfig.leverageHookAddress, + '0x', + params.feeReceiver, + params.feeAmount, + ); + + const intent = { + intentId: randomUint256(), + creator: userAddress, + inputToken: params.debtAsset as ViemAddress, + outputToken: params.collateralAsset as ViemAddress, + inputAmount: BigInt(params.borrowAmount), + minOutputAmount: BigInt(params.collateralAmount), + deadline: BigInt(params.deadline || '0'), + allowPartialFill: false, + srcChain: BigInt(chainId), + dstChain: BigInt(chainId), + srcAddress: userAddress.toLowerCase() as Hex, + dstAddress: (this.hooksConfig.leverageHookAddress as string).toLowerCase() as Hex, + solver: (params.solver || ZERO_ADDRESS) as ViemAddress, // Use provided solver or zero address (any solver) + data: intentData, + }; + + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'createIntent', + args: [intent], + chain: this.walletClient.chain, + account: this.walletClient.account, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + // === DEBT SIDE LEVERAGE HOOK === + + /** + * Get the debt side leverage status for a user + * @param debtAsset - The address of the debt asset + * @param userAddress - The user's wallet address + * @returns Debt side leverage status + */ + async getDebtSideLeverageStatus(debtAsset: Address, userAddress: Address): Promise> { + try { + const debtTokenAddress = await this.getVariableDebtToken(debtAsset); + const hookAddress = this.hooksConfig.debtSideLeverageHookAddress; + + const [tokenAllowance, creditDelegation, tokenBalance] = await Promise.all([ + this.publicClient.readContract({ + address: debtAsset as ViemAddress, + abi: erc20Abi, + functionName: 'allowance', + args: [userAddress as ViemAddress, hookAddress as ViemAddress], + }), + this.publicClient.readContract({ + address: debtTokenAddress, + abi: variableDebtTokenAbi, + functionName: 'borrowAllowance', + args: [userAddress as ViemAddress, hookAddress as ViemAddress], + }), + this.publicClient.readContract({ + address: debtAsset as ViemAddress, + abi: erc20Abi, + functionName: 'balanceOf', + args: [userAddress as ViemAddress], + }), + ]); + + return { + ok: true, + value: { + tokenAllowance: tokenAllowance.toString(), + creditDelegation: creditDelegation.toString(), + tokenBalance: tokenBalance.toString(), + isReady: tokenAllowance > 0n && creditDelegation > 0n && tokenBalance > 0n, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Create a debt side leverage intent + * @param params - Debt side leverage hook parameters + * @param chainId - The chain ID + * @returns Intent creation result with transaction hash + */ + async createDebtSideLeverageIntent( + params: DebtSideLeverageHookParams, + chainId: number, + ): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const hookData = this.encodeHookDataWithUint256(params.userProvidedAmount); + const intentData = this.encodeIntentData( + this.hooksConfig.debtSideLeverageHookAddress, + hookData, + params.feeReceiver, + params.feeAmount, + ); + + const intent = { + intentId: randomUint256(), + creator: userAddress, + inputToken: params.debtAsset as ViemAddress, + outputToken: params.collateralAsset as ViemAddress, + inputAmount: BigInt(params.totalBorrowAmount), + minOutputAmount: BigInt(params.collateralAmount), + deadline: BigInt(params.deadline || '0'), + allowPartialFill: false, + srcChain: BigInt(chainId), + dstChain: BigInt(chainId), + srcAddress: userAddress.toLowerCase() as Hex, + dstAddress: (this.hooksConfig.debtSideLeverageHookAddress as string).toLowerCase() as Hex, + solver: (params.solver || ZERO_ADDRESS) as ViemAddress, // Use provided solver or zero address (any solver) + data: intentData, + }; + + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'createIntent', + args: [intent], + chain: this.walletClient.chain, + account: this.walletClient.account, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + // === DELEVERAGE HOOK === + + /** + * Get aToken approval information for deleverage + * Note: The hook contract checks aToken for intent.inputToken (collateralAsset), not debtAsset + * @param collateralAsset - The address of the collateral asset (inputToken in the intent) + * @param userAddress - The user's wallet address + * @param withdrawAmount - The amount to withdraw (inputAmount in the intent) + * @param feeAmount - Optional fee amount + * @returns aToken approval information + */ + async getATokenApprovalInfo( + collateralAsset: Address, + userAddress: Address, + withdrawAmount: string, + feeAmount?: string, + ): Promise> { + try { + // Hook checks aToken for intent.inputToken (collateralAsset), which is what we withdraw + const aTokenAddress = await this.getAToken(collateralAsset); + const currentAllowance = await this.publicClient.readContract({ + address: aTokenAddress, + abi: erc20Abi, + functionName: 'allowance', + args: [userAddress as ViemAddress, this.hooksConfig.deleverageHookAddress as ViemAddress], + }); + + // Hook requires: allowance >= intent.inputAmount + fee + const aTokensNeeded = BigInt(withdrawAmount) + BigInt(feeAmount || '0'); + + return { + ok: true, + value: { + aTokenAddress, + aTokensNeeded: aTokensNeeded.toString(), + currentAllowance: currentAllowance.toString(), + isApproved: BigInt(currentAllowance) >= aTokensNeeded, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Create a deleverage intent + * @param params - Deleverage hook parameters + * @param chainId - The chain ID + * @returns Intent creation result with transaction hash + */ + async createDeleverageIntent(params: DeleverageHookParams, chainId: number): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const intentData = this.encodeIntentData( + this.hooksConfig.deleverageHookAddress, + '0x', + params.feeReceiver, + params.feeAmount, + ); + + const intent = { + intentId: randomUint256(), + creator: userAddress, + // For deleverage: Hook's _onFillIntent expects: + // - intent.inputToken = collateralAsset (to withdraw using inputAmount) + // - intent.outputToken = debtAsset (to repay using outputAmount) + // The hook will pull outputToken (debtAsset) from its balance to repay + inputToken: params.collateralAsset as ViemAddress, // Collateral to withdraw from pool + outputToken: params.debtAsset as ViemAddress, // Debt to repay + inputAmount: BigInt(params.withdrawAmount), // Amount of collateral to withdraw + minOutputAmount: BigInt(params.repayAmount), // Amount of debt to repay + deadline: BigInt(params.deadline || '0'), + allowPartialFill: false, + srcChain: BigInt(chainId), + dstChain: BigInt(chainId), + srcAddress: userAddress.toLowerCase() as Hex, + dstAddress: (this.hooksConfig.deleverageHookAddress as string).toLowerCase() as Hex, + solver: (params.solver || ZERO_ADDRESS) as ViemAddress, // Use provided solver or zero address (any solver) + data: intentData, + }; + + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'createIntent', + args: [intent], + chain: this.walletClient.chain, + account: this.walletClient.account, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + // === LIQUIDATION HOOK === + + /** + * Get liquidation opportunity for a user + * @param userAddress - The user's wallet address to check + * @returns Liquidation opportunity information + */ + async getLiquidationOpportunity(userAddress: Address): Promise> { + try { + const accountData = await this.publicClient.readContract({ + address: this.poolAddress as ViemAddress, + abi: poolAbi, + functionName: 'getUserAccountData', + args: [userAddress as ViemAddress], + }); + + const [totalCollateralBase, totalDebtBase, availableBorrowsBase, currentLiquidationThreshold, ltv, healthFactor] = + accountData; + + return { + ok: true, + value: { + userAddress, + healthFactor: healthFactor.toString(), + isLiquidatable: healthFactor < BigInt(1e18), + accountData: { + totalCollateralBase: totalCollateralBase.toString(), + totalDebtBase: totalDebtBase.toString(), + availableBorrowsBase: availableBorrowsBase.toString(), + currentLiquidationThreshold: currentLiquidationThreshold.toString(), + ltv: ltv.toString(), + healthFactor: healthFactor.toString(), + }, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Create a liquidation intent + * @param params - Liquidation hook parameters + * @param chainId - The chain ID + * @returns Intent creation result with transaction hash + */ + async createLiquidationIntent(params: LiquidationHookParams, chainId: number): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + // Check if position is liquidatable + const opportunityResult = await this.getLiquidationOpportunity(params.userToLiquidate); + if (!opportunityResult.ok) { + return opportunityResult; + } + + if (!opportunityResult.value.isLiquidatable) { + return { + ok: false, + error: new Error( + `Position is not liquidatable. Health factor: ${opportunityResult.value.healthFactor} (must be < 1.0)`, + ), + }; + } + + const creatorAddress = await getWalletAddress(this.walletClient); + const hookData = this.encodeAddressToHookData(params.userToLiquidate); + const intentData = this.encodeIntentData( + this.hooksConfig.liquidationHookAddress, + hookData, + params.feeReceiver, + params.feeAmount, + ); + + const intent = { + intentId: randomUint256(), + creator: creatorAddress, + inputToken: params.collateralAsset as ViemAddress, + outputToken: params.debtAsset as ViemAddress, + inputAmount: BigInt(params.collateralAmount), + minOutputAmount: BigInt(params.debtAmount), + deadline: BigInt(params.deadline || '0'), + allowPartialFill: false, + srcChain: BigInt(chainId), + dstChain: BigInt(chainId), + srcAddress: creatorAddress.toLowerCase() as Hex, + dstAddress: (this.hooksConfig.liquidationHookAddress as string).toLowerCase() as Hex, + solver: (params.solver || ZERO_ADDRESS) as ViemAddress, // Use provided solver or zero address (any solver) + data: intentData, + }; + + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'createIntent', + args: [intent], + account: creatorAddress, + chain: this.walletClient.chain, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + // === INTENT LIFECYCLE METHODS === + + /** + * Cancel a pending intent + * @param intent - The full intent object to cancel + * @returns Cancel result with transaction hash + */ + async cancelIntent(intent: HookIntent): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'cancelIntent', + args: [this.intentToContractFormat(intent)], + account: this.walletClient.account, + chain: this.walletClient.chain, + }); + + await this.publicClient.waitForTransactionReceipt({ hash }); + + return { + ok: true, + value: { txHash: hash, cancelled: true }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Fill an intent (for solvers) + * @param params - Fill parameters including intent, amounts, and optional external fill ID + * @returns Fill result with transaction hash + */ + async fillIntent(params: FillHookIntentParams): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + if (!this.walletClient.account) { + throw new Error('Wallet client account is required'); + } + const externalFillId = params.externalFillId ? BigInt(params.externalFillId) : 0n; + + // For fillIntent, solver needs to provide output tokens + // If output token is native, solver needs to send ETH with the transaction + const isNativeOutput = params.intent.outputToken === ZERO_ADDRESS; + + // Estimate gas first, then add 20% buffer for complex operations + const estimatedGas = await this.publicClient.estimateContractGas({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'fillIntent', + args: [ + this.intentToContractFormat(params.intent), + BigInt(params.inputAmount), + BigInt(params.outputAmount), + externalFillId, + ], + account: this.walletClient.account, + ...(isNativeOutput ? { value: BigInt(params.outputAmount) } : {}), + }); + + // Add 30% buffer to handle gas estimation inaccuracies for complex hook operations + const gasLimit = (estimatedGas * 130n) / 100n; + + const hash = await this.walletClient.writeContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'fillIntent', + args: [ + this.intentToContractFormat(params.intent), + BigInt(params.inputAmount), + BigInt(params.outputAmount), + externalFillId, + ], + account: this.walletClient.account, + chain: this.walletClient.chain, + gas: gasLimit, + ...(isNativeOutput ? { value: BigInt(params.outputAmount) } : {}), + }); + + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + }); + + if (receipt.status === 'reverted') { + return { + ok: false, + error: new Error('Transaction reverted'), + }; + } + + return { + ok: true, + value: { txHash: hash, filled: true }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Get the state of an intent by its hash + * @param intentHash - The keccak256 hash of the intent + * @returns Intent state (exists, remainingInput, receivedOutput, pendingPayment) + */ + async getIntentState(intentHash: Hex): Promise> { + try { + const state = await this.publicClient.readContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'intentStates', + args: [intentHash], + }); + + return { + ok: true, + value: { + exists: state[0], + remainingInput: state[1].toString(), + receivedOutput: state[2].toString(), + pendingPayment: state[3], + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Get the pending state of an intent + * @param intentHash - The keccak256 hash of the intent + * @returns Pending intent state (pendingInput, pendingOutput) + */ + async getPendingIntentState(intentHash: Hex): Promise> { + try { + const state = await this.publicClient.readContract({ + address: this.intentsAddress as ViemAddress, + abi: IntentsAbi, + functionName: 'pendingIntentStates', + args: [intentHash], + }); + + return { + ok: true, + value: { + pendingInput: state[0].toString(), + pendingOutput: state[1].toString(), + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Compute the keccak256 hash of an intent (used as intent ID on-chain) + * @param intent - The intent object + * @returns The intent hash + */ + computeIntentHash(intent: HookIntent): Hex { + const encoded = encodeAbiParameters( + [ + { + type: 'tuple', + components: [ + { name: 'intentId', type: 'uint256' }, + { name: 'creator', type: 'address' }, + { name: 'inputToken', type: 'address' }, + { name: 'outputToken', type: 'address' }, + { name: 'inputAmount', type: 'uint256' }, + { name: 'minOutputAmount', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + { name: 'allowPartialFill', type: 'bool' }, + { name: 'srcChain', type: 'uint256' }, + { name: 'dstChain', type: 'uint256' }, + { name: 'srcAddress', type: 'bytes' }, + { name: 'dstAddress', type: 'bytes' }, + { name: 'solver', type: 'address' }, + { name: 'data', type: 'bytes' }, + ], + }, + ], + [this.intentToContractFormat(intent)], + ); + return keccak256(encoded); + } + + /** + * Check if an intent exists and is fillable + * @param intentHash - The intent hash + * @returns Whether the intent can be filled + */ + async isFillable(intentHash: Hex): Promise> { + try { + const stateResult = await this.getIntentState(intentHash); + if (!stateResult.ok) { + return stateResult; + } + + const pendingResult = await this.getPendingIntentState(intentHash); + if (!pendingResult.ok) { + return pendingResult; + } + + const { exists, remainingInput, pendingPayment } = stateResult.value; + const { pendingInput } = pendingResult.value; + + // Intent is fillable if it exists, has remaining input, no pending payment, and available input + const availableInput = BigInt(remainingInput) - BigInt(pendingInput); + const fillable = exists && !pendingPayment && availableInput > 0n; + + return { ok: true, value: fillable }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Convert Intent object to contract format for contract calls + */ + private intentToContractFormat(intent: HookIntent): { + intentId: bigint; + creator: ViemAddress; + inputToken: ViemAddress; + outputToken: ViemAddress; + inputAmount: bigint; + minOutputAmount: bigint; + deadline: bigint; + allowPartialFill: boolean; + srcChain: bigint; + dstChain: bigint; + srcAddress: Hex; + dstAddress: Hex; + solver: ViemAddress; + data: Hex; + } { + return { + intentId: intent.intentId, + creator: intent.creator as ViemAddress, + inputToken: intent.inputToken as ViemAddress, + outputToken: intent.outputToken as ViemAddress, + inputAmount: intent.inputAmount, + minOutputAmount: intent.minOutputAmount, + deadline: intent.deadline, + allowPartialFill: intent.allowPartialFill, + srcChain: intent.srcChain, + dstChain: intent.dstChain, + srcAddress: intent.srcAddress as Hex, + dstAddress: intent.dstAddress as Hex, + solver: intent.solver as ViemAddress, + data: intent.data as Hex, + }; + } + + // === HELPER METHODS === + + /** + * Get the variable debt token address for an asset + * @param asset - The asset address + * @returns The variable debt token address + */ + private async getVariableDebtToken(asset: Address): Promise { + const reserveData = await this.publicClient.readContract({ + address: this.poolAddress as ViemAddress, + abi: poolAbi, + functionName: 'getReserveData', + args: [asset as ViemAddress], + }); + + return reserveData.variableDebtTokenAddress; + } + + /** + * Get the aToken address for an asset + * @param asset - The asset address + * @returns The aToken address + */ + private async getAToken(asset: Address): Promise { + const reserveData = await this.publicClient.readContract({ + address: this.poolAddress as ViemAddress, + abi: poolAbi, + functionName: 'getReserveData', + args: [asset as ViemAddress], + }); + + return reserveData.aTokenAddress; + } + + /** + * Encode intent data with hook address and optional fee data + * Format matches IntentDataLib: + * - Without fee: uint8(2) + abi.encode(HookData({hook: address, data: bytes})) + * - With fee: uint8(0) + abi.encode(ArrayData({data: [DataEntry(FeeData), DataEntry(HookData)]})) + * + * IntentDataLib.decodeIntentData expects: + * - First byte: dataType (0=ARRAY, 1=FEE, 2=HOOK) + * - Rest: abi-encoded data + */ + private encodeIntentData(hookAddress: Address, hookData: string, feeReceiver?: Address, feeAmount?: string): Hex { + // Encode HookData struct: {hook: address, data: bytes} + // Use tuple encoding without names to match Solidity's abi.decode expectations + const hookDataBytes = hookData === '0x' || hookData === '' ? '0x' : (hookData as Hex); + const encodedHookData = encodeAbiParameters( + [ + { + type: 'tuple', + components: [ + { type: 'address' }, // NO name field + { type: 'bytes' }, // NO name field + ], + }, + ], + [[hookAddress, hookDataBytes]], // Note: extra array wrapper for tuple + ); + + if (feeReceiver && feeAmount) { + // With fee: Use ArrayData format + // Step 1: Encode FeeData struct: {fee: uint256, receiver: address} + // Use tuple encoding without names to match Solidity's abi.decode expectations + const encodedFeeData = encodeAbiParameters( + [ + { + type: 'tuple', + components: [ + { type: 'uint256' }, // NO name field + { type: 'address' }, // NO name field + ], + }, + ], + [[BigInt(feeAmount), feeReceiver]], // Note: extra array wrapper for tuple + ); + + // Step 3: Encode ArrayData struct: {data: DataEntry[]} + // Match Solidity: ArrayData memory arrayData = ArrayData({data: entries}); + // Then: bytes memory arrayEncoded = abi.encode(arrayData); + // ArrayData is a struct with one field: data (DataEntry[]) + // DataEntry is a struct: {dataType: uint8, data: bytes} + const arrayData = encodeAbiParameters( + [ + { + type: 'tuple', + components: [ + { + name: 'data', + type: 'tuple[]', + components: [ + { name: 'dataType', type: 'uint8' }, + { name: 'data', type: 'bytes' }, + ], + }, + ], + }, + ], + [ + { + data: [ + { dataType: 1, data: encodedFeeData }, + { dataType: 2, data: encodedHookData }, + ], + }, + ], + ); + + // Step 4: Final encoding: abi.encodePacked(uint8(0), abi.encode(ArrayData)) + // Match Solidity: bytes memory rawData = abi.encodePacked(uint8(0), arrayEncoded); + return encodePacked(['uint8', 'bytes'], [0, arrayData]) as Hex; + } + + // Without fee: abi.encodePacked(uint8(2), abi.encode(HookData)) + // Match Solidity: abi.encodePacked(uint8(2), abi.encode(HookData({hook: address, data: bytes}))) + // Use encodePacked to match Solidity's abi.encodePacked exactly + return encodePacked(['uint8', 'bytes'], [2, encodedHookData]) as Hex; + } + + /** + * Encode a uint256 value as hook data + */ + private encodeHookDataWithUint256(value: string): Hex { + return `0x${BigInt(value).toString(16).padStart(64, '0')}` as Hex; + } + + /** + * Encode an address as hook data + */ + private encodeAddressToHookData(address: Address): Hex { + return `0x${(address as string).replace('0x', '')}` as Hex; + } + + // === CONVENIENCE METHODS (WITH PREREQUISITES) === + + /** + * Create a credit intent with automatic prerequisite handling + * Automatically checks and approves credit delegation if needed + * @param params - Credit hook parameters + * @param chainId - The chain ID + * @param options - Optional settings + * - checkOnly: If true, only checks prerequisites without creating intent + * - autoApprove: If true, automatically approves if needed (default: true) + * @returns Intent creation result with transaction hash and prerequisite info + */ + async createCreditIntentWithPrerequisites( + params: CreditHookParams, + chainId: number, + options?: { checkOnly?: boolean; autoApprove?: boolean }, + ): Promise< + Result< + IntentCreationResult & { + prerequisites: { creditDelegationApproved: boolean }; + } + > + > { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + // In check-only mode, don't auto-approve + const autoApprove = options?.checkOnly ? false : options?.autoApprove !== false; + + // Check credit delegation status + const delegationStatus = await this.getCreditDelegationStatus(params.debtAsset, userAddress, HookType.Credit); + if (!delegationStatus.ok) { + return delegationStatus; + } + + let creditDelegationApproved = delegationStatus.value.delegated; + const neededAmount = BigInt(params.maxPayment); + + // Approve credit delegation if needed + if (!creditDelegationApproved && autoApprove && !options?.checkOnly) { + const approveResult = await this.approveCreditDelegation(params.debtAsset, params.maxPayment, HookType.Credit); + if (!approveResult.ok) { + return approveResult; + } + creditDelegationApproved = true; + } else if (!creditDelegationApproved) { + return { + ok: false, + error: new Error( + `Credit delegation not approved. Current allowance: ${delegationStatus.value.allowance}, needed: ${params.maxPayment}`, + ), + }; + } else if (BigInt(delegationStatus.value.allowance) < neededAmount) { + // Check if existing allowance is sufficient + if (autoApprove && !options?.checkOnly) { + const approveResult = await this.approveCreditDelegation( + params.debtAsset, + params.maxPayment, + HookType.Credit, + ); + if (!approveResult.ok) { + return approveResult; + } + } else { + return { + ok: false, + error: new Error( + `Insufficient credit delegation allowance. Current: ${delegationStatus.value.allowance}, needed: ${params.maxPayment}`, + ), + }; + } + } + + if (options?.checkOnly) { + return { + ok: true, + value: { + txHash: '0x' as Hex, + prerequisites: { creditDelegationApproved }, + }, + }; + } + + // Create the intent + const createResult = await this.createCreditIntent(params, chainId); + if (!createResult.ok) { + return createResult; + } + + return { + ok: true, + value: { + ...createResult.value, + prerequisites: { creditDelegationApproved }, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Create a leverage intent with automatic prerequisite handling + * Automatically checks and approves credit delegation if needed + */ + async createLeverageIntentWithPrerequisites( + params: LeverageHookParams, + chainId: number, + options?: { checkOnly?: boolean; autoApprove?: boolean }, + ): Promise< + Result< + IntentCreationResult & { + prerequisites: { creditDelegationApproved: boolean }; + } + > + > { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const autoApprove = options?.autoApprove !== false; + + // Check credit delegation status + const delegationStatus = await this.getCreditDelegationStatus(params.debtAsset, userAddress, HookType.Leverage); + if (!delegationStatus.ok) { + return delegationStatus; + } + + // When there's a fee, the hook checks: allowance >= borrowAmount + fee + // From unit test: ICreditDelegationToken(vDebtWeth).approveDelegation(address(_hook), borrowAmount+fee); + const feeAmount = params.feeAmount ? BigInt(params.feeAmount) : 0n; + const neededAmount = BigInt(params.borrowAmount) + feeAmount; + + let creditDelegationApproved = delegationStatus.value.delegated; + + // Approve credit delegation if needed (must include fee if present) + if (!creditDelegationApproved && autoApprove) { + const approveResult = await this.approveCreditDelegation( + params.debtAsset, + neededAmount.toString(), + HookType.Leverage, + ); + if (!approveResult.ok) { + return approveResult; + } + creditDelegationApproved = true; + } else if (!creditDelegationApproved) { + return { + ok: false, + error: new Error( + `Credit delegation not approved. Current allowance: ${delegationStatus.value.allowance}, needed: ${neededAmount.toString()} (borrowAmount: ${params.borrowAmount}${feeAmount > 0n ? ` + fee: ${feeAmount.toString()}` : ''})`, + ), + }; + } else if (BigInt(delegationStatus.value.allowance) < neededAmount) { + if (autoApprove) { + const approveResult = await this.approveCreditDelegation( + params.debtAsset, + neededAmount.toString(), + HookType.Leverage, + ); + if (!approveResult.ok) { + return approveResult; + } + } else { + return { + ok: false, + error: new Error( + `Insufficient credit delegation allowance. Current: ${delegationStatus.value.allowance}, needed: ${neededAmount.toString()} (borrowAmount: ${params.borrowAmount}${feeAmount > 0n ? ` + fee: ${feeAmount.toString()}` : ''})`, + ), + }; + } + } + + if (options?.checkOnly) { + return { + ok: true, + value: { + txHash: '0x' as Hex, + prerequisites: { creditDelegationApproved }, + }, + }; + } + + // Create the intent + const createResult = await this.createLeverageIntent(params, chainId); + if (!createResult.ok) { + return createResult; + } + + return { + ok: true, + value: { + ...createResult.value, + prerequisites: { creditDelegationApproved }, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Create a debt side leverage intent with automatic prerequisite handling + * Automatically checks and approves credit delegation and token spending if needed + */ + async createDebtSideLeverageIntentWithPrerequisites( + params: DebtSideLeverageHookParams, + chainId: number, + options?: { checkOnly?: boolean; autoApprove?: boolean }, + ): Promise< + Result< + IntentCreationResult & { + prerequisites: { + creditDelegationApproved: boolean; + tokenApproved: boolean; + }; + } + > + > { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const autoApprove = options?.autoApprove !== false; + + // Check credit delegation status + const delegationStatus = await this.getCreditDelegationStatus( + params.debtAsset, + userAddress, + HookType.DebtSideLeverage, + ); + if (!delegationStatus.ok) { + return delegationStatus; + } + + // Check token approval status + const statusResult = await this.getDebtSideLeverageStatus(params.debtAsset, userAddress); + if (!statusResult.ok) { + return statusResult; + } + + let creditDelegationApproved = delegationStatus.value.delegated; + let tokenApproved = BigInt(statusResult.value.tokenAllowance) >= BigInt(params.userProvidedAmount); + + const neededDelegationAmount = BigInt(params.totalBorrowAmount) - BigInt(params.userProvidedAmount); + const neededTokenAmount = BigInt(params.userProvidedAmount); + + // Approve credit delegation if needed + if (!creditDelegationApproved && autoApprove) { + const approveResult = await this.approveCreditDelegation( + params.debtAsset, + neededDelegationAmount.toString(), + HookType.DebtSideLeverage, + ); + if (!approveResult.ok) { + return approveResult; + } + creditDelegationApproved = true; + } else if (!creditDelegationApproved) { + return { + ok: false, + error: new Error( + `Credit delegation not approved. Current allowance: ${delegationStatus.value.allowance}, needed: ${neededDelegationAmount.toString()}`, + ), + }; + } else if (BigInt(delegationStatus.value.allowance) < neededDelegationAmount) { + if (autoApprove) { + const approveResult = await this.approveCreditDelegation( + params.debtAsset, + neededDelegationAmount.toString(), + HookType.DebtSideLeverage, + ); + if (!approveResult.ok) { + return approveResult; + } + } else { + return { + ok: false, + error: new Error( + `Insufficient credit delegation allowance. Current: ${delegationStatus.value.allowance}, needed: ${neededDelegationAmount.toString()}`, + ), + }; + } + } + + // Approve token if needed + if (!tokenApproved && autoApprove) { + const approveResult = await this.approveToken( + params.debtAsset, + params.userProvidedAmount, + HookType.DebtSideLeverage, + ); + if (!approveResult.ok) { + return approveResult; + } + tokenApproved = true; + } else if (!tokenApproved) { + return { + ok: false, + error: new Error( + `Token not approved. Current allowance: ${statusResult.value.tokenAllowance}, needed: ${params.userProvidedAmount}`, + ), + }; + } else if (BigInt(statusResult.value.tokenAllowance) < neededTokenAmount) { + if (autoApprove) { + const approveResult = await this.approveToken( + params.debtAsset, + params.userProvidedAmount, + HookType.DebtSideLeverage, + ); + if (!approveResult.ok) { + return approveResult; + } + } else { + return { + ok: false, + error: new Error( + `Insufficient token allowance. Current: ${statusResult.value.tokenAllowance}, needed: ${params.userProvidedAmount}`, + ), + }; + } + } + + if (options?.checkOnly) { + return { + ok: true, + value: { + txHash: '0x' as Hex, + prerequisites: { creditDelegationApproved, tokenApproved }, + }, + }; + } + + // Create the intent + const createResult = await this.createDebtSideLeverageIntent(params, chainId); + if (!createResult.ok) { + return createResult; + } + + return { + ok: true, + value: { + ...createResult.value, + prerequisites: { creditDelegationApproved, tokenApproved }, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } + + /** + * Create a deleverage intent with automatic prerequisite handling + * Automatically checks and approves aToken spending if needed + */ + async createDeleverageIntentWithPrerequisites( + params: DeleverageHookParams, + chainId: number, + options?: { checkOnly?: boolean; autoApprove?: boolean }, + ): Promise> { + try { + if (!this.walletClient) { + return { ok: false, error: new Error('Wallet client required') }; + } + + const userAddress = await getWalletAddress(this.walletClient); + const autoApprove = options?.autoApprove !== false; + + // Check aToken approval info + // Hook checks aToken for the collateral asset (outputToken) which is what we withdraw + const approvalInfo = await this.getATokenApprovalInfo( + params.collateralAsset, + userAddress, + params.withdrawAmount, + params.feeAmount, + ); + if (!approvalInfo.ok) { + return approvalInfo; + } + + let aTokenApproved = approvalInfo.value.isApproved; + + // Approve aToken if needed + if (!aTokenApproved && autoApprove) { + const approveResult = await this.approveAToken( + params.collateralAsset, + approvalInfo.value.aTokensNeeded, + HookType.Deleverage, + ); + if (!approveResult.ok) { + return approveResult; + } + aTokenApproved = true; + } else if (!aTokenApproved) { + return { + ok: false, + error: new Error( + `aToken not approved. Current allowance: ${approvalInfo.value.currentAllowance}, needed: ${approvalInfo.value.aTokensNeeded}`, + ), + }; + } + + if (options?.checkOnly) { + return { + ok: true, + value: { + txHash: '0x' as Hex, + prerequisites: { aTokenApproved }, + }, + }; + } + + // Create the intent + const createResult = await this.createDeleverageIntent(params, chainId); + if (!createResult.ok) { + return createResult; + } + + return { + ok: true, + value: { + ...createResult.value, + prerequisites: { aTokenApproved }, + }, + }; + } catch (error) { + return { ok: false, error }; + } + } +} diff --git a/packages/sdk/src/intentHooks/index.ts b/packages/sdk/src/intentHooks/index.ts new file mode 100644 index 000000000..d418b1a2b --- /dev/null +++ b/packages/sdk/src/intentHooks/index.ts @@ -0,0 +1 @@ +export * from './HooksService.js'; diff --git a/packages/sdk/src/shared/abis/intents.abi.ts b/packages/sdk/src/shared/abis/intents.abi.ts index 42b27ed56..e3dcdcbc7 100644 --- a/packages/sdk/src/shared/abis/intents.abi.ts +++ b/packages/sdk/src/shared/abis/intents.abi.ts @@ -1,1157 +1,582 @@ export const IntentsAbi = [ + { type: "constructor", inputs: [], stateMutability: "nonpayable" }, + { type: "receive", stateMutability: "payable" }, { - type: 'constructor', + type: "function", + name: "HUB_CHAIN_ID", inputs: [], - stateMutability: 'nonpayable', + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", }, { - type: 'function', - name: 'HUB_CHAIN_ID', + type: "function", + name: "NATIVE_TOKEN", inputs: [], - outputs: [ - { - name: '', - type: 'uint256', - internalType: 'uint256', - }, - ], - stateMutability: 'view', + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", }, { - type: 'function', - name: 'UPGRADE_INTERFACE_VERSION', + type: "function", + name: "UPGRADE_INTERFACE_VERSION", inputs: [], - outputs: [ - { - name: '', - type: 'string', - internalType: 'string', - }, - ], - stateMutability: 'view', + outputs: [{ name: "", type: "string", internalType: "string" }], + stateMutability: "view", + }, + { + type: "function", + name: "acceptOwnership", + inputs: [], + outputs: [], + stateMutability: "nonpayable", }, { - type: 'function', - name: 'addSpoke', + type: "function", + name: "addSpoke", inputs: [ - { - name: 'chainID', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'spokeAddress', - type: 'bytes', - internalType: 'bytes', - }, + { name: "chainID", type: "uint256", internalType: "uint256" }, + { name: "spokeAddress", type: "bytes", internalType: "bytes" }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'assetManager', + type: "function", + name: "assetManager", inputs: [], outputs: [ - { - name: '', - type: 'address', - internalType: 'contract IAssetManager', - }, + { name: "", type: "address", internalType: "contract IAssetManager" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'cancelIntent', + type: "function", + name: "cancelIntent", inputs: [ { - name: 'intent', - type: 'tuple', - internalType: 'struct Intents.Intent', + name: "intent", + type: "tuple", + internalType: "struct Intents.Intent", components: [ - { - name: 'intentId', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'creator', - type: 'address', - internalType: 'address', - }, - { - name: 'inputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'outputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'minOutputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'deadline', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'allowPartialFill', - type: 'bool', - internalType: 'bool', - }, - { - name: 'srcChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'dstChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'srcAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'dstAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, + { name: "intentId", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + { name: "inputToken", type: "address", internalType: "address" }, + { name: "outputToken", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "minOutputAmount", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { name: "allowPartialFill", type: "bool", internalType: "bool" }, + { name: "srcChain", type: "uint256", internalType: "uint256" }, + { name: "dstChain", type: "uint256", internalType: "uint256" }, + { name: "srcAddress", type: "bytes", internalType: "bytes" }, + { name: "dstAddress", type: "bytes", internalType: "bytes" }, + { name: "solver", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, ], }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'connection', + type: "function", + name: "connection", inputs: [], outputs: [ - { - name: '', - type: 'address', - internalType: 'contract IConnection', - }, + { name: "", type: "address", internalType: "contract IConnection" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'createIntent', + type: "function", + name: "createIntent", inputs: [ { - name: 'intent', - type: 'tuple', - internalType: 'struct Intents.Intent', + name: "intent", + type: "tuple", + internalType: "struct Intents.Intent", components: [ - { - name: 'intentId', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'creator', - type: 'address', - internalType: 'address', - }, - { - name: 'inputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'outputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'minOutputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'deadline', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'allowPartialFill', - type: 'bool', - internalType: 'bool', - }, - { - name: 'srcChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'dstChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'srcAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'dstAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, + { name: "intentId", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + { name: "inputToken", type: "address", internalType: "address" }, + { name: "outputToken", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "minOutputAmount", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { name: "allowPartialFill", type: "bool", internalType: "bool" }, + { name: "srcChain", type: "uint256", internalType: "uint256" }, + { name: "dstChain", type: "uint256", internalType: "uint256" }, + { name: "srcAddress", type: "bytes", internalType: "bytes" }, + { name: "dstAddress", type: "bytes", internalType: "bytes" }, + { name: "solver", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, ], }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "payable", }, { - type: 'function', - name: 'externalFills', - inputs: [ - { - name: '', - type: 'uint256', - internalType: 'uint256', - }, - ], + type: "function", + name: "externalFills", + inputs: [{ name: "", type: "uint256", internalType: "uint256" }], outputs: [ - { - name: 'intentHash', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'to', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'outputAmount', - type: 'uint256', - internalType: 'uint256', - }, + { name: "intentHash", type: "bytes32", internalType: "bytes32" }, + { name: "to", type: "address", internalType: "address" }, + { name: "token", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "outputAmount", type: "uint256", internalType: "uint256" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'fillIntent', + type: "function", + name: "fillIntent", inputs: [ { - name: 'intent', - type: 'tuple', - internalType: 'struct Intents.Intent', + name: "intent", + type: "tuple", + internalType: "struct Intents.Intent", components: [ - { - name: 'intentId', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'creator', - type: 'address', - internalType: 'address', - }, - { - name: 'inputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'outputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'minOutputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'deadline', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'allowPartialFill', - type: 'bool', - internalType: 'bool', - }, - { - name: 'srcChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'dstChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'srcAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'dstAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, + { name: "intentId", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + { name: "inputToken", type: "address", internalType: "address" }, + { name: "outputToken", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "minOutputAmount", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { name: "allowPartialFill", type: "bool", internalType: "bool" }, + { name: "srcChain", type: "uint256", internalType: "uint256" }, + { name: "dstChain", type: "uint256", internalType: "uint256" }, + { name: "srcAddress", type: "bytes", internalType: "bytes" }, + { name: "dstAddress", type: "bytes", internalType: "bytes" }, + { name: "solver", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, ], }, - { - name: '_inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: '_outputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: '_externalFillId', - type: 'uint256', - internalType: 'uint256', - }, + { name: "_inputAmount", type: "uint256", internalType: "uint256" }, + { name: "_outputAmount", type: "uint256", internalType: "uint256" }, + { name: "_externalFillId", type: "uint256", internalType: "uint256" }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "payable", }, { - type: 'function', - name: 'getImplementation', + type: "function", + name: "getImplementation", inputs: [], - outputs: [ - { - name: '', - type: 'address', - internalType: 'address', - }, - ], - stateMutability: 'view', + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", }, { - type: 'function', - name: 'initialize', + type: "function", + name: "initialize", inputs: [ { - name: '_walletFactory', - type: 'address', - internalType: 'contract IWalletFactory', + name: "_walletFactory", + type: "address", + internalType: "contract IWalletFactory", }, { - name: '_assetManager', - type: 'address', - internalType: 'contract IAssetManager', + name: "_assetManager", + type: "address", + internalType: "contract IAssetManager", }, { - name: '_connection', - type: 'address', - internalType: 'contract IConnection', - }, - { - name: '_HUB_CHAIN_ID', - type: 'uint256', - internalType: 'uint256', + name: "_connection", + type: "address", + internalType: "contract IConnection", }, + { name: "_HUB_CHAIN_ID", type: "uint256", internalType: "uint256" }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'intentStates', - inputs: [ - { - name: '', - type: 'bytes32', - internalType: 'bytes32', - }, - ], + type: "function", + name: "intentStates", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], outputs: [ - { - name: 'exists', - type: 'bool', - internalType: 'bool', - }, - { - name: 'remainingInput', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'receivedOutput', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'pendingPayment', - type: 'bool', - internalType: 'bool', - }, + { name: "exists", type: "bool", internalType: "bool" }, + { name: "remainingInput", type: "uint256", internalType: "uint256" }, + { name: "receivedOutput", type: "uint256", internalType: "uint256" }, + { name: "pendingPayment", type: "bool", internalType: "bool" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'owner', + type: "function", + name: "owner", inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", + }, + { + type: "function", + name: "partnerFee", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], outputs: [ - { - name: '', - type: 'address', - internalType: 'address', - }, + { name: "fee", type: "uint256", internalType: "uint256" }, + { name: "receiver", type: "address", internalType: "address" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'pendingIntentStates', - inputs: [ - { - name: '', - type: 'bytes32', - internalType: 'bytes32', - }, - ], + type: "function", + name: "pendingIntentStates", + inputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], outputs: [ - { - name: 'pendingInput', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'pendingOutput', - type: 'uint256', - internalType: 'uint256', - }, + { name: "pendingInput", type: "uint256", internalType: "uint256" }, + { name: "pendingOutput", type: "uint256", internalType: "uint256" }, ], - stateMutability: 'view', + stateMutability: "view", + }, + { + type: "function", + name: "pendingOwner", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "address" }], + stateMutability: "view", }, { - type: 'function', - name: 'pendingPayouts', + type: "function", + name: "pendingPayouts", inputs: [ - { - name: '', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: '', - type: 'uint256', - internalType: 'uint256', - }, + { name: "", type: "bytes32", internalType: "bytes32" }, + { name: "", type: "uint256", internalType: "uint256" }, ], outputs: [ - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'amount', - type: 'uint256', - internalType: 'uint256', - }, + { name: "solver", type: "address", internalType: "address" }, + { name: "amount", type: "uint256", internalType: "uint256" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'preFillIntent', + type: "function", + name: "preFillIntent", inputs: [ { - name: 'intent', - type: 'tuple', - internalType: 'struct Intents.Intent', + name: "intent", + type: "tuple", + internalType: "struct Intents.Intent", components: [ - { - name: 'intentId', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'creator', - type: 'address', - internalType: 'address', - }, - { - name: 'inputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'outputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'minOutputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'deadline', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'allowPartialFill', - type: 'bool', - internalType: 'bool', - }, - { - name: 'srcChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'dstChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'srcAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'dstAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, + { name: "intentId", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + { name: "inputToken", type: "address", internalType: "address" }, + { name: "outputToken", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "minOutputAmount", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { name: "allowPartialFill", type: "bool", internalType: "bool" }, + { name: "srcChain", type: "uint256", internalType: "uint256" }, + { name: "dstChain", type: "uint256", internalType: "uint256" }, + { name: "srcAddress", type: "bytes", internalType: "bytes" }, + { name: "dstAddress", type: "bytes", internalType: "bytes" }, + { name: "solver", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, ], }, - { - name: '_inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: '_outputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: '_externalFillId', - type: 'uint256', - internalType: 'uint256', - }, + { name: "_inputAmount", type: "uint256", internalType: "uint256" }, + { name: "_outputAmount", type: "uint256", internalType: "uint256" }, + { name: "_externalFillId", type: "uint256", internalType: "uint256" }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "payable", }, { - type: 'function', - name: 'proxiableUUID', + type: "function", + name: "proxiableUUID", inputs: [], - outputs: [ - { - name: '', - type: 'bytes32', - internalType: 'bytes32', - }, - ], - stateMutability: 'view', + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "view", }, { - type: 'function', - name: 'recvMessage', + type: "function", + name: "recvMessage", inputs: [ - { - name: 'srcChainId', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'srcAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'connSn', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'payload', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'signatures', - type: 'bytes[]', - internalType: 'bytes[]', - }, + { name: "srcChainId", type: "uint256", internalType: "uint256" }, + { name: "srcAddress", type: "bytes", internalType: "bytes" }, + { name: "connSn", type: "uint256", internalType: "uint256" }, + { name: "payload", type: "bytes", internalType: "bytes" }, + { name: "signatures", type: "bytes[]", internalType: "bytes[]" }, ], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'renounceOwnership', + type: "function", + name: "registerMe", inputs: [], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'setWhitelistedSolver', - inputs: [ - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'whitelisted', - type: 'bool', - internalType: 'bool', - }, - ], + type: "function", + name: "renounceOwnership", + inputs: [], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'spokes', + type: "function", + name: "setWhitelistedSolver", inputs: [ - { - name: '', - type: 'uint256', - internalType: 'uint256', - }, + { name: "solver", type: "address", internalType: "address" }, + { name: "whitelisted", type: "bool", internalType: "bool" }, ], - outputs: [ - { - name: '', - type: 'bytes', - internalType: 'bytes', - }, - ], - stateMutability: 'view', + outputs: [], + stateMutability: "nonpayable", }, { - type: 'function', - name: 'transferOwnership', - inputs: [ - { - name: 'newOwner', - type: 'address', - internalType: 'address', - }, - ], + type: "function", + name: "spokes", + inputs: [{ name: "", type: "uint256", internalType: "uint256" }], + outputs: [{ name: "", type: "bytes", internalType: "bytes" }], + stateMutability: "view", + }, + { + type: "function", + name: "transferOwnership", + inputs: [{ name: "newOwner", type: "address", internalType: "address" }], outputs: [], - stateMutability: 'nonpayable', + stateMutability: "nonpayable", }, { - type: 'function', - name: 'upgradeToAndCall', + type: "function", + name: "upgradeToAndCall", inputs: [ - { - name: 'newImplementation', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, + { name: "newImplementation", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, ], outputs: [], - stateMutability: 'payable', + stateMutability: "payable", }, { - type: 'function', - name: 'walletFactory', + type: "function", + name: "walletFactory", inputs: [], outputs: [ - { - name: '', - type: 'address', - internalType: 'contract IWalletFactory', - }, + { name: "", type: "address", internalType: "contract IWalletFactory" }, ], - stateMutability: 'view', + stateMutability: "view", }, { - type: 'function', - name: 'whitelistedSolvers', - inputs: [ - { - name: '', - type: 'address', - internalType: 'address', - }, - ], - outputs: [ - { - name: '', - type: 'bool', - internalType: 'bool', - }, - ], - stateMutability: 'view', + type: "function", + name: "whitelistedSolvers", + inputs: [{ name: "", type: "address", internalType: "address" }], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", }, { - type: 'event', - name: 'ExternalFillFailed', + type: "event", + name: "ExternalFillFailed", inputs: [ { - name: 'fillId', - type: 'uint256', + name: "fillId", + type: "uint256", indexed: false, - internalType: 'uint256', + internalType: "uint256", }, { - name: 'fill', - type: 'tuple', + name: "fill", + type: "tuple", indexed: false, - internalType: 'struct Intents.ExternalFill', + internalType: "struct Intents.ExternalFill", components: [ - { - name: 'intentHash', - type: 'bytes32', - internalType: 'bytes32', - }, - { - name: 'to', - type: 'address', - internalType: 'address', - }, - { - name: 'token', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'outputAmount', - type: 'uint256', - internalType: 'uint256', - }, + { name: "intentHash", type: "bytes32", internalType: "bytes32" }, + { name: "to", type: "address", internalType: "address" }, + { name: "token", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "outputAmount", type: "uint256", internalType: "uint256" }, ], }, ], anonymous: false, }, { - type: 'event', - name: 'Initialized', + type: "event", + name: "Initialized", inputs: [ { - name: 'version', - type: 'uint64', + name: "version", + type: "uint64", indexed: false, - internalType: 'uint64', + internalType: "uint64", }, ], anonymous: false, }, { - type: 'event', - name: 'IntentCancelled', + type: "event", + name: "IntentCancelled", inputs: [ { - name: 'intentHash', - type: 'bytes32', + name: "intentHash", + type: "bytes32", indexed: false, - internalType: 'bytes32', + internalType: "bytes32", }, ], anonymous: false, }, { - type: 'event', - name: 'IntentCreated', + type: "event", + name: "IntentCreated", inputs: [ { - name: 'intentHash', - type: 'bytes32', + name: "intentHash", + type: "bytes32", indexed: false, - internalType: 'bytes32', + internalType: "bytes32", }, { - name: 'intent', - type: 'tuple', + name: "intent", + type: "tuple", indexed: false, - internalType: 'struct Intents.Intent', + internalType: "struct Intents.Intent", components: [ - { - name: 'intentId', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'creator', - type: 'address', - internalType: 'address', - }, - { - name: 'inputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'outputToken', - type: 'address', - internalType: 'address', - }, - { - name: 'inputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'minOutputAmount', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'deadline', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'allowPartialFill', - type: 'bool', - internalType: 'bool', - }, - { - name: 'srcChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'dstChain', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'srcAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'dstAddress', - type: 'bytes', - internalType: 'bytes', - }, - { - name: 'solver', - type: 'address', - internalType: 'address', - }, - { - name: 'data', - type: 'bytes', - internalType: 'bytes', - }, + { name: "intentId", type: "uint256", internalType: "uint256" }, + { name: "creator", type: "address", internalType: "address" }, + { name: "inputToken", type: "address", internalType: "address" }, + { name: "outputToken", type: "address", internalType: "address" }, + { name: "inputAmount", type: "uint256", internalType: "uint256" }, + { name: "minOutputAmount", type: "uint256", internalType: "uint256" }, + { name: "deadline", type: "uint256", internalType: "uint256" }, + { name: "allowPartialFill", type: "bool", internalType: "bool" }, + { name: "srcChain", type: "uint256", internalType: "uint256" }, + { name: "dstChain", type: "uint256", internalType: "uint256" }, + { name: "srcAddress", type: "bytes", internalType: "bytes" }, + { name: "dstAddress", type: "bytes", internalType: "bytes" }, + { name: "solver", type: "address", internalType: "address" }, + { name: "data", type: "bytes", internalType: "bytes" }, ], }, ], anonymous: false, }, { - type: 'event', - name: 'IntentFilled', + type: "event", + name: "IntentFilled", inputs: [ { - name: 'intentHash', - type: 'bytes32', + name: "intentHash", + type: "bytes32", indexed: false, - internalType: 'bytes32', + internalType: "bytes32", }, { - name: 'intentState', - type: 'tuple', + name: "intentState", + type: "tuple", indexed: false, - internalType: 'struct Intents.IntentState', + internalType: "struct Intents.IntentState", components: [ - { - name: 'exists', - type: 'bool', - internalType: 'bool', - }, - { - name: 'remainingInput', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'receivedOutput', - type: 'uint256', - internalType: 'uint256', - }, - { - name: 'pendingPayment', - type: 'bool', - internalType: 'bool', - }, + { name: "exists", type: "bool", internalType: "bool" }, + { name: "remainingInput", type: "uint256", internalType: "uint256" }, + { name: "receivedOutput", type: "uint256", internalType: "uint256" }, + { name: "pendingPayment", type: "bool", internalType: "bool" }, ], }, ], anonymous: false, }, { - type: 'event', - name: 'OwnershipTransferred', + type: "event", + name: "OwnershipTransferStarted", inputs: [ { - name: 'previousOwner', - type: 'address', + name: "previousOwner", + type: "address", indexed: true, - internalType: 'address', + internalType: "address", }, { - name: 'newOwner', - type: 'address', + name: "newOwner", + type: "address", indexed: true, - internalType: 'address', + internalType: "address", }, ], anonymous: false, }, { - type: 'event', - name: 'Upgraded', + type: "event", + name: "OwnershipTransferred", inputs: [ { - name: 'implementation', - type: 'address', + name: "previousOwner", + type: "address", indexed: true, - internalType: 'address', + internalType: "address", }, - ], - anonymous: false, - }, - { - type: 'error', - name: 'AddressEmptyCode', - inputs: [ - { - name: 'target', - type: 'address', - internalType: 'address', - }, - ], - }, - { - type: 'error', - name: 'ERC1967InvalidImplementation', - inputs: [ { - name: 'implementation', - type: 'address', - internalType: 'address', - }, - ], - }, - { - type: 'error', - name: 'ERC1967NonPayable', - inputs: [], - }, - { - type: 'error', - name: 'FailedCall', - inputs: [], - }, - { - type: 'error', - name: 'FillAlreadyExists', - inputs: [], - }, - { - type: 'error', - name: 'IntentAlreadyExists', - inputs: [], - }, - { - type: 'error', - name: 'IntentNotFound', - inputs: [], - }, - { - type: 'error', - name: 'InvalidAmount', - inputs: [], - }, - { - type: 'error', - name: 'InvalidInitialization', - inputs: [], - }, - { - type: 'error', - name: 'InvalidOutputToken', - inputs: [], - }, - { - type: 'error', - name: 'InvalidSolver', - inputs: [], - }, - { - type: 'error', - name: 'NotInitializing', - inputs: [], - }, - { - type: 'error', - name: 'OwnableInvalidOwner', - inputs: [ - { - name: 'owner', - type: 'address', - internalType: 'address', + name: "newOwner", + type: "address", + indexed: true, + internalType: "address", }, ], + anonymous: false, }, { - type: 'error', - name: 'OwnableUnauthorizedAccount', + type: "event", + name: "Upgraded", inputs: [ { - name: 'account', - type: 'address', - internalType: 'address', + name: "implementation", + type: "address", + indexed: true, + internalType: "address", }, ], + anonymous: false, }, { - type: 'error', - name: 'PartialFillNotAllowed', - inputs: [], - }, - { - type: 'error', - name: 'PendingFillExists', - inputs: [], + type: "error", + name: "AddressEmptyCode", + inputs: [{ name: "target", type: "address", internalType: "address" }], }, { - type: 'error', - name: 'SafeERC20FailedOperation', + type: "error", + name: "ERC1967InvalidImplementation", inputs: [ - { - name: 'token', - type: 'address', - internalType: 'address', - }, + { name: "implementation", type: "address", internalType: "address" }, ], }, + { type: "error", name: "ERC1967NonPayable", inputs: [] }, + { type: "error", name: "ExternalFillNotAllowed", inputs: [] }, + { type: "error", name: "FailedCall", inputs: [] }, + { type: "error", name: "FillAlreadyExists", inputs: [] }, + { type: "error", name: "IntentAlreadyExists", inputs: [] }, + { type: "error", name: "IntentNotFound", inputs: [] }, + { type: "error", name: "InvalidAmount", inputs: [] }, + { type: "error", name: "InvalidInitialization", inputs: [] }, + { type: "error", name: "InvalidOutputToken", inputs: [] }, + { type: "error", name: "InvalidSolver", inputs: [] }, + { type: "error", name: "NotInitializing", inputs: [] }, { - type: 'error', - name: 'SpokeNotConfigured', - inputs: [], + type: "error", + name: "OwnableInvalidOwner", + inputs: [{ name: "owner", type: "address", internalType: "address" }], }, { - type: 'error', - name: 'UUPSUnauthorizedCallContext', - inputs: [], + type: "error", + name: "OwnableUnauthorizedAccount", + inputs: [{ name: "account", type: "address", internalType: "address" }], }, + { type: "error", name: "PartialFillNotAllowed", inputs: [] }, + { type: "error", name: "PendingFillExists", inputs: [] }, + { type: "error", name: "ReentrancyGuardReentrantCall", inputs: [] }, { - type: 'error', - name: 'UUPSUnsupportedProxiableUUID', - inputs: [ - { - name: 'slot', - type: 'bytes32', - internalType: 'bytes32', - }, - ], + type: "error", + name: "SafeERC20FailedOperation", + inputs: [{ name: "token", type: "address", internalType: "address" }], }, + { type: "error", name: "SpokeNotConfigured", inputs: [] }, + { type: "error", name: "TransferFailed", inputs: [] }, + { type: "error", name: "UUPSUnauthorizedCallContext", inputs: [] }, { - type: 'error', - name: 'Unauthorized', - inputs: [], + type: "error", + name: "UUPSUnsupportedProxiableUUID", + inputs: [{ name: "slot", type: "bytes32", internalType: "bytes32" }], }, + { type: "error", name: "Unauthorized", inputs: [] }, ] as const; diff --git a/packages/types/src/constants/index.ts b/packages/types/src/constants/index.ts index 8c8dd6589..f31bb4255 100644 --- a/packages/types/src/constants/index.ts +++ b/packages/types/src/constants/index.ts @@ -2302,6 +2302,33 @@ const moneyMarketConfig = { export const getMoneyMarketConfig = (chainId: HubChainId): MoneyMarketConfig => moneyMarketConfig[chainId]; +// === HOOKS CONFIG === + +/** + * Hook contract addresses configuration + * Note: intentsAddress and poolAddress are obtained from getSolverConfig and getMoneyMarketConfig + */ +export type HooksConfig = { + creditHookAddress: Address; + leverageHookAddress: Address; + debtSideLeverageHookAddress: Address; + deleverageHookAddress: Address; + liquidationHookAddress: Address; +}; + +// Mainnet hook contract addresses +const hooksConfig = { + [SONIC_MAINNET_CHAIN_ID]: { + creditHookAddress: '0xe2A8E6023eB4C88c51472c8eB1332b87Dd09d8f7', + leverageHookAddress: '0xB0E2ee3C1dA131d4004f0b8cc2ca159FaA129B86', + debtSideLeverageHookAddress: '0x34aFac3b87c5585942D74a1F12eA13a33821D4bd', + deleverageHookAddress: '0x5fF9c34f1734c2B62c53231E6923D0967F95a8A3', + liquidationHookAddress: '0x9e6D9D2D9c900Be023d839910855A864eDE3ABBD', + } satisfies HooksConfig, +} as const; + +export const getHooksConfig = (chainId: HubChainId): HooksConfig => hooksConfig[chainId]; + // currently supported spoke chain tokens for money market export const moneyMarketSupportedTokens = { [AVALANCHE_MAINNET_CHAIN_ID]: [ diff --git a/packages/types/src/hooks/index.ts b/packages/types/src/hooks/index.ts new file mode 100644 index 000000000..99bb392ca --- /dev/null +++ b/packages/types/src/hooks/index.ts @@ -0,0 +1,157 @@ +// packages/types/src/hooks/index.ts +import type { Address } from '../common/index.js'; + +// Credit Hook (Limit Orders) +export interface CreditHookParams { + debtAsset: Address; + targetAsset: Address; + maxPayment: string; + minReceive: string; + deadline?: string; + feeReceiver?: Address; + feeAmount?: string; + solver?: Address; // Optional specific solver address (address(0) or undefined = any solver) +} + +export interface CreditDelegationStatus { + delegated: boolean; + allowance: string; +} + +// Leverage Hook +export interface LeverageHookParams { + collateralAsset: Address; + debtAsset: Address; + collateralAmount: string; + borrowAmount: string; + deadline?: string; + feeReceiver?: Address; + feeAmount?: string; + solver?: Address; // Optional specific solver address (address(0) or undefined = any solver) +} + +// Debt Side Leverage Hook +export interface DebtSideLeverageHookParams { + collateralAsset: Address; + debtAsset: Address; + collateralAmount: string; + userProvidedAmount: string; + totalBorrowAmount: string; + deadline?: string; + feeReceiver?: Address; + feeAmount?: string; + solver?: Address; // Optional specific solver address (address(0) or undefined = any solver) +} + +export interface DebtSideLeverageStatus { + tokenAllowance: string; + creditDelegation: string; + tokenBalance: string; + isReady: boolean; +} + +// Deleverage Hook +export interface DeleverageHookParams { + collateralAsset: Address; + debtAsset: Address; + withdrawAmount: string; + repayAmount: string; + deadline?: string; + feeReceiver?: Address; + feeAmount?: string; + solver?: Address; // Optional specific solver address (address(0) or undefined = any solver) +} + +export interface ATokenApprovalInfo { + aTokenAddress: Address; + aTokensNeeded: string; + currentAllowance: string; + isApproved: boolean; +} + +// Liquidation Hook +export interface LiquidationHookParams { + collateralAsset: Address; + debtAsset: Address; + userToLiquidate: Address; + collateralAmount: string; + debtAmount: string; + deadline?: string; + feeReceiver?: Address; + feeAmount?: string; + solver?: Address; // Optional specific solver address (address(0) or undefined = any solver) +} + +export interface UserAccountData { + totalCollateralBase: string; + totalDebtBase: string; + availableBorrowsBase: string; + currentLiquidationThreshold: string; + ltv: string; + healthFactor: string; +} + +export interface LiquidationOpportunity { + userAddress: Address; + healthFactor: string; + isLiquidatable: boolean; + accountData: UserAccountData; +} + +// Common +export interface IntentCreationResult { + txHash: string; + intentId?: string; +} + +export interface ApprovalResult { + txHash: string; + approved: boolean; +} + +// Hook Intent Types (matching contract struct) +export interface HookIntent { + intentId: bigint; + creator: Address; + inputToken: Address; + outputToken: Address; + inputAmount: bigint; + minOutputAmount: bigint; + deadline: bigint; + allowPartialFill: boolean; + srcChain: bigint; + dstChain: bigint; + srcAddress: string; // bytes as hex string + dstAddress: string; // bytes as hex string + solver: Address; + data: string; // bytes as hex string +} + +export interface HookIntentState { + exists: boolean; + remainingInput: string; + receivedOutput: string; + pendingPayment: boolean; +} + +export interface HookPendingIntentState { + pendingInput: string; + pendingOutput: string; +} + +export interface FillHookIntentParams { + intent: HookIntent; + inputAmount: string; + outputAmount: string; + externalFillId?: string; // 0 for same-chain fills +} + +export interface CancelIntentResult { + txHash: string; + cancelled: boolean; +} + +export interface FillIntentResult { + txHash: string; + filled: boolean; +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index ebd92aeae..125281632 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -7,3 +7,4 @@ export * from './common/index.js'; export * from './injective/index.js'; export * from './solana/index.js'; export * from './backend/index.js'; +export * from './hooks/index.js'