Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"@chakra-ui/icons": "^2.0.12",
"@chakra-ui/react": "2.8.2",
"@defillama/sdk": "^3.0.25",
"@ekubo/evm-hyper-router-sdk": "0.1.0-alpha.1",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@rainbow-me/rainbowkit": "^2.2.8",
Expand Down
153 changes: 153 additions & 0 deletions src/components/Aggregator/adapters/ekubo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { sendTx } from '../utils/sendTx';
import { getTxs } from '../utils/getTxs';
import { zeroAddress, pad, Hex } from 'viem';
import { MAINNET_ADDRESS, MultiHop, Parameters, generateCalldata } from '@ekubo/evm-hyper-router-sdk';

export const chainToId = {
ethereum: 1,
};

export const name = 'Ekubo';
export const token = 'EKUBO';
export const referral = false;
export const isOutputAvailable = true;

const logo = 'https://app.ekubo.org/logo.svg';

export function approvalAddress(chain: string) {
if (chain === 'ethereum') return MAINNET_ADDRESS;
throw new Error('Ekubo: unsupported network');
}

export function ekuboApiEndpoint(chain: string) {
if (chain === 'ethereum') return 'https://eth-mainnet-quoter-api.ekubo.org';
throw new Error('Ekubo: unsupported network');
}

function normalizeAddress(address: string): Hex {
if (address === zeroAddress) return zeroAddress;
return pad(address as Hex, { size: 20 });
}

function normalizeConfig(config: string): Hex {
return pad(config as Hex, { size: 32 });
}

export async function getQuote(chain: string, from: string, to: string, amount: string, extra) {
const ekuboRouter = approvalAddress(chain);
const quoterEndpoint = ekuboApiEndpoint(chain);
const isExactOut = extra.amountOut && extra.amountOut !== '0';

// Call Ekubo API - for exact out, we swap the tokens and negate the amount
const apiUrl = isExactOut
? `${quoterEndpoint}/-${extra.amountOut}/${to}/${from}`
: `${quoterEndpoint}/${amount}/${from}/${to}`;

const data = await fetch(apiUrl).then((r) => r.json());

if (!data.splits || data.splits.length === 0) {
throw new Error('[Ekubo] No valid routes found');
}

const multiHops: MultiHop[] = data.splits.map(split => {
return {
specifiedAmount: BigInt(split.amount_specified),
hops: split.route.map(hop => {
if (hop.swap) {
return {
type: "swap",
poolKey: {
config: normalizeConfig(hop.swap.pool_key.config),
token0: normalizeAddress(hop.swap.pool_key.token0),
token1: normalizeAddress(hop.swap.pool_key.token1)
},
skipAhead: hop.swap.skip_ahead,
sqrtRatioLimit: BigInt(hop.swap.sqrt_ratio_limit)
}
} else {
return {
type: "wrappedToken",
underlying: normalizeAddress(hop.wrapped_token.underlying),
wrapped: normalizeAddress(hop.wrapped_token.wrapped)
}
}
})
}
})

const slippageFactor = extra.slippage ? parseFloat(extra.slippage) / 100 : 0.001;

let amountReturned, amountIn, calculatedAmountThreshold;

// Apply slippage
if (isExactOut) {
// For exact out, amountIn is adjusted upwards
// total_calculated and calculatedAmountThreshold should be negative
amountIn = BigInt(Math.floor(Math.abs(data.total_calculated))).toString();
amountReturned = extra.amountOut;
calculatedAmountThreshold = -BigInt(Math.ceil(amountIn * (1 + slippageFactor)));
} else {
// For exact in, amountReturned is adjusted downwards
amountIn = amount;
amountReturned = data.total_calculated;
calculatedAmountThreshold = BigInt(Math.floor(amountReturned * (1 - slippageFactor)));
}

const calldata = generateCalldata({
specifiedToken: isExactOut ? to : from,
multiHops: multiHops,
calculatedAmountThreshold
} as Parameters);

const rawQuote = extra.userAddress !== zeroAddress ? {
from: extra.userAddress,
to: ekuboRouter,
data: calldata,
value: from === zeroAddress ? (isExactOut ? -calculatedAmountThreshold : amountIn) : '0'
} : null;

// Base transaction costs + swap execution + token transfers
const estimatedGas = 21000 + data.estimated_gas_cost + (from === zeroAddress ? 0 : 30000) + (to === zeroAddress ? 0 : 30000);

const result = {
amountIn,
amountReturned,
estimatedGas,
tokenApprovalAddress: ekuboRouter,
rawQuote,
logo
};

return result;
}

export async function swap({ tokens, fromAmount, rawQuote, eip5792 }) {
const txs = getTxs({
fromAddress: rawQuote.from,
routerAddress: rawQuote.to,
data: rawQuote.data,
value: rawQuote.value,
fromTokenAddress: tokens.fromToken.address,
fromAmount,
eip5792,
tokenApprovalAddress: rawQuote.to
});

const tx = await sendTx(txs);

return tx;
}

export const getTxData = ({ rawQuote }) => rawQuote?.data;

