diff --git a/packages/swapper/src/api.ts b/packages/swapper/src/api.ts index 33eb204dd..a4f941356 100644 --- a/packages/swapper/src/api.ts +++ b/packages/swapper/src/api.ts @@ -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 {} @@ -12,13 +20,13 @@ export interface Swapper { * @param input * @param wallet **/ - buildQuoteTx(args: BuildQuoteTxArgs): Promise + buildQuoteTx(args: BuildQuoteTxInput): Promise /** * Get a basic quote (rate) for a trading pair * @param input */ - getQuote(input: GetQuoteInput, wallet?: HDWallet): Promise + getQuote(input: GetQuoteInput, wallet?: HDWallet): Promise /** * Get a list of available assets based on the array of assets you send it @@ -38,4 +46,11 @@ export interface Swapper { * @param input */ getUsdRate(input: Pick): Promise + + /** + * Execute a quote built with buildQuoteTx by signing and broadcasting + * @param input + * @param wallet + */ + executeQuote(args: ExecQuoteInput): Promise } diff --git a/packages/swapper/src/swappers/thorchain/ThorchainSwapper.ts b/packages/swapper/src/swappers/thorchain/ThorchainSwapper.ts index d2fac11d1..9e280337e 100644 --- a/packages/swapper/src/swappers/thorchain/ThorchainSwapper.ts +++ b/packages/swapper/src/swappers/thorchain/ThorchainSwapper.ts @@ -1,4 +1,4 @@ -import { Asset, SwapperType } from '@shapeshiftoss/types' +import { Asset, SwapperType, Quote, ExecQuoteOutput } from '@shapeshiftoss/types' import { Swapper } from '../../api' export class ThorchainSwapper implements Swapper { @@ -6,25 +6,29 @@ export class ThorchainSwapper implements Swapper { return SwapperType.Thorchain } - async getQuote() { - return undefined + async getQuote(): Promise { + throw new Error('ThorchainSwapper: getQuote unimplemented') } - async buildQuoteTx() { - return undefined + async buildQuoteTx(): Promise { + throw new Error('ThorchainSwapper: getQuote unimplemented') } getUsdRate(input: Pick): Promise { 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 { + throw new Error('ThorchainSwapper: executeQuote unimplemented') } } diff --git a/packages/swapper/src/swappers/zrx/ZrxSwapper.ts b/packages/swapper/src/swappers/zrx/ZrxSwapper.ts index f89167ec6..5d7a8bf10 100644 --- a/packages/swapper/src/swappers/zrx/ZrxSwapper.ts +++ b/packages/swapper/src/swappers/zrx/ZrxSwapper.ts @@ -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 @@ -41,8 +44,8 @@ export class ZrxSwapper implements Swapper { return SwapperType.Zrx } - async buildQuoteTx({ input, wallet }: BuildQuoteTxArgs): Promise { - return buildQuoteTx(this.deps, { input, wallet }) + async buildQuoteTx(args: BuildQuoteTxInput): Promise { + return buildQuoteTx(this.deps, args) } async getQuote(input: GetQuoteInput): Promise { @@ -74,4 +77,8 @@ export class ZrxSwapper implements Swapper { const availableAssets = this.getAvailableAssets([sellAsset, buyAsset]) return availableAssets.length === 2 } + + async executeQuote(args: ExecQuoteInput): Promise { + return executeQuote(this.deps, args) + } } diff --git a/packages/swapper/src/swappers/zrx/buildQuoteTx/buildQuoteTx.ts b/packages/swapper/src/swappers/zrx/buildQuoteTx/buildQuoteTx.ts index cd84cbf82..877782ab1 100644 --- a/packages/swapper/src/swappers/zrx/buildQuoteTx/buildQuoteTx.ts +++ b/packages/swapper/src/swappers/zrx/buildQuoteTx/buildQuoteTx.ts @@ -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' @@ -44,7 +44,7 @@ type QuoteResponse = { export async function buildQuoteTx( { adapterManager, web3 }: ZrxSwapperDeps, - { input, wallet }: BuildQuoteTxArgs + { input, wallet }: BuildQuoteTxInput ): Promise { const { sellAsset, diff --git a/packages/swapper/src/swappers/zrx/executeQuote/executeQuote.test.ts b/packages/swapper/src/swappers/zrx/executeQuote/executeQuote.test.ts new file mode 100644 index 000000000..829ee7414 --- /dev/null +++ b/packages/swapper/src/swappers/zrx/executeQuote/executeQuote.test.ts @@ -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 = {} + 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 }) + }) +}) diff --git a/packages/swapper/src/swappers/zrx/executeQuote/executeQuote.ts b/packages/swapper/src/swappers/zrx/executeQuote/executeQuote.ts new file mode 100644 index 000000000..9ef22f494 --- /dev/null +++ b/packages/swapper/src/swappers/zrx/executeQuote/executeQuote.ts @@ -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 { + 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 } +} diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 40145fb25..c0559dfbd 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -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 = {