diff --git a/src/contracts/abstractions/Arbitrator.js b/src/contracts/abstractions/Arbitrator.js index 33a5702..d9aa9d9 100644 --- a/src/contracts/abstractions/Arbitrator.js +++ b/src/contracts/abstractions/Arbitrator.js @@ -44,21 +44,29 @@ class Arbitrator extends AbstractContract { // update user profile for each dispute await Promise.all( myDisputes.map(async dispute => { + const disputeCreationLog = await this._contractImplementation.getDisputeCreationEvent( + dispute.disputeId + ) + + if (!disputeCreationLog) + throw new Error('Could not fetch dispute creation event log') // update profile for account await this._StoreProvider.updateDisputeProfile( account, dispute.arbitratorAddress, dispute.disputeId, { - appealDraws: dispute.appealDraws + appealDraws: dispute.appealDraws, + blockNumber: disputeCreationLog.blockNumber } ) }) ) - this._StoreProvider.updateUserProfile(account, { - session: currentSession - }) + // FIXME do we want to store session? + // this._StoreProvider.updateUserProfile(account, { + // session: currentSession + // }) } return _getDisputesForUserFromStore(account) diff --git a/src/contracts/implementations/arbitrator/KlerosPOC.js b/src/contracts/implementations/arbitrator/KlerosPOC.js index f61b7aa..9d52a83 100644 --- a/src/contracts/implementations/arbitrator/KlerosPOC.js +++ b/src/contracts/implementations/arbitrator/KlerosPOC.js @@ -434,12 +434,13 @@ class KlerosPOC extends ContractImplementation { disputeId, draws ) + const lastRuling = (await this.contractInstance.getLastSessionVote( disputeId, account )).toNumber() - const currentSession = await this.getSession(this.contractAddress) + const currentSession = await this.getSession(this.contractAddress) return validDraws && lastRuling !== currentSession } @@ -631,18 +632,19 @@ class KlerosPOC extends ContractImplementation { /** * Get the event log for the dispute creation. - * @param {number} startBlock - The block number that the dispute was created. + * @param {number} blockNumber - The block number that the dispute was created. * @param {number} numberOfAppeals - the number of appeals we need to fetch events for. * @returns {number[]} an array of timestamps */ - getAppealCreationTimestamps = async (startBlock, numberOfAppeals) => { + getAppealCreationTimestamps = async (blockNumber, numberOfAppeals) => { const eventLogs = await this._getNewPeriodEventLogs( - startBlock, + blockNumber, arbitratorConstants.PERIOD.VOTE, numberOfAppeals + 1 ) - const eventLogTimestamps = [startBlock] + const creationTimestamp = await this._getTimestampForBlock(blockNumber) + const eventLogTimestamps = [creationTimestamp * 1000] // skip first execute phase as this is the original ruling for (let i = 1; i < eventLogs.length; i++) { @@ -662,16 +664,18 @@ class KlerosPOC extends ContractImplementation { * @returns {object} dispute creation event log. */ getDisputeCreationEvent = async disputeId => { - const eventLogs = await EventListener.getNextEventLogs( + const eventLogs = await EventListener.getEventLogs( this, 'DisputeCreation', - 0 + 0, + 'latest', + { _disputeId: disputeId } ) for (let i = 0; i < eventLogs.length; i++) { const log = eventLogs[i] - if (log.args.disputeID.toNumber() === disputeId) return log + if (log.args._disputeID.toNumber() === disputeId) return log } return null @@ -684,7 +688,7 @@ class KlerosPOC extends ContractImplementation { * @returns {number} The net total PNK */ getNetTokensForDispute = async (disputeId, account) => { - const eventLogs = await EventListener.getNextEventLogs( + const eventLogs = await EventListener.getEventLogs( this, 'TokenShift', 0, @@ -695,7 +699,7 @@ class KlerosPOC extends ContractImplementation { let netPNK = 0 for (let i = 0; i < eventLogs.length; i++) { const event = eventLogs[i] - if (event.args.disputeID.toNumber() === disputeId) + if (event.args._disputeID.toNumber() === disputeId) netPNK += event.args._amount.toNumber() } @@ -718,7 +722,7 @@ class KlerosPOC extends ContractImplementation { * @returns {object[]} an array of event logs. */ _getNewPeriodEventLogs = async (blockNumber, periodNumber, appeals = 0) => { - const logs = await EventListener.getNextEventLogs( + const logs = await EventListener.getEventLogs( this, 'NewPeriod', blockNumber diff --git a/src/resources/Disputes.js b/src/resources/Disputes.js index 721f197..9f48c78 100644 --- a/src/resources/Disputes.js +++ b/src/resources/Disputes.js @@ -17,6 +17,7 @@ class Disputes { this._ArbitratorInstance = arbitratorInstance this._ArbitrableInstance = arbitrableInstance this._StoreProviderInstance = storeProviderInstance + this.disputeCache = {} } /** * Set arbitrator instance. @@ -54,9 +55,7 @@ class Disputes { eventListener = isRequired('eventListener') ) => { const eventHandlerMap = { - DisputeCreation: [this._storeNewDisputeHandler], - TokenShift: [this._storeTokensMovedForJuror], - NewPeriod: [this._storeDisputeRuledAtTimestamp, this._storeAppealDeadline] + DisputeCreation: [this._storeNewDisputeHandler] } for (let event in eventHandlerMap) { @@ -103,12 +102,70 @@ class Disputes { contractAddress: disputeData.arbitrableContractAddress, partyA: arbitrableContractData.partyA, partyB: arbitrableContractData.partyB, - blockNumber: event.blockNumber + blockNumber: event.blockNumber.toNumber() } ) } } + // **************************** // + // * Internal * // + // **************************** // + + /** + * Add new data to the cache + * @param {number} disputeId - The index of the dispute. Used as the key in cache + * @param {object} newCacheData - Freeform data to cache. Will overwrite data with the same keys. + */ + _updateDisputeCache = (disputeId, newCacheData = {}) => { + this.disputeCache[disputeId] = { + ...this.disputeCache[disputeId], + ...newCacheData + } + } + + /** + * Get the block at which a dispute was created. Used to find timestamps for dispute. + * The start block is cached after it has been found once as it will never change. + * @param {number} disputeId - The index of the dispute. + * @param {string} account - The address of the user. + * @returns {number} The block number that the dispute was created. + */ + _getDisputeStartBlock = async (disputeId, account) => { + const cachedDispute = this.disputeCache[disputeId] + if (cachedDispute && cachedDispute.startBlock) + return cachedDispute.startBlock + + const arbitratorAddress = this._ArbitratorInstance.getContractAddress() + + let blockNumber + + try { + const userData = await this._StoreProviderInstance.getDispute( + account, + arbitratorAddress, + disputeId + ) + blockNumber = userData.blockNumber + // eslint-disable-next-line no-unused-vars + } catch (err) {} + // if block number is not stored we can look it up + if (!blockNumber) { + // Fetching a dispute will fail if it hasn't been added to the store yet. This is ok, we can just not return store data + // see if we can get dispute start block from events + const disputeCreationEvent = await this._ArbitratorInstance.getDisputeCreationEvent( + disputeId + ) + if (disputeCreationEvent) { + blockNumber = disputeCreationEvent.blockNumber + } + } + + // cache start block for dispute + this._updateDisputeCache(disputeId, { startBlock: blockNumber }) + return blockNumber + } + // **************************** // // * Public * // // **************************** // @@ -118,7 +175,7 @@ class Disputes { * @param {string} disputeId - The index of the dispute. * @returns {Promise} The dispute data in the store. */ - getDisputeFromStore = (account, disputeId) => { + getDisputeFromStore = async (account, disputeId) => { const arbitratorAddress = this._ArbitratorInstance.getContractAddress() return this._StoreProviderInstance.getDispute( account, @@ -127,6 +184,110 @@ class Disputes { ) } + /** + * Get the dispute deadline for the appeal. + * @param {number} disputeId - The index of the dispute. + * @param {string} account - The users address. + * @param {number} appeal - The appeal number. 0 if there have been no appeals. + * @returns {number} timestamp of the appeal + */ + getDisputeDeadline = async (disputeId, account, appeal = 0) => { + const cachedDispute = this.disputeCache[disputeId] + if ( + cachedDispute && + cachedDispute.appealDeadlines && + cachedDispute.appealDeadlines[appeal] + ) + return cachedDispute.appealDeadlines[appeal] + + const startBlock = await this._getDisputeStartBlock(disputeId, account) + // if there is no start block that means that dispute has not been created yet. + if (!startBlock) return [] + + const deadlineTimestamps = await this._ArbitratorInstance.getDisputeDeadlineTimestamps( + startBlock, + appeal + ) + + // cache the deadline for the appeal + if (deadlineTimestamps.length > 0) + this._updateDisputeCache(disputeId, { + appealDeadlines: deadlineTimestamps + }) + + return deadlineTimestamps[appeal] + } + + /** + * Get the timestamp on when the dispute's ruling was finalized. + * @param {number} disputeId - The index of the dispute. + * @param {string} account - The users address. + * @param {number} appeal - The appeal number. 0 if there have been no appeals. + * @returns {number} timestamp of the appeal + */ + getAppealRuledAt = async (disputeId, account, appeal = 0) => { + const cachedDispute = this.disputeCache[disputeId] + if ( + cachedDispute && + cachedDispute.appealRuledAt && + cachedDispute.appealRuledAt[appeal] + ) + return cachedDispute.appealRuledAt[appeal] + + const startBlock = await this._getDisputeStartBlock(disputeId, account) + // if there is no start block that means that dispute has not been created yet. + if (!startBlock) return [] + + const appealRuledAtTimestamps = await this._ArbitratorInstance.getAppealRuledAtTimestamps( + startBlock, + appeal + ) + + // cache the deadline for the appeal + if (appealRuledAtTimestamps.length > 0) { + this._updateDisputeCache(disputeId, { + appealRuledAt: appealRuledAtTimestamps + }) + } + + return appealRuledAtTimestamps[appeal] + } + + /** + * Get the timestamp on when the dispute's appeal was created + * @param {number} disputeId - The index of the dispute. + * @param {string} account - The users address. + * @param {number} appeal - The appeal number. 0 if there have been no appeals. + * @returns {number} timestamp of the appeal + */ + getAppealCreatedAt = async (disputeId, account, appeal = 0) => { + const cachedDispute = this.disputeCache[disputeId] + if ( + cachedDispute && + cachedDispute.appealCreatedAt && + cachedDispute.appealCreatedAt[appeal] + ) + return cachedDispute.appealCreatedAt[appeal] + + const startBlock = await this._getDisputeStartBlock(disputeId, account) + // if there is no start block that means that dispute has not been created yet. + if (!startBlock) return [] + + const appealCreatedAtTimestamps = await this._ArbitratorInstance.getDisputeDeadlineTimestamps( + startBlock, + appeal + ) + + // cache the deadline for the appeal + if (appealCreatedAtTimestamps) { + this._updateDisputeCache(disputeId, { + appealCreatedAt: appealCreatedAtTimestamps + }) + } + + return appealCreatedAtTimestamps[appeal] + } + /** * Get data for a dispute. This method provides data from the store as well as both * arbitrator and arbitrable contracts. Used to get all relevant data on a dispute. @@ -161,10 +322,8 @@ class Disputes { // Get dispute data from the store let appealDraws = [] - let appealCreatedAt = [] - let appealDeadlines = [] - let appealRuledAt = [] - let startBlock + + // get draws if they have been added to store. try { const userData = await this._StoreProviderInstance.getDispute( account, @@ -172,31 +331,10 @@ class Disputes { disputeId ) if (userData.appealDraws) appealDraws = userData.appealDraws || [] - startBlock = userData.blockNumber // eslint-disable-next-line no-unused-vars } catch (err) { - // Fetching a dispute will fail if it hasn't been added to the store yet. This is ok, we can just not return store data - // see if we can get dispute start block from events - const disputeCreationEvent = this._ArbitratorInstance.getDisputeCreationEvent( - disputeId - ) - if (disputeCreationEvent) startBlock = disputeCreationEvent.blockNumber - } - - if (startBlock) { - // get timestamps - appealDeadlines = await this._ArbitratorInstance.getDisputeDeadlineTimestamps( - startBlock, - dispute.numberOfAppeals - ) - appealRuledAt = await this._ArbitratorInstance.getAppealRuledAtTimestamps( - startBlock, - dispute.numberOfAppeals - ) - appealCreatedAt = await this._ArbitratorInstance.getAppealCreationTimestamps( - startBlock, - dispute.numberOfAppeals - ) + // Dispute exists on chain but not in store. We have lost draws for past disputes. + console.error('Dispute does not exist in store.') } const netPNK = await this._ArbitratorInstance.getNetTokensForDispute( @@ -239,16 +377,32 @@ class Disputes { // Wait for parallel requests to complete ;[ruling, canRule] = await Promise.all(rulingPromises) + const appealCreatedAt = await this.getAppealCreatedAt( + dispute.disputeId, + account, + appeal + ) + const appealDeadline = await this.getDisputeDeadline( + dispute.disputeId, + account, + appeal + ) + const appealRuledAt = await this.getAppealRuledAt( + dispute.disputeId, + account, + appeal + ) + appealJuror[appeal] = { - createdAt: appealCreatedAt[appeal], + createdAt: appealCreatedAt, fee: dispute.arbitrationFeePerJuror * draws.length, draws, canRule } appealRulings[appeal] = { voteCounter: dispute.voteCounters[appeal], - deadline: appealDeadlines[appeal], - ruledAt: appealRuledAt[appeal], + deadline: appealDeadline, + ruledAt: appealRuledAt, ruling, canRepartition, canExecute diff --git a/src/utils/EventListener.js b/src/utils/EventListener.js index 3f1d56c..875208f 100644 --- a/src/utils/EventListener.js +++ b/src/utils/EventListener.js @@ -65,7 +65,7 @@ class EventListener { * @param {object} filters - Extra filters * @returns {Promise} All events in block range. */ - static getNextEventLogs = async ( + static getEventLogs = async ( contractImplementationInstance = isRequired( 'contractImplementationInstance' ), diff --git a/src/utils/StoreProviderWrapper.js b/src/utils/StoreProviderWrapper.js index 3d8c5e8..9c7a5de 100644 --- a/src/utils/StoreProviderWrapper.js +++ b/src/utils/StoreProviderWrapper.js @@ -101,8 +101,7 @@ class StoreProviderWrapper { */ getContractByAddress = async (userAddress, addressContract) => { const userProfile = await this.getUserProfile(userAddress) - if (!userProfile) - throw new Error(errorConstants.PROFILE_NOT_FOUND(userAddress)) + if (!userProfile) return {} let contract = _.filter( userProfile.contracts, diff --git a/tests/unit/contracts/abstractions/Arbitrator.test.js b/tests/unit/contracts/abstractions/Arbitrator.test.js index be44dde..12792ec 100644 --- a/tests/unit/contracts/abstractions/Arbitrator.test.js +++ b/tests/unit/contracts/abstractions/Arbitrator.test.js @@ -83,7 +83,6 @@ describe('Arbitrator', () => { const mockGetDisputesForUser = jest.fn() const mockSetUpUserProfile = jest.fn() const mockGetDisputesForJuror = jest.fn() - const mockUpdateUserProfile = jest.fn() const mockUpdateDisputeProfile = jest.fn() const mockDispute = { arbitratorAddress: arbitratorAddress, @@ -99,7 +98,6 @@ describe('Arbitrator', () => { session: 1 }) ), - updateUserProfile: mockUpdateUserProfile, updateDisputeProfile: mockUpdateDisputeProfile } @@ -111,12 +109,11 @@ describe('Arbitrator', () => { getDispute: jest.fn().mockReturnValue(_asyncMockResponse(mockDispute)), getDisputesForJuror: mockGetDisputesForJuror.mockReturnValue( _asyncMockResponse([mockDispute]) - ) + ), + getDisputeCreationEvent: jest.fn().mockReturnValue({ blockNumber: 1 }) } arbitratorInstance._contractImplementation = mockArbitrator - arbitratorInstance.updateUserProfile = mockUpdateUserProfile - const disputes = await arbitratorInstance.getDisputesForUser(account) expect(disputes.length).toBe(1) @@ -136,13 +133,8 @@ describe('Arbitrator', () => { mockDispute.disputeId ) expect(mockUpdateDisputeProfile.mock.calls[0][3]).toEqual({ - appealDraws: mockDispute.appealDraws - }) - - expect(mockUpdateUserProfile.mock.calls.length).toBe(1) - expect(mockUpdateUserProfile.mock.calls[0][0]).toBe(account) - expect(mockUpdateUserProfile.mock.calls[0][1]).toEqual({ - session: 2 + appealDraws: mockDispute.appealDraws, + blockNumber: 1 }) }) }) diff --git a/tests/unit/resources/Disputes.test.js b/tests/unit/resources/Disputes.test.js index cdaee6c..c00b105 100644 --- a/tests/unit/resources/Disputes.test.js +++ b/tests/unit/resources/Disputes.test.js @@ -138,7 +138,7 @@ describe('Disputes', () => { const partyA = '0x0' const partyB = '0x1' const appealDeadlines = [1] - const appealRuledAt = [2] + const appealRuledAt = [] const appealCreatedAt = [3] const mockArbitratorGetDispute = jest.fn().mockReturnValue( @@ -231,6 +231,7 @@ describe('Disputes', () => { const appealData = disputeData.appealRulings[0] expect(appealData.voteCounter).toEqual(voteCounters[numberOfAppeals]) expect(appealData.ruledAt).toBeFalsy() + expect(appealData.deadline).toEqual(appealDeadlines[numberOfAppeals]) expect(appealData.ruling).toEqual(2) expect(appealData.canRepartition).toBeFalsy() expect(appealData.canExecute).toBeFalsy()