From 50ca1bcda96de8b847ee0c2d6d99870889426066 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Wed, 4 May 2022 13:25:42 +1000 Subject: [PATCH] added namespace field and signature using it for our own 'retrieve' --- ts/session/apis/snode_api/SNodeAPI.ts | 63 +++- ts/session/apis/snode_api/hfHandling.ts | 92 +++++ ts/session/apis/snode_api/onions.ts | 4 +- ts/session/apis/snode_api/sessionRpc.ts | 3 +- ts/session/utils/String.ts | 9 - ts/session/utils/User.ts | 4 +- ts/test/session/unit/onion/hfHandling_test.ts | 347 ++++++++++++++++++ 7 files changed, 503 insertions(+), 19 deletions(-) create mode 100644 ts/session/apis/snode_api/hfHandling.ts create mode 100644 ts/test/session/unit/onion/hfHandling_test.ts diff --git a/ts/session/apis/snode_api/SNodeAPI.ts b/ts/session/apis/snode_api/SNodeAPI.ts index 9c84bf4cf..30868c66c 100644 --- a/ts/session/apis/snode_api/SNodeAPI.ts +++ b/ts/session/apis/snode_api/SNodeAPI.ts @@ -7,7 +7,7 @@ import { requiredSnodesForAgreement, } from './snodePool'; import { getSodiumRenderer } from '../../crypto'; -import _, { range } from 'lodash'; +import _, { isEmpty, range } from 'lodash'; import pRetry from 'p-retry'; import { fromBase64ToArray, @@ -21,6 +21,7 @@ import { updateIsOnline } from '../../../state/ducks/onion'; import { ed25519Str } from '../../onions/onionPath'; import { StringUtils, UserUtils } from '../../utils'; import { SnodePool } from '.'; +import { handleHardforkResult } from './hfHandling'; // ONS name can have [a-zA-Z0-9_-] except that - is not allowed as start or end // do not define a regex but rather create it on the fly to avoid https://stackoverflow.com/questions/3891641/regex-test-only-works-every-other-time @@ -454,22 +455,71 @@ export async function storeOnNode( } } +async function getRetrieveSignatureParams( + params: RetrieveRequestParams +): Promise<{ timestamp: number; signature: string; pubkey_ed25519: string } | null> { + const ourPubkey = UserUtils.getOurPubKeyFromCache(); + const ourEd25519Key = await UserUtils.getUserED25519KeyPair(); + + if (isEmpty(params?.pubKey) || ourPubkey.key !== params.pubKey || !ourEd25519Key) { + return null; + } + const hasNamespace = params.namespace && params.namespace !== 0; + const namespace = params.namespace || 0; + const edKeyPrivBytes = fromHexToArray(ourEd25519Key?.privKey); + + const signatureTimestamp = getNowWithNetworkOffset(); + + const verificationData = hasNamespace + ? StringUtils.encode(`retrieve${namespace}${signatureTimestamp}`, 'utf8') + : StringUtils.encode(`retrieve${signatureTimestamp}`, 'utf8'); + const message = new Uint8Array(verificationData); + + const sodium = await getSodiumRenderer(); + try { + const signature = sodium.crypto_sign_detached(message, edKeyPrivBytes); + const signatureBase64 = fromUInt8ArrayToBase64(signature); + + const namespaceObject = hasNamespace ? { namespace } : {}; + + return { + timestamp: signatureTimestamp, + signature: signatureBase64, + pubkey_ed25519: ourEd25519Key.pubKey, + ...namespaceObject, + }; + } catch (e) { + window.log.warn('getSignatureParams failed with: ', e.message); + return null; + } +} + +type RetrieveRequestParams = { + pubKey: string; + lastHash: string; + namespace?: number; +}; + /** */ export async function retrieveNextMessages( targetNode: Snode, lastHash: string, - associatedWith: string + associatedWith: string, + namespace?: number ): Promise> { - const params = { + const params: RetrieveRequestParams = { pubKey: associatedWith, lastHash: lastHash || '', + namespace: namespace || 0, }; + const signatureParams = (await getRetrieveSignatureParams(params)) || {}; + // let exceptions bubble up // no retry for this one as this a call we do every few seconds while polling for messages const result = await snodeRpc({ method: 'retrieve', - params, + params: { ...signatureParams, ...params }, targetNode, associatedWith, timeout: 4000, @@ -498,6 +548,7 @@ export async function retrieveNextMessages( } handleTimestampOffset('retrieve', json.t); + await handleHardforkResult(json); return json.messages || []; } catch (e) { @@ -517,7 +568,7 @@ export async function retrieveNextMessages( * @returns timestamp of the response from snode */ // tslint:disable-next-line: variable-name -export const TEST_getNetworkTime = async (snode: Snode): Promise => { +export const getNetworkTime = async (snode: Snode): Promise => { const response: any = await snodeRpc({ method: 'info', params: {}, targetNode: snode }); const body = JSON.parse(response.body); const timestamp = body?.timestamp; @@ -554,7 +605,7 @@ export const forceNetworkDeletion = async (): Promise | null> => { return pRetry( async () => { - const timestamp = await exports.TEST_getNetworkTime(snodeToMakeRequestTo); + const timestamp = await exports.getNetworkTime(snodeToMakeRequestTo); const verificationData = StringUtils.encode(`delete_all${timestamp}`, 'utf8'); const message = new Uint8Array(verificationData); diff --git a/ts/session/apis/snode_api/hfHandling.ts b/ts/session/apis/snode_api/hfHandling.ts new file mode 100644 index 000000000..4933dd002 --- /dev/null +++ b/ts/session/apis/snode_api/hfHandling.ts @@ -0,0 +1,92 @@ +import { isNumber } from 'lodash'; +import { createOrUpdateItem, getItemById } from '../../../data/channelsItem'; + +let hasSeenHardfork190: boolean | undefined; +let hasSeenHardfork191: boolean | undefined; + +/** + * this is only intended for testing. Do not call this in production. + */ +export function resetHardForkCachedValues() { + hasSeenHardfork190 = hasSeenHardfork191 = undefined; +} + +export async function getHasSeenHF190() { + if (hasSeenHardfork190 === undefined) { + // read values from db and cache them as it looks like we did not + const oldHhasSeenHardfork190 = (await getItemById('hasSeenHardfork190'))?.value; + + // values do not exist in the db yet. Let's store false for now in the db and update our cached value. + if (oldHhasSeenHardfork190 === undefined) { + await createOrUpdateItem({ id: 'hasSeenHardfork190', value: false }); + hasSeenHardfork190 = false; + } else { + hasSeenHardfork190 = oldHhasSeenHardfork190; + } + } + return hasSeenHardfork190; +} + +export async function getHasSeenHF191() { + if (hasSeenHardfork191 === undefined) { + // read values from db and cache them as it looks like we did not + const oldHhasSeenHardfork191 = (await getItemById('hasSeenHardfork191'))?.value; + + // values do not exist in the db yet. Let's store false for now in the db and update our cached value. + if (oldHhasSeenHardfork191 === undefined) { + await createOrUpdateItem({ id: 'hasSeenHardfork191', value: false }); + hasSeenHardfork191 = false; + } else { + hasSeenHardfork191 = oldHhasSeenHardfork191; + } + } + return hasSeenHardfork191; +} + +export async function handleHardforkResult(json: Record) { + if (hasSeenHardfork190 === undefined || hasSeenHardfork191 === undefined) { + // read values from db and cache them as it looks like we did not + const oldHhasSeenHardfork190 = (await getItemById('hasSeenHardfork190'))?.value; + const oldHasSeenHardfork191 = (await getItemById('hasSeenHardfork191'))?.value; + + // values do not exist in the db yet. Let's store false for now in the db and update our cached value. + if (oldHhasSeenHardfork190 === undefined) { + await createOrUpdateItem({ id: 'hasSeenHardfork190', value: false }); + hasSeenHardfork190 = false; + } else { + hasSeenHardfork190 = oldHhasSeenHardfork190; + } + if (oldHasSeenHardfork191 === undefined) { + await createOrUpdateItem({ id: 'hasSeenHardfork191', value: false }); + hasSeenHardfork191 = false; + } else { + hasSeenHardfork191 = oldHasSeenHardfork191; + } + } + + if (hasSeenHardfork191 && hasSeenHardfork190) { + // no need to do any of this if we already know both forks happened + window.log.info('hardfork 19.1 already happened. No need to go any further'); + return; + } + + // json.hf is an array of 2 number if it is set. Make sure this is the case before doing anything else + if ( + json?.hf && + Array.isArray(json.hf) && + json.hf.length === 2 && + isNumber(json.hf[0]) && + isNumber(json.hf[1]) + ) { + if (!hasSeenHardfork190 && json.hf[0] >= 19 && json.hf[1] >= 0) { + window.log.info('[HF]: We just detected HF 19.0 on "retrieve"'); + await createOrUpdateItem({ id: 'hasSeenHardfork190', value: true }); + hasSeenHardfork190 = true; + } + if (!hasSeenHardfork191 && json.hf[0] >= 19 && json.hf[1] >= 1) { + window.log.info('[HF]: We just detected HF 19.1 on "retrieve"'); + await createOrUpdateItem({ id: 'hasSeenHardfork191', value: true }); + hasSeenHardfork191 = true; + } + } +} diff --git a/ts/session/apis/snode_api/onions.ts b/ts/session/apis/snode_api/onions.ts index bec4da6de..5eb15ddf4 100644 --- a/ts/session/apis/snode_api/onions.ts +++ b/ts/session/apis/snode_api/onions.ts @@ -259,7 +259,9 @@ async function processOnionRequestErrorAtDestination({ if (statusCode === 200) { return; } - window?.log?.info('processOnionRequestErrorAtDestination. statusCode nok:', statusCode); + window?.log?.info( + `processOnionRequestErrorAtDestination. statusCode nok: ${statusCode}: "${body}"` + ); process406Error(statusCode); await process421Error(statusCode, body, associatedWith, destinationEd25519); diff --git a/ts/session/apis/snode_api/sessionRpc.ts b/ts/session/apis/snode_api/sessionRpc.ts index 638b96795..31e1d3a31 100644 --- a/ts/session/apis/snode_api/sessionRpc.ts +++ b/ts/session/apis/snode_api/sessionRpc.ts @@ -111,7 +111,7 @@ export async function snodeRpc( timeout = 10000, }: { method: string; - params: any; + params: Record; targetNode: Snode; associatedWith?: string; timeout?: number; @@ -128,6 +128,7 @@ export async function snodeRpc( pubKey: getStoragePubKey(params.pubKey), }; } + const body = { jsonrpc: '2.0', id: '0', diff --git a/ts/session/utils/String.ts b/ts/session/utils/String.ts index d91a4cb39..76969c905 100644 --- a/ts/session/utils/String.ts +++ b/ts/session/utils/String.ts @@ -21,15 +21,6 @@ export function decode(buffer: BufferType, stringEncoding: Encoding): string { return ByteBuffer.wrap(buffer).toString(stringEncoding); } -/** - * Typescript which can be used to filter out undefined or null values from an array. - * And making typescript realize that there is no nullish value in the type anymore. - * @param v the value to evaluate - */ -export function nonNullish(v: V): v is NonNullable { - return v !== undefined && v !== null; -} - export const toHex = (d: BufferType) => decode(d, 'hex'); export const fromHex = (d: string) => encode(d, 'hex'); diff --git a/ts/session/utils/User.ts b/ts/session/utils/User.ts index 1ce66ac2e..5d3bac9d2 100644 --- a/ts/session/utils/User.ts +++ b/ts/session/utils/User.ts @@ -66,8 +66,8 @@ export async function getIdentityKeyPair(): Promise export async function getUserED25519KeyPair(): Promise { // 'identityKey' keeps the ed25519KeyPair under a ed25519KeyPair field. // it is only set if the user migrated to the ed25519 way of generating a key - const item = await getItemById('identityKey'); - const ed25519KeyPair = item?.value?.ed25519KeyPair; + const item = await getIdentityKeyPair(); + const ed25519KeyPair = (item as any)?.ed25519KeyPair; if (ed25519KeyPair?.publicKey && ed25519KeyPair?.privateKey) { const pubKeyAsArray = _.map(ed25519KeyPair.publicKey, a => a); const privKeyAsArray = _.map(ed25519KeyPair.privateKey, a => a); diff --git a/ts/test/session/unit/onion/hfHandling_test.ts b/ts/test/session/unit/onion/hfHandling_test.ts new file mode 100644 index 000000000..d93e6eb75 --- /dev/null +++ b/ts/test/session/unit/onion/hfHandling_test.ts @@ -0,0 +1,347 @@ +import { expect } from 'chai'; +import Sinon from 'sinon'; +import { + getHasSeenHF190, + getHasSeenHF191, + handleHardforkResult, + resetHardForkCachedValues, +} from '../../../../session/apis/snode_api/hfHandling'; +import { TestUtils } from '../../../test-utils'; + +// tslint:disable-next-line: max-func-body-length +describe('hardfork handling', () => { + describe('getHasSeenHF190', () => { + afterEach(() => { + Sinon.restore(); + resetHardForkCachedValues(); + }); + + it('fetches from db if undefined, and write to db false if db value is undefined', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF190(); + expect(ret).to.be.eq(false, 'getHasSeenHF190 should return false'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.calledOnce).to.be.eq(true, 'createItem should have been called'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + }); + + it('fetches from db if undefined, and does not write to db if db value is not undefined', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ + id: 'getHasSeenHF190', + value: false, + }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF190(); + expect(ret).to.be.eq(false, 'getHasSeenHF190 should return false'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called'); + }); + + it('fetches from db if undefined, and does not write to db if db value is not undefined - 2', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ + id: 'getHasSeenHF190', + value: true, + }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF190(); + expect(ret).to.be.eq(true, 'getHasSeenHF190 should return false'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called'); + }); + + it('fetches from db only the value is not cached already', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ + id: 'getHasSeenHF190', + value: true, + }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF190(); + const ret2 = await getHasSeenHF190(); + expect(ret).to.be.eq(true, 'getHasSeenHF190 should return false'); + expect(ret2).to.be.eq(true, 'getHasSeenHF190 should return false - 2'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called'); + }); + }); + + describe('getHasSeenHF191', () => { + afterEach(() => { + Sinon.restore(); + resetHardForkCachedValues(); + }); + + it('fetches from db if undefined, and write to db false if db value is undefined', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF191(); + expect(ret).to.be.eq(false, 'getHasSeenHF191 should return false'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.calledOnce).to.be.eq(true, 'createItem should have been called'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('fetches from db if undefined, and does not write to db if db value is not undefined', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ + id: 'getHasSeenHF191', + value: false, + }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF191(); + expect(ret).to.be.eq(false, 'getHasSeenHF191 should return false'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called'); + }); + + it('fetches from db if undefined, and does not write to db if db value is not undefined - 2', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ + id: 'getHasSeenHF191', + value: true, + }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF191(); + expect(ret).to.be.eq(true, 'getHasSeenHF191 should return false'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called'); + }); + + it('fetches from db only the value is not cached already', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ + id: 'getHasSeenHF191', + value: true, + }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + const ret = await getHasSeenHF191(); + const ret2 = await getHasSeenHF191(); + expect(ret).to.be.eq(true, 'getHasSeenHF191 should return false'); + expect(ret2).to.be.eq(true, 'getHasSeenHF191 should return false - 2'); + + expect(getItemById.calledOnce).to.be.eq(true, 'getItemById should have been called'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called'); + }); + }); + + // tslint:disable-next-line: max-func-body-length + describe('handleHardforkResult', () => { + afterEach(() => { + Sinon.restore(); + resetHardForkCachedValues(); + }); + + it('does not fail if null is given as json', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult(null as any); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does not fail on empty json object', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({}); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does not fail with invalid array length of 3', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [1, 2, 3] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does not fail with invalid array length of 3', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [1, 2, 3] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does not fail with invalid array length of but not numbers', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: ['1', 2] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does not fail with invalid array length of 1 ', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [1] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does not write new data if hf major is <= 18 ', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [18, 9] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledTwice).to.be.eq(true, 'createItem should have been calledTwice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + }); + + it('does write new data if hf major is === 19 and minor === 0 ', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [19, 0] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.calledThrice).to.be.eq(true, 'createItem should have been calledThrice'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + + expect(createItem.args[2][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: true }, + 'createItem should have been to set hasSeenHardfork191 to true in the db' + ); + + getItemById.resetHistory(); + createItem.resetHistory(); + expect(await getHasSeenHF190()).to.be.eq(true, 'getHasSeenHF190 should have been true'); + expect(getItemById.notCalled).to.be.eq(true, 'getItemById should not have been called more'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called more'); + }); + + it('does write new data if hf major is === 19 and minor === 1 ', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves(undefined); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [19, 1] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.callCount).to.be.eq(4, 'createItem should have been 4'); + expect(createItem.args[0][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: false }, + 'createItem should have been to set hasSeenHardfork190 to false in the db' + ); + expect(createItem.args[1][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: false }, + 'createItem should have been to set hasSeenHardfork191 to false in the db' + ); + + expect(createItem.args[2][0]).to.be.deep.eq( + { id: 'hasSeenHardfork190', value: true }, + 'createItem should have been to set hasSeenHardfork190 to true in the db' + ); + expect(createItem.args[3][0]).to.be.deep.eq( + { id: 'hasSeenHardfork191', value: true }, + 'createItem should have been to set hasSeenHardfork191 to true in the db' + ); + getItemById.resetHistory(); + createItem.resetHistory(); + expect(await getHasSeenHF190()).to.be.eq(true, 'getHasSeenHF190 should have been true'); + expect(await getHasSeenHF191()).to.be.eq(true, 'getHasSeenHF191 should have been true'); + expect(getItemById.notCalled).to.be.eq(true, 'getItemById should not have been called more'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called more'); + }); + + it('does not write new data if hf major is === 19 and minor === 1 but it is already known we have seen both forks ', async () => { + const getItemById = TestUtils.stubDataItem('getItemById').resolves({ id: '', value: true }); + const createItem = TestUtils.stubDataItem('createOrUpdateItem').resolves(); + + await handleHardforkResult({ hf: [19, 1] }); + expect(getItemById.calledTwice).to.be.eq(true, 'getItemById should have been calledTwice'); + expect(createItem.callCount).to.be.eq(0, 'createItem should have been 0'); + + getItemById.resetHistory(); + createItem.resetHistory(); + expect(await getHasSeenHF190()).to.be.eq(true, 'getHasSeenHF190 should have been true'); + expect(await getHasSeenHF191()).to.be.eq(true, 'getHasSeenHF191 should have been true'); + expect(getItemById.notCalled).to.be.eq(true, 'getItemById should not have been called more'); + expect(createItem.notCalled).to.be.eq(true, 'createItem should not have been called more'); + }); + }); +});