@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.
325 lines (307 loc) • 8.58 kB
text/typescript
import {
MAINNET_RELAY_API,
type RelayChain,
convertViemChainToRelayChain,
createClient,
getClient,
type QuoteBodyOptions,
} from '@relayprotocol/relay-sdk'
import { type WalletClient, formatUnits } from 'viem'
import {
arbitrum,
avalanche,
base,
berachain,
blast,
bsc,
fantom,
linea,
mainnet,
mantle,
optimism,
polygon,
ronin,
scroll,
sonic,
unichain,
zksync,
celo,
monad,
} from 'viem/chains'
import type { Currency } from '../constants/index.js'
import {
CROSS_CHAIN_FEE_RECEIVER,
MAINNET_NETWORKS,
ZERO_ADDRESS,
} from '../constants/index.js'
import type { Quote } from '../registry.js'
import {
BaseSwapAdapter,
type Chain,
NOT_SUPPORTED_CHAINS_PRICE_SERVICE,
NonEvmChain,
NewEvmChain,
type NormalizedQuote,
type NormalizedTxResponse,
type QuoteParams,
type SwapStatus,
} from './BaseSwapAdapter.js'
import { defineChain } from 'viem'
import { ChainId } from '../../index.js'
const hyperEvm = defineChain({
id: 999,
name: 'HyperEvm',
network: 'hyperevm',
nativeCurrency: {
decimals: 18,
name: 'HYPE',
symbol: 'HYPE',
},
rpcUrls: {
default: {
http: ['https://rpc.hyperliquid.xyz/evm'],
},
public: {
http: ['https://rpc.hyperliquid.xyz/evm'],
},
},
blockExplorers: {
default: { name: 'Explorer', url: 'https://hyperevmscan.io' },
},
})
const plasma = defineChain({
id: 9745,
name: 'Plasma',
blockTime: 1000,
nativeCurrency: {
name: 'Plasma',
symbol: 'XPL',
decimals: 18,
},
rpcUrls: {
default: {
http: ['https://rpc.plasma.to'],
},
},
blockExplorers: {
default: {
name: 'PlasmaScan',
url: 'https://plasmascan.to',
},
},
contracts: {
multicall3: {
address: '0xcA11bde05977b3631167028862bE2a173976CA11',
blockCreated: 0,
},
},
})
const SolanaChainId = 792703809
const BitcoinChainId = 8253038
const BitcoinAddress = 'bc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqmql8k8'
const solanaChain = {
id: SolanaChainId,
name: 'Solana',
displayName: 'Solana',
vmType: 'svm' as const,
} as RelayChain
const bitcoinChain = {
id: BitcoinChainId,
name: 'Bitcoin',
displayName: 'Bitcoin',
vmType: 'bvm' as const,
} as RelayChain
export class RelayAdapter extends BaseSwapAdapter {
constructor() {
super()
createClient({
baseApiUrl: MAINNET_RELAY_API,
source: 'openocean',
chains: [
arbitrum,
avalanche,
base,
berachain,
blast,
bsc,
fantom,
linea,
mainnet,
mantle,
optimism,
polygon,
scroll,
sonic,
zksync,
ronin,
unichain,
hyperEvm,
plasma,
celo,
monad,
]
.map(convertViemChainToRelayChain)
.concat(solanaChain as any, bitcoinChain as any),
})
}
getName(): string {
return 'Relay'
}
getIcon(): string {
return 'https://storage.googleapis.com/ks-setting-1d682dca/84e906bb-eaeb-45d3-a64c-2aa9c84eb3ea1747759080942.png'
}
getSupportedChains(): Chain[] {
return [NonEvmChain.Solana, NonEvmChain.Bitcoin, NewEvmChain.Plasma, NewEvmChain.Monad, ...MAINNET_NETWORKS.filter(chain => chain !== ChainId.FLR)]
// return [...MAINNET_NETWORKS]
}
getSupportedTokens(_sourceChain: Chain, _destChain: Chain): Currency[] {
return []
}
async getQuote(params: QuoteParams): Promise<NormalizedQuote> {
const evmFromToken = params.fromToken as Currency
const evmToToken = params.toToken as Currency
let currency = evmFromToken.isNative ? ZERO_ADDRESS : evmFromToken.address
let toCurrency = evmToToken.isNative ? ZERO_ADDRESS : evmToToken.address
let chainId = +params.fromChain
if (params.fromChain === NonEvmChain.Solana) {
chainId = SolanaChainId
} else if (params.fromChain === NonEvmChain.Bitcoin) {
chainId = BitcoinChainId
currency = BitcoinAddress
}
let toChainId = +params.toChain
if (params.toChain === NonEvmChain.Solana) {
toChainId = SolanaChainId
} else if (params.toChain === NonEvmChain.Bitcoin) {
toChainId = BitcoinChainId
toCurrency = BitcoinAddress
}
const quoteParams = {
chainId: chainId,
toChainId: toChainId,
currency,
toCurrency,
amount: params.amount,
tradeType: 'EXACT_INPUT' as QuoteBodyOptions['tradeType'],
wallet: params.walletClient,
recipient: params.recipient,
user: params.sender,
options: {
appFees: [
{
recipient: CROSS_CHAIN_FEE_RECEIVER,
// recipient:
// params.fromChain === NonEvmChain.Solana
// ? CROSS_CHAIN_FEE_RECEIVER_SOLANA
// : CROSS_CHAIN_FEE_RECEIVER,
fee: params.feeBps.toString(),
},
],
protocolVersion: 'preferV2' as 'preferV2' | 'v1' | 'v2',
// includedSwapSources: ['open-ocean'],
},
}
const resp = await getClient().actions.getQuote(quoteParams)
const formattedOutputAmount = formatUnits(
BigInt(resp.details?.currencyOut?.amount || '0'),
params.toToken.decimals
)
const formattedInputAmount = formatUnits(
BigInt(params.amount),
params.fromToken.decimals
)
const inputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes(
params.fromChain
)
? Number(resp.details?.currencyIn?.amountUsd || 0)
: params.tokenInUsd * +formattedInputAmount
const outputUsd = NOT_SUPPORTED_CHAINS_PRICE_SERVICE.includes(
params.toChain
)
? Number(resp.details?.currencyOut?.amountUsd || 0)
: params.tokenOutUsd * +formattedOutputAmount
if (params.walletClient) {
params.walletClient = {
account: {
address: params.sender,
},
} as any
}
const protocolFee: any = resp.fees?.relayer?.amountUsd || 0
return {
quoteParams: params,
outputAmount: BigInt(resp.details?.currencyOut?.amount || '0'),
formattedOutputAmount,
inputUsd,
outputUsd,
priceImpact:
!inputUsd || !outputUsd
? Number.NaN
: ((inputUsd - outputUsd) * 100) / inputUsd,
//rate: Number(resp.details?.rate || 0),
rate: +formattedOutputAmount / +formattedInputAmount,
gasFeeUsd: Number(resp.fees?.gas?.amountUsd || 0),
timeEstimate: resp.details?.timeEstimate || 0,
// Relay dont need to approve, we send token to contract directly
contractAddress: ZERO_ADDRESS,
rawQuote: resp,
protocolFee: protocolFee,
platformFeePercent: (params.feeBps * 100) / 10000,
}
}
async executeSwap(
{ quote }: Quote,
walletClient: WalletClient
): Promise<NormalizedTxResponse> {
return new Promise<NormalizedTxResponse>((resolve, reject) => {
getClient()
.actions.execute({
quote: quote.rawQuote,
wallet: walletClient,
onProgress: ({ currentStep }) => {
if (
currentStep?.id === 'deposit' &&
currentStep.requestId &&
currentStep.kind === 'transaction'
) {
const txHash = currentStep.items?.[0]?.txHashes?.[0].txHash
if (txHash) {
resolve({
sender: quote.quoteParams.sender,
sourceTxHash: txHash,
adapter: this.getName(),
id: currentStep.requestId,
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(),
})
}
}
},
})
.catch((e) => {
reject(e) // Make sure errors from execute are also caught
})
})
}
async getTransactionStatus(p: NormalizedTxResponse): Promise<SwapStatus> {
const res = await fetch(
`https://api.relay.link/intents/status/v2?requestId=${p.id}`
).then((r) => r.json())
return {
txHash: res.txHashes?.[0] || '',
status:
res.status === 'success'
? 'Success'
: res.status === 'refund'
? 'Refunded'
: res.status === 'failure'
? 'Failed'
: 'Processing',
}
}
}