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
282 changes: 282 additions & 0 deletions src/components/Aggregator/adapters/nearintents/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
// Source: https://docs.near-intents.org/near-intents/integration/distribution-channels/1click-api

import { zeroAddress, erc20Abi } from 'viem';
import { sendTransaction, writeContract } from 'wagmi/actions';
import { config } from '../../../WalletProvider';
import { chainsMap } from '../../constants';

export const chainToId = {
ethereum: 'eth',
arbitrum: 'arb',
base: 'base',
optimism: 'op',
polygon: 'pol',
bsc: 'bsc',
avax: 'avax',
gnosis: 'gnosis'
};

export const name = 'NEAR Intents';
export const token = 'NEAR';
export const referral = true;

export function approvalAddress() {
return null;
}

const API_BASE = 'https://1click.chaindefuser.com';

interface TokenInfo {
assetId: string;
decimals: number;
blockchain: string;
symbol: string;
price: number;
contractAddress?: string;
}

let tokensCache: TokenInfo[] | null = null;
let tokensCacheTime = 0;

const CACHE_TTL = 60_000;
const POLL_INTERVAL = 5_000;
const POLL_INITIAL_DELAY = 10_000;
const MAX_POLL_ATTEMPTS = 120;
const DEADLINE_MINUTES = 30;

async function getTokens(): Promise<TokenInfo[] | null> {
const now = Date.now();
if (tokensCache && now - tokensCacheTime < CACHE_TTL) {
return tokensCache;
}

try {
const response = await fetch(`${API_BASE}/v0/tokens`);
if (!response.ok) {
return tokensCache; // Return stale cache if available, otherwise null
}

tokensCache = await response.json();
tokensCacheTime = now;
return tokensCache;
} catch {
return tokensCache; // Return stale cache on network error
}
}

function findToken(chain: string, tokenAddress: string, tokens: TokenInfo[]): TokenInfo | null {
const blockchain = chainToId[chain];
if (!blockchain) return null;

const isNative = tokenAddress === zeroAddress;

return (
tokens.find((t) => {
if (t.blockchain !== blockchain) return false;
if (isNative) return !t.contractAddress;
return t.contractAddress?.toLowerCase() === tokenAddress.toLowerCase();
}) ?? null
);
}

const waitForOrder =
({ depositAddress }) =>
(onSuccess) => {
let attempts = 0;

const poll = async () => {
if (attempts >= MAX_POLL_ATTEMPTS) return;
attempts++;

try {
const response = await fetch(`${API_BASE}/v0/status?depositAddress=${depositAddress}`);
if (!response.ok) {
setTimeout(poll, POLL_INTERVAL);
return;
}

const status = await response.json();

if (status.status === 'SUCCESS') {
onSuccess();
return;
}

if (status.status === 'FAILED' || status.status === 'REFUNDED') {
return;
}

setTimeout(poll, POLL_INTERVAL);
} catch {
setTimeout(poll, POLL_INTERVAL);
}
};

setTimeout(poll, POLL_INITIAL_DELAY);
};

