Skip to content

Commit

Permalink
feat: cowswap executeTrade and getTradeTxs methods (shapeshift#868)
Browse files Browse the repository at this point in the history
* feat: implements cowswapper skeleton

* feat: implements getUsdRate method

* fix: review comments

* fix: review comments

* feat: start implement filterBuyAssetsBySellAssetId and filterAssetIdsBySellable

* fix: issues after merge main

* fix: review comment

* fix: review comments

* feat: polish filtering functions and implements getCowSwapMinMax

* fix: lint

* fix: brackets

* feat: add comment

* feat: wip cow swap trade quote

* feat: implements cow swap quote trade

* feat: wip gas price and tests

* feat: fee estimate for trade quote

* feat: unit tests

* feat: implements buildTrade for cowswap

* fix: integration issues when used in web

* fix: unit test

* fix unit tests

* feat: implements buildTrade

* fix: couple issues with approval

* feat: implements executeTrade without signature

* wip: signature

* feat: order signing wip

* fix: merge

* fix: previous review comments

* feat: wip execute trade

* fix: build

* fix: naming convention for methods

* fix: validTo errors from api

* fix: naming conventions

* feat: finish implementing execute trade and unit tests

* feat: implements getTradeTxs

* feat: polish getTradeTxs and add tests

* fix: max trade issue and trade fee display issue

* feat: commenting

* fix: review comments

* feat: update hdwallet core dependency

* fix: incorrect fees for quote
  • Loading branch information
chxash authored Jul 22, 2022
1 parent c72a6c3 commit b9ce707
Show file tree
Hide file tree
Showing 19 changed files with 663 additions and 111 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@
"@commitlint/config-conventional": "^13.1.0",
"@semantic-release/changelog": "^5.0.1",
"@semantic-release/git": "^9.0.0",
"@shapeshiftoss/hdwallet-core": "^1.23.0",
"@shapeshiftoss/hdwallet-native": "^1.23.0",
"@shapeshiftoss/hdwallet-core": "^1.27.0",
"@shapeshiftoss/hdwallet-native": "^1.27.0",
"@types/jest": "^27.0.1",
"@types/lodash": "^4.14.182",
"@types/node": "^14.17.3",
Expand Down
8 changes: 4 additions & 4 deletions packages/chain-adapters/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,16 @@
},
"peerDependencies": {
"@shapeshiftoss/caip": "^6.2.0",
"@shapeshiftoss/hdwallet-core": "^1.26.0",
"@shapeshiftoss/hdwallet-native": "^1.26.0",
"@shapeshiftoss/hdwallet-core": "^1.27.0",
"@shapeshiftoss/hdwallet-native": "^1.27.0",
"@shapeshiftoss/types": "^7.0.0",
"@shapeshiftoss/unchained-client": "^9.0.1",
"bs58check": "^2.0.2"
},
"devDependencies": {
"@shapeshiftoss/caip": "^6.2.0",
"@shapeshiftoss/hdwallet-core": "^1.26.0",
"@shapeshiftoss/hdwallet-native": "^1.26.0",
"@shapeshiftoss/hdwallet-core": "^1.27.0",
"@shapeshiftoss/hdwallet-native": "^1.27.0",
"@shapeshiftoss/types": "^7.0.0",
"@shapeshiftoss/unchained-client": "^9.0.1",
"@types/bs58check": "^2.1.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/swapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@
"@shapeshiftoss/caip": "^6.2.0",
"@shapeshiftoss/chain-adapters": "^7.2.0",
"@shapeshiftoss/errors": "^1.1.2",
"@shapeshiftoss/hdwallet-core": "^1.23.0",
"@shapeshiftoss/hdwallet-core": "^1.27.0",
"@shapeshiftoss/types": "^7.0.0"
},
"devDependencies": {
"@shapeshiftoss/asset-service": "^6.2.1",
"@shapeshiftoss/caip": "^6.2.0",
"@shapeshiftoss/chain-adapters": "^7.2.0",
"@shapeshiftoss/errors": "^1.1.2",
"@shapeshiftoss/hdwallet-core": "^1.23.0",
"@shapeshiftoss/hdwallet-core": "^1.27.0",
"@shapeshiftoss/types": "^7.0.0",
"@types/readline-sync": "^1.4.4",
"readline-sync": "^1.4.10",
Expand Down
3 changes: 2 additions & 1 deletion packages/swapper/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ export enum SwapErrorTypes {
VALIDATION_FAILED = 'VALIDATION_FAILED',
MAKE_MEMO_FAILED = 'MAKE_MEMO_FAILED',
PRICE_RATIO_FAILED = 'PRICE_RATIO_FAILED',
POOL_NOT_FOUND = 'POOL_NOT_FOUND'
POOL_NOT_FOUND = 'POOL_NOT_FOUND',
GET_TRADE_TXS_FAILED = 'GET_TRADE_TXS_FAILED'
}

export interface Swapper<T extends ChainId> {
Expand Down
54 changes: 53 additions & 1 deletion packages/swapper/src/swappers/cow/CowSwapper.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { ethereum } from '@shapeshiftoss/chain-adapters'
import { HDWallet } from '@shapeshiftoss/hdwallet-core'
import { KnownChainIds } from '@shapeshiftoss/types'
import Web3 from 'web3'

import { SwapperType } from '../../api'
import { SwapperType, TradeResult } from '../../api'
import { BTC, ETH, FOX, WBTC, WETH } from '../utils/test-data/assets'
import { setupBuildTrade, setupQuote } from '../utils/test-data/setupSwapQuote'
import { cowApprovalNeeded } from './cowApprovalNeeded/cowApprovalNeeded'
import { cowApproveInfinite } from './cowApproveInfinite/cowApproveInfinite'
import { cowBuildTrade } from './cowBuildTrade/cowBuildTrade'
import { cowExecuteTrade } from './cowExecuteTrade/cowExecuteTrade'
import { cowGetTradeTxs } from './cowGetTradeTxs/cowGetTradeTxs'
import { CowSwapper, CowSwapperDeps } from './CowSwapper'
import { getCowSwapTradeQuote } from './getCowSwapTradeQuote/getCowSwapTradeQuote'
import { CowTrade } from './types'
import { getUsdRate } from './utils/helpers/helpers'

jest.mock('./utils/helpers/helpers')
Expand Down Expand Up @@ -37,6 +41,14 @@ jest.mock('./cowBuildTrade/cowBuildTrade', () => ({
cowBuildTrade: jest.fn()
}))

jest.mock('./cowExecuteTrade/cowExecuteTrade', () => ({
cowExecuteTrade: jest.fn()
}))

jest.mock('./cowGetTradeTxs/cowGetTradeTxs', () => ({
cowGetTradeTxs: jest.fn()
}))

const ASSET_IDS = [ETH.assetId, WBTC.assetId, WETH.assetId, BTC.assetId, FOX.assetId]

describe('CowSwapper', () => {
Expand Down Expand Up @@ -176,4 +188,44 @@ describe('CowSwapper', () => {
expect(cowApproveInfinite).toHaveBeenCalledWith(COW_SWAPPER_DEPS, args)
})
})

describe('executeTrade', () => {
it('calls executeTrade on swapper.buildTrade', async () => {
const cowSwapTrade: CowTrade<KnownChainIds.EthereumMainnet> = {
sellAmount: '1000000000000000000',
buyAmount: '14501811818247595090576',
sources: [{ name: 'CowSwap', proportion: '1' }],
buyAsset: FOX,
sellAsset: WETH,
sellAssetAccountNumber: 0,
receiveAddress: 'address11',
feeAmountInSellToken: '14557942658757988',
rate: '14716.04718939437505555958',
feeData: {
fee: '14557942658757988',
chainSpecific: {
estimatedGas: '100000',
gasPrice: '79036500000'
},
tradeFee: '0'
},
sellAmountWithoutFee: '985442057341242012'
}
const args = { trade: cowSwapTrade, wallet }
await swapper.executeTrade(args)
expect(cowExecuteTrade).toHaveBeenCalledTimes(1)
expect(cowExecuteTrade).toHaveBeenCalledWith(COW_SWAPPER_DEPS, args)
})
})

describe('getTradeTxs', () => {
it('calls cowGetTradeTxs on swapper.getTradeTxs', async () => {
const args: TradeResult = {
tradeId: 'tradeId789456'
}
await swapper.getTradeTxs(args)
expect(cowGetTradeTxs).toHaveBeenCalledTimes(1)
expect(cowGetTradeTxs).toHaveBeenCalledWith(COW_SWAPPER_DEPS, args)
})
})
})
13 changes: 7 additions & 6 deletions packages/swapper/src/swappers/cow/CowSwapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import {
GetTradeQuoteInput,
Swapper,
SwapperType,
Trade,
TradeQuote,
TradeResult,
TradeTxs
} from '../../api'
import { cowApprovalNeeded } from './cowApprovalNeeded/cowApprovalNeeded'
import { cowApproveInfinite } from './cowApproveInfinite/cowApproveInfinite'
import { cowBuildTrade } from './cowBuildTrade/cowBuildTrade'
import { cowExecuteTrade } from './cowExecuteTrade/cowExecuteTrade'
import { cowGetTradeTxs } from './cowGetTradeTxs/cowGetTradeTxs'
import { getCowSwapTradeQuote } from './getCowSwapTradeQuote/getCowSwapTradeQuote'
import { CowTrade } from './types'
import { COWSWAP_UNSUPPORTED_ASSETS } from './utils/blacklist'
import { getUsdRate } from './utils/helpers/helpers'