export const getTx = ({ rawQuote }) => {
if (rawQuote === null) {
return {};
}
return {
from: rawQuote.from,
to: rawQuote.to,
data: rawQuote.data,
value: rawQuote.value
};
};
3 changes: 2 additions & 1 deletion src/components/Aggregator/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import * as odos from './adapters/odos';
// import * as krystal from './adapters/krystal'
import * as matchaGasless from './adapters/0xGasless';
import * as matchaV2 from './adapters/0xV2';
import * as ekubo from './adapters/ekubo';

export const adapters = [matcha, cowswap, paraswap, kyberswap, inch, matchaGasless, odos, matchaV2];
export const adapters = [matcha, cowswap, paraswap, kyberswap, inch, matchaGasless, odos, matchaV2, ekubo];

export const inifiniteApprovalAllowed = [matcha.name, cowswap.name, matchaGasless.name];

Expand Down
52 changes: 52 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,13 @@
resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.3.tgz#963805e46d07e646550098ac29cbcc5b132218ea"
integrity sha512-tapn6XhOueMwht3E2UzY0ZZjYokdaw9XtL9kEyjhQ/Fb9vL9xTFbOaI+fV0AWvTpYu4BNloC6getKW6NtSg4mA==

"@ekubo/[email protected]":
version "0.1.0-alpha.1"
resolved "https://registry.yarnpkg.com/@ekubo/evm-hyper-router-sdk/-/evm-hyper-router-sdk-0.1.0-alpha.1.tgz#15de1ca493c198ab622269b36b9748be70c2b61d"
integrity sha512-otWeh/AHJCeKJkFTGG/nPbcDSgdjKggFsPENhn8liHRs/EzbVj7cObAdMqNt9rI3T1ARLwR8t4to9PjDT0jP4w==
dependencies:
viem "^2.33.0"

"@emotion/babel-plugin@^11.10.5":
version "11.10.5"
resolved "https://registry.yarnpkg.com/@emotion/babel-plugin/-/babel-plugin-11.10.5.tgz#65fa6e1790ddc9e23cc22658a4c5dea423c55c3c"
Expand Down Expand Up @@ -1978,6 +1985,13 @@
dependencies:
"@noble/hashes" "1.7.1"

"@noble/[email protected]":
version "1.9.1"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.1.tgz#9654a0bc6c13420ae252ddcf975eaf0f58f0a35c"
integrity sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==
dependencies:
"@noble/hashes" "1.8.0"

"@noble/[email protected]", "@noble/curves@^1.9.1", "@noble/curves@~1.9.0":
version "1.9.2"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.9.2.tgz#73388356ce733922396214a933ff7c95afcef911"
Expand Down Expand Up @@ -2995,6 +3009,11 @@ [email protected], abitype@^1.0.8:
resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.8.tgz#3554f28b2e9d6e9f35eb59878193eabd1b9f46ba"
integrity sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==

[email protected], abitype@^1.0.9:
version "1.1.0"
resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.1.0.tgz#510c5b3f92901877977af5e864841f443bf55406"
integrity sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==

abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
Expand Down Expand Up @@ -5463,6 +5482,20 @@ [email protected]:
abitype "^1.0.8"
eventemitter3 "5.0.1"

[email protected]:
version "0.9.3"
resolved "https://registry.yarnpkg.com/ox/-/ox-0.9.3.tgz#92cc1008dcd913e919364fd4175c860b3eeb18db"
integrity sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==
dependencies:
"@adraffy/ens-normalize" "^1.11.0"
"@noble/ciphers" "^1.3.0"
"@noble/curves" "1.9.1"
"@noble/hashes" "^1.8.0"
"@scure/bip32" "^1.7.0"
"@scure/bip39" "^1.6.0"
abitype "^1.0.9"
eventemitter3 "5.0.1"

p-limit@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
Expand Down Expand Up @@ -6624,6 +6657,20 @@ viem@^2.1.1:
webauthn-p256 "0.0.10"
ws "8.18.0"

viem@^2.33.0:
version "2.37.4"
resolved "https://registry.yarnpkg.com/viem/-/viem-2.37.4.tgz#94c9e837b4a7ef6f7b6c033487a12625534bd8bc"
integrity sha512-1ig5O6l1wJmaw3yrSrUimjRLQEZon2ymTqSDjdntu6Bry1/tLC2GClXeS3SiCzrifpLxzfCLQWDITYVTBA10KA==
dependencies:
"@noble/curves" "1.9.1"
"@noble/hashes" "1.8.0"
"@scure/bip32" "1.7.0"
"@scure/bip39" "1.6.0"
abitype "1.1.0"
isows "1.0.7"
ox "0.9.3"
ws "8.18.3"

[email protected]:
version "2.15.6"
resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.15.6.tgz#eaad3576f29f383bb082cac53694fae3a9075393"
Expand Down Expand Up @@ -6748,6 +6795,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.2.tgz#42738b2be57ced85f46154320aabb51ab003705a"
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==

[email protected]:
version "8.18.3"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==

ws@^7.3.1, ws@^7.5.1:
version "7.5.9"
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.9.tgz#54fa7db29f4c7cec68b1ddd3a89de099942bb591"
Expand Down