async function fetchQuote(chain: string, from: string, to: string, amount: string, extra, isDry: boolean) {
try {
const tokens = await getTokens();
if (!tokens) {
return null;
}

const fromToken = findToken(chain, from, tokens);
const toToken = findToken(chain, to, tokens);

if (!fromToken || !toToken) {
return null;
}

const userAddr = extra.userAddress?.toLowerCase() ?? zeroAddress;
const slippageBps = Math.round(Number(extra.slippage || 1) * 100);

const quoteRequest = {
dry: isDry,
swapType: 'EXACT_INPUT',
slippageTolerance: slippageBps,
originAsset: fromToken.assetId,
destinationAsset: toToken.assetId,
amount,
depositType: 'ORIGIN_CHAIN',
refundTo: isDry ? zeroAddress : userAddr,
refundType: 'ORIGIN_CHAIN',
recipient: isDry ? zeroAddress : userAddr,
recipientType: 'DESTINATION_CHAIN',
deadline: new Date(Date.now() + DEADLINE_MINUTES * 60 * 1000).toISOString(),
referral: 'llamaswap'
};

const response = await fetch(`${API_BASE}/v0/quote`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(quoteRequest)
});

if (!response.ok) {
return null;
}

const quote = await response.json();

if (!quote?.quote?.amountOut) {
return null;
}

return {
amountReturned: quote.quote.amountOut,
amountIn: quote.quote.amountIn || amount,
estimatedGas: 21000,
tokenApprovalAddress: null,
rawQuote: {
...quote,
fromToken,
toToken,
chain,
fromAddress: from,
userAddress: extra.userAddress,
slippage: extra.slippage
},
logo: 'https://assets.coingecko.com/coins/images/10365/small/near.jpg',
isMEVSafe: true
};
} catch {
return null;
}
}

export async function getQuote(chain: string, from: string, to: string, amount: string, extra) {
return fetchQuote(chain, from, to, amount, extra, true);
}

export async function swap({ chain, rawQuote, from }) {
const extra = {
userAddress: rawQuote.userAddress,
slippage: rawQuote.slippage
};
const toAddress = rawQuote.toToken.contractAddress || zeroAddress;
const liveQuote = await fetchQuote(
chain,
from,
toAddress,
rawQuote.quote.amountIn,
extra,
false
);

if (!liveQuote?.rawQuote?.quote?.depositAddress) {
throw { reason: 'Failed to get deposit address. Please try again.' };
}

// Validate price hasn't moved beyond slippage tolerance
const originalAmountOut = BigInt(rawQuote.quote.amountOut);
const liveAmountOut = BigInt(liveQuote.rawQuote.quote.amountOut);
const slippageTolerance = rawQuote.slippage ?? 1;
const slippageBps = BigInt(Math.round(slippageTolerance * 100));
const minAcceptableAmount = originalAmountOut - (originalAmountOut * slippageBps) / BigInt(10000);

if (originalAmountOut > 0n && liveAmountOut < minAcceptableAmount) {
const priceChangePercent = Number(((originalAmountOut - liveAmountOut) * BigInt(10000)) / originalAmountOut) / 100;
throw {
reason: `Price has moved unfavorably by ${priceChangePercent.toFixed(2)}%, which exceeds your slippage tolerance of ${slippageTolerance.toFixed(2)}%. Please refresh the quote and try again.`
};
}

const depositAddress = liveQuote.rawQuote.quote.depositAddress;
const amount = liveQuote.rawQuote.quote.amountIn;

const isNative = !liveQuote.rawQuote.fromToken.contractAddress;
let txHash: string;

if (isNative) {
txHash = await sendTransaction(config, {
to: depositAddress as `0x${string}`,
value: BigInt(amount),
chainId: chainsMap[chain]
});
} else {
txHash = await writeContract(config, {
address: from as `0x${string}`,
abi: erc20Abi,
functionName: 'transfer',
args: [depositAddress as `0x${string}`, BigInt(amount)],
chainId: chainsMap[chain]
});
}

// Notify API of deposit (non-critical)
fetch(`${API_BASE}/v0/deposit`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ txHash, depositAddress })
}).catch(() => {});

return {
hash: txHash,
waitForOrder: waitForOrder({ depositAddress })
};
}

export const getTxData = () => '';

export const getTx = ({ rawQuote }) => {
if (!rawQuote?.quote?.depositAddress) {
return {};
}

const isNative = !rawQuote.fromToken?.contractAddress;

if (isNative) {
return {
to: rawQuote.quote.depositAddress,
value: rawQuote.quote.amountIn
};
}

return {
to: rawQuote.fromAddress,
data: '',
value: '0'
};
};
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 nearintents from './adapters/nearintents';

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

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

Expand Down