Skip to content

Commit

Permalink
Create data feed update loop (#37)
Browse files Browse the repository at this point in the history
* Create update data feed loop

* Small implementation changes, add tests

* Fix lint

* Fix tests and update configs

* Add documentation

* Update test configuration

* Make deviationThresholdCoefficient optional

* Rename startUpdateFeedsLoops

* Fix tests, remove unnecessary flag
  • Loading branch information
Siegrift authored Oct 27, 2023
1 parent 6aca977 commit ffe5d3e
Show file tree
Hide file tree
Showing 18 changed files with 405 additions and 44 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ jobs:
- name: Lint
run: pnpm run prettier:check && pnpm run eslint:check
- name: Test
run: pnpm run test
run: pnpm run test --bail

test-e2e:
name: Test e2e
runs-on: ubuntu-latest
Expand All @@ -62,7 +62,7 @@ jobs:
- name: Start Hardhat
run: pnpm dev:eth-node&
- name: Test E2E
run: pnpm test:e2e
run: pnpm test:e2e --bail

required-checks-passed:
name: All required checks passed
Expand Down
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,14 +191,24 @@ The number of minutes used to calculate the scaling multiplier if a pending tran

The maximum scaling multiplier used when the pending transaction lag exceeds the `scalingWindow`.

#### `deviationThresholdCoefficient`
#### `deviationThresholdCoefficient` _(optional)_

The global coefficient applied to all deviation checks. Used to differentiate alternate deployments. For example:

```jsonc
"deviationThresholdCoefficient": 1,
```

Defaults to `1`.

#### `dataFeedUpdateInterval`

The interval specifying how often to run the data feed update loop. In seconds.

#### `dataFeedBatchSize`

The batch size of active dAPIs that are to be fetched in a single RPC call.

## Docker

### Build
Expand Down
4 changes: 3 additions & 1 deletion config/airseeker.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"sanitizationPercentile": 80,
"scalingWindow": 2,
"maxScalingMultiplier": 2
}
},
"dataFeedUpdateInterval": 60,
"dataFeedBatchSize": 10
}
},
"deviationThresholdCoefficient": 1,
Expand Down
18 changes: 15 additions & 3 deletions jest-e2e.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
const config = require('./jest.config');
const { join } = require('node:path');

/**
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
* @type {import('jest').Config}
*/
module.exports = {
...config,
displayName: 'e2e',
collectCoverage: false, // It doesn't make sense to collect coverage for e2e tests because they target high level features and interaction with other services.
maxWorkers: 1, // We don't want to run tests in parallel because they might interfere with each other. This option is the same as --runInBand. See: https://stackoverflow.com/a/46489246.

preset: 'ts-jest',
restoreMocks: true,
setupFiles: [join(__dirname, './jest.setup.js')],
testEnvironment: 'jest-environment-node',
testMatch: ['**/?(*.)+(feature).[t]s?(x)'],
testPathIgnorePatterns: ['<rootDir>/.build', '<rootDir>/dist/', '<rootDir>/build/'],
verbose: true,
};
20 changes: 17 additions & 3 deletions jest-unit.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
const config = require('./jest.config');
const { join } = require('node:path');

/**
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/configuration
* @type {import('jest').Config}
*/
module.exports = {
...config,
displayName: 'unit',
collectCoverage: true,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: ['node_modules', '<rootDir>/typechain-types'], // Coverage is collected for all files imported by the tests. We want to exclude files generated by Typechain.
coverageProvider: 'v8',

preset: 'ts-jest',
restoreMocks: true,
setupFiles: [join(__dirname, './jest.setup.js')],
testEnvironment: 'jest-environment-node',
testMatch: ['**/?(*.)+(spec|test).[t]s?(x)'],
testPathIgnorePatterns: ['<rootDir>/.build', '<rootDir>/dist/', '<rootDir>/build/'],
verbose: true,
};
17 changes: 0 additions & 17 deletions jest.config.js

