Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement 0xgasless swap #334

Merged
merged 4 commits into from
Jul 3, 2024
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- added: 0x Gasless Swap plugin

## 2.6.0 (2024-06-24)

- added: (Lifi) Add Solana
Expand Down
4 changes: 3 additions & 1 deletion 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 { make0xGaslessPlugin } from './swap/defi/0x/0xGasless'
import { makeCosmosIbcPlugin } from './swap/defi/cosmosIbc'
import { makeLifiPlugin } from './swap/defi/lifi'
import { makeRangoPlugin } from './swap/defi/rango'
Expand Down Expand Up @@ -38,7 +39,8 @@ const plugins = {
tombSwap: makeTombSwapPlugin,
transfer: makeTransferPlugin,
velodrome: makeVelodromePlugin,
xrpdex
xrpdex,
'0xgasless': make0xGaslessPlugin
}

declare global {
Expand Down
234 changes: 234 additions & 0 deletions src/swap/defi/0x/0xGasless.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
import {
EdgeAssetAction,
EdgeCorePluginFactory,
EdgeSwapApproveOptions,
EdgeSwapInfo,
EdgeSwapQuote,
EdgeSwapResult,
EdgeTransaction,
EdgeTxAction
} from 'edge-core-js/types'

import { snooze } from '../../../util/utils'
import { EXPIRATION_MS, NATIVE_TOKEN_ADDRESS } from './constants'
import { asInitOptions } from './types'
import { getCurrencyCode, getTokenAddress, makeSignatureStruct } from './util'
import { ZeroXApi } from './ZeroXApi'
import {
GaslessSwapStatusResponse,
GaslessSwapSubmitRequest
} from './zeroXApiTypes'

const swapInfo: EdgeSwapInfo = {
displayName: '0x Gasless Swap',
isDex: true,
pluginId: '0xgasless',
supportEmail: 'support@edge.app'
}

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

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

return {
swapInfo,
fetchSwapQuote: async (swapRequest): Promise<EdgeSwapQuote> => {
// The fromWallet and toWallet must be of the same because the swap
// service only supports swaps of the same network and for the same
// account/address.
if (swapRequest.toWallet.id !== swapRequest.fromWallet.id) {
throw new Error('Swap between different wallets is not supported')
}

const fromTokenAddress = getTokenAddress(
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, {
checkApproval: true,
sellToken: fromTokenAddress ?? NATIVE_TOKEN_ADDRESS,
buyToken: toTokenAddress ?? NATIVE_TOKEN_ADDRESS,
takerAddress: fromWalletAddress,
[amountField]: swapRequest.nativeAmount
})

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

// The plugin only supports gasless swaps, so if approval is required
// it must be gasless.
if (
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
apiSwapQuote.approval != null &&
apiSwapQuote.approval.isRequired &&
!apiSwapQuote.approval.isGaslessAvailable
) {
throw new Error('Approval is required but gasless is not available')
}

return {
approve: async (
opts?: EdgeSwapApproveOptions
): Promise<EdgeSwapResult> => {
let approvalData: GaslessSwapSubmitRequest['approval']
if (
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain
apiSwapQuote.approval != null &&
apiSwapQuote.approval.isRequired
) {
// Assert that that approval is gasless, otherwise it would have
// been caught above, so this case should be unreachable.
if (!apiSwapQuote.approval.isGaslessAvailable) {
throw new Error('Unreachable non-gasless approval condition')
}

const approvalTypeData = JSON.stringify(
apiSwapQuote.approval.eip712
)
const approvalSignatureHash = await swapRequest.fromWallet.signMessage(
approvalTypeData,
{ otherParams: { typedData: true } }
)
const approvalSignature = makeSignatureStruct(approvalSignatureHash)
approvalData = {
type: apiSwapQuote.approval.type,
eip712: apiSwapQuote.approval.eip712,
signature: approvalSignature
}
}

const tradeTypeData = JSON.stringify(apiSwapQuote.trade.eip712)
const tradeSignatureHash = await swapRequest.fromWallet.signMessage(
tradeTypeData,
{ otherParams: { typedData: true } }
)
const tradeSignature = makeSignatureStruct(tradeSignatureHash)
const tradeData: GaslessSwapSubmitRequest['trade'] = {
type: apiSwapQuote.trade.type,
eip712: apiSwapQuote.trade.eip712,
signature: tradeSignature
}

const apiSwapSubmition = await api.gaslessSwapSubmit(chainId, {
...(approvalData !== undefined ? { approval: approvalData } : {}),
trade: tradeData
})

let apiSwapStatus: GaslessSwapStatusResponse
do {
// Wait before checking
await snooze(500)
apiSwapStatus = await api.gaslessSwapStatus(
chainId,
apiSwapSubmition.tradeHash
)
} while (apiSwapStatus.status === 'pending')

if (apiSwapStatus.status === 'failed') {
throw new Error(`Swap failed: ${apiSwapStatus.reason ?? 'unknown'}`)
}

const assetAction: EdgeAssetAction = {
assetActionType: 'swap'
}
const orderId = apiSwapSubmition.tradeHash

const savedAction: EdgeTxAction = {
actionType: 'swap',
canBePartial: false,
isEstimate: false,
fromAsset: {
pluginId: swapRequest.fromWallet.currencyInfo.pluginId,
tokenId: swapRequest.fromTokenId,
nativeAmount: swapRequest.nativeAmount
},
orderId,
// The payout address is the same as the fromWalletAddress because
// the swap service only supports swaps of the same network and
// account/address.
payoutAddress: fromWalletAddress,
payoutWalletId: swapRequest.toWallet.id,
refundAddress: fromWalletAddress,
swapInfo,
toAsset: {
pluginId: swapRequest.toWallet.currencyInfo.pluginId,
tokenId: swapRequest.toTokenId,
nativeAmount: apiSwapQuote.buyAmount
}
}

// Create the minimal transaction object for the swap.
// Some values may be updated later when the transaction is
// updated from queries to the network.
const fromCurrencyCode = getCurrencyCode(
swapRequest.fromWallet,
swapRequest.fromTokenId
)
const transaction: EdgeTransaction = {
assetAction,
blockHeight: 0,
currencyCode: fromCurrencyCode,
date: Date.now(),
isSend: true,
memos: [],
nativeAmount: swapRequest.nativeAmount,
// There is no fee for a gasless swap
networkFee: '0',
ourReceiveAddresses: [],
savedAction,
signedTx: '', // Signing is done by the tx-relay server
tokenId: swapRequest.fromTokenId,
txid: apiSwapStatus.transactions[0].hash,
walletId: swapRequest.fromWallet.id
}

// Don't forget to save the transaction to the wallet:
await swapRequest.fromWallet.saveTx(transaction)

return {
orderId,
transaction
}
},
close: async () => {},
expirationDate: new Date(Date.now() + EXPIRATION_MS),
fromNativeAmount: apiSwapQuote.sellAmount,
isEstimate: false,
networkFee: {
currencyCode: swapRequest.fromWallet.currencyInfo.currencyCode,
nativeAmount: '0' // There is no fee for a gasless swap
},
pluginId: swapInfo.pluginId,
request: swapRequest,
swapInfo: swapInfo,
toNativeAmount: apiSwapQuote.buyAmount
}
}
}
}
Loading
Loading