Skip to content

Commit

Permalink
Finance: fix token fallbacks and error handling (#813)
Browse files Browse the repository at this point in the history
Fixes aragon/client#734.

With aragon/aragon.js#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.
  • Loading branch information
sohkai authored Apr 18, 2019
1 parent 0bf6ff4 commit f207a7e
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 120 deletions.
111 changes: 59 additions & 52 deletions apps/finance/app/src/components/NewTransfer/Deposit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 &&
Expand Down
34 changes: 18 additions & 16 deletions apps/finance/app/src/lib/token-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
104 changes: 52 additions & 52 deletions apps/finance/app/src/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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()
)
}

Expand Down

0 comments on commit f207a7e

Please sign in to comment.