Skip to content
This repository has been archived by the owner on Apr 11, 2023. It is now read-only.

Commit

Permalink
add rest of approvalNeeded and tests; extract web3 logic to a helper …
Browse files Browse the repository at this point in the history
…function
  • Loading branch information
DaoDev44 committed Oct 5, 2021
1 parent cc48480 commit 3fde9e7
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 16 deletions.
2 changes: 1 addition & 1 deletion packages/swapper/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
Quote,
SwapperType,
ExecQuoteInput,
ExecQuoteOutput,
ExecQuoteOutput
} from '@shapeshiftoss/types'

export class SwapError extends Error {}
Expand Down
8 changes: 7 additions & 1 deletion packages/swapper/src/swappers/thorchain/ThorchainSwapper.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { Asset, SwapperType, Quote, ExecQuoteOutput, ApprovalNeededOutput } from '@shapeshiftoss/types'
import {
Asset,
SwapperType,
Quote,
ExecQuoteOutput,
ApprovalNeededOutput
} from '@shapeshiftoss/types'
import { Swapper } from '../../api'

export class ThorchainSwapper implements Swapper {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ import { ChainTypes } from '@shapeshiftoss/types'
import { ChainAdapterManager } from '@shapeshiftoss/chain-adapters'
import { approvalNeeded } from './approvalNeeded'
import { setupQuote } from '../utils/test-data/setupSwapQuote'
import { zrxService } from '../utils/zrxService'
import { APPROVAL_GAS_LIMIT } from '../utils/constants'

jest.mock('web3')
jest.mock('axios', () => ({
create: jest.fn(() => ({
get: jest.fn()
}))
}))

// @ts-ignore
Web3.mockImplementation(() => ({
eth: {
Contract: jest.fn(() => ({
methods: {
allowance: jest.fn(() => ({
call: jest.fn()
}))
}
}))
}
}))

const setup = () => {
const unchainedUrls = {
Expand All @@ -18,13 +40,76 @@ const setup = () => {
}

describe('approvalNeeded', () => {
const args = setup()
const wallet = <HDWallet>{}
const { web3, adapterManager } = setup()
const args = { web3, adapterManager }
const walletAddress = '0xc770eefad204b5180df6a14ee197d99d808ee52d'
const wallet = ({
ethGetAddress: jest.fn(() => Promise.resolve(walletAddress))
} as unknown) as HDWallet

const { quoteInput, sellAsset } = setupQuote()

it('returns false if sellAsset symbol is ETH', async () => {
const input = { quote: { ...quoteInput, sellAsset: { ...sellAsset, symbol: 'ETH' } }, wallet }
const input = {
quote: { ...quoteInput, sellAsset: { ...sellAsset, symbol: 'ETH' } },
wallet
}

expect(await approvalNeeded(args, input)).toEqual({ approvalNeeded: false })
})

it('returns false if sellAsset chain is not ETH', async () => {
const input = {
quote: { ...quoteInput, sellAsset: { ...sellAsset, chain: ChainTypes.Bitcoin } },
wallet
}

expect(await approvalNeeded(args, input)).toEqual({ approvalNeeded: false })
})

it('returns false if allowanceOnChain is greater than quote.sellAmount', async () => {
const allowanceOnChain = '50'
const data = { gasPrice: '1000', allowanceTarget: '10' }
const input = {
quote: { ...quoteInput, sellAmount: '10' },
wallet
}
;(web3.eth.Contract as jest.Mock<unknown>).mockImplementation(() => ({
methods: {
allowance: jest.fn(() => ({
call: jest.fn(() => allowanceOnChain)
}))
}
}))
;(zrxService.get as jest.Mock<unknown>).mockReturnValue(Promise.resolve({ data }))

expect(await approvalNeeded(args, input)).toEqual({
approvalNeeded: false,
gas: APPROVAL_GAS_LIMIT,
gasPrice: data.gasPrice
})
})

it('returns true if allowanceOnChain is less than quote.sellAmount', async () => {
const allowanceOnChain = '5'
const data = { gasPrice: '1000', allowanceTarget: '10' }
const input = {
quote: { ...quoteInput, sellAmount: '10' },
wallet
}
;(web3.eth.Contract as jest.Mock<unknown>).mockImplementation(() => ({
methods: {
allowance: jest.fn(() => ({
call: jest.fn(() => allowanceOnChain)
}))
}
}))
;(zrxService.get as jest.Mock<unknown>).mockReturnValue(Promise.resolve({ data }))

expect(await approvalNeeded(args, input)).toEqual({
approvalNeeded: true,
gas: APPROVAL_GAS_LIMIT,
gasPrice: data.gasPrice
})
})
})
65 changes: 60 additions & 5 deletions packages/swapper/src/swappers/zrx/approvalNeeded/approvalNeeded.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { BigNumber } from 'bignumber.js'
import { AxiosResponse } from 'axios'
import { ChainAdapter } from '@shapeshiftoss/chain-adapters'
import { ApprovalNeededInput, ApprovalNeededOutput, ChainTypes, QuoteResponse } from '@shapeshiftoss/types'
import { DEFAULT_SLIPPAGE, AFFILIATE_ADDRESS, DEFAULT_ETH_PATH } from '../utils/constants'
import {
ApprovalNeededInput,
ApprovalNeededOutput,
ChainTypes,
QuoteResponse
} from '@shapeshiftoss/types'
import {
AFFILIATE_ADDRESS,
APPROVAL_BUY_AMOUNT,
APPROVAL_GAS_LIMIT,
DEFAULT_ETH_PATH,
DEFAULT_SLIPPAGE
} from '../utils/constants'

import { ZrxSwapperDeps } from '../ZrxSwapper'
import { zrxService } from '../utils/zrxService'
const APPROVAL_BUY_AMOUNT = '100000000000000000' // A valid buy amount - 0.1 ETH
import { getERC20Allowance } from '../utils/helpers/helpers'
import { erc20AllowanceAbi } from '../utils/abi/erc20Allowance-abi'

export async function approvalNeeded(
{ adapterManager }: ZrxSwapperDeps,
{ adapterManager, web3 }: ZrxSwapperDeps,
{ quote, wallet }: ApprovalNeededInput
): Promise<ApprovalNeededOutput> {
const { sellAsset } = quote
Expand All @@ -17,6 +30,48 @@ export async function approvalNeeded(
return { approvalNeeded: false }
}

const adapter: ChainAdapter = adapterManager.byChain(sellAsset.chain)
const receiveAddress = await adapter.getAddress({ wallet, path: DEFAULT_ETH_PATH })

return { approvalNeeded: true }
/**
* /swap/v1/quote
* params: {
* sellToken: contract address (or symbol) of token to sell
* buyToken: contractAddress (or symbol) of token to buy
* sellAmount?: integer string value of the smallest increment of the sell token
* buyAmount?: integer string value of the smallest incremtent of the buy token
* }
*/
const quoteResponse: AxiosResponse<QuoteResponse> = await zrxService.get<QuoteResponse>(
'/swap/v1/quote',
{
params: {
buyToken: 'ETH',
sellToken: quote.sellAsset.tokenId || quote.sellAsset.symbol || quote.sellAsset.chain,
buyAmount: APPROVAL_BUY_AMOUNT,
takerAddress: receiveAddress,
slippagePercentage: DEFAULT_SLIPPAGE,
skipValidation: true,
affiliateAddress: AFFILIATE_ADDRESS
}
}
)
const { data } = quoteResponse

const allowanceResult = getERC20Allowance(
{ web3, erc20AllowanceAbi },
{
tokenId: quote.sellAsset.tokenId as string,
spenderAddress: data.allowanceTarget as string,
ownerAddress: receiveAddress
}
)

const allowanceOnChain = new BigNumber(allowanceResult || '0')

return {
approvalNeeded: allowanceOnChain.lt(new BigNumber(quote.sellAmount || 1)),
gas: APPROVAL_GAS_LIMIT,
gasPrice: data.gasPrice
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { SwapError } from '../../..'
import { Quote, QuoteResponse, BuildQuoteTxInput } from '@shapeshiftoss/types'
import { ZrxSwapperDeps } from '../ZrxSwapper'
import { applyAxiosRetry } from '../utils/applyAxiosRetry'
import { erc20AllowanceAbi } from '../utils/abi/erc20-abi'
import { erc20AllowanceAbi } from '../utils/abi/erc20Allowance-abi'
import { normalizeAmount, getAllowanceRequired } from '../utils/helpers/helpers'
import { zrxService } from '../utils/zrxService'
import {
Expand Down
1 change: 1 addition & 0 deletions packages/swapper/src/swappers/zrx/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export const DEFAULT_SLIPPAGE = '3.0' // 3%
export const MAX_SLIPPAGE = '30.0' // 30%
export const DEFAULT_ETH_PATH = `m/44'/60'/0'/0/0` // TODO: remove when `adapter.getAddress` changes to take an account instead of default path
export const AFFILIATE_ADDRESS = '0xc770eefad204b5180df6a14ee197d99d808ee52d'
export const APPROVAL_BUY_AMOUNT = '100000000000000000' // A valid buy amount - 0.1 ETH
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Web3 from 'web3'
import BigNumber from 'bignumber.js'
import { setupQuote } from '../test-data/setupSwapQuote'
import { erc20AllowanceAbi } from '../../utils/abi/erc20-abi'
import { erc20AllowanceAbi } from '../abi/erc20Allowance-abi'
import { normalizeAmount, getAllowanceRequired } from '../helpers/helpers'

jest.mock('web3')
Expand Down
30 changes: 26 additions & 4 deletions packages/swapper/src/swappers/zrx/utils/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ export type GetAllowanceRequiredArgs = {
erc20AllowanceAbi: AbiItem[]
}

export type GetERC20AllowanceDeps = {
erc20AllowanceAbi: AbiItem[]
web3: Web3
}

export type GetERC20AllowanceArgs = {
tokenId: string
ownerAddress: string
spenderAddress: string
}

/**
* Very large amounts like those found in ERC20s with a precision of 18 get converted
* to exponential notation ('1.6e+21') in javascript. The 0x api doesn't play well with
Expand All @@ -23,6 +34,14 @@ export const normalizeAmount = (amount: string | undefined): string | undefined
return new BigNumber(amount).toNumber().toLocaleString('fullwide', { useGrouping: false })
}

export const getERC20Allowance = (
{ erc20AllowanceAbi, web3 }: GetERC20AllowanceDeps,
{ tokenId, ownerAddress, spenderAddress }: GetERC20AllowanceArgs
) => {
const erc20Contract = new web3.eth.Contract(erc20AllowanceAbi, tokenId)
return erc20Contract.methods.allowance(ownerAddress, spenderAddress).call()
}

export const getAllowanceRequired = async ({
quote,
web3,
Expand All @@ -32,11 +51,14 @@ export const getAllowanceRequired = async ({
return new BigNumber(0)
}

const ownerAddress = quote.receiveAddress
const spenderAddress = quote.allowanceContract
const ownerAddress = quote.receiveAddress as string
const spenderAddress = quote.allowanceContract as string
const tokenId = quote.sellAsset.tokenId as string

const erc20Contract = new web3.eth.Contract(erc20AllowanceAbi, quote.sellAsset.tokenId)
const allowanceOnChain = erc20Contract.methods.allowance(ownerAddress, spenderAddress).call()
const allowanceOnChain = getERC20Allowance(
{ web3, erc20AllowanceAbi },
{ ownerAddress, spenderAddress, tokenId }
)

if (allowanceOnChain === '0') {
return new BigNumber(quote.sellAmount || 0)
Expand Down

0 comments on commit 3fde9e7

Please sign in to comment.