Skip to content
This repository was archived by the owner on Mar 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a506332
feat: add swap strategy
royvardhan May 5, 2025
2cd8065
feat: use batch txn feature for swaps and fill order
royvardhan May 7, 2025
bb89f74
refactor: replace 1inch calls with UniswapRouter02
royvardhan May 12, 2025
c4b3e31
feat: calculate swap operations based on current balances
royvardhan May 13, 2025
e6a8d8e
feat: update config with deployed addresses, add test
royvardhan May 13, 2025
54b87c1
test: handle pair creation and approvals
royvardhan May 14, 2025
5f339db
test: fix univ2 contract issues and update deployments
royvardhan May 14, 2025
41ccabe
feat: deploy BatchExecutor and use storage ovveride for token balances
royvardhan May 19, 2025
d75dd42
Merge branch 'main' into feat/filler-swap-strategy
royvardhan May 19, 2025
0ad4159
test: use indexer stream to check filled status
royvardhan May 20, 2025
1d3af00
test: update link
royvardhan May 20, 2025
2687b93
test: fix relaying and use erc20 token as inputs
royvardhan May 20, 2025
4da62b2
filler tests pass
royvardhan May 20, 2025
3c69c7a
feat: add fill,post,swap gas estimate caching to speed up execution
royvardhan May 20, 2025
4f212d5
refactor: use in-memory cache
royvardhan May 20, 2025
5db23d9
feat: add safe service, resolve pr comment
royvardhan May 22, 2025
60d0b01
feat: rm safeService, deploy universal router and add it, debug
royvardhan May 22, 2025
050e5da
feat: update deployments after fixing initCodeHash
royvardhan May 22, 2025
d012a78
test pass with universal router integration
royvardhan May 22, 2025
e172b1a
feat: add findBestProtocol
royvardhan May 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/filler/src/services/ContractInteractionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,4 +399,59 @@ export class ContractInteractionService {
})
return hostParams
}

async getTokenUsdValue(order: Order): Promise<{ outputUsdValue: bigint; inputUsdValue: bigint }> {
let outputUsdValue = BigInt(0)
let inputUsdValue = BigInt(0)
const outputs = order.outputs
const inputs = order.inputs

for (const output of outputs) {
const tokenAddress = bytes32ToBytes20(output.token)
const amount = output.amount
const price = await this.getTokenPrice(tokenAddress, order.destChain)
outputUsdValue = outputUsdValue + amount * price
}

for (const input of inputs) {
const tokenAddress = bytes32ToBytes20(input.token)
const amount = input.amount
const price = await this.getTokenPrice(tokenAddress, order.sourceChain)
inputUsdValue = inputUsdValue + amount * price
}

return { outputUsdValue, inputUsdValue }
}

async getTokenPrice(tokenAddress: string, chain: string): Promise<bigint> {
const decimals = await this.getTokenDecimals(tokenAddress, chain)
const usdValue = await fetchTokenUsdPriceOnchain(tokenAddress, decimals)
return usdValue
}

async getFillerBalanceUSD(order: Order, chain: string): Promise<bigint> {
// As part of the protocol, the filler will have only two tokens:
// 1. The native token of the chain. And 2. DAI
Comment thread
royvardhan marked this conversation as resolved.
Outdated
// We need to get the balance of the filler in both tokens
// and convert them to USD
const fillerWalletAddress = privateKeyToAddress(this.privateKey)
const destClient = this.clientManager.getPublicClient(chain)
// Native token balance
const nativeTokenBalance = await destClient.getBalance({ address: fillerWalletAddress })
const nativeTokenPriceUsd = await this.getNativeTokenPriceUsd(order)
const nativeTokenUsdValue = nativeTokenBalance * nativeTokenPriceUsd

// DAI balance
const daiBalance = await destClient.readContract({
abi: ERC20_ABI,
address: this.configService.getDaiAsset(chain),
functionName: "balanceOf",
args: [fillerWalletAddress],
})

const daiPriceUsd = await this.getTokenPrice(this.configService.getDaiAsset(chain), chain)
const daiUsdValue = daiBalance * daiPriceUsd

return nativeTokenUsdValue + daiUsdValue
}
}
243 changes: 243 additions & 0 deletions packages/filler/src/strategies/swap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
import { ChainConfigService, ChainClientManager, ContractInteractionService } from "@/services"
import {
bytes32ToBytes20,
constructRedeemEscrowRequestBody,
estimateGasForPost,
ExecutionResult,
FillOptions,
HexString,
IPostRequest,
Order,
} from "hyperbridge-sdk"
import { FillerStrategy } from "./base"
import { privateKeyToAccount, privateKeyToAddress } from "viem/accounts"
import { INTENT_GATEWAY_ABI } from "@/config/abis/IntentGateway"
import { get1inchExactOutputQuote } from "@/utils"
import { encodeFunctionData } from "viem"
import { erc7821Actions } from "viem/experimental"

