Skip to content

Commit

Permalink
Implement 0x gasless swap plugin without approve
Browse files Browse the repository at this point in the history
  • Loading branch information
samholmes committed Jun 14, 2024
1 parent 44eeb7c commit eab9a8b
Show file tree
Hide file tree
Showing 5 changed files with 643 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { makeGodexPlugin } from './swap/central/godex'
import { makeLetsExchangePlugin } from './swap/central/letsexchange'
import { makeSideshiftPlugin } from './swap/central/sideshift'
import { makeSwapuzPlugin } from './swap/central/swapuz'
import { make0xGaslessSwap } from './swap/defi/0x/0xGaslessSwap'
import { make0xSwap } from './swap/defi/0x/0xSwap'
import { makeCosmosIbcPlugin } from './swap/defi/cosmosIbc'
import { makeLifiPlugin } from './swap/defi/lifi'
Expand Down Expand Up @@ -38,6 +39,7 @@ const plugins = {
transfer: makeTransferPlugin,
velodrome: makeVelodromePlugin,
xrpdex,
zeroxgaslessswap: make0xGaslessSwap,
zeroxswap: make0xSwap
}

Expand Down
125 changes: 125 additions & 0 deletions src/swap/defi/0x/0xGaslessSwap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { add } from 'biggystring'
import {
EdgeCorePluginFactory,
EdgeNetworkFee,
EdgeSwapApproveOptions,
EdgeSwapInfo,
EdgeSwapPlugin,
EdgeSwapQuote,
EdgeSwapResult
} from 'edge-core-js/types'

import { ZeroXApi } from './api'
import { EXPIRATION_MS, NATIVE_TOKEN_ADDRESS } from './constants'
import { asInitOptions } from './types'
import { getCurrencyCode, getTokenAddress } from './util'

const swapInfo: EdgeSwapInfo = {
displayName: '0x Gasless Swap',
pluginId: 'zeroxgaslessswap',
supportEmail: '[email protected]'
}

export const make0xGaslessSwap: EdgeCorePluginFactory = (
opts
): EdgeSwapPlugin => {
const { io } = opts
const initOptions = asInitOptions(opts.initOptions)

const api = new ZeroXApi(io, initOptions.apiKey)

return {
swapInfo,
fetchSwapQuote: async (
swapRequest,
userSettings,
opts
): Promise<EdgeSwapQuote> => {
// The fromWallet and toWallet must be of the same currency plugin
// type and therefore of the same network.
if (
swapRequest.fromWallet.currencyInfo.pluginId !==
swapRequest.toWallet.currencyInfo.pluginId
) {
throw new Error('Swap between different networks is not supported')
}

const fromTokenAddress = getTokenAddress(
swapRequest.fromWallet,
swapRequest.fromTokenId
)
const fromCurrencyCode = getCurrencyCode(
swapRequest.fromWallet,
swapRequest.fromTokenId
)
const toTokenAddress = getTokenAddress(
swapRequest.toWallet,
swapRequest.toTokenId
)

if (swapRequest.quoteFor === 'max') {
throw new Error('Max quotes not supported')
}

// From wallet address
const {
publicAddress: fromWalletAddress
} = await swapRequest.fromWallet.getReceiveAddress({
tokenId: swapRequest.fromTokenId
})

// Amount request parameter/field name to use in the quote request
const amountField =
swapRequest.quoteFor === 'from' ? 'sellAmount' : 'buyAmount'

// Get quote from ZeroXApi
const chainId = api.getChainIdFromPluginId(
swapRequest.fromWallet.currencyInfo.pluginId
)
const apiSwapQuote = await api.gaslessSwapQuote(chainId, {
sellToken: fromTokenAddress ?? NATIVE_TOKEN_ADDRESS,
buyToken: toTokenAddress ?? NATIVE_TOKEN_ADDRESS,
takerAddress: fromWalletAddress,
[amountField]: swapRequest.nativeAmount
})

if (!apiSwapQuote.liquidityAvailable)
throw new Error('No liquidity available')

const { gasFee, zeroExFee } = apiSwapQuote.fees

if (
gasFee.feeToken.toLocaleLowerCase() !==
fromTokenAddress?.toLocaleLowerCase() ||
zeroExFee.feeToken.toLocaleLowerCase() !==
fromTokenAddress?.toLocaleLowerCase()
) {
throw new Error(
'Quoted fees must be in the same token as the from token in the swap request'
)
}

const networkFee: EdgeNetworkFee = {
currencyCode: fromCurrencyCode,
nativeAmount: add(gasFee.feeAmount, zeroExFee.feeAmount)
}

return {
approve: async (
opts?: EdgeSwapApproveOptions
): Promise<EdgeSwapResult> => {
throw new Error('Approve not yet implemented')
},
close: async () => {},
expirationDate: new Date(Date.now() + EXPIRATION_MS),
fromNativeAmount: apiSwapQuote.sellAmount,
isEstimate: false,
networkFee,
pluginId: swapInfo.pluginId,
request: swapRequest,
swapInfo: swapInfo,
toNativeAmount: apiSwapQuote.buyAmount
}
}
}
}
70 changes: 70 additions & 0 deletions src/swap/defi/0x/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { FetchResponse } from 'serverlet'