This file was deleted.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
"eslint:fix": "pnpm run eslint:check --fix",
"prettier:check": "prettier --check \"./**/*.{js,ts,md,json}\"",
"prettier:fix": "prettier --write \"./**/*.{js,ts,md,json}\"",
"test": "jest --selectProjects unit --verbose --runInBand --bail --detectOpenHandles --silent",
"test:e2e": "jest --selectProjects e2e --runInBand",
"test": "jest --config=jest-unit.config.js",
"test:e2e": "jest ---config=jest-e2e.config.js",
"tsc": "tsc --project .",
"docker:build": "docker build -t api3/airseekerv2:latest -f docker/Dockerfile .",
"docker:run": "docker run -it --rm api3/airseekerv2:latest",
Expand Down
10 changes: 10 additions & 0 deletions src/config/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ describe('chains schema', () => {
activeDapiNames: [],
},
gasSettings,
dataFeedBatchSize: 10,
dataFeedUpdateInterval: 60,
},
};

Expand All @@ -83,6 +85,8 @@ describe('chains schema', () => {
activeDapiNames: [],
},
gasSettings,
dataFeedBatchSize: 10,
dataFeedUpdateInterval: 60,
},
};

Expand All @@ -107,6 +111,8 @@ describe('chains schema', () => {
activeDapiNames: [],
},
gasSettings,
dataFeedBatchSize: 10,
dataFeedUpdateInterval: 60,
},
};

Expand Down Expand Up @@ -138,6 +144,8 @@ describe('chains schema', () => {
activeDapiNames: [],
},
gasSettings,
dataFeedBatchSize: 10,
dataFeedUpdateInterval: 60,
},
};

Expand Down Expand Up @@ -180,6 +188,8 @@ describe('chains schema', () => {
activeDapiNames: [],
},
gasSettings,
dataFeedBatchSize: 10,
dataFeedUpdateInterval: 60,
},
};

Expand Down
6 changes: 4 additions & 2 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export const optionalChainSchema = z
__Temporary__DapiDataRegistry: temporaryDapiDataRegistrySchema,
contracts: optionalContractsSchema.optional(),
gasSettings: gasSettingsSchema,
dataFeedUpdateInterval: z.number().positive(),
dataFeedBatchSize: z.number().positive(),
})
.strict();

Expand Down Expand Up @@ -133,8 +135,8 @@ export const configSchema = z
.object({
sponsorWalletMnemonic: z.string().refine((mnemonic) => ethers.utils.isValidMnemonic(mnemonic), 'Invalid mnemonic'),
chains: chainsSchema,
deviationThresholdCoefficient: z.number().optional().default(1),
fetchInterval: z.number().positive(),
fetchInterval: z.number().positive(), // TODO: Rename to signedDataFetchInterval
deviationThresholdCoefficient: z.number().positive().optional().default(1), // Explicitly agreed to make this optional. See: https://github.com/api3dao/airseeker-v2/pull/20#issuecomment-1750856113.
})
.strict();

Expand Down
4 changes: 2 additions & 2 deletions src/signed-data-store/signed-data-store.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { BigNumber, ethers } from 'ethers';

import { generateRandomBytes32, signData } from '../../test/utils/evm';
import { generateRandomBytes32, signData } from '../../test/utils';
import type { SignedData } from '../types';

import * as localDataStore from './signed-data-store';
import { verifySignedDataIntegrity } from './signed-data-store';
import * as localDataStore from './signed-data-store';

describe('datastore', () => {
let testDataPoint: SignedData;
Expand Down
1 change: 1 addition & 0 deletions src/update-feeds/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './update-feeds';
18 changes: 18 additions & 0 deletions src/update-feeds/temporary-contract-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// NOTE: The function is currently returning static data, because the contract is not yet finalized, but we mark it as
// async in advance.
//
// eslint-disable-next-line @typescript-eslint/require-await
export const getStaticActiveDapis = async (_offset: number, _limit: number) => {
return {
totalCount: 1,
dapiNames: ['MOCK_FEED'],
dataFeedIds: ['0xebba8507d616ed80766292d200a3598fdba656d9938cecc392765d4a284a69a4'],
updateParameters: [{ deviationThresholdInPercentage: 0.5, deviationReference: 0.5, heartbeatInterval: 100 }],
// NOTE: We will need to decode this from the contract, because it will store the template IDs as encoded bytes.
dataFeedTemplateIds: [['0xcc35bd1800c06c12856a87311dd95bfcbb3add875844021d59a929d79f3c99bd']],
signedApiUrls: [['http://localhost:8080']],
airnodeAddresses: ['0xbF3137b0a7574563a23a8fC8badC6537F98197CC'],
};
};

export type ActiveDapisBatch = Awaited<ReturnType<typeof getStaticActiveDapis>>;
Loading

0 comments on commit ffe5d3e

Please sign in to comment.