Skip to content

Commit

Permalink
[Wallet] Migrate app view functions to contractkit (#1381)
Browse files Browse the repository at this point in the history
  • Loading branch information
annakaz committed Oct 25, 2019
1 parent 167d6c4 commit a6a7269
Show file tree
Hide file tree
Showing 15 changed files with 348 additions and 306 deletions.
1 change: 1 addition & 0 deletions packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
},
"dependencies": {
"@celo/client": "9575a01",
"@celo/contractkit": "^0.1.6",
"@celo/react-components": "1.0.0",
"@celo/react-native-sms-retriever": "git+https://github.com/celo-org/react-native-sms-retriever#d3a2fdb",
"@celo/utils": "^0.1.1",
Expand Down
3 changes: 2 additions & 1 deletion packages/mobile/rn-cli.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const root = path.resolve(cwd, '../..')
const escapedRoot = escapeStringRegexp(root)
const rnRegex = new RegExp(`${escapedRoot}\/node_modules\/(react-native)\/.*`)
const celoRegex = new RegExp(
`${escapedRoot}\/packages\/(?!mobile|utils|walletkit|react-components).*`
`${escapedRoot}\/packages\/(?!mobile|utils|walletkit|contractkit|react-components).*`
)
const nestedRnRegex = new RegExp(`.*\/node_modules\/.*\/node_modules\/(react-native)\/.*`)
const componentsRnRegex = new RegExp(`.*react-components\/node_modules\/(react-native)\/.*`)
Expand All @@ -24,6 +24,7 @@ module.exports = {
extraNodeModules: {
...nodeLibs,
'crypto-js': path.resolve(cwd, 'node_modules/crypto-js'),
fs: require.resolve('react-native-fs'),
'isomorphic-fetch': require.resolve('cross-fetch'),
net: require.resolve('react-native-tcp'),
'react-native': path.resolve(cwd, 'node_modules/react-native'),
Expand Down
2 changes: 2 additions & 0 deletions packages/mobile/src/exchange/Activity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { RootState } from 'src/redux/reducers'
import { resetStandbyTransactions } from 'src/transactions/actions'
import { StandbyTransaction, TransactionTypes } from 'src/transactions/reducer'
import TransactionFeed, { FeedType } from 'src/transactions/TransactionFeed'
import Logger from 'src/utils/Logger'
import { currentAccountSelector } from 'src/web3/selectors'

interface DispatchProps {
Expand All @@ -29,6 +30,7 @@ function filterToExchangeTxs(tx: StandbyTransaction) {
export class Activity extends React.Component<Props> {
componentDidMount() {
this.props.resetStandbyTransactions()
Logger.info('Activity feed', JSON.stringify(transactionQuery))
}

render() {
Expand Down
251 changes: 0 additions & 251 deletions packages/mobile/src/exchange/actions.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,6 @@
import {
ContractUtils,
getExchangeContract,
getGoldTokenContract,
getStableTokenContract,
} from '@celo/walletkit'
import { Exchange as ExchangeType } from '@celo/walletkit/types/Exchange'
import { GoldToken as GoldTokenType } from '@celo/walletkit/types/GoldToken'
import { StableToken as StableTokenType } from '@celo/walletkit/types/StableToken'
import BigNumber from 'bignumber.js'
import { call, put, select } from 'redux-saga/effects'
import { showError } from 'src/alert/actions'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { ExchangeRatePair } from 'src/exchange/reducer'
import { CURRENCY_ENUM } from 'src/geth/consts'
import { RootState } from 'src/redux/reducers'
import {
addStandbyTransaction,
generateStandbyTransactionId,
removeStandbyTransaction,
} from 'src/transactions/actions'
import { TransactionStatus, TransactionTypes } from 'src/transactions/reducer'
import { sendAndMonitorTransaction } from 'src/transactions/saga'
import { sendTransaction } from 'src/transactions/send'
import { getRateForMakerToken, getTakerAmount } from 'src/utils/currencyExchange'
import { roundDown } from 'src/utils/formatting'
import Logger from 'src/utils/Logger'
import { web3 } from 'src/web3/contracts'
import { getConnectedAccount, getConnectedUnlockedAccount } from 'src/web3/saga'
import * as util from 'util'

const TAG = 'exchange/actions'
const LARGE_DOLLARS_SELL_AMOUNT_IN_WEI = new BigNumber(1000 * 1000000000000000000) // To estimate exchange rate from exchange contract
const LARGE_GOLD_SELL_AMOUNT_IN_WEI = new BigNumber(100 * 1000000000000000000)
const EXCHANGE_DIFFERENCE_TOLERATED = 0.01 // Maximum difference between actual and displayed takerAmount

export enum Actions {
FETCH_EXCHANGE_RATE = 'EXCHANGE/FETCH_EXCHANGE_RATE',
Expand Down Expand Up @@ -70,223 +38,4 @@ export const exchangeTokens = (
makerToken,
makerAmount,
})

export type ActionTypes = SetExchangeRateAction | ExchangeTokensAction

export function* doFetchExchangeRate(makerAmount?: BigNumber, makerToken?: CURRENCY_ENUM) {
Logger.debug(TAG, 'Calling @doFetchExchangeRate')

let dollarMakerAmount: BigNumber
let goldMakerAmount: BigNumber
if (makerAmount && makerToken === CURRENCY_ENUM.GOLD) {
dollarMakerAmount = LARGE_DOLLARS_SELL_AMOUNT_IN_WEI
goldMakerAmount = makerAmount
} else if (makerAmount && makerToken === CURRENCY_ENUM.DOLLAR) {
dollarMakerAmount = makerAmount
goldMakerAmount = LARGE_GOLD_SELL_AMOUNT_IN_WEI
} else {
dollarMakerAmount = LARGE_DOLLARS_SELL_AMOUNT_IN_WEI
goldMakerAmount = LARGE_GOLD_SELL_AMOUNT_IN_WEI
if (makerAmount || makerToken) {
Logger.debug(
TAG,
'Using default makerAmount estimates. Need both makerAmount and makerToken to override. '
)
}
}

try {
yield call(getConnectedAccount)

const dollarMakerExchangeRate: BigNumber = yield call(
ContractUtils.getExchangeRate,
web3,
CURRENCY_ENUM.DOLLAR,
new BigNumber(dollarMakerAmount)
)
const goldMakerExchangeRate: BigNumber = yield call(
ContractUtils.getExchangeRate,
web3,
CURRENCY_ENUM.GOLD,
new BigNumber(goldMakerAmount)
)

if (!dollarMakerExchangeRate || !goldMakerExchangeRate) {
Logger.error(TAG, 'Invalid exchange rate')
throw new Error('Invalid exchange rate')
}

Logger.debug(
TAG,
`Retrieved exchange rate:
${dollarMakerExchangeRate.toString()} gold per dollar, estimated at ${dollarMakerAmount}
${goldMakerExchangeRate.toString()} dollar per gold, estimated at ${goldMakerAmount}`
)

yield put(
setExchangeRate({
goldMaker: goldMakerExchangeRate.toString(),
dollarMaker: dollarMakerExchangeRate.toString(),
})
)
} catch (error) {
Logger.error(TAG, 'Error fetching exchange rate', error)
yield put(showError(ErrorMessages.EXCHANGE_RATE_FAILED))
}
}

export function* exchangeGoldAndStableTokens(action: ExchangeTokensAction) {
Logger.debug(`${TAG}@exchangeGoldAndStableTokens`, 'Exchanging gold and stable CURRENCY_ENUM')
const { makerToken, makerAmount } = action
Logger.debug(TAG, `Exchanging ${makerAmount.toString()} of CURRENCY_ENUM ${makerToken}`)
let txId: string | null = null
try {
const account: string = yield call(getConnectedUnlockedAccount)
const exchangeRatePair: ExchangeRatePair = yield select(
(state: RootState) => state.exchange.exchangeRatePair
)
const exchangeRate = getRateForMakerToken(exchangeRatePair, makerToken)
if (!exchangeRate) {
Logger.error(TAG, 'Invalid exchange rate from exchange contract')
return
}
if (exchangeRate.isZero()) {
Logger.error(TAG, 'Cannot do exchange with rate of 0. Stopping.')
throw new Error('Invalid exchange rate')
}

txId = yield createStandbyTx(makerToken, makerAmount, exchangeRate, account)

const goldTokenContract: GoldTokenType = yield call(getGoldTokenContract, web3)
const stableTokenContract: StableTokenType = yield call(getStableTokenContract, web3)
const exchangeContract: ExchangeType = yield call(getExchangeContract, web3)

const makerTokenContract =
makerToken === CURRENCY_ENUM.DOLLAR ? stableTokenContract : goldTokenContract

const convertedMakerAmount: BigNumber = yield call(
convertToContractDecimals,
makerAmount,
makerTokenContract
)
const sellGold = makerToken === CURRENCY_ENUM.GOLD

const updatedExchangeRate: BigNumber = yield call(
// Updating with actual makerAmount, rather than conservative estimate displayed
ContractUtils.getExchangeRate,
web3,
makerToken,
convertedMakerAmount
)

const exceedsExpectedSize =
makerToken === CURRENCY_ENUM.GOLD
? convertedMakerAmount.isGreaterThan(LARGE_GOLD_SELL_AMOUNT_IN_WEI)
: convertedMakerAmount.isGreaterThan(LARGE_DOLLARS_SELL_AMOUNT_IN_WEI)

if (exceedsExpectedSize) {
Logger.error(
TAG,
`Displayed exchange rate was estimated with a smaller makerAmount than actual ${convertedMakerAmount}`
)
// Note that exchange will still go through if makerAmount difference is within EXCHANGE_DIFFERENCE_TOLERATED
}

// Ensure the user gets makerAmount at least as good as displayed (rounded to EXCHANGE_DIFFERENCE_TOLERATED)
const minimumTakerAmount = getTakerAmount(makerAmount, exchangeRate).minus(
EXCHANGE_DIFFERENCE_TOLERATED
)
const updatedTakerAmount = getTakerAmount(makerAmount, updatedExchangeRate)
if (minimumTakerAmount.isGreaterThan(updatedTakerAmount)) {
Logger.error(
TAG,
`Not receiving enough ${makerToken} due to change in exchange rate. Exchange failed.`
)
yield put(showError(ErrorMessages.EXCHANGE_RATE_CHANGE))
return
}

const takerTokenContract =
makerToken === CURRENCY_ENUM.DOLLAR ? goldTokenContract : stableTokenContract
const convertedTakerAmount: BigNumber = roundDown(
yield call(convertToContractDecimals, minimumTakerAmount, takerTokenContract),
0
)
Logger.debug(
TAG,
`Will receive at least ${convertedTakerAmount}
wei for ${convertedMakerAmount} wei of ${makerToken}`
)

let approveTx
if (makerToken === CURRENCY_ENUM.GOLD) {
approveTx = goldTokenContract.methods.approve(
exchangeContract._address,
convertedMakerAmount.toString()
)
} else if (makerToken === CURRENCY_ENUM.DOLLAR) {
approveTx = stableTokenContract.methods.approve(
exchangeContract._address,
convertedMakerAmount.toString()
)
} else {
Logger.error(TAG, `Unexpected maker token ${makerToken}`)
return
}
yield call(sendTransaction, approveTx, account, TAG, 'approval')
Logger.debug(TAG, `Transaction approved: ${util.inspect(approveTx.arguments)}`)

const tx = exchangeContract.methods.exchange(
convertedMakerAmount.toString(),
convertedTakerAmount.toString(),
sellGold
)

if (!txId) {
Logger.error(TAG, 'No txId. Did not exchange.')
return
}
yield call(sendAndMonitorTransaction, txId, tx, account)
} catch (error) {
Logger.error(TAG, 'Error doing exchange', error)
if (txId) {
yield put(removeStandbyTransaction(txId))
}

if (error.message === ErrorMessages.INCORRECT_PIN) {
yield put(showError(ErrorMessages.INCORRECT_PIN))
} else {
yield put(showError(ErrorMessages.EXCHANGE_FAILED))
}
}
}

function* createStandbyTx(
makerToken: CURRENCY_ENUM,
makerAmount: BigNumber,
exchangeRate: BigNumber,
account: string
) {
const takerAmount = getTakerAmount(makerAmount, exchangeRate, 2)
const txId = generateStandbyTransactionId(account)
yield put(
addStandbyTransaction({
id: txId,
type: TransactionTypes.EXCHANGE,
status: TransactionStatus.Pending,
inSymbol: makerToken,
inValue: makerAmount.toString(),
outSymbol: makerToken === CURRENCY_ENUM.DOLLAR ? CURRENCY_ENUM.GOLD : CURRENCY_ENUM.DOLLAR,
outValue: takerAmount.toString(),
timestamp: Math.floor(Date.now() / 1000),
})
)
return txId
}

async function convertToContractDecimals(value: BigNumber | string | number, contract: any) {
// TODO(Rossy): Move this function to SDK and cache this decimals amount
const decimals = await contract.methods.decimals().call()
const one = new BigNumber(10).pow(new BigNumber(decimals).toNumber())
return one.times(value)
}
3 changes: 3 additions & 0 deletions packages/mobile/src/exchange/reducer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Actions, ActionTypes } from 'src/exchange/actions'
import { RootState } from 'src/redux/reducers'

export interface ExchangeRatePair {
goldMaker: string // number of dollarTokens received for one goldToken
Expand All @@ -13,6 +14,8 @@ const initialState = {
exchangeRatePair: null,
}

export const exchangeRatePairSelector = (state: RootState) => state.exchange.exchangeRatePair

export const reducer = (state: State | undefined = initialState, action: ActionTypes): State => {
switch (action.type) {
case Actions.SET_EXCHANGE_RATE:
Expand Down
Loading

0 comments on commit a6a7269

Please sign in to comment.