diff --git a/CHANGELOG.md b/CHANGELOG.md index 914bf2b..ea99c10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,31 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. + +## 0.0.62 (2018-02-28) + + +### Bug Fixes + +* **getDeadlineForDispute:** fix wrong computation and return date object ([7ee05f5](https://github.com/kleros/kleros-api/commit/7ee05f5)) +* skip broken test assertions ([d210c53](https://github.com/kleros/kleros-api/commit/d210c53)) +* test suite ([98b777d](https://github.com/kleros/kleros-api/commit/98b777d)) + + +### Features + +* normalize token units ([d0d40f8](https://github.com/kleros/kleros-api/commit/d0d40f8)) +* **disputes:** build voteCounters and PNKRepartitions from getters ([ba48e67](https://github.com/kleros/kleros-api/commit/ba48e67)) +* **disputes:** change approach to getting netPNK and votesCounter ([366340f](https://github.com/kleros/kleros-api/commit/366340f)) +* **disputes:** createdAt and ruledAt timestamps added in events ([8e9cafa](https://github.com/kleros/kleros-api/commit/8e9cafa)) +* **disputes:** remove netPNK until events are fixed ([255bf03](https://github.com/kleros/kleros-api/commit/255bf03)) +* **disputes:** return appealsRepartitioned ([1246968](https://github.com/kleros/kleros-api/commit/1246968)) +* **disputes:** return deadline as epoch in ms instead of a date object ([90d4856](https://github.com/kleros/kleros-api/commit/90d4856)) +* **disputes:** return dispute status ([32db45c](https://github.com/kleros/kleros-api/commit/32db45c)) +* **disputes:** submittedAt timestamp for evidence ([3656d33](https://github.com/kleros/kleros-api/commit/3656d33)) + + + ## 0.0.61 (2018-02-27) diff --git a/README.md b/README.md index e0ab2cd..8ee99cc 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,8 @@

