Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Commit

Permalink
Add example of a PriceIndexer
Browse files Browse the repository at this point in the history
  • Loading branch information
sz-piotr committed Mar 21, 2024
1 parent 4ace707 commit 9b21f1c
Show file tree
Hide file tree
Showing 16 changed files with 271 additions and 58 deletions.
52 changes: 51 additions & 1 deletion packages/uif-example/src/Application.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { Logger } from '@l2beat/backend-tools'

import { HourlyIndexer } from './HourlyIndexer'
import { PriceIndexer } from './prices/PriceIndexer'
import { PriceIndexerRepository } from './prices/PriceIndexerRepository'
import { PriceRepository } from './prices/PriceRepository'
import { PriceService } from './prices/PriceService'

export class Application {
start: () => Promise<void>
Expand All @@ -10,10 +14,56 @@ export class Application {

const hourlyIndexer = new HourlyIndexer(logger)

const priceService = new PriceService()
const priceRepository = new PriceRepository()
const priceIndexerRepository = new PriceIndexerRepository()

const ethereumPriceIndexer = new PriceIndexer(
'price-ethereum',
priceService,
priceRepository,
priceIndexerRepository,
logger,
[hourlyIndexer],
[
{
// could be a hash of properties & minHeight instead
id: 'eth-ethereum',
properties: { tokenSymbol: 'ETH', apiId: 'ethereum' },
minHeight: new Date('2021-01-01T00:00:00Z').getTime(),
maxHeight: null,
},
{
id: 'weth-ethereum',
properties: { tokenSymbol: 'WETH', apiId: 'ethereum' },
minHeight: new Date('2022-01-01T00:00:00Z').getTime(),
maxHeight: null,
},
],
)
const bitcoinPriceIndexer = new PriceIndexer(
'price-bitcoin',
priceService,
priceRepository,
priceIndexerRepository,
logger,
[hourlyIndexer],
[
{
id: 'btc-bitcoin',
properties: { tokenSymbol: 'BTC', apiId: 'bitcoin' },
minHeight: new Date('2022-01-01T00:00:00Z').getTime(),
maxHeight: null,
},
],
)

this.start = async (): Promise<void> => {
logger.for('Application').info('Starting')

hourlyIndexer.start()
await hourlyIndexer.start()
await ethereumPriceIndexer.start()
await bitcoinPriceIndexer.start()

logger.for('Application').info('Started')
}
Expand Down
4 changes: 4 additions & 0 deletions packages/uif-example/src/prices/PriceConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface PriceConfig {
tokenSymbol: string
apiId: string
}
99 changes: 99 additions & 0 deletions packages/uif-example/src/prices/PriceIndexer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Logger } from '@l2beat/backend-tools'
import {
Configuration,
Indexer,
IndexerOptions,
MultiIndexer,
RemovalConfiguration,
SavedConfiguration,
UpdateConfiguration,
} from '@l2beat/uif'

import { PriceConfig } from './PriceConfig'
import { PriceIndexerRepository } from './PriceIndexerRepository'
import { PriceRepository } from './PriceRepository'
import { PriceService } from './PriceService'

const ONE_HOUR = 60 * 60 * 1000

export class PriceIndexer extends MultiIndexer<PriceConfig> {
private readonly apiId: string

constructor(
private readonly indexerId: string,
private readonly priceService: PriceService,
private readonly priceRepository: PriceRepository,
private readonly priceIndexerRepository: PriceIndexerRepository,
logger: Logger,
parents: Indexer[],
configurations: Configuration<PriceConfig>[],
options?: IndexerOptions,
) {
super(logger, parents, configurations, options)
const apiId = configurations[0]?.properties.apiId
if (!apiId) {
throw new Error('At least one configuration is required')
}
if (configurations.some((c) => c.properties.apiId !== apiId)) {
throw new Error('All configurations must have the same apiId')
}
this.apiId = apiId
}

override async multiInitialize(): Promise<SavedConfiguration<PriceConfig>[]> {
return this.priceIndexerRepository.load(this.indexerId)
}

override async multiUpdate(
currentHeight: number,
targetHeight: number,
configurations: UpdateConfiguration<PriceConfig>[],
): Promise<number> {
const startHour = currentHeight - (currentHeight % ONE_HOUR) + ONE_HOUR
const endHour = Math.min(
targetHeight - (targetHeight % ONE_HOUR),
// for example the api costs us more money for larger ranges
startHour + 23 * ONE_HOUR,
)

if (startHour >= endHour) {
return startHour
}

const prices = await this.priceService.getHourlyPrices(
this.apiId,
startHour,
endHour,
)

const dataToSave = configurations.flatMap((configuration) => {
return prices.map(({ timestamp, price }) => ({
tokenSymbol: configuration.properties.tokenSymbol,
timestamp,
price,
}))
})
await this.priceRepository.save(dataToSave)

// TODO: Maybe if targetHeight is not exactly an hour we can return it?
return endHour
}

override async removeData(
configurations: RemovalConfiguration<PriceConfig>[],
): Promise<void> {
for (const c of configurations) {
await this.priceRepository.deletePrices(
c.properties.tokenSymbol,
c.fromHeightInclusive,
c.toHeightInclusive,
)
}
}

override async saveConfigurations(
configurations: SavedConfiguration<PriceConfig>[],
): Promise<void> {
return this.priceIndexerRepository.save(this.indexerId, configurations)
}
}
19 changes: 19 additions & 0 deletions packages/uif-example/src/prices/PriceIndexerRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { SavedConfiguration } from '@l2beat/uif'

import { PriceConfig } from './PriceConfig'

export class PriceIndexerRepository {
private data: Record<string, SavedConfiguration<PriceConfig>[]> = {}

async save(
indexerId: string,
configurations: SavedConfiguration<PriceConfig>[],
): Promise<void> {
this.data[indexerId] = configurations
return Promise.resolve()
}

async load(indexerId: string): Promise<SavedConfiguration<PriceConfig>[]> {
return Promise.resolve(this.data[indexerId] ?? [])
}
}
19 changes: 19 additions & 0 deletions packages/uif-example/src/prices/PriceRepository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export class PriceRepository {
async save(
prices: { tokenSymbol: string; timestamp: number; price: number }[],
): Promise<void> {
prices // use it so that eslint doesn't complain
return Promise.resolve()
}

async deletePrices(
tokenSymbol: string,
fromTimestampInclusive: number,
toTimestampInclusive: number,
): Promise<void> {
tokenSymbol // use it so that eslint doesn't complain
fromTimestampInclusive // use it so that eslint doesn't complain
toTimestampInclusive // use it so that eslint doesn't complain
return Promise.resolve()
}
}
17 changes: 17 additions & 0 deletions packages/uif-example/src/prices/PriceService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const ONE_HOUR = 60 * 60 * 1000

export class PriceService {
async getHourlyPrices(
apiId: string,
startHourInclusive: number,
endHourInclusive: number,
): Promise<{ timestamp: number; price: number }[]> {
apiId // use it so that eslint doesn't complain

const prices: { timestamp: number; price: number }[] = []
for (let t = startHourInclusive; t <= endHourInclusive; t += ONE_HOUR) {
prices.push({ timestamp: t, price: Math.random() * 1000 })
}
return Promise.resolve(prices)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { Logger } from '@l2beat/backend-tools'
import { install } from '@sinonjs/fake-timers'
import { expect, mockFn } from 'earl'

import { BaseIndexer } from './BaseIndexer'
import { Indexer } from './Indexer'
import { ChildIndexer } from './indexers/ChildIndexer'
import { RootIndexer } from './indexers/RootIndexer'
import { IndexerAction } from './reducer/types/IndexerAction'
import { RetryStrategy } from './Retries'

describe(BaseIndexer.name, () => {
describe(Indexer.name, () => {
describe('correctly informs about updates', () => {
it('first invalidate then parent update', async () => {
const parent = new TestRootIndexer(0)
Expand Down Expand Up @@ -324,7 +324,7 @@ class TestChildIndexer extends ChildIndexer {
public invalidateTo = 0

constructor(
parents: BaseIndexer[],
parents: Indexer[],
private testSafeHeight: number,
name?: string,
retryStrategy?: {
Expand Down
30 changes: 16 additions & 14 deletions packages/uif/src/BaseIndexer.ts → packages/uif/src/Indexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,14 @@ import {
import { IndexerState } from './reducer/types/IndexerState'
import { Retries, RetryStrategy } from './Retries'

export abstract class BaseIndexer {
private readonly children: BaseIndexer[] = []
export interface IndexerOptions {
tickRetryStrategy?: RetryStrategy
updateRetryStrategy?: RetryStrategy
invalidateRetryStrategy?: RetryStrategy
}

export abstract class Indexer {
private readonly children: Indexer[] = []

/**
* This can be overridden to provide a custom retry strategy. It will be
Expand Down Expand Up @@ -123,12 +129,8 @@ export abstract class BaseIndexer {

constructor(
protected logger: Logger,
public readonly parents: BaseIndexer[],
opts?: {
tickRetryStrategy?: RetryStrategy
updateRetryStrategy?: RetryStrategy
invalidateRetryStrategy?: RetryStrategy
},
public readonly parents: Indexer[],
options?: IndexerOptions,
) {
this.logger = this.logger.for(this)
this.state = getInitialState(parents.length)
Expand All @@ -140,11 +142,11 @@ export abstract class BaseIndexer {
})

this.tickRetryStrategy =
opts?.tickRetryStrategy ?? BaseIndexer.GET_DEFAULT_RETRY_STRATEGY()
options?.tickRetryStrategy ?? Indexer.GET_DEFAULT_RETRY_STRATEGY()
this.updateRetryStrategy =
opts?.updateRetryStrategy ?? BaseIndexer.GET_DEFAULT_RETRY_STRATEGY()
options?.updateRetryStrategy ?? Indexer.GET_DEFAULT_RETRY_STRATEGY()
this.invalidateRetryStrategy =
opts?.invalidateRetryStrategy ?? BaseIndexer.GET_DEFAULT_RETRY_STRATEGY()
options?.invalidateRetryStrategy ?? Indexer.GET_DEFAULT_RETRY_STRATEGY()
}

get safeHeight(): number | null {
Expand All @@ -162,20 +164,20 @@ export abstract class BaseIndexer {
})
}

subscribe(child: BaseIndexer): void {
subscribe(child: Indexer): void {
assert(!this.started, 'Indexer already started')
this.logger.debug('Child subscribed', { child: child.constructor.name })
this.children.push(child)
}

notifyReady(child: BaseIndexer): void {
notifyReady(child: Indexer): void {
this.logger.debug('Someone is ready', { child: child.constructor.name })
const index = this.children.indexOf(child)
assert(index !== -1, 'Received ready from unknown child')
this.dispatch({ type: 'ChildReady', index })
}

notifyUpdate(parent: BaseIndexer, safeHeight: number | null): void {
notifyUpdate(parent: Indexer, safeHeight: number | null): void {
this.logger.debug('Someone has updated', {
parent: parent.constructor.name,
})
Expand Down
4 changes: 2 additions & 2 deletions packages/uif/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export * from './BaseIndexer'
export * from './height'
export * from './Indexer'
export * from './indexers/ChildIndexer'
export * from './indexers/multi/MultiIndexer'
export type { Configuration } from './indexers/multi/types'
export * from './indexers/multi/types'
export * from './indexers/RootIndexer'
export * from './Retries'
4 changes: 2 additions & 2 deletions packages/uif/src/indexers/ChildIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BaseIndexer } from '../BaseIndexer'
import { Indexer } from '../Indexer'

export abstract class ChildIndexer extends BaseIndexer {
export abstract class ChildIndexer extends Indexer {
override async tick(): Promise<number> {
return Promise.reject(new Error('ChildIndexer cannot tick'))
}
Expand Down
7 changes: 3 additions & 4 deletions packages/uif/src/indexers/RootIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Logger } from '@l2beat/backend-tools'

import { BaseIndexer } from '../BaseIndexer'
import { RetryStrategy } from '../Retries'
import { Indexer, IndexerOptions } from '../Indexer'

export abstract class RootIndexer extends BaseIndexer {
constructor(logger: Logger, opts?: { tickRetryStrategy?: RetryStrategy }) {
export abstract class RootIndexer extends Indexer {
constructor(logger: Logger, opts?: IndexerOptions) {
super(logger, [], opts)
}

Expand Down
8 changes: 4 additions & 4 deletions packages/uif/src/indexers/multi/MultiIndexer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,12 +300,12 @@ describe(MultiIndexer.name, () => {
class TestMultiIndexer extends MultiIndexer<null> {
constructor(
configurations: Configuration<null>[],
private readonly _saved: SavedConfiguration[],
private readonly _saved: SavedConfiguration<null>[],
) {
super(Logger.SILENT, [], configurations)
}

override multiInitialize(): Promise<SavedConfiguration[]> {
override multiInitialize(): Promise<SavedConfiguration<null>[]> {
return Promise.resolve(this._saved)
}

Expand All @@ -324,7 +324,7 @@ function actual(id: string, minHeight: number, maxHeight: number | null) {
}

function saved(id: string, minHeight: number, currentHeight: number) {
return { id, minHeight, currentHeight }
return { id, properties: null, minHeight, currentHeight }
}

function update(
Expand All @@ -341,5 +341,5 @@ function removal(
fromHeightInclusive: number,
toHeightInclusive: number,
) {
return { id, fromHeightInclusive, toHeightInclusive }
return { id, properties: null, fromHeightInclusive, toHeightInclusive }
}
Loading

0 comments on commit 9b21f1c

Please sign in to comment.