export class BasicFiller implements FillerStrategy {
name = "BasicFiller"
private privateKey: HexString
private clientManager: ChainClientManager
private contractService: ContractInteractionService
private configService: ChainConfigService

constructor(privateKey: HexString) {
this.privateKey = privateKey
this.configService = new ChainConfigService()
this.clientManager = new ChainClientManager(privateKey)
this.contractService = new ContractInteractionService(this.clientManager, privateKey)
}

/**
* Checks the USD value of the filler's balance against the order's USD value
* @param order The order to check if it can be filled
* @returns True if the filler has enough balance, false otherwise
*/
async canFill(order: Order): Promise<boolean> {
try {
const destClient = this.clientManager.getPublicClient(order.destChain)
const currentBlock = await destClient.getBlockNumber()
const deadline = BigInt(order.deadline)

if (deadline < currentBlock) {
console.debug(`Order expired at block ${deadline}, current block ${currentBlock}`)
return false
}

const isAlreadyFilled = await this.contractService.checkIfOrderFilled(order)
if (isAlreadyFilled) {
console.debug(`Order is already filled`)
return false
}

const fillerBalanceUsd = await this.contractService.getFillerBalanceUSD(order, order.destChain)

// Check if the filler has enough USD value to fill the order
const { outputUsdValue } = await this.contractService.getTokenUsdValue(order)

if (fillerBalanceUsd < outputUsdValue) {
console.debug(`Insufficient USD value for order`)
return false
}

return true
} catch (error) {
console.error(`Error in canFill:`, error)
return false
}
}

/**
* Calculates the USD value of the order's inputs, outputs, fees and compares
* what will the filler receive and what will the filler pay
* @param order The order to calculate the USD value for
* @returns The profit in USD (BigInt)
*/
async calculateProfitability(order: Order): Promise<bigint> {
try {
const { fillGas, postGas } = await this.contractService.estimateGasFillPost(order)
Comment thread
royvardhan marked this conversation as resolved.
const nativeTokenPriceUsd = await this.contractService.getNativeTokenPriceUsd(order)

const relayerFeeEth = postGas + (postGas * BigInt(200)) / BigInt(10000)

const protocolFeeUSD = await this.contractService.getProtocolFeeUSD(order, relayerFeeEth)

const totalGasWei = fillGas + relayerFeeEth

const gasCostUsd = (totalGasWei * nativeTokenPriceUsd) / BigInt(10 ** 18)

const totalGasCostUsd = gasCostUsd + protocolFeeUSD

const { outputUsdValue, inputUsdValue } = await this.contractService.getTokenUsdValue(order)

const toReceive = outputUsdValue + order.fees
const toPay = inputUsdValue + totalGasCostUsd

const profit = toReceive - toPay

return profit
} catch (error) {
console.error(`Error calculating profitability:`, error)
return BigInt(0)
}
}

async executeOrder(order: Order): Promise<ExecutionResult> {
try {
const { destClient, walletClient } = this.clientManager.getClientsForOrder(order)
const startTime = Date.now()

const fillerWalletAddress = privateKeyToAddress(this.privateKey)

const operations: { calls: { to: HexString; data: HexString; value?: bigint }[] }[] = []

for (const token of order.outputs) {
const tokenAddress = bytes32ToBytes20(token.token)
const tokenBalance = await this.contractService.getTokenBalance(
tokenAddress,
fillerWalletAddress,
order.destChain,
)
if (tokenBalance === BigInt(0)) {
const chainId = Number(order.destChain.split("-")[1])

const swapData = await get1inchExactOutputQuote({
chainId,
srcToken: this.configService.getDaiAsset(order.destChain),
dstToken: tokenAddress,
amount: token.amount.toString(),
fromAddress: fillerWalletAddress,
slippage: 200,
isExactOut: true,
})

try {
// Simulating individual swaps for early debugging
await destClient.simulateCalls({
account: fillerWalletAddress,
calls: [
{
to: swapData.tx.to as HexString,
data: swapData.tx.data as HexString,
value: BigInt(swapData.tx.value || 0),
},
],
})

operations.push({
calls: [
{
to: swapData.tx.to as HexString,
data: swapData.tx.data as HexString,
value: BigInt(swapData.tx.value || 0),
},
],
})
} catch (simulationError) {
console.error("Swap simulation failed:", simulationError)
throw new Error("Swap simulation failed")
}
}
}

const postRequest: IPostRequest = {
source: order.destChain,
dest: order.sourceChain,
body: constructRedeemEscrowRequestBody(order, privateKeyToAddress(this.privateKey)),
timeoutTimestamp: 0n,
nonce: await this.contractService.getHostNonce(order.sourceChain),
from: this.configService.getIntentGatewayAddress(order.sourceChain),
to: this.configService.getIntentGatewayAddress(order.destChain),
}

const postGasEstimate = await estimateGasForPost({
postRequest: postRequest,
sourceClient: this.clientManager.getPublicClient(order.sourceChain) as any,
hostLatestStateMachineHeight: await this.contractService.getHostLatestStateMachineHeight(),
hostAddress: this.configService.getHostAddress(order.sourceChain),
})
const fillOptions: FillOptions = {
relayerFee: postGasEstimate + (postGasEstimate * BigInt(200)) / BigInt(10000),
}

await this.contractService.approveTokensIfNeeded(order)

const fillOrderData = encodeFunctionData({
abi: INTENT_GATEWAY_ABI,
functionName: "fillOrder",
args: [this.contractService.transformOrderForContract(order), fillOptions as any],
})

operations.push({
calls: [
{
to: this.configService.getIntentGatewayAddress(order.destChain),
data: fillOrderData,
value: this.contractService.calculateRequiredEthValue(order.outputs),
},
],
})

try {
// Simulating all calls together
await destClient.simulateCalls({
account: fillerWalletAddress,
calls: operations.flatMap((op) => op.calls),
})
} catch (batchSimulationError) {
console.error("Batch simulation failed:", batchSimulationError)
throw new Error("Batch simulation failed")
}

const tx = await walletClient.extend(erc7821Actions()).executeBatches({
address: fillerWalletAddress,
batches: operations,
account: privateKeyToAccount(this.privateKey),
chain: destClient.chain,
})

const endTime = Date.now()
const processingTimeMs = endTime - startTime

const receipt = await destClient.waitForTransactionReceipt({ hash: tx })

return {
success: true,
txHash: receipt.transactionHash,
gasUsed: receipt.gasUsed.toString(),
gasPrice: receipt.effectiveGasPrice.toString(),
confirmedAtBlock: Number(receipt.blockNumber),
confirmedAt: new Date(),
strategyUsed: this.name,
processingTimeMs,
}
} catch (error) {
console.error(`Error executing order:`, error)
return {
success: false,
}
}
}
}
34 changes: 34 additions & 0 deletions packages/filler/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,37 @@ export async function fetchTokenUsdPriceOnchain(address: string, decimals: numbe
throw error
}
}

export async function get1inchExactOutputQuote(params: {
chainId: number
srcToken: string
dstToken: string
amount: string
fromAddress: string
slippage: number
isExactOut: boolean
}) {
const API_URL = `https://api.1inch.io/v5.0/${params.chainId}/swap`

const queryParams = new URLSearchParams({
fromTokenAddress: params.srcToken,
toTokenAddress: params.dstToken,
fromAddress: params.fromAddress,
slippage: params.slippage.toString(),
disableEstimate: "true",
protocols: "DEXES",
})

// Handle exact output case
if (params.isExactOut) {
queryParams.set("destAmount", params.amount)
} else {
queryParams.set("amount", params.amount)
}

const response = await fetch(`${API_URL}?${queryParams}`)
if (!response.ok) {
throw new Error(`1inch API error: ${await response.text()}`)
}
return await response.json()
}