Expand All @@ -48,7 +50,7 @@ export class CowSwapper implements Swapper<KnownChainIds.EthereumMainnet> {
return SwapperType.CowSwap
}

async buildTrade(args: BuildTradeInput): Promise<Trade<KnownChainIds.EthereumMainnet>> {
async buildTrade(args: BuildTradeInput): Promise<CowTrade<KnownChainIds.EthereumMainnet>> {
return cowBuildTrade(this.deps, args)
}

Expand All @@ -63,8 +65,7 @@ export class CowSwapper implements Swapper<KnownChainIds.EthereumMainnet> {
}

async executeTrade(args: ExecuteTradeInput<KnownChainIds.EthereumMainnet>): Promise<TradeResult> {
console.info(args)
throw new Error('CowSwapper: executeTrade unimplemented')
return cowExecuteTrade(this.deps, args)
}

async approvalNeeded(
Expand Down Expand Up @@ -101,7 +102,7 @@ export class CowSwapper implements Swapper<KnownChainIds.EthereumMainnet> {
)
}

async getTradeTxs(): Promise<TradeTxs> {
throw new Error('CowSwapper: executeTrade unimplemented')
async getTradeTxs(args: TradeResult): Promise<TradeTxs> {
return cowGetTradeTxs(this.deps, args)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import { HDWallet } from '@shapeshiftoss/hdwallet-core'
import { Asset, KnownChainIds } from '@shapeshiftoss/types'
import Web3 from 'web3'

import { BuildTradeInput, Trade } from '../../../api'
import { BuildTradeInput } from '../../../api'
import { bn } from '../../utils/bignumber'
import { GetAllowanceRequiredArgs } from '../../utils/helpers/helpers'
import { ETH, FOX, WBTC, WETH } from '../../utils/test-data/assets'
import { CowSwapperDeps } from '../CowSwapper'
import { CowTrade } from '../types'
import { cowService } from '../utils/cowService'
import { CowSwapQuoteApiInput } from '../utils/helpers/helpers'
import { cowBuildTrade } from './cowBuildTrade'
Expand Down Expand Up @@ -95,43 +96,47 @@ const expectedApiInputWbtcToWeth: CowSwapQuoteApiInput = {
validTo: 1656797787
}

const expectedTradeWethToFox: Trade<KnownChainIds.EthereumMainnet> = {
const expectedTradeWethToFox: CowTrade<KnownChainIds.EthereumMainnet> = {
rate: '14716.04718939437505555958', // 14716 FOX per WETH
feeData: {
fee: '14557942658757988', // fee in WETH
fee: '0',
chainSpecific: {
estimatedGas: '100000',
gasPrice: '79036500000'
},
tradeFee: '0'
tradeFee: '17.95954294012756741283729339486489192096'
},
sellAmount: '1000000000000000000',
buyAmount: '14501811818247595090576', // 14501 FOX
sources: [{ name: 'CowSwap', proportion: '1' }],
buyAsset: FOX,
sellAsset: WETH,
sellAssetAccountNumber: 0,
receiveAddress: 'address11'
receiveAddress: 'address11',
feeAmountInSellToken: '14557942658757988',
sellAmountWithoutFee: '985442057341242012'
}

const expectedTradeQuoteWbtcToWethWithApprovalFee: Trade<KnownChainIds.EthereumMainnet> = {
const expectedTradeQuoteWbtcToWethWithApprovalFee: CowTrade<KnownChainIds.EthereumMainnet> = {
rate: '19.13939810252384532346', // 19.14 WETH per WBTC
feeData: {
fee: '2931322143956216.36', // fee in WETH
fee: '0',
chainSpecific: {
estimatedGas: '100000',
gasPrice: '79036500000',
approvalFee: '7903650000000000'
},
tradeFee: '0'
tradeFee: '3.6162531444'
},
sellAmount: '100000000',
buyAmount: '19136098853078932263', // 19.13 WETH
sources: [{ name: 'CowSwap', proportion: '1' }],
buyAsset: WETH,
sellAsset: WBTC,
sellAssetAccountNumber: 0,
receiveAddress: 'address11'
receiveAddress: 'address11',
feeAmountInSellToken: '17238',
sellAmountWithoutFee: '99982762'
}

const defaultDeps: CowSwapperDeps = {
Expand Down
39 changes: 15 additions & 24 deletions packages/swapper/src/swappers/cow/cowBuildTrade/cowBuildTrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fromAssetId } from '@shapeshiftoss/caip'
import { KnownChainIds } from '@shapeshiftoss/types'
import { AxiosResponse } from 'axios'

import { BuildTradeInput, SwapError, SwapErrorTypes, Trade } from '../../../api'
import { BuildTradeInput, SwapError, SwapErrorTypes } from '../../../api'
import { erc20AllowanceAbi } from '../../utils/abi/erc20Allowance-abi'
import { bn, bnOrZero } from '../../utils/bignumber'
import {
Expand All @@ -11,7 +11,7 @@ import {
normalizeAmount
} from '../../utils/helpers/helpers'
import { CowSwapperDeps } from '../CowSwapper'
import { CowSwapQuoteResponse } from '../types'
import { CowSwapQuoteResponse, CowTrade } from '../types'
import {
COW_SWAP_VAULT_RELAYER_ADDRESS,
DEFAULT_APP_DATA,
Expand All @@ -24,10 +24,10 @@ import { getNowPlusThirtyMinutesTimestamp, getUsdRate } from '../utils/helpers/h
export async function cowBuildTrade(
deps: CowSwapperDeps,
input: BuildTradeInput
): Promise<Trade<KnownChainIds.EthereumMainnet>> {
): Promise<CowTrade<KnownChainIds.EthereumMainnet>> {
try {
const { sellAsset, buyAsset, sellAmount, sellAssetAccountNumber, wallet } = input
const { adapter, feeAsset, web3 } = deps
const { adapter, web3 } = deps

const { assetReference: sellAssetErc20Address, assetNamespace: sellAssetNamespace } =
fromAssetId(sellAsset.assetId)
Expand Down Expand Up @@ -86,50 +86,41 @@ export async function cowBuildTrade(
contractAddress: sellAssetErc20Address
})

const [feeDataOptions, feeAssetUsdRate, sellAssetUsdRate] = await Promise.all([
const [feeDataOptions, sellAssetUsdRate] = await Promise.all([
adapter.getFeeData({
to: sellAssetErc20Address,
value: '0',
chainSpecific: { from: receiveAddress, contractData: data }
}),
getUsdRate(deps, feeAsset),
getUsdRate(deps, sellAsset)
])
const feeData = feeDataOptions['fast']

// quote.feeAmount is expressed in sellAsset token
// We need fee to be expressed in feeAsset token
// feeInFeeAsset * feeAssetUsdRate = feeInSellAsset * sellAssetUsdRate
// hence feeInFeeAsset = feeInSellAsset * sellAssetUsdRate / feeAssetUsdRate
const feeInSellAsset = bnOrZero(quote.feeAmount).div(
bn(10).exponentiatedBy(sellAsset.precision)
)

// feeInFeeAsset = feeInSellAsset * sellAssetUsdRate / feeAssetUsdRate
const feeInFeeAsset = feeInSellAsset
// calculating trade fee in USD
const tradeFeeFiat = bnOrZero(quote.feeAmount)
.div(bn(10).exponentiatedBy(sellAsset.precision))
.multipliedBy(bnOrZero(sellAssetUsdRate))
.div(bnOrZero(feeAssetUsdRate))
.toString()

// taking precision into account
const fee = feeInFeeAsset.multipliedBy(bn(10).exponentiatedBy(feeAsset.precision)).toString()

const trade: Trade<KnownChainIds.EthereumMainnet> = {
const trade: CowTrade<KnownChainIds.EthereumMainnet> = {
rate,
feeData: {
fee,
fee: '0', // no miner fee for CowSwap
chainSpecific: {
estimatedGas: feeData.chainSpecific.gasLimit,
gasPrice: feeData.chainSpecific.gasPrice
},
tradeFee: '0'
tradeFee: tradeFeeFiat
},
sellAmount: normalizedSellAmount,
buyAmount: quote.buyAmount,
sources: DEFAULT_SOURCE,
buyAsset,
sellAsset,
sellAssetAccountNumber,
receiveAddress
receiveAddress,
feeAmountInSellToken: quote.feeAmount,
sellAmountWithoutFee: quote.sellAmount
}

const allowanceRequired = await getAllowanceRequired({
Expand Down
Loading

0 comments on commit b9ce707

Please sign in to comment.