import {
asErrorResponse,
asGaslessSwapQuoteResponse,
asSwapQuoteResponse,
ChainId,
GaslessSwapQuoteRequest,
GaslessSwapQuoteResponse,
SwapQuoteRequest,
SwapQuoteResponse
} from './apiTypes'
Expand All @@ -21,6 +25,32 @@ export class ZeroXApi {
this.io = io
}

/**
* Retrieves the ChainId based on the provided pluginId.
*
* @param pluginId The currency pluginId to retrieve the ChainId for.
* @returns The ChainId associated with the pluginId.
* @throws Error if the pluginId is not supported.
*/
getChainIdFromPluginId(pluginId: string): ChainId {
switch (pluginId) {
case 'arbitrum':
return ChainId.Arbitrum
case 'base':
return ChainId.Base
case 'ethereum':
return ChainId.Ethereum
case 'optimism':
return ChainId.Optimism
case 'polygon':
return ChainId.Polygon
default:
throw new Error(
`ZeroXApi: Unsupported ChainId for currency plugin: '${pluginId}'`
)
}
}

/**
* Get the 0x API endpoint based on the currency plugin ID. The endpoint is
* the appropriate 0x API server for a particular network (Ethereum, Polygon,
Expand Down Expand Up @@ -92,6 +122,46 @@ export class ZeroXApi {

return responseData
}

/**
* Retrieves a gasless swap quote from the API.
*
* @param {ChainId} chainId - The ID of the chain (see {@link getChainIdFromPluginId}).
* @param {GaslessSwapQuoteRequest} request - The request object containing
* the necessary parameters for the swap quote.
* @returns {Promise<GaslessSwapQuoteResponse>} - A promise that resolves to
* the gasless swap quote response.
*/
async gaslessSwapQuote(
chainId: ChainId,
request: GaslessSwapQuoteRequest
): Promise<GaslessSwapQuoteResponse> {
// Gasless API is only available on Ethereum network
const endpoint = this.getEndpointFromPluginId('ethereum')

const queryParams = requestToParams(request)
const queryString = new URLSearchParams(queryParams).toString()

const response = await this.io.fetch(
`${endpoint}/tx-relay/v1/swap/quote?${queryString}`,
{
headers: {
'content-type': 'application/json',
'0x-api-key': this.apiKey,
'0x-chain-id': chainId.toString()
}
}
)

if (!response.ok) {
await handledErrorResponse(response)
}

const responseText = await response.text()
const responseData = asGaslessSwapQuoteResponse(responseText)

return responseData
}
}

async function handledErrorResponse(response: FetchResponse): Promise<void> {
Expand Down
Loading

0 comments on commit eab9a8b

Please sign in to comment.