Skip to content

Commit

Permalink
Merge pull request #80 from gnosis/more-error-reporting
Browse files Browse the repository at this point in the history
More error reporting
  • Loading branch information
cag authored Dec 19, 2017
2 parents a4a241a + a3db7f3 commit 7a24ded
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 37 deletions.
4 changes: 3 additions & 1 deletion src/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ export const createScalarEvent = wrapWeb3Function((self) => ({
* @alias Gnosis#publishEventDescription
*/
export async function publishEventDescription (description) {
return await this.ipfs.addJSONAsync(description)
const resultHash = await this.ipfs.addJSONAsync(description)
this.log(`published event description on IPFS at ${resultHash}`)
return resultHash
}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class Gnosis {
* @param {string} [opts.ipfs.host='ipfs.infura.io'] - IPFS node address
* @param {Number} [opts.ipfs.port=5001] - IPFS protocol port
* @param {string} [opts.ipfs.protocol='https'] - IPFS protocol name
* @param {Function} [opts.logger] - A callback for logging. Can also provide 'console' to use `console.log`.
* @returns {Gnosis} An instance of the gnosis.js API
*/
static async create (opts) {
Expand All @@ -89,6 +90,10 @@ class Gnosis {
* @constructor
*/
constructor (opts) {
// Logger setup
const { logger } = opts
this.log = logger == null ? () => {} : logger === 'console' ? console.log : logger

// IPFS instantiation
this.ipfs = utils.promisifyAll(new IPFS(opts.ipfs))

Expand Down
78 changes: 56 additions & 22 deletions src/markets.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {
normalizeWeb3Args,
wrapWeb3Function,
requireEventFromTXResult,
formatCallSignature,
TransactionError,
} from './utils'

/**
Expand Down Expand Up @@ -30,6 +32,34 @@ export const createMarket = wrapWeb3Function((self, opts) => ({
}
}))

const pushDescribedTransaction = async (txInfo, log, opts) => {
const { caller, methodName, methodArgs } = opts
let txHash
try {
txHash = await caller[methodName].sendTransaction(...methodArgs)
log(`got tx hash ${txHash} for call ${
formatCallSignature(opts)
}`)
txInfo.push(Object.assign({ txHash }, opts))
} catch(subError) {
throw new TransactionError(Object.assign({ txHash, subError }, opts))
}
}

const syncDescribedTransactions = async (txInfo, log) =>
(await Promise.all(
txInfo.map(opts => opts.caller.constructor
.syncTransaction(opts.txHash)
.then(res => {
log(`tx ${opts.txHash} synced`)
return res
})
.catch(err =>
new TransactionError(Object.assign({ subError: err }, opts))
)
)
)).map((res, i) => requireEventFromTXResult(res, txInfo[i].requiredEventName))

/**
* Buys outcome tokens. If you have ether and plan on transacting with a market on an event which
* uses EtherToken as collateral, be sure to convert the ether into EtherToken by sending ether to
Expand Down Expand Up @@ -94,28 +124,30 @@ export async function buyOutcomeTokens() {
const marketAllowance = await collateralToken.allowance(buyer, marketAddress, opts)

if(marketAllowance.lt(cost)) {
txInfo.push({
tx: await collateralToken.approve.sendTransaction(marketAddress, approvalResetAmount, approveTxOpts),
contract: this.contracts.Token,
await pushDescribedTransaction(txInfo, this.log, {
caller: collateralToken,
methodName: 'approve',
methodArgs: [marketAddress, approvalResetAmount, approveTxOpts],
requiredEventName: 'Approval',
})
}
} else if(this.web3.toBigNumber(0).lt(approvalAmount)) {
txInfo.push({
tx: await collateralToken.approve.sendTransaction(marketAddress, approvalAmount, approveTxOpts),
contract: this.contracts.Token,
await pushDescribedTransaction(txInfo, this.log, {
caller: collateralToken,
methodName: 'approve',
methodArgs: [marketAddress, approvalAmount, approveTxOpts],
requiredEventName: 'Approval',
})
}

txInfo.push({
tx: await market.buy.sendTransaction(outcomeTokenIndex, outcomeTokenCount, cost, buyTxOpts),
contract: this.contracts.Market,
await pushDescribedTransaction(txInfo, this.log, {
caller: market,
methodName: 'buy',
methodArgs: [outcomeTokenIndex, outcomeTokenCount, cost, buyTxOpts],
requiredEventName: 'OutcomeTokenPurchase',
})

const txRequiredEvents = (await Promise.all(txInfo.map(({ tx, contract }, i) => contract.syncTransaction(tx))))
.map((res, i) => requireEventFromTXResult(res, txInfo[i].requiredEventName))
const txRequiredEvents = await syncDescribedTransactions(txInfo, this.log)
const purchaseEvent = txRequiredEvents[txRequiredEvents.length - 1]

return purchaseEvent.args.outcomeTokenCost.plus(purchaseEvent.args.marketFees)
Expand Down Expand Up @@ -193,28 +225,30 @@ export async function sellOutcomeTokens() {
const marketAllowance = await outcomeToken.allowance(seller, marketAddress, opts)

if(marketAllowance.lt(outcomeTokenCount)) {
txInfo.push({
tx: await outcomeToken.approve.sendTransaction(marketAddress, approvalResetAmount, approveTxOpts),
contract: this.contracts.Token,
await pushDescribedTransaction(txInfo, this.log, {
caller: outcomeToken,
methodName: 'approve',
methodArgs: [marketAddress, approvalResetAmount, approveTxOpts],
requiredEventName: 'Approval',
})
}
} else if(this.web3.toBigNumber(0).lt(approvalAmount)) {
txInfo.push({
tx: await outcomeToken.approve.sendTransaction(marketAddress, approvalAmount, approveTxOpts),
contract: this.contracts.Token,
await pushDescribedTransaction(txInfo, this.log, {
caller: outcomeToken,
methodName: 'approve',
methodArgs: [marketAddress, approvalAmount, approveTxOpts],
requiredEventName: 'Approval',
})
}

txInfo.push({
tx: await market.sell.sendTransaction(outcomeTokenIndex, outcomeTokenCount, minProfit, sellTxOpts),
contract: this.contracts.Market,
await pushDescribedTransaction(txInfo, this.log, {
caller: market,
methodName: 'sell',
methodArgs: [outcomeTokenIndex, outcomeTokenCount, minProfit, sellTxOpts],
requiredEventName: 'OutcomeTokenSale',
})

const txRequiredEvents = (await Promise.all(txInfo.map(({ tx, contract }, i) => contract.syncTransaction(tx))))
.map((res, i) => requireEventFromTXResult(res, txInfo[i].requiredEventName))
const txRequiredEvents = await syncDescribedTransactions(txInfo, this.log)
const saleEvent = txRequiredEvents[txRequiredEvents.length - 1]

return saleEvent.args.outcomeTokenProfit.minus(saleEvent.args.marketFees)
Expand Down
66 changes: 53 additions & 13 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,10 @@ export function wrapWeb3Function(spec) {
const wrappedFn = async function() {
const opts = getOptsFromArgs(arguments)
const speccedOpts = spec(this, opts)
const callMetadata = getWeb3CallMetadata(arguments, opts, speccedOpts)
callMetadata.log = this.log

return await sendTransactionAndGetResult(
getWeb3CallMetadata(arguments, opts, speccedOpts)
)
return await sendTransactionAndGetResult(callMetadata)
}

wrappedFn.estimateGas = async function() {
Expand Down Expand Up @@ -269,21 +269,61 @@ export function requireEventFromTXResult (result, eventName) {
return matchingLogs[0]
}

export function formatCallSignature(opts) {
return `${
opts.caller.constructor.contractName
}(${opts.caller.address.slice(0, 6)}..${opts.caller.address.slice(-4)}).${opts.methodName}(${
opts.methodArgs.map(v => JSON.stringify(v)).join(', ')
})`
}

export class TransactionError extends Error {
constructor(opts) {
super(`${formatCallSignature(opts)}${opts.txHash == null ? '' : `
with transaction hash ${opts.txHash}`}
failed with ${opts.subError}`)

Object.assign(this, opts)

this.name = 'TransactionError'
}
}

export async function sendTransactionAndGetResult (opts) {
opts = opts || {}
let caller, txHash, txResult, matchingLog

let caller = opts.callerContract
if (_.has(caller, 'deployed')) {
caller = await caller.deployed()
}
try {
caller = opts.callerContract
if (_.has(caller, 'deployed')) {
caller = await caller.deployed()
}

txHash = await caller[opts.methodName].sendTransaction(...opts.methodArgs)

if(opts.log != null) {
opts.log(`got tx hash ${txHash} for call ${
formatCallSignature({ caller, methodName: opts.methodName, methodArgs: opts.methodArgs })
}`)
}

let result = await caller[opts.methodName](...opts.methodArgs)
let matchingLog = requireEventFromTXResult(result, opts.eventName)
txResult = await caller.constructor.syncTransaction(txHash)
matchingLog = requireEventFromTXResult(txResult, opts.eventName)

if(opts.resultContract == null)
return matchingLog.args[opts.eventArgName]
else
return await opts.resultContract.at(matchingLog.args[opts.eventArgName])
if(opts.resultContract == null) {
return matchingLog.args[opts.eventArgName]
} else {
opts.log(`tx hash ${txHash.slice(0, 6)}..${txHash.slice(-4)} returned ${opts.resultContract.contractName}(${matchingLog.args[opts.eventArgName]})`)
return await opts.resultContract.at(matchingLog.args[opts.eventArgName])
}
} catch(err) {
throw new TransactionError(Object.assign({
caller, txHash, txResult, matchingLog,
subError: err,
}, opts))
}
}

// I know bluebird does this, but it's heavy
Expand Down
120 changes: 119 additions & 1 deletion test/test_gnosis.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import _ from 'lodash'
import Gnosis from '../src/index'
import TestRPC from 'ethereumjs-testrpc'

const options = process.env.GNOSIS_OPTIONS ? JSON.parse(process.env.GNOSIS_OPTIONS) : null
const options = process.env.GNOSIS_OPTIONS ? JSON.parse(process.env.GNOSIS_OPTIONS) : {}

const { requireEventFromTXResult, sendTransactionAndGetResult, Decimal } = Gnosis
const ONE = Math.pow(2, 64)
Expand Down Expand Up @@ -92,6 +92,124 @@ describe('Gnosis', function () {
assert(gnosis.standardMarketFactory.gasStats)
})

it('reports more informative error messages and logs messages', async () => {
const logs = []

const gnosis = await Gnosis.create({
logger: (s) => { logs.push(s) },
})

const netOutcomeTokensSold = [0, 0]
const feeFactor = 5000 // 0.5%
const participants = gnosis.web3.eth.accounts.slice(0, 4)

const ipfsHash = await gnosis.publishEventDescription(description)

assert.equal(logs.length, 1)
assert(/\b\w{46}\b/.test(logs[0]), 'no IPFS hash found in log message ' + logs[0])

const oracle = await gnosis.createCentralizedOracle(ipfsHash)

assert.equal(logs.length, 3)
assert(/\b0x[a-f0-9]{64}\b/i.test(logs[1]), 'no transaction hash found in log message ' + logs[1])
assert(logs[2].indexOf(oracle.address) !== -1, 'oracle address not found in log message ' + logs[2])

let errorString = (await requireRejection(gnosis.createCategoricalEvent({
collateralToken: gnosis.etherToken,
oracle,
outcomeCount: 1, // < wrong outcomeCount
}))).toString()

assert(
errorString.indexOf('EventFactory') !== -1 &&
errorString.indexOf('createCategoricalEvent') !== -1 &&
errorString.indexOf(gnosis.etherToken.address) !== -1 &&
errorString.indexOf(oracle.address) !== -1 &&
/\b1\b/.test(errorString),
'could not find call info in error message'
)

// ^ depending on whether we're running geth or testrpc, the above might have generated 0 or 1 logs
assert(logs.length === 3 || logs.length === 4)
logs.length = 3

const event = await gnosis.createCategoricalEvent({
collateralToken: gnosis.etherToken,
oracle: oracle,
outcomeCount: netOutcomeTokensSold.length
})

assert.equal(logs.length, 5)
assert(/\b0x[a-f0-9]{64}\b/i.test(logs[3]), 'no transaction hash found in log message ' + logs[3])
assert(logs[4].indexOf(event.address) !== -1, 'event address not found in log message ' + logs[4])

const market = await gnosis.createMarket({
event: event,
marketMaker: gnosis.lmsrMarketMaker,
fee: feeFactor, // 0%
})

assert.equal(logs.length, 7)
assert(/\b0x[a-f0-9]{64}\b/i.test(logs[5]), 'no transaction hash found in log message ' + logs[5])
assert(logs[6].indexOf(market.address) !== -1, 'market address not found in log message ' + logs[6])

requireEventFromTXResult(await gnosis.etherToken.deposit({ value: 8e18 }), 'Deposit')

const funding = 1e18
requireEventFromTXResult(await gnosis.etherToken.approve(market.address, funding), 'Approval')
requireEventFromTXResult(await market.fund(funding), 'MarketFunding')

errorString = (await requireRejection(gnosis.buyOutcomeTokens({
market, outcomeTokenIndex: 0, outcomeTokenCount: 1e18, cost: 1
}))).toString()

assert(
errorString.indexOf('Market') !== -1 &&
errorString.indexOf('buy') !== -1 &&
/\b0\b/.test(errorString),
`could not find call info in error message ${errorString}`
)

// ^ depending on whether we're running geth or testrpc, the above might have generated 1 to 3 logs
// the approve should go through, but a transaction hash may or may not be generated for the buy
// also there is a race condition on the all promise which may or may not let a log through for the approve
assert(logs.length >= 8 || logs.length <= 10)
logs.length = 7

await gnosis.buyOutcomeTokens({
market, outcomeTokenIndex: 0, outcomeTokenCount: 1e18
})

assert.equal(logs.length, 11)
for(let i = 7; i < 11; ++i)
assert(/\b0x[a-f0-9]{64}\b/i.test(logs[i]), 'no transaction hash found in log message ' + logs[i])

// same deal for selling

errorString = (await requireRejection(gnosis.sellOutcomeTokens({
market, outcomeTokenIndex: 0, outcomeTokenCount: 1, minProfit: gnosis.web3.toBigNumber(2).pow(256).sub(1)
}))).toString()

assert(
errorString.indexOf('Market') !== -1 &&
errorString.indexOf('sell') !== -1 &&
/\b0\b/.test(errorString),
`could not find call info in error message ${errorString}`
)

// ^ depending on whether we're running geth or testrpc, the above might have generated 1 to 3 logs
assert(logs.length >= 12 && logs.length <= 14)
logs.length = 11

await gnosis.sellOutcomeTokens({
market, outcomeTokenIndex: 0, outcomeTokenCount: 1e18
})

assert.equal(logs.length, 15)
for(let i = 11; i < 15; ++i)
assert(/\b0x[a-f0-9]{64}\b/i.test(logs[i]), 'no transaction hash found in log message ' + logs[i])
})

it('custom options to be passed to provider stuff', async () => {
let gnosis = await Gnosis.create(options)

Expand Down

0 comments on commit 7a24ded

Please sign in to comment.