Skip to content

Commit

Permalink
Implement 0xgasless approve method
Browse files Browse the repository at this point in the history
  • Loading branch information
samholmes committed Jun 25, 2024
1 parent 25eccb7 commit 367ebe7
Show file tree
Hide file tree
Showing 4 changed files with 310 additions and 20 deletions.
104 changes: 101 additions & 3 deletions src/swap/defi/0x/0xGasless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import {
EdgeSwapApproveOptions,
EdgeSwapInfo,
EdgeSwapQuote,
EdgeSwapResult
EdgeSwapResult,
EdgeTransaction
} from 'edge-core-js/types'

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

const swapInfo: EdgeSwapInfo = {
displayName: '0x Gasless Swap',
isDex: true,
pluginId: '0xgasless',
supportEmail: 'support@edge.app'
}
Expand Down Expand Up @@ -66,6 +70,7 @@ export const make0xGaslessPlugin: EdgeCorePluginFactory = opts => {
swapRequest.fromWallet.currencyInfo.pluginId
)
const apiSwapQuote = await api.gaslessSwapQuote(chainId, {
checkApproval: true,
sellToken: fromTokenAddress ?? NATIVE_TOKEN_ADDRESS,
buyToken: toTokenAddress ?? NATIVE_TOKEN_ADDRESS,
takerAddress: fromWalletAddress,
Expand All @@ -75,6 +80,17 @@ export const make0xGaslessPlugin: EdgeCorePluginFactory = opts => {
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')
}

const { gasFee, zeroExFee } = apiSwapQuote.fees

if (
Expand All @@ -101,7 +117,89 @@ export const make0xGaslessPlugin: EdgeCorePluginFactory = opts => {
approve: async (
opts?: EdgeSwapApproveOptions
): Promise<EdgeSwapResult> => {
throw new Error('Approve not yet implemented')
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'}`)
}

// 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 transaction: EdgeTransaction = {
blockHeight: 0,
currencyCode: fromCurrencyCode,
date: Date.now(),
isSend: true,
memos: [],
nativeAmount: swapRequest.nativeAmount,
networkFee: networkFee.nativeAmount,
ourReceiveAddresses: [],
signedTx: '',
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: apiSwapSubmition.tradeHash,
transaction
}
},
close: async () => {},
expirationDate: new Date(Date.now() + EXPIRATION_MS),
Expand Down
73 changes: 72 additions & 1 deletion src/swap/defi/0x/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import { FetchResponse } from 'serverlet'
import {
asErrorResponse,
asGaslessSwapQuoteResponse,
asGaslessSwapStatusResponse,
asGaslessSwapSubmitResponse,
ChainId,
GaslessSwapQuoteRequest,
GaslessSwapQuoteResponse,
GaslessSwapStatusResponse,
GaslessSwapSubmitRequest,
GaslessSwapSubmitResponse,
SwapQuoteRequest
} from './apiTypes'

Expand Down Expand Up @@ -100,7 +105,7 @@ export class ZeroXApi {
chainId: ChainId,
request: GaslessSwapQuoteRequest
): Promise<GaslessSwapQuoteResponse> {
// Gasless API is only available on Ethereum network
// Gasless API uses the Ethereum network
const endpoint = this.getEndpointFromPluginId('ethereum')

const queryParams = requestToParams(request)
Expand All @@ -126,6 +131,72 @@ export class ZeroXApi {

return responseData
}

/**
* Submits a gasless swap request to the 0x API.
*
* @param chainId - The chain ID of the network.
* @param request - The gasless swap submit request.
* @returns A promise that resolves to the gasless swap response.
*/
async gaslessSwapSubmit(
chainId: ChainId,
request: GaslessSwapSubmitRequest
): Promise<GaslessSwapSubmitResponse> {
// Gasless API uses the Ethereum network
const endpoint = this.getEndpointFromPluginId('ethereum')

const response = await this.io.fetch(
`${endpoint}/tx-relay/v1/swap/submit`,
{
method: 'POST',
body: JSON.stringify(request),
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 = asGaslessSwapSubmitResponse(responseText)

return responseData
}

async gaslessSwapStatus(
chainId: ChainId,
tradeHash: string
): Promise<GaslessSwapStatusResponse> {
// Gasless API uses the Ethereum network
const endpoint = this.getEndpointFromPluginId('ethereum')

const response = await this.io.fetch(
`${endpoint}/tx-relay/v1/swap/status/${tradeHash}`,
{
method: 'GET',
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 = asGaslessSwapStatusResponse(responseText)

return responseData
}
}

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

0 comments on commit 367ebe7

Please sign in to comment.