From dbd3609c8f73ccb89f842a5e31fd695e88482ca9 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Thu, 18 Apr 2019 10:05:56 +0200 Subject: [PATCH] Finance: fix token fallbacks and error handling (#813) Fixes https://github.com/aragon/aragon/issues/734. With https://github.com/aragon/aragon.js/pull/277 we now get the actual errors back from the RPC when a call goes wrong (e.g. naive `token.symbol()` on DAI) and we need to handle these explicitly. This PR converts a lot of the token fallback-related bits into simpler Promise-based code and adds explicit handlers for their error cases. --- .../app/src/components/NewTransfer/Deposit.js | 111 ++++++++++-------- apps/finance/app/src/lib/token-utils.js | 34 +++--- apps/finance/app/src/script.js | 104 ++++++++-------- 3 files changed, 129 insertions(+), 120 deletions(-) diff --git a/apps/finance/app/src/components/NewTransfer/Deposit.js b/apps/finance/app/src/components/NewTransfer/Deposit.js index 4f4c4a34b7..9671a79d57 100644 --- a/apps/finance/app/src/components/NewTransfer/Deposit.js +++ b/apps/finance/app/src/components/NewTransfer/Deposit.js @@ -39,6 +39,19 @@ const TOKEN_ALLOWANCE_WEBSITE = 'https://tokenallowance.io/' const tokenAbi = [].concat(tokenBalanceOfAbi, tokenDecimalsAbi, tokenSymbolAbi) +const renderBalanceForSelectedToken = selectedToken => { + const { decimals, loading, symbol, userBalance } = selectedToken.data + if (loading || !userBalance) { + return '' + } + + return userBalance === '-1' + ? `Your balance could not be found for ${symbol}` + : `You have ${ + userBalance === '0' ? 'no' : fromDecimals(userBalance, decimals) + } ${symbol} available` +} + const initialState = { amount: { error: NO_ERROR, @@ -129,58 +142,61 @@ class Deposit extends React.Component { const { selectedToken } = this.state return selectedToken.value && !selectedToken.data.loading } - loadTokenData(address) { + async loadTokenData(address) { const { api, network, connectedAccount } = this.props // ETH if (addressesEqual(address, ETHER_TOKEN_FAKE_ADDRESS)) { - return new Promise((resolve, reject) => - api.web3Eth('getBalance', connectedAccount).subscribe( - ethBalance => - resolve({ - decimals: 18, - loading: false, - symbol: 'ETH', - userBalance: ethBalance, - }), - reject - ) - ) - } - - // Tokens - const token = api.external(address, tokenAbi) - - return new Promise(async (resolve, reject) => { - const userBalance = await token.balanceOf(connectedAccount).toPromise() - - const decimalsFallback = - tokenDataFallback(address, 'decimals', network.type) || '0' - const symbolFallback = - tokenDataFallback(address, 'symbol', network.type) || '' + const userBalance = await api + .web3Eth('getBalance', connectedAccount) + .toPromise() + .catch(() => '-1') - const tokenData = { - userBalance, - decimals: parseInt(decimalsFallback, 10), + return { + decimals: 18, loading: false, - symbol: symbolFallback, + symbol: 'ETH', + userBalance, } + } - const [tokenSymbol, tokenDecimals] = await Promise.all([ - getTokenSymbol(api, address), - token.decimals().toPromise(), - ]) + // Tokens + const token = api.external(address, tokenAbi) + const userBalance = await token + .balanceOf(connectedAccount) + .toPromise() + .catch(() => '-1') + + const decimalsFallback = + tokenDataFallback(address, 'decimals', network.type) || '0' + const symbolFallback = + tokenDataFallback(address, 'symbol', network.type) || '' + + const tokenData = { + userBalance, + decimals: parseInt(decimalsFallback, 10), + loading: false, + symbol: symbolFallback, + } - // If symbol or decimals are resolved, overwrite the fallbacks - if (tokenSymbol) { - tokenData.symbol = tokenSymbol - } - if (tokenDecimals) { - tokenData.decimals = parseInt(tokenDecimals, 10) - } + const [tokenSymbol, tokenDecimals] = await Promise.all([ + getTokenSymbol(api, address).catch(() => ''), + token + .decimals() + .toPromise() + .then(decimals => parseInt(decimals, 10)) + .catch(() => ''), + ]) + + // If symbol or decimals are resolved, overwrite the fallbacks + if (tokenSymbol) { + tokenData.symbol = tokenSymbol + } + if (tokenDecimals) { + tokenData.decimals = tokenDecimals + } - resolve(tokenData) - }) + return tokenData } validateInputs({ amount, selectedToken } = {}) { amount = amount || this.state.amount @@ -255,16 +271,7 @@ class Deposit extends React.Component { const selectedTokenIsAddress = isAddress(selectedToken.value) const showTokenBadge = selectedTokenIsAddress && selectedToken.coerced - const tokenBalanceMessage = selectedToken.data.userBalance - ? `You have ${ - selectedToken.data.userBalance === '0' - ? 'no' - : fromDecimals( - selectedToken.data.userBalance, - selectedToken.data.decimals - ) - } ${selectedToken.data.symbol} available` - : '' + const tokenBalanceMessage = renderBalanceForSelectedToken(selectedToken) const ethSelected = selectedTokenIsAddress && diff --git a/apps/finance/app/src/lib/token-utils.js b/apps/finance/app/src/lib/token-utils.js index ce5f4e5972..7704b4c31b 100644 --- a/apps/finance/app/src/lib/token-utils.js +++ b/apps/finance/app/src/lib/token-utils.js @@ -47,29 +47,31 @@ export const tokenDataFallback = (tokenAddress, fieldName, networkType) => { export async function getTokenSymbol(app, address) { // Symbol is optional; note that aragon.js doesn't return an error (only an falsey value) when // getting this value fails - let token = app.external(address, tokenSymbolAbi) - let tokenSymbol = await token.symbol().toPromise() - if (tokenSymbol) { - return tokenSymbol + let tokenSymbol + try { + const token = app.external(address, tokenSymbolAbi) + tokenSymbol = await token.symbol().toPromise() + } catch (err) { + // Some tokens (e.g. DS-Token) use bytes32 as the return type for symbol(). + const token = app.external(address, tokenSymbolBytesAbi) + tokenSymbol = toUtf8(await token.symbol().toPromise()) } - // Some tokens (e.g. DS-Token) use bytes32 as the return type for symbol(). - token = app.external(address, tokenSymbolBytesAbi) - tokenSymbol = await token.symbol().toPromise() - return tokenSymbol ? toUtf8(tokenSymbol) : null + return tokenSymbol || null } export async function getTokenName(app, address) { // Name is optional; note that aragon.js doesn't return an error (only an falsey value) when // getting this value fails - let token = app.external(address, tokenNameAbi) - let tokenName = await token.name().toPromise() - if (tokenName) { - return tokenName + let tokenName + try { + const token = app.external(address, tokenNameAbi) + tokenName = await token.name().toPromise() + } catch (err) { + // Some tokens (e.g. DS-Token) use bytes32 as the return type for name(). + const token = app.external(address, tokenNameBytesAbi) + tokenName = toUtf8(await token.name().toPromise()) } - // Some tokens (e.g. DS-Token) use bytes32 as the return type for name(). - token = app.external(address, tokenNameBytesAbi) - tokenName = await token.name().toPromise() - return tokenName ? toUtf8(tokenName) : null + return tokenName || null } diff --git a/apps/finance/app/src/script.js b/apps/finance/app/src/script.js index 4d4763ea38..d87e2d4c63 100644 --- a/apps/finance/app/src/script.js +++ b/apps/finance/app/src/script.js @@ -28,7 +28,7 @@ const INITIALIZATION_TRIGGER = Symbol('INITIALIZATION_TRIGGER') const TEST_TOKEN_ADDRESSES = [] const tokenContracts = new Map() // Addr -> External contract const tokenDecimals = new Map() // External contract -> decimals -const tokenName = new Map() // External contract -> name +const tokenNames = new Map() // External contract -> name const tokenSymbols = new Map() // External contract -> symbol const ETH_CONTRACT = Symbol('ETH_CONTRACT') @@ -89,7 +89,7 @@ async function initialize(vaultAddress, ethAddress) { // Set up ETH placeholders tokenContracts.set(ethAddress, ETH_CONTRACT) tokenDecimals.set(ETH_CONTRACT, '18') - tokenName.set(ETH_CONTRACT, 'Ether') + tokenNames.set(ETH_CONTRACT, 'Ether') tokenSymbols.set(ETH_CONTRACT, 'ETH') return createStore({ @@ -317,62 +317,62 @@ function loadTokenBalance(tokenAddress, { vault }) { return vault.contract.balance(tokenAddress).toPromise() } -function loadTokenDecimals(tokenContract, tokenAddress, { network }) { - return new Promise((resolve, reject) => { - if (tokenDecimals.has(tokenContract)) { - resolve(tokenDecimals.get(tokenContract)) - } else { - const fallback = - tokenDataFallback(tokenAddress, 'decimals', network.type) || '0' - - tokenContract.decimals().subscribe( - (decimals = fallback) => { - tokenDecimals.set(tokenContract, decimals) - resolve(decimals) - }, - () => { - // Decimals is optional - resolve(fallback) - } - ) - } - }) +async function loadTokenDecimals(tokenContract, tokenAddress, { network }) { + if (tokenDecimals.has(tokenContract)) { + return tokenDecimals.get(tokenContract) + } + + const fallback = + tokenDataFallback(tokenAddress, 'decimals', network.type) || '0' + + let decimals + try { + decimals = (await tokenContract.decimals().toPromise()) || fallback + tokenDecimals.set(tokenContract, decimals) + } catch (err) { + // decimals is optional + decimals = fallback + } + return decimals } -function loadTokenName(tokenContract, tokenAddress, { network }) { - return new Promise((resolve, reject) => { - if (tokenName.has(tokenContract)) { - resolve(tokenName.get(tokenContract)) - } else { - const fallback = - tokenDataFallback(tokenAddress, 'name', network.type) || '' - const name = getTokenName(app, tokenAddress) - resolve(name || fallback) - } - }) +async function loadTokenName(tokenContract, tokenAddress, { network }) { + if (tokenNames.has(tokenContract)) { + return tokenNames.get(tokenContract) + } + const fallback = tokenDataFallback(tokenAddress, 'name', network.type) || '' + + let name + try { + name = (await getTokenName(app, tokenAddress)) || fallback + tokenNames.set(tokenContract, name) + } catch (err) { + // name is optional + name = fallback + } + return name } -function loadTokenSymbol(tokenContract, tokenAddress, { network }) { - return new Promise((resolve, reject) => { - if (tokenSymbols.has(tokenContract)) { - resolve(tokenSymbols.get(tokenContract)) - } else { - const fallback = - tokenDataFallback(tokenAddress, 'symbol', network.type) || '' - const tokenSymbol = getTokenSymbol(app, tokenAddress) - resolve(tokenSymbol || fallback) - } - }) +async function loadTokenSymbol(tokenContract, tokenAddress, { network }) { + if (tokenSymbols.has(tokenContract)) { + return tokenSymbols.get(tokenContract) + } + const fallback = tokenDataFallback(tokenAddress, 'symbol', network.type) || '' + + let symbol + try { + symbol = (await getTokenSymbol(app, tokenAddress)) || fallback + tokenSymbols.set(tokenContract, symbol) + } catch (err) { + // symbol is optional + symbol = fallback + } + return symbol } -function loadTransactionDetails(id) { - return new Promise((resolve, reject) => - app - .call('getTransaction', id) - .subscribe( - transaction => resolve(marshallTransactionDetails(transaction)), - reject - ) +async function loadTransactionDetails(id) { + return marshallTransactionDetails( + await app.call('getTransaction', id).toPromise() ) }