+ NPM Version Build Status - npm version Coverage Status Dependencies Dev Dependencies diff --git a/package.json b/package.json index 4a1bb38..3e17dd1 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { "name": "kleros-api", - "version": "0.0.61", - "description": - "A Javascript library that makes it easy to build relayers and other DApps that use the Kleros protocol.", - "keywords": ["Blockchain", "Ethereum", "Kleros"], + "version": "0.0.62", + "description": "A Javascript library that makes it easy to build relayers and other DApps that use the Kleros protocol.", + "keywords": [ + "Blockchain", + "Ethereum", + "Kleros" + ], "homepage": "https://kleros.io", "repository": "github:kleros/kleros-api", "bugs": "https://github.com/kleros/kleros-api/issues", @@ -28,7 +31,9 @@ "build": "webpack --env.NODE_ENV=production -p" }, "commitlint": { - "extends": ["@commitlint/config-conventional"] + "extends": [ + "@commitlint/config-conventional" + ] }, "devDependencies": { "babel-core": "^6.26.0", diff --git a/src/abstractWrappers/Disputes.js b/src/abstractWrappers/Disputes.js index d5df842..1dc421f 100644 --- a/src/abstractWrappers/Disputes.js +++ b/src/abstractWrappers/Disputes.js @@ -2,7 +2,7 @@ import _ from 'lodash' import * as ethConstants from '../constants/eth' import * as arbitratorConstants from '../constants/arbitrator' -import * as contractConstants from '../constants/contract' +import * as notificationConstants from '../constants/notification' import AbstractWrapper from './AbstractWrapper' @@ -36,7 +36,16 @@ class Disputes extends AbstractWrapper { ) // if listener is a party in dispute add to store if (disputeData.partyA === address || disputeData.partyB === address) { - await this._updateStoreForDispute(contractAddress, disputeId, address) + const blockNumber = event.blockNumber + const block = this._Arbitrator._Web3Wrapper.getBlock(blockNumber) + + // add new dispute with timestamp + await this._updateStoreForDispute( + contractAddress, + disputeId, + address, + block.timestamp * 1000 + ) } } @@ -97,6 +106,127 @@ class Disputes extends AbstractWrapper { ) } + /** + * Event listener that sends notification when a dispute has been ruled on. + * @param {string} arbitratorAddress - The arbitrator contract's address. + * @param {string} account - The users eth account. + * @param {function} callback - function to be called when event is triggered. + */ + addDisputeRulingHandler = async (arbitratorAddress, account, callback) => { + if (!this._eventListener) return + + const _disputeRulingHandler = async ( + event, + contractAddress = arbitratorAddress, + address = account, + notificationCallback = callback + ) => { + const newPeriod = event.args._period.toNumber() + const txHash = event.transactionHash + // send appeal possible notifications + if (newPeriod === arbitratorConstants.PERIOD.APPEAL) { + this._checkArbitratorWrappersSet() + const userProfile = await this._StoreProvider.getUserProfile(address) + // contract data + const arbitratorData = await this._Arbitrator.getData( + contractAddress, + address + ) + let disputeId = 0 + const currentSession = arbitratorData.session + + let dispute + while (1) { + // iterate over all disputes (FIXME inefficient) + try { + dispute = await this._Arbitrator.getDispute( + contractAddress, + disputeId + ) + if (dispute.arbitratedContract === ethConstants.NULL_ADDRESS) break + // session + number of appeals + const disputeSession = + dispute.firstSession + dispute.numberOfAppeals + // if dispute not in current session skip + if (disputeSession !== currentSession) { + disputeId++ + dispute = await this._Arbitrator.getDispute( + contractAddress, + disputeId + ) + continue + } + + const ruling = await this._Arbitrator.currentRulingForDispute( + contractAddress, + disputeId + ) + + if ( + _.findIndex( + userProfile.disputes, + dispute => + dispute.disputeId === disputeId && + dispute.arbitratorAddress === contractAddress + ) >= 0 + ) { + await this._StoreProvider.newNotification( + address, + txHash, + disputeId, // use disputeId instead of logIndex since it doens't have its own event + notificationConstants.TYPE.APPEAL_POSSIBLE, + 'A ruling has been made. Appeal is possible', + { + disputeId, + contractAddress, + ruling + } + ) + // get ruledAt from block timestamp + const blockNumber = event.blockNumber + const block = this._Arbitrator._Web3Wrapper.getBlock(blockNumber) + // add ruledAt to store + await this._updateStoreForDispute( + contractAddress, + disputeId, + address, + null, + block.timestamp * 1000 + ) + + if (notificationCallback) { + const userProfile = await this._StoreProvider.getUserProfile( + address + ) + const notification = _.filter( + userProfile.notifications, + notification => + notification.txHash === txHash && + notification.logIndex === disputeId + ) + + if (notification) { + notificationCallback(notification[0]) + } + } + } + // check next dispute + disputeId += 1 + // eslint-disable-next-line no-unused-vars + } catch (err) { + // getDispute(n) throws an error if index out of range + break + } + } + } + } + + await this._eventListener.registerArbitratorEvent( + 'NewPeriod', + _disputeRulingHandler + ) + } + // **************************** // // * Public * // // **************************** // @@ -123,7 +253,6 @@ class Disputes extends AbstractWrapper { ) if (!txHash) throw new Error('unable to pay arbitration fee for party A') - await this._storeNewDispute(arbitrableContractAddress, account) return txHash } catch (err) { throw new Error(err) @@ -151,39 +280,14 @@ class Disputes extends AbstractWrapper { ) if (!txHash) throw new Error('unable to pay arbitration fee for party B') - await this._storeNewDispute(arbitrableContractAddress, account) return txHash } /** - * If there is a dispute in contract update store. - * @param {string} arbitrableContractAddress - The arbitrable contract's address. - * @param {string} account - The account. - */ - _storeNewDispute = async (arbitrableContractAddress, account) => { - this._checkArbitratorWrappersSet() - this._checkArbitrableWrappersSet() - - const arbitrableContractData = await this._ArbitrableContract.getData( - arbitrableContractAddress - ) - - if ( - arbitrableContractData.status === contractConstants.STATUS.DISPUTE_CREATED - ) { - await this._updateStoreForDispute( - arbitrableContractData.arbitrator, - arbitrableContractData.disputeId, - account - ) - } - } - - /** - * Get disputes for user with extra data from arbitrated transaction and store. - * @param {string} arbitratorAddress - Address of Kleros contract. - * @param {string} account - Address of user. - * @returns {object[]} - Dispute data objects for user. + * Get disputes for user with extra data from arbitrated transaction and store + * @param {string} arbitratorAddress address of Kleros contract + * @param {string} account address of user + * @returns {object[]} dispute data objects for user */ getDisputesForUser = async (arbitratorAddress, account) => { // FIXME don't like having to call this every fnc @@ -381,8 +485,8 @@ class Disputes extends AbstractWrapper { /** * Gets the deadline for an arbitrator's period, which is also the deadline for all its disputes. * @param {string} arbitratorAddress - The address of the arbitrator contract. - * @param {number} [period=arbitratorConstants.PERIOD.VOTE] - The period to get the deadline for. - * @returns {Date} - A date object. + * @param {number} [period=PERIODS.VOTE] - The period to get the deadline for. + * @returns {number} - epoch timestamp */ getDeadlineForDispute = async ( arbitratorAddress, @@ -404,8 +508,17 @@ class Disputes extends AbstractWrapper { * @param {string} arbitratorAddress Address address of arbitrator contract * @param {int} disputeId index of dispute * @param {string} account address of party to update dispute or + * @param {number} createdAt epoch timestamp of when dispute was created + * @param {number} ruledAt epoch timestamp of when dispute was ruled on + * @returns {object} updated dispute object */ - _updateStoreForDispute = async (arbitratorAddress, disputeId, account) => { + _updateStoreForDispute = async ( + arbitratorAddress, + disputeId, + account, + createdAt, + ruledAt + ) => { const disputeData = await this.getDataForDispute( arbitratorAddress, disputeId, @@ -413,10 +526,11 @@ class Disputes extends AbstractWrapper { ) // update dispute - await this._StoreProvider.updateDispute( + const dispute = await this._StoreProvider.updateDispute( disputeData.disputeId, disputeData.arbitratorAddress, disputeData.hash, + disputeData.arbitrableContractAddress, disputeData.partyA, disputeData.partyB, disputeData.title, @@ -425,8 +539,10 @@ class Disputes extends AbstractWrapper { disputeData.fee, disputeData.information, disputeData.justification, - disputeData.resolutionOptions - ).body + disputeData.resolutionOptions, + createdAt || disputeData.createdAt, + ruledAt || disputeData.ruledAt + ) // update profile for account await this._StoreProvider.updateDisputeProfile( @@ -435,9 +551,11 @@ class Disputes extends AbstractWrapper { disputeData.arbitratorAddress, disputeData.disputeId, disputeData.votes.length > 0, - false, - 0 + disputeData.hasRuled, + disputeData.netPNK ? disputeData.netPNK : 0 ) + + return dispute } /** @@ -561,10 +679,12 @@ class Disputes extends AbstractWrapper { let votes = [] let hasRuled = false let netPNK = 0 + let createdAt + let ruledAt if (account) { votes = await this.getVotesForJuror(arbitratorAddress, disputeId, account) try { - const userData = await this.getUserDisputeFromStore( + const userData = await this._StoreProvider.getDisputeData( arbitratorAddress, disputeId, account @@ -572,6 +692,8 @@ class Disputes extends AbstractWrapper { isJuror = userData.isJuror hasRuled = userData.hasRuled netPNK = userData.netPNK + createdAt = userData.createdAt + ruledAt = userData.ruledAt // eslint-disable-next-line no-unused-vars } catch (err) { // fetching dispute will fail if it hasn't been added to the store yet. this is ok we can just not return store data @@ -622,7 +744,9 @@ class Disputes extends AbstractWrapper { hasRuled, ruling, evidence, - netPNK + netPNK, + ruledAt, + createdAt } } } diff --git a/src/abstractWrappers/Notifications.js b/src/abstractWrappers/Notifications.js index e3b35c3..4e5b6c0 100644 --- a/src/abstractWrappers/Notifications.js +++ b/src/abstractWrappers/Notifications.js @@ -167,7 +167,6 @@ class Notifications extends AbstractWrapper { dispute.arbitratorAddress, dispute.disputeId ) - if ( disputeData.firstSession + disputeData.numberOfAppeals === currentSession diff --git a/src/kleros.js b/src/kleros.js index bcd9dba..4cbc6b5 100644 --- a/src/kleros.js +++ b/src/kleros.js @@ -85,7 +85,13 @@ class Kleros { await this.disputes.addDisputeEventListener(arbitratorAddress, account) await this.disputes.addTokenShiftToJurorProfileEventListener( arbitratorAddress, - account + account, + callback + ) + await this.disputes.addDisputeRulingHandler( + arbitratorAddress, + account, + callback ) await this.notifications.registerNotificationListeners( arbitratorAddress, diff --git a/src/utils/StoreProviderWrapper.js b/src/utils/StoreProviderWrapper.js index 96f44db..706d15f 100644 --- a/src/utils/StoreProviderWrapper.js +++ b/src/utils/StoreProviderWrapper.js @@ -21,9 +21,8 @@ class StoreProviderWrapper { let body = null try { body = JSON.parse(httpRequest.responseText) - } catch (err) { - console.log(err) - } + // eslint-disable-next-line no-unused-vars + } catch (err) {} resolve({ body: body, status: httpRequest.status @@ -80,7 +79,7 @@ class StoreProviderWrapper { return httpResponse } - getDisputeData = async (userAddress, arbitratorAddress, disputeId) => { + getDisputeData = async (arbitratorAddress, disputeId, userAddress) => { const userProfile = await this.getUserProfile(userAddress) if (!userProfile) throw new Error(`No profile found for address: ${userAddress}`) @@ -91,7 +90,6 @@ class StoreProviderWrapper { o.arbitratorAddress === arbitratorAddress && o.disputeId === disputeId ) - if (_.isEmpty(disputeData)) return null const httpResponse = await this._makeRequest( 'GET', `${this._storeUri}/arbitrators/${arbitratorAddress}/disputes/${disputeId}` @@ -154,13 +152,16 @@ class StoreProviderWrapper { } addEvidenceContract = async (address, account, name, description, url) => { + // get timestamp for submission + const submittedAt = new Date().getTime() const httpResponse = await this._makeRequest( 'POST', `${this._storeUri}/${account}/contracts/${address}/evidence`, JSON.stringify({ name, description, - url + url, + submittedAt }) ) @@ -218,7 +219,9 @@ class StoreProviderWrapper { fee, information, justification, - resolutionOptions + resolutionOptions, + createdAt, + ruledAt ) => { const httpResponse = await this._makeRequest( 'POST', @@ -238,10 +241,11 @@ class StoreProviderWrapper { fee, information, justification, - resolutionOptions + resolutionOptions, + createdAt, + ruledAt }) ) - return httpResponse } @@ -302,7 +306,6 @@ class StoreProviderWrapper { data }) ) - return httpResponse } } diff --git a/src/utils/Web3Wrapper.js b/src/utils/Web3Wrapper.js index e42a585..a2025a1 100644 --- a/src/utils/Web3Wrapper.js +++ b/src/utils/Web3Wrapper.js @@ -32,6 +32,8 @@ class Web3Wrapper { blockNumber = () => this._web3.eth.blockNumber + getBlock = blockNumber => this._web3.eth.getBlock(blockNumber) + doesContractExistAtAddressAsync = async address => { const code = await this._web3.eth.getCode(address) // Regex matches 0x0, 0x00, 0x in order to accommodate poorly implemented clients diff --git a/tests/kleros.test.js b/tests/kleros.test.js index 8c51f51..d788b5d 100644 --- a/tests/kleros.test.js +++ b/tests/kleros.test.js @@ -537,7 +537,6 @@ describe('Kleros', () => { expect(resolutionOptions.length).toEqual(2) // add an evidence for partyA - // FIXME use arbitrableTransaction const testName = 'test name' const testDesc = 'test description' const testURL = 'http://test.com' @@ -563,6 +562,7 @@ describe('Kleros', () => { ) expect(contractStoreData.evidences[0].url).toBe(testURL) + expect(contractStoreData.evidences[0].submittedAt).toBeTruthy() // check initial state of contract // FIXME var must be more explicit @@ -580,7 +580,7 @@ describe('Kleros', () => { }) let newState - // pass state so juror1s are selected + // pass state so jurors are selected for (let i = 1; i < 3; i++) { // NOTE we need to make another block before we can generate the random number. Should not be an issue on main nets where avg block time < period length if (i === 2) @@ -687,6 +687,7 @@ describe('Kleros', () => { // delay 1 second await delaySecond() + // move to execute period await KlerosInstance.arbitrator.passPeriod(klerosCourt.address, other) // stateful notifications @@ -700,13 +701,13 @@ describe('Kleros', () => { notificationConstants.TYPE.CAN_REPARTITION ) - partyBStatefullNotifications = await KlerosInstance.notifications.getStatefulNotifications( + let partyAStatefullNotifications = await KlerosInstance.notifications.getStatefulNotifications( klerosCourt.address, - partyB, + partyA, false ) - expect(partyBStatefullNotifications.length).toEqual(1) - expect(partyBStatefullNotifications[0].notificationType).toEqual( + expect(partyAStatefullNotifications.length).toEqual(1) + expect(partyAStatefullNotifications[0].notificationType).toEqual( notificationConstants.TYPE.CAN_REPARTITION ) @@ -731,13 +732,13 @@ describe('Kleros', () => { notificationConstants.TYPE.CAN_EXECUTE ) - partyBStatefullNotifications = await KlerosInstance.notifications.getStatefulNotifications( + partyAStatefullNotifications = await KlerosInstance.notifications.getStatefulNotifications( klerosCourt.address, - partyB, + partyA, false ) - expect(partyBStatefullNotifications.length).toEqual(1) - expect(partyBStatefullNotifications[0].notificationType).toEqual( + expect(partyAStatefullNotifications.length).toEqual(1) + expect(partyAStatefullNotifications[0].notificationType).toEqual( notificationConstants.TYPE.CAN_EXECUTE ) @@ -779,12 +780,12 @@ describe('Kleros', () => { ) expect(juror1StatefullNotifications.length).toEqual(0) - partyBStatefullNotifications = await KlerosInstance.notifications.getStatefulNotifications( + partyAStatefullNotifications = await KlerosInstance.notifications.getStatefulNotifications( klerosCourt.address, - partyB, + partyA, false ) - expect(partyBStatefullNotifications.length).toEqual(0) + expect(partyAStatefullNotifications.length).toEqual(0) expect(notifications.length).toBeTruthy() // partyA got notifications @@ -854,6 +855,16 @@ describe('Kleros', () => { KlerosInstance.eventListener.stopWatchingArbitratorEvents( klerosCourt.address ) + + // make sure createdAt set + const disputeData = await KlerosInstance.disputes.getDataForDispute( + klerosCourt.address, + 0, + partyA + ) + + expect(disputeData.createdAt).toBeTruthy() + expect(disputeData.ruledAt).toBeTruthy() }, 80000 )