Skip to content

Commit

Permalink
feat: add execute quote (shapeshift#87)
Browse files Browse the repository at this point in the history
* initial setup of executeQuote

* add guard clause logic to executeQuote with tests

* cleanup from merge

* add adapter logic to executeQuote; add tests

* fix unimplemented methods for thorchain swapper
  • Loading branch information
DaoDev44 authored Oct 4, 2021
1 parent 1d5d36e commit 463a06d
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 18 deletions.
21 changes: 18 additions & 3 deletions packages/swapper/src/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { HDWallet } from '@shapeshiftoss/hdwallet-core'
import { Asset, BuildQuoteTxArgs, GetQuoteInput, Quote, SwapperType } from '@shapeshiftoss/types'
import {
Asset,
BuildQuoteTxInput,
GetQuoteInput,
Quote,
SwapperType,
ExecQuoteInput,
ExecQuoteOutput
} from '@shapeshiftoss/types'

export class SwapError extends Error {}

Expand All @@ -12,13 +20,13 @@ export interface Swapper {
* @param input
* @param wallet
**/
buildQuoteTx(args: BuildQuoteTxArgs): Promise<Quote | undefined>
buildQuoteTx(args: BuildQuoteTxInput): Promise<Quote>

/**
* Get a basic quote (rate) for a trading pair
* @param input
*/
getQuote(input: GetQuoteInput, wallet?: HDWallet): Promise<Quote | undefined>
getQuote(input: GetQuoteInput, wallet?: HDWallet): Promise<Quote>

/**
* Get a list of available assets based on the array of assets you send it
Expand All @@ -38,4 +46,11 @@ export interface Swapper {
* @param input
*/
getUsdRate(input: Pick<Asset, 'symbol' | 'tokenId'>): Promise<string>

/**
* Execute a quote built with buildQuoteTx by signing and broadcasting
* @param input
* @param wallet
*/
executeQuote(args: ExecQuoteInput): Promise<ExecQuoteOutput>
}
20 changes: 12 additions & 8 deletions packages/swapper/src/swappers/thorchain/ThorchainSwapper.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,34 @@
import { Asset, SwapperType } from '@shapeshiftoss/types'
import { Asset, SwapperType, Quote, ExecQuoteOutput } from '@shapeshiftoss/types'
import { Swapper } from '../../api'

export class ThorchainSwapper implements Swapper {
getType() {
return SwapperType.Thorchain
}

async getQuote() {
return undefined
async getQuote(): Promise<Quote> {
throw new Error('ThorchainSwapper: getQuote unimplemented')
}

async buildQuoteTx() {
return undefined
async buildQuoteTx(): Promise<Quote> {
throw new Error('ThorchainSwapper: getQuote unimplemented')
}

getUsdRate(input: Pick<Asset, 'symbol' | 'tokenId'>): Promise<string> {
console.info(input)
throw new Error('Method not implemented.')
throw new Error('ThorchainSwapper: getUsdRate unimplemented')
}

getAvailableAssets(assets: Asset[]): Asset[] {
console.info(assets)
throw new Error('Method not implemented.')
throw new Error('ThorchainSwapper: getAvailableAssets unimplemented')
}
canTradePair(sellAsset: Asset, buyAsset: Asset): boolean {
console.info(sellAsset, buyAsset)
throw new Error('Method not implemented.')
throw new Error('ThorchainSwapper: canTradePair unimplemented')
}

async executeQuote(): Promise<ExecQuoteOutput> {
throw new Error('ThorchainSwapper: executeQuote unimplemented')
}
}
15 changes: 11 additions & 4 deletions packages/swapper/src/swappers/zrx/ZrxSwapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@ import BigNumber from 'bignumber.js'
import { zrxService } from './utils/zrxService'
import {
Asset,
BuildQuoteTxArgs,
BuildQuoteTxInput,
ChainTypes,
GetQuoteInput,
Quote,
SwapperType,
QuoteResponse
QuoteResponse,
ExecQuoteInput,
ExecQuoteOutput
} from '@shapeshiftoss/types'
import { ChainAdapterManager } from '@shapeshiftoss/chain-adapters'
import { Swapper } from '../../api'

import { buildQuoteTx } from './buildQuoteTx/buildQuoteTx'
import { getZrxQuote } from './getQuote/getQuote'
import { executeQuote } from './executeQuote/executeQuote'

export type ZrxSwapperDeps = {
adapterManager: ChainAdapterManager
Expand All @@ -41,8 +44,8 @@ export class ZrxSwapper implements Swapper {
return SwapperType.Zrx
}

async buildQuoteTx({ input, wallet }: BuildQuoteTxArgs): Promise<Quote> {
return buildQuoteTx(this.deps, { input, wallet })
async buildQuoteTx(args: BuildQuoteTxInput): Promise<Quote> {
return buildQuoteTx(this.deps, args)
}

async getQuote(input: GetQuoteInput): Promise<Quote> {
Expand Down Expand Up @@ -74,4 +77,8 @@ export class ZrxSwapper implements Swapper {
const availableAssets = this.getAvailableAssets([sellAsset, buyAsset])
return availableAssets.length === 2
}

async executeQuote(args: ExecQuoteInput): Promise<ExecQuoteOutput> {
return executeQuote(this.deps, args)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AxiosResponse } from 'axios'
import * as rax from 'retry-axios'
import { ChainAdapter } from '@shapeshiftoss/chain-adapters'
import { SwapError } from '../../..'
import { Quote, BuildQuoteTxArgs } from '@shapeshiftoss/types'
import { Quote, BuildQuoteTxInput } from '@shapeshiftoss/types'
import { ZrxSwapperDeps } from '../ZrxSwapper'
import { applyAxiosRetry } from '../utils/applyAxiosRetry'
import { erc20AllowanceAbi } from '../utils/abi/erc20-abi'
Expand Down Expand Up @@ -44,7 +44,7 @@ type QuoteResponse = {

export async function buildQuoteTx(
{ adapterManager, web3 }: ZrxSwapperDeps,
{ input, wallet }: BuildQuoteTxArgs
{ input, wallet }: BuildQuoteTxInput
): Promise<Quote> {
const {
sellAsset,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { HDWallet } from '@shapeshiftoss/hdwallet-core'
import { ExecQuoteInput } from '@shapeshiftoss/types'
import { executeQuote } from './executeQuote'
import { setupQuote } from '../utils/test-data/setupSwapQuote'
import { ZrxSwapperDeps } from '../ZrxSwapper'

describe('executeQuote', () => {
const { quoteInput, sellAsset } = setupQuote()
const txid = '0xffaac3dd529171e8a9a2adaf36b0344877c4894720d65dfd86e4b3a56c5a857e'
const wallet = <HDWallet>{}
const adapterManager = {
byChain: jest.fn(() => ({
buildSendTransaction: jest.fn(() => Promise.resolve({ txToSign: '0000000000000000' })),
signTransaction: jest.fn(() => Promise.resolve('0000000000000000000')),
broadcastTransaction: jest.fn(() => Promise.resolve(txid))
}))
}
const deps = ({ adapterManager } as unknown) as ZrxSwapperDeps

it('throws an error if quote.success is false', async () => {
const args = {
quote: { ...quoteInput, success: false },
wallet
}
await expect(executeQuote(deps, args)).rejects.toThrow(
'ZrxSwapper:executeQuote Cannot execute a failed quote'
)
})

it('throws an error if sellAsset.network is not provided', async () => {
const args = ({
quote: { ...quoteInput, sellAsset: { ...sellAsset, network: '' } },
wallet
} as unknown) as ExecQuoteInput
await expect(executeQuote(deps, args)).rejects.toThrow(
'ZrxSwapper:executeQuote sellAssetNetwork and sellAssetSymbol are required'
)
})

it('throws an error if sellAsset.symbol is not provided', async () => {
const args = {
quote: { ...quoteInput, sellAsset: { ...sellAsset, symbol: '' } },
wallet
}
await expect(executeQuote(deps, args)).rejects.toThrow(
'ZrxSwapper:executeQuote sellAssetNetwork and sellAssetSymbol are required'
)
})

it('throws an error if quote.sellAssetAccountId is not provided', async () => {
const args = {
quote: { ...quoteInput, sellAssetAccountId: '' },
wallet
}
await expect(executeQuote(deps, args)).rejects.toThrow(
'ZrxSwapper:executeQuote sellAssetAccountId is required'
)
})

it('throws an error if quote.sellAmount is not provided', async () => {
const args = {
quote: { ...quoteInput, sellAmount: '' },
wallet
}
await expect(executeQuote(deps, args)).rejects.toThrow(
'ZrxSwapper:executeQuote sellAmount is required'
)
})

it('throws an error if quote.depositAddress is not provided', async () => {
const args = {
quote: { ...quoteInput, depositAddress: '' },
wallet
}
await expect(executeQuote(deps, args)).rejects.toThrow(
'ZrxSwapper:executeQuote depositAddress is required'
)
})

it('returns txid', async () => {
const args = {
quote: { ...quoteInput, depositAddress: '0x728F1973c71f7567dE2a34Fa2838D4F0FB7f9765' },
wallet
}

expect(await executeQuote(deps, args)).toEqual({ txid })
})
})
51 changes: 51 additions & 0 deletions packages/swapper/src/swappers/zrx/executeQuote/executeQuote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { numberToHex } from 'web3-utils'
import { ExecQuoteInput, ExecQuoteOutput } from '@shapeshiftoss/types'
import { SwapError } from '../../../api'
import { ZrxSwapperDeps } from '../ZrxSwapper'
import { DEFAULT_ETH_PATH } from '../utils/constants'

export async function executeQuote(
{ adapterManager }: ZrxSwapperDeps,
{ quote, wallet }: ExecQuoteInput
): Promise<ExecQuoteOutput> {
const { sellAsset } = quote

if (!quote.success) {
throw new SwapError('ZrxSwapper:executeQuote Cannot execute a failed quote')
}

if (!sellAsset.network || !sellAsset.symbol) {
throw new SwapError('ZrxSwapper:executeQuote sellAssetNetwork and sellAssetSymbol are required')
}

if (!quote.sellAssetAccountId) {
throw new SwapError('ZrxSwapper:executeQuote sellAssetAccountId is required')
}

if (!quote.sellAmount) {
throw new SwapError('ZrxSwapper:executeQuote sellAmount is required')
}

if (!quote.depositAddress) {
throw new SwapError('ZrxSwapper:executeQuote depositAddress is required')
}

// value is 0 for erc20s
const value = sellAsset.symbol === 'ETH' ? numberToHex(quote.sellAmount || 0) : '0x0'
const adapter = adapterManager.byChain(sellAsset.chain)

const { txToSign } = await adapter.buildSendTransaction({
value,
wallet,
to: quote.depositAddress,
path: DEFAULT_ETH_PATH,
fee: numberToHex(quote.feeData?.gasPrice || 0),
limit: numberToHex(quote.feeData?.estimatedGas || 0)
})

const signedTx = await adapter.signTransaction({ txToSign, wallet })

const txid = await adapter.broadcastTransaction(signedTx)

return { txid }
}
11 changes: 10 additions & 1 deletion packages/types/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,20 @@ export type GetQuoteInput = {
minimum?: string
}

export type BuildQuoteTxArgs = {
export type BuildQuoteTxInput = {
input: GetQuoteInput
wallet: HDWallet
}

export type ExecQuoteInput = {
quote: Quote
wallet: HDWallet
}

export type ExecQuoteOutput = {
txid: string
}

// chain-adapters

export type Transaction = {
Expand Down

0 comments on commit 463a06d

Please sign in to comment.