@openocean.finance/widget
Version:
Openocean Widget for cross-chain bridging and swapping. It will drive your multi-chain strategy and attract new users from everywhere.
672 lines • 31.4 kB
JavaScript
import { OneClickService, OpenAPI, QuoteRequest } from '@defuse-protocol/one-click-sdk-typescript';
import { ChainId } from '@openocean.finance/widget-sdk';
import { ASSOCIATED_TOKEN_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountInstruction, createTransferInstruction, getAccount, getAssociatedTokenAddress, } from '@solana/spl-token';
import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js';
import { formatUnits } from 'viem';
import { BTC_DEFAULT_RECEIVER, CROSS_CHAIN_FEE_RECEIVER, SOLANA_NATIVE, ZERO_ADDRESS } from '../constants/index.js';
import { BaseSwapAdapter, NonEvmChain, } from './BaseSwapAdapter.js';
export const MappingChainIdToBlockChain = {
[NonEvmChain.Bitcoin]: 'btc',
[NonEvmChain.Solana]: 'sol',
[ChainId.ETH]: 'eth',
[ChainId.ARB]: 'arb',
[ChainId.BSC]: 'bsc',
[ChainId.ERA]: 'bera',
[ChainId.POL]: 'pol',
[ChainId.BAS]: 'base',
[ChainId.NEAR]: 'near',
[ChainId.MONAD]: 'monad',
[ChainId.FLR]: 'flr',
};
const erc20Abi = [
{
inputs: [
{ type: 'address', name: 'recipient' },
{ type: 'uint256', name: 'amount' },
],
name: 'transfer',
outputs: [{ type: 'bool', name: '' }],
stateMutability: 'nonpayable',
type: 'function',
},
];
const getTokenLogoUrl = (token) => {
const { symbol, contractAddress } = token;
// For major tokens without contract addresses or as fallbacks
switch (symbol) {
case 'ETH':
return 'https://assets.coingecko.com/coins/images/279/small/ethereum.png';
case 'BTC':
case 'wBTC':
return 'https://assets.coingecko.com/coins/images/1/small/bitcoin.png';
case 'USDC':
return 'https://assets.coingecko.com/coins/images/6319/small/USD_Coin_icon.png';
case 'USDT':
return 'https://assets.coingecko.com/coins/images/325/small/Tether.png';
case 'DAI':
return 'https://assets.coingecko.com/coins/images/9956/small/4943.png';
case 'SOL':
return 'https://assets.coingecko.com/coins/images/4128/small/solana.png';
case 'NEAR':
case 'wNEAR':
return 'https://assets.coingecko.com/coins/images/10365/small/near.jpg';
case 'BNB':
return 'https://assets.coingecko.com/coins/images/825/small/bnb-icon2_2x.png';
case 'DOGE':
return 'https://assets.coingecko.com/coins/images/5/small/dogecoin.png';
case 'XRP':
return 'https://assets.coingecko.com/coins/images/44/small/xrp-symbol-white-128.png';
case 'TRX':
return 'https://assets.coingecko.com/coins/images/1094/small/tron-logo.png';
case 'FRAX':
return 'https://assets.coingecko.com/coins/images/13422/small/FRAX_icon.png';
case 'LINK':
return 'https://assets.coingecko.com/coins/images/877/small/chainlink-new-logo.png';
case 'UNI':
return 'https://assets.coingecko.com/coins/images/12504/small/uni.jpg';
case 'AAVE':
return 'https://assets.coingecko.com/coins/images/12645/small/AAVE.png';
case 'SHIB':
return 'https://assets.coingecko.com/coins/images/11939/small/shiba.png';
case 'PEPE':
return 'https://assets.coingecko.com/coins/images/29850/small/pepe-token.jpeg';
case 'REF':
return 'https://s2.coinmarketcap.com/static/img/coins/64x64/11809.png';
case 'AURORA':
return 'https://s2.coinmarketcap.com/static/img/coins/64x64/14803.png';
case 'BLACKDRAGON':
return 'https://s2.coinmarketcap.com/static/img/coins/64x64/29627.png';
// Add more cases as needed
default:
// Fallback to a generic token icon
return `https://raw.githubusercontent.com/Uniswap/assets/master/blockchains/ethereum/assets/0x${contractAddress}/logo.png`;
}
};
export class NearIntentsAdapter extends BaseSwapAdapter {
constructor() {
super();
this.nearTokens = [];
// Initialize the API client
// OpenAPI.BASE = 'https://1click.chaindefuser.com'
OpenAPI.BASE = 'https://1click.openocean.finance';
if (this.nearTokens.length === 0) {
this.getAllSupportedTokens();
}
}
getName() {
return 'Near Intents';
}
getIcon() {
return 'https://storage.googleapis.com/ks-setting-1d682dca/000c677f-2ebc-44cc-8d76-e4c6d07627631744962669170.png';
}
getSupportedChains() {
return [
NonEvmChain.Solana,
NonEvmChain.Bitcoin,
NonEvmChain.Near,
...Object.keys(MappingChainIdToBlockChain).map(Number),
];
}
async getAllSupportedTokens() {
fetch(`https://1click.chaindefuser.com/v0/tokens`, {
headers: {
'Authorization': `Bearer ${OpenAPI.TOKEN}`,
},
})
.then(res => res.json())
.then(res => {
const wNear = res.find((token) => token.contractAddress === 'wrap.near');
const native = wNear
? {
...wNear,
symbol: 'NEAR',
contractAddress: '',
assetId: 'near',
logo: getTokenLogoUrl(wNear),
}
: {
assetId: 'near',
decimals: 24,
blockchain: 'near',
symbol: 'NEAR',
price: 0,
priceUpdatedAt: 0,
contractAddress: '',
logo: getTokenLogoUrl(wNear),
};
this.nearTokens = [
native,
...(res?.map((item) => {
if (item.blockchain == 'btc') {
console.log(item);
}
return {
...item,
logo: getTokenLogoUrl(item),
};
}) || []),
];
localStorage.setItem('nearTokens', JSON.stringify(this.nearTokens));
})
.catch(error => {
console.error('Failed to fetch near tokens:', error);
let nearTokens = localStorage.getItem('nearTokens');
if (nearTokens) {
this.nearTokens = JSON.parse(nearTokens);
}
else {
this.nearTokens = [];
}
// Reset loading state on error
});
}
getSupportedTokens(_sourceChain, _destChain) {
return [];
}
async getQuote(params) {
const deadline = new Date();
// 1 hour for Bitcoin, 20 minutes for other chains
deadline.setSeconds(deadline.getSeconds() + (params.fromChain === NonEvmChain.Bitcoin ? 60 * 60 : 60 * 20));
if (this.nearTokens.length === 0) {
await this.getAllSupportedTokens();
await new Promise(resolve => setTimeout(resolve, 3000));
}
let fromAssetId = '';
if (params.fromToken.address === 'near.near') {
fromAssetId = 'nep141:wrap.near';
}
else {
fromAssetId = this.nearTokens.find(token => {
const blockchain = MappingChainIdToBlockChain[params.fromChain];
if (params.fromChain === 1151111081099710) {
return params.fromToken.address === SOLANA_NATIVE
? token.symbol === 'SOL' && token.blockchain === 'sol'
: token.blockchain === blockchain && token.contractAddress === params.fromToken.address;
}
if (token.blockchain === blockchain) {
// console.log(token.symbol)
// console.log(token.assetId)
// console.log(token)
if (!token.contractAddress && params.fromToken.isNative && token.symbol.toLowerCase() === params.fromToken.symbol?.toLowerCase()) {
return true;
}
return token.contractAddress?.toLowerCase() === params.fromToken.address.toLowerCase();
}
else {
return false;
}
})?.assetId;
}
let toAssetId = '';
if (params.toToken.address === 'near.near') {
toAssetId = 'nep141:wrap.near';
}
else {
toAssetId = this.nearTokens.find((token) => {
const blockchain = MappingChainIdToBlockChain[params.toChain];
if (params.toChain === 1151111081099710) {
return params.toToken.address === SOLANA_NATIVE
? token.symbol === 'SOL' && token.blockchain === 'sol'
: token.blockchain === blockchain && token.contractAddress === params.toToken.address;
}
if (token.blockchain === blockchain) {
// console.log(token.symbol)
// console.log(token.assetId)
// console.log(token)
if (!token.contractAddress && params.toToken.isNative && token.symbol.toLowerCase() === params.toToken.symbol?.toLowerCase()) {
return true;
}
return token.contractAddress?.toLowerCase() === params.toToken.address.toLowerCase();
}
else {
return false;
}
})?.assetId;
}
if (!fromAssetId) {
throw new Error('not supported from token');
}
if (!toAssetId) {
throw new Error('not supported to token');
}
// Create a quote request
const quoteRequest = {
dry: true,
deadline: deadline.toISOString(),
slippageTolerance: params.slippage,
swapType: QuoteRequest.swapType.EXACT_INPUT,
originAsset: fromAssetId,
depositType: QuoteRequest.depositType.ORIGIN_CHAIN,
destinationAsset: toAssetId,
amount: params.amount,
refundTo: params.sender,
refundType: QuoteRequest.refundType.ORIGIN_CHAIN,
referral: 'kyberswap',
recipient: params.recipient,
recipientType: QuoteRequest.recipientType.DESTINATION_CHAIN,
appFees: [
{
recipient: CROSS_CHAIN_FEE_RECEIVER.toLowerCase(),
fee: params.feeBps,
},
],
};
try {
const quote = await OneClickService.getQuote(quoteRequest);
const formattedInputAmount = formatUnits(BigInt(params.amount), params.fromToken.decimals);
const rawAmountOut = Number(quote?.quote?.amountOut ?? 0);
const amountOut = rawAmountOut / 10 ** params.toToken.decimals;
const formattedOutputAmount = amountOut.toString();
const inputUsd = Number(quote?.quote?.amountInUsd ?? 0);
const outputUsd = +quote.quote.amountOutUsd;
const platformFeePercent = (params.feeBps * 100) / 10000;
const protocolFee = +quote.quote.amountInUsd * params.feeBps / 10000;
return {
quoteParams: params,
outputAmount: BigInt(rawAmountOut),
formattedOutputAmount,
inputUsd: +quote.quote.amountInUsd,
outputUsd: +quote.quote.amountOutUsd,
priceImpact: !inputUsd || !outputUsd ? NaN : ((inputUsd - outputUsd) * 100) / inputUsd,
rate: +formattedOutputAmount / +formattedInputAmount,
gasFeeUsd: 0,
timeEstimate: quote.quote.timeEstimate || 0,
contractAddress: ZERO_ADDRESS,
rawQuote: quote,
protocolFee: protocolFee,
platformFeePercent: platformFeePercent,
};
}
catch (error) {
// console.log('NearIntentsAdapter getQuote error', error)
if (error && typeof error === 'object' && 'body' in error) {
const errorWithBody = error;
if (errorWithBody.body?.message) {
throw new Error(errorWithBody.body.message);
}
}
throw error;
}
}
async executeSwap({ quote }, walletClient, nearWallet) {
const quoteParams = {
...quote.rawQuote.quoteRequest,
dry: false,
// adjust slippage to 0,01% to accept the rate change
slippageTolerance: Math.floor(quote.quoteParams.slippage * 0.9) > 1
? Math.floor(quote.quoteParams.slippage * 0.9)
: quote.quoteParams.slippage,
};
delete quoteParams.correlationId;
const refreshedQuote = await OneClickService.getQuote(quoteParams);
const depositAddress = refreshedQuote?.quote?.depositAddress;
if (!depositAddress) {
throw new Error('Deposit address not found');
}
if (refreshedQuote.quoteRequest.recipient === ZERO_ADDRESS ||
refreshedQuote.quoteRequest.refundTo === ZERO_ADDRESS ||
refreshedQuote.quoteRequest.recipient.toLowerCase() === BTC_DEFAULT_RECEIVER ||
refreshedQuote.quoteRequest.refundTo.toLowerCase() === BTC_DEFAULT_RECEIVER) {
throw new Error('Near Intent recipient or refundTo is ZERO ADDRESS');
}
if (BigInt(refreshedQuote.quote.minAmountOut) < BigInt(quote.rawQuote.quote.minAmountOut)) {
throw new Error('Quote amount out is less than expected');
}
const params = {
sender: quote.quoteParams.sender,
id: depositAddress, // specific id for each provider
adapter: this.getName(),
sourceChain: quote.quoteParams.fromChain,
targetChain: quote.quoteParams.toChain,
inputAmount: quote.quoteParams.amount,
outputAmount: quote.outputAmount.toString(),
sourceToken: quote.quoteParams.fromToken,
targetToken: quote.quoteParams.toToken,
timestamp: new Date().getTime(),
};
if (quote.quoteParams.fromChain === NonEvmChain.Solana) {
return new Promise(async (resolve, reject) => {
// Use walletClient (adaptedWallet) from ExecuteRoute.ts
const adaptedWallet = walletClient;
if (!adaptedWallet || !adaptedWallet.sendTransaction) {
reject('Not connected or walletClient does not support sendTransaction');
return;
}
// Get connection from adaptedWallet (exposed by ExecuteRoute.ts)
const connection = adaptedWallet.connection;
if (!connection) {
reject('Connection not available from walletClient');
return;
}
const waitForConfirmation = async (txId) => {
try {
const latestBlockhash = await connection.getLatestBlockhash();
// Wait for confirmation with timeout
const confirmation = await Promise.race([
connection.confirmTransaction({
signature: txId,
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
}, 'confirmed'),
new Promise((_, reject) => setTimeout(() => reject(new Error('Transaction confirmation timeout')), 60000)),
]);
const confirmationResult = confirmation;
if (confirmationResult.value.err) {
throw new Error(`Transaction failed: ${JSON.stringify(confirmationResult.value.err)}`);
}
console.log('Transaction confirmed successfully!');
}
catch (confirmError) {
console.error('Transaction confirmation failed:', confirmError);
// Check if transaction actually succeeded despite timeout
const txStatus = await connection.getSignatureStatus(txId);
if (txStatus?.value?.confirmationStatus !== 'confirmed') {
throw new Error(`Transaction was not confirmed: ${confirmError instanceof Error ? confirmError.message : 'Unknown error'}`);
}
}
};
const fromPubkey = new PublicKey(quote.quoteParams.sender);
const recipientPubkey = new PublicKey(depositAddress);
const fromToken = quote.quoteParams.fromToken;
if (fromToken.address === SOLANA_NATIVE) {
// Get latest blockhash before creating transaction
const { blockhash } = await connection.getLatestBlockhash('confirmed');
const transaction = new Transaction({
recentBlockhash: blockhash,
feePayer: fromPubkey,
}).add(SystemProgram.transfer({
fromPubkey: fromPubkey,
toPubkey: recipientPubkey,
lamports: BigInt(quote.quoteParams.amount),
}));
try {
// Use adaptedWallet.sendTransaction (exposed by ExecuteRoute.ts)
const result = await adaptedWallet.sendTransaction(transaction);
const signature = result?.signature || result;
await waitForConfirmation(signature);
resolve({
...params,
sourceTxHash: signature,
});
}
catch (error) {
reject(error);
}
}
else {
const mintPubkey = new PublicKey(fromToken.address);
// Get associated token addresses
const senderTokenAddress = await getAssociatedTokenAddress(mintPubkey, fromPubkey, false, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID);
const recipientTokenAddress = await getAssociatedTokenAddress(mintPubkey, recipientPubkey, false, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID);
// Get latest blockhash before creating transaction
const { blockhash } = await connection.getLatestBlockhash('confirmed');
const transaction = new Transaction({
recentBlockhash: blockhash,
feePayer: fromPubkey,
});
try {
// Check if recipient's token account exists
await getAccount(connection, recipientTokenAddress);
}
catch (err) {
// Account doesn't exist, create it
console.log('Creating recipient token account...');
transaction.add(createAssociatedTokenAccountInstruction(fromPubkey, // payer
recipientTokenAddress, // associated token account
recipientPubkey, // owner
mintPubkey, // mint
TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID));
}
// Add transfer instruction
transaction.add(createTransferInstruction(senderTokenAddress, // source
recipientTokenAddress, // destination
fromPubkey, // owner
BigInt(quote.quoteParams.amount), [], TOKEN_PROGRAM_ID));
try {
// Use adaptedWallet.sendTransaction (exposed by ExecuteRoute.ts)
const result = await adaptedWallet.sendTransaction(transaction);
const signature = result?.signature || result;
await waitForConfirmation(signature);
resolve({
...params,
sourceTxHash: signature,
});
}
catch (error) {
reject(error);
}
}
return;
});
}
if (quote.quoteParams.fromChain === NonEvmChain.Bitcoin) {
return new Promise(async (resolve, reject) => {
if (!walletClient || !walletClient.sendTransaction) {
reject('Not connected');
return;
}
try {
const tx = await walletClient.sendTransaction({
recipient: depositAddress,
amount: quote.quoteParams.amount,
chain: undefined,
account: walletClient.account?.address,
kzg: undefined,
});
await OneClickService.submitDepositTx({
txHash: tx,
depositAddress,
}).catch(e => {
console.log('NearIntents submitDepositTx failed', e);
});
resolve({
...params,
sourceTxHash: tx,
});
}
catch (e) {
console.log(e);
reject(e);
return;
}
});
}
if (quote.quoteParams.fromChain === NonEvmChain.Near) {
return new Promise(async (resolve, reject) => {
if (!nearWallet || !nearWallet.signedAccountId) {
reject('Not connected');
return;
}
const fromToken = quote.quoteParams.fromToken;
const isNative = fromToken.address === 'near.near';
const rawAmount = quote.quoteParams.amount || '0';
const amount = String(rawAmount); // yoctoNEAR 字符串
const transactions = [];
// wNEAR 合约地址(标准 wrap.near)
const WRAP_CONTRACT_ID = 'wrap.near';
const tokenContract = fromToken.address;
if (isNative) {
// 原生 NEAR:先在 wNEAR 合约上给桥地址做 storage_deposit,再 near_deposit 包成 wNEAR,然后 ft_transfer 给桥地址
transactions.push({
signerId: nearWallet.signedAccountId,
receiverId: WRAP_CONTRACT_ID,
actions: [
{
// 1) storage_deposit,确保桥地址在 wNEAR 上已注册
type: 'FunctionCall',
params: {
methodName: 'storage_deposit',
args: {
account_id: depositAddress,
registration_only: true,
},
gas: '30000000000000',
deposit: '1250000000000000000000', // 0.00125 NEAR
},
},
{
// 2) near_deposit:把原生 NEAR 包成 wNEAR
type: 'FunctionCall',
params: {
methodName: 'near_deposit',
args: {},
gas: '30000000000000',
deposit: amount, // 使用原始 NEAR 数量(yoctoNEAR)
},
},
{
// 3) ft_transfer:将 wNEAR 发送到桥地址
type: 'FunctionCall',
params: {
methodName: 'ft_transfer',
args: {
receiver_id: depositAddress,
amount,
},
gas: '30000000000000',
deposit: '1', // NEP-141 规范要求 1 yoctoNEAR
},
},
],
});
}
else if (tokenContract) {
// 非原生 NEP-141 token:先在 token 合约上给桥地址做 storage_deposit,再 ft_transfer
transactions.push({
signerId: nearWallet.signedAccountId,
receiverId: tokenContract,
actions: [
{
type: 'FunctionCall',
params: {
methodName: 'storage_deposit',
args: {
account_id: depositAddress,
registration_only: true,
},
gas: '30000000000000',
deposit: '1250000000000000000000', // 0.00125 NEAR
},
},
{
type: 'FunctionCall',
params: {
methodName: 'ft_transfer',
args: {
receiver_id: depositAddress,
amount,
},
gas: '30000000000000',
deposit: '1',
},
},
],
});
}
else {
reject('Invalid NEAR token contract');
return;
}
// MyNearWallet 会跳转到网页,需要在本地记录一次
if (nearWallet?.wallet?.id === 'my-near-wallet') {
localStorage.setItem('cross-chain-swap-my-near-wallet-tx', JSON.stringify({
...params,
sourceTxHash: depositAddress,
}));
}
const txResult = await nearWallet
.signAndSendTransactions({ transactions })
.catch((e) => {
console.log('NearIntents signAndSendTransactions failed', e);
if (nearWallet?.wallet?.id === 'my-near-wallet')
reject();
else
reject(e);
});
let transaction = { hash: "" };
if (txResult && txResult.length === 1) {
transaction = txResult[txResult.length - 1].transaction || {};
}
else if (txResult && txResult.length > 1) {
transaction = txResult.filter((item) => {
const { actions = [] } = item && item.transaction || {};
const _actions = actions.filter((fc) => {
const { FunctionCall = {} } = fc;
const { method_name } = FunctionCall;
return method_name === 'ft_transfer_call';
});
return _actions && _actions.length > 0;
});
if (transaction && transaction.length) {
transaction = transaction[0].transaction;
}
else {
transaction = txResult[txResult.length - 1].transaction || {};
}
}
const { hash } = transaction;
resolve({
...params,
sourceTxHash: hash,
});
});
}
return new Promise(async (resolve, reject) => {
try {
if (!walletClient || !walletClient.account)
reject('Not connected');
if (quote.quoteParams.sender === ZERO_ADDRESS || quote.quoteParams.recipient === ZERO_ADDRESS) {
reject('Near Intent refundTo or recipient is ZERO ADDRESS');
return;
}
const account = walletClient.account?.address;
const fromToken = quote.quoteParams.fromToken;
const hash = await (fromToken.isNative
? walletClient.sendTransaction({
to: depositAddress,
value: BigInt(quote.quoteParams.amount),
chain: undefined,
account,
kzg: undefined
})
: walletClient.writeContract({
address: ('contractAddress' in fromToken
? fromToken.contractAddress
: fromToken.address),
abi: erc20Abi,
functionName: 'transfer',
args: [depositAddress, quote.quoteParams.amount],
chain: undefined,
account,
}));
await OneClickService.submitDepositTx({
txHash: hash,
depositAddress,
}).catch(e => {
console.log('NearIntents submitDepositTx failed', e);
});
resolve({
...params,
sourceTxHash: hash,
});
}
catch (e) {
reject(e);
}
});
}
async getTransactionStatus(p) {
const res = await OneClickService.getExecutionStatus(p.id);
return {
txHash: res.swapDetails?.destinationChainTxHashes[0]?.hash || '',
status: res.status === 'SUCCESS'
? 'Success'
: res.status === 'FAILED'
? 'Failed'
: res.status === 'REFUNDED'
? 'Refunded'
: 'Processing',
};
}
}
//# sourceMappingURL=NearIntentsAdapter.js.map