From d41d94a11c03063516b62686bddae4be0c35c295 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Wed, 29 Nov 2023 18:55:13 -0500 Subject: [PATCH 1/7] [IND-494]: Create trading_rewards postgres table --- .../postgres/__tests__/helpers/constants.ts | 10 ++ .../stores/trading-rewards-table.test.ts | 55 +++++++++ ...1129153017_create_trading_rewards_table.ts | 25 +++++ .../postgres/src/helpers/db-helpers.ts | 1 + .../src/models/trading-reward-model.ts | 75 +++++++++++++ .../postgres/src/models/wallet-model.ts | 35 +++++- .../src/stores/trading-reward-table.ts | 105 ++++++++++++++++++ .../postgres/src/types/db-model-types.ts | 8 ++ indexer/packages/postgres/src/types/index.ts | 1 + .../postgres/src/types/query-types.ts | 7 ++ .../src/types/trading-reward-types.ts | 16 +++ 11 files changed, 335 insertions(+), 3 deletions(-) create mode 100644 indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts create mode 100644 indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts create mode 100644 indexer/packages/postgres/src/models/trading-reward-model.ts create mode 100644 indexer/packages/postgres/src/stores/trading-reward-table.ts create mode 100644 indexer/packages/postgres/src/types/trading-reward-types.ts diff --git a/indexer/packages/postgres/__tests__/helpers/constants.ts b/indexer/packages/postgres/__tests__/helpers/constants.ts index 49df378318..9acd0fad96 100644 --- a/indexer/packages/postgres/__tests__/helpers/constants.ts +++ b/indexer/packages/postgres/__tests__/helpers/constants.ts @@ -43,6 +43,7 @@ import { SubaccountCreateObject, TendermintEventCreateObject, TimeInForce, + TradingRewardCreateObject, TransactionCreateObject, TransferCreateObject, WalletCreateObject, @@ -581,3 +582,12 @@ export const nonBlockedComplianceData: ComplianceDataCreateObject = { riskScore: '10.00', updatedAt: createdDateTime.plus(1).toISO(), }; + +// ========= Trading Reward Data ========== + +export const defaultTradingReward: TradingRewardCreateObject = { + address: defaultAddress, + blockHeight: createdHeight, + blockTime: createdDateTime.toISO(), + amount: '1.00', +}; diff --git a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts new file mode 100644 index 0000000000..1c2f47c79c --- /dev/null +++ b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts @@ -0,0 +1,55 @@ +import { TradingRewardFromDatabase } from '../../src/types'; +import { clearData, migrate, teardown } from '../../src/helpers/db-helpers'; +import { defaultTradingReward } from '../helpers/constants'; +import * as TradingRewardTable from '../../src/stores/trading-reward-table'; + +describe('TradingReward store', () => { + beforeAll(async () => { + await migrate(); + }); + + afterEach(async () => { + await clearData(); + }); + + afterAll(async () => { + await teardown(); + }); + + it('Successfully creates a TradingReward', async () => { + await TradingRewardTable.create(defaultTradingReward); + }); + + it('Successfully finds all TradingRewards', async () => { + await Promise.all([ + TradingRewardTable.create(defaultTradingReward), + TradingRewardTable.create({ + ...defaultTradingReward, + blockHeight: '20', + }), + ]); + + const tradingRewards: TradingRewardFromDatabase[] = await TradingRewardTable.findAll( + {}, + [], + { readReplica: true }, + ); + + expect(tradingRewards.length).toEqual(2); + expect(tradingRewards[0]).toEqual(expect.objectContaining(defaultTradingReward)); + expect(tradingRewards[1]).toEqual(expect.objectContaining({ + ...defaultTradingReward, + blockHeight: '20', + })); + }); + + it('Successfully finds a TradingReward', async () => { + await TradingRewardTable.create(defaultTradingReward); + + const tradingReward: TradingRewardFromDatabase | undefined = await TradingRewardTable.findById( + TradingRewardTable.uuid(defaultTradingReward.address, defaultTradingReward.blockHeight), + ); + + expect(tradingReward).toEqual(expect.objectContaining(defaultTradingReward)); + }); +}); diff --git a/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts b/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts new file mode 100644 index 0000000000..8e6283f60f --- /dev/null +++ b/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts @@ -0,0 +1,25 @@ +import * as Knex from 'knex'; + +export async function up(knex: Knex): Promise { + return knex + .schema + .createTable('trading_rewards', (table) => { + table.uuid('id').primary(); + table.string('address').notNullable(); + table.timestamp('blockTime').notNullable(); + table.bigInteger('blockHeight').notNullable(); + table.decimal('amount').notNullable(); + + // Foreign + table.foreign('address').references('wallet.address'); + + // Indices + table.index(['address']); + table.index(['blockTime']); + table.index(['blockHeight']); + }); +} + +export async function down(knex: Knex): Promise { + return knex.schema.dropTableIfExists('trading_rewards'); +} diff --git a/indexer/packages/postgres/src/helpers/db-helpers.ts b/indexer/packages/postgres/src/helpers/db-helpers.ts index a70f4ada0b..43cafa4e67 100644 --- a/indexer/packages/postgres/src/helpers/db-helpers.ts +++ b/indexer/packages/postgres/src/helpers/db-helpers.ts @@ -23,6 +23,7 @@ const layer1Tables = [ 'liquidity_tiers', 'wallets', 'compliance_data', + 'trading_rewards', ]; /** diff --git a/indexer/packages/postgres/src/models/trading-reward-model.ts b/indexer/packages/postgres/src/models/trading-reward-model.ts new file mode 100644 index 0000000000..15a33a27a2 --- /dev/null +++ b/indexer/packages/postgres/src/models/trading-reward-model.ts @@ -0,0 +1,75 @@ +import path from 'path'; + +import { Model } from 'objection'; + +import { IntegerPattern, NonNegativeNumericPattern } from '../lib/validators'; +import UpsertQueryBuilder from '../query-builders/upsert'; + +export default class TradingRewardModel extends Model { + static get tableName() { + return 'trading_rewards'; + } + + static get idColumn() { + return 'id'; + } + + static relationMappings = { + wallet: { + relation: Model.BelongsToOneRelation, + modelClass: path.join(__dirname, 'wallet-model'), + join: { + from: 'trading_rewards.address', + to: 'wallets.address', + }, + }, + }; + + static get jsonSchema() { + return { + type: 'object', + required: [ + 'id', + 'address', + 'blockTime', + 'blockHeight', + 'amount', + ], + properties: { + id: { type: 'string', format: 'uuid' }, + address: { type: 'string' }, + blockTime: { type: 'string', format: 'date-time' }, + blockHeight: { type: 'string', pattern: IntegerPattern }, + amount: { type: 'string', pattern: NonNegativeNumericPattern }, + }, + }; + } + + /** + * A mapping from column name to JSON conversion expected. + * See getSqlConversionForDydxModelTypes for valid conversions. + * + * TODO(IND-239): Ensure that jsonSchema() / sqlToJsonConversions() / model fields match. + */ + static get sqlToJsonConversions() { + return { + id: 'string', + address: 'string', + blockTime: 'date-time', + blockHeight: 'string', + amount: 'string', + }; + } + + QueryBuilderType!: UpsertQueryBuilder; + + id!: string; + + address!: string; + + blockTime!: string; + + blockHeight!: string; + + amount!: string; +} diff --git a/indexer/packages/postgres/src/models/wallet-model.ts b/indexer/packages/postgres/src/models/wallet-model.ts index e4661abbcb..182abd3d25 100644 --- a/indexer/packages/postgres/src/models/wallet-model.ts +++ b/indexer/packages/postgres/src/models/wallet-model.ts @@ -1,4 +1,8 @@ -import { NumericPattern } from '../lib/validators'; +import path from 'path'; + +import { Model } from 'objection'; + +import { NonNegativeNumericPattern } from '../lib/validators'; import UpsertQueryBuilder from '../query-builders/upsert'; import BaseModel from './base-model'; @@ -11,7 +15,32 @@ export default class WalletModel extends BaseModel { return 'address'; } - static relationMappings = {}; + static relationMappings = { + transferRecipientWallet: { + relation: Model.HasManyRelation, + modelClass: path.join(__dirname, 'transfer-model'), + join: { + from: 'wallets.address', + to: 'transfers.recipientWalletAddress', + }, + }, + transferSenderWallet: { + relation: Model.HasManyRelation, + modelClass: path.join(__dirname, 'transfer-model'), + join: { + from: 'wallets.address', + to: 'transfers.senderWalletAddress', + }, + }, + tradingRewards: { + relation: Model.HasManyRelation, + modelClass: path.join(__dirname, 'trading-reward-model'), + join: { + from: 'wallets.address', + to: 'trading_rewards.address', + }, + }, + }; static get jsonSchema() { return { @@ -22,7 +51,7 @@ export default class WalletModel extends BaseModel { ], properties: { address: { type: 'string' }, - totalTradingRewards: { type: 'string', pattern: NumericPattern }, + totalTradingRewards: { type: 'string', pattern: NonNegativeNumericPattern }, }, }; } diff --git a/indexer/packages/postgres/src/stores/trading-reward-table.ts b/indexer/packages/postgres/src/stores/trading-reward-table.ts new file mode 100644 index 0000000000..fc00b761f0 --- /dev/null +++ b/indexer/packages/postgres/src/stores/trading-reward-table.ts @@ -0,0 +1,105 @@ +import { QueryBuilder } from 'objection'; + +import { BUFFER_ENCODING_UTF_8, DEFAULT_POSTGRES_OPTIONS } from '../constants'; +import { setupBaseQuery, verifyAllRequiredFields } from '../helpers/stores-helpers'; +import Transaction from '../helpers/transaction'; +import { getUuid } from '../helpers/uuid'; +import TradingRewardModel from '../models/trading-reward-model'; +import { + Options, + Ordering, + QueryableField, + QueryConfig, + TradingRewardColumns, + TradingRewardCreateObject, + TradingRewardFromDatabase, + TradingRewardQueryConfig, +} from '../types'; + +export function uuid(address: string, blockHeight: string): string { + // TODO(IND-483): Fix all uuid string substitutions to use Array.join. + return getUuid(Buffer.from(`${address}-${blockHeight}`, BUFFER_ENCODING_UTF_8)); +} + +export async function findAll( + { + address, + blockHeight, + blockTimeBeforeOrAt, + limit, + }: TradingRewardQueryConfig, + requiredFields: QueryableField[], + options: Options = DEFAULT_POSTGRES_OPTIONS, +): Promise { + verifyAllRequiredFields( + { + address, + blockHeight, + blockTimeBeforeOrAt, + limit, + } as QueryConfig, + requiredFields, + ); + + let baseQuery: QueryBuilder = setupBaseQuery( + TradingRewardModel, + options, + ); + + if (address) { + baseQuery = baseQuery.where(TradingRewardColumns.address, address); + } + + if (blockHeight) { + baseQuery = baseQuery.where(TradingRewardColumns.blockHeight, blockHeight); + } + + if (blockTimeBeforeOrAt) { + baseQuery = baseQuery.where(TradingRewardColumns.blockTime, '<=', blockTimeBeforeOrAt); + } + + if (options.orderBy !== undefined) { + for (const [column, order] of options.orderBy) { + baseQuery = baseQuery.orderBy( + column, + order, + ); + } + } else { + baseQuery = baseQuery.orderBy( + TradingRewardColumns.blockHeight, + Ordering.ASC, + ); + } + + if (limit) { + baseQuery = baseQuery.limit(limit); + } + + return baseQuery.returning('*'); +} + +export async function create( + tradingRewardToCreate: TradingRewardCreateObject, + options: Options = { txId: undefined }, +): Promise { + return TradingRewardModel.query( + Transaction.get(options.txId), + ).insert({ + id: uuid(tradingRewardToCreate.address, tradingRewardToCreate.blockHeight), + ...tradingRewardToCreate, + }).returning('*'); +} + +export async function findById( + address: string, + options: Options = DEFAULT_POSTGRES_OPTIONS, +): Promise { + const baseQuery: QueryBuilder = setupBaseQuery( + TradingRewardModel, + options, + ); + return baseQuery + .findById(address) + .returning('*'); +} diff --git a/indexer/packages/postgres/src/types/db-model-types.ts b/indexer/packages/postgres/src/types/db-model-types.ts index fff59d251c..d7b0fea6d6 100644 --- a/indexer/packages/postgres/src/types/db-model-types.ts +++ b/indexer/packages/postgres/src/types/db-model-types.ts @@ -223,6 +223,14 @@ export interface ComplianceDataFromDatabase { updatedAt: string; } +export interface TradingRewardFromDatabase { + id: string; + address: string; + blockTime: IsoString; + blockHeight: string; + amount: string; +} + export type SubaccountAssetNetTransferMap = { [subaccountId: string]: { [assetId: string]: string } }; export type SubaccountToPerpetualPositionsMap = { [subaccountId: string]: diff --git a/indexer/packages/postgres/src/types/index.ts b/indexer/packages/postgres/src/types/index.ts index 18452d6304..f6c3646921 100644 --- a/indexer/packages/postgres/src/types/index.ts +++ b/indexer/packages/postgres/src/types/index.ts @@ -22,4 +22,5 @@ export * from './funding-index-updates-types'; export * from './liquidity-tiers-types'; export * from './wallet-types'; export * from './compliance-data-types'; +export * from './trading-reward-types'; export { PositionSide } from './position-types'; diff --git a/indexer/packages/postgres/src/types/query-types.ts b/indexer/packages/postgres/src/types/query-types.ts index 10b61c77ca..423229203c 100644 --- a/indexer/packages/postgres/src/types/query-types.ts +++ b/indexer/packages/postgres/src/types/query-types.ts @@ -71,6 +71,7 @@ export enum QueryableField { UPDATED_ON_OR_AFTER = 'updatedOnOrAfter', PROVIDER = 'provider', BLOCKED = 'blocked', + BLOCK_TIME_BEFORE_OR_AT = 'blockTimeBeforeOrAt', } export interface QueryConfig { @@ -263,3 +264,9 @@ export interface ComplianceDataQueryConfig extends QueryConfig { [QueryableField.PROVIDER]?: string; [QueryableField.BLOCKED]?: boolean; } + +export interface TradingRewardQueryConfig extends QueryConfig { + [QueryableField.ADDRESS]?: string; + [QueryableField.BLOCK_HEIGHT]?: string; + [QueryableField.BLOCK_TIME_BEFORE_OR_AT]?: IsoString; +} diff --git a/indexer/packages/postgres/src/types/trading-reward-types.ts b/indexer/packages/postgres/src/types/trading-reward-types.ts new file mode 100644 index 0000000000..da1dc81676 --- /dev/null +++ b/indexer/packages/postgres/src/types/trading-reward-types.ts @@ -0,0 +1,16 @@ +import { IsoString } from './utility-types'; + +export interface TradingRewardCreateObject { + address: string; + blockTime: IsoString; + blockHeight: string; + amount: string; +} + +export enum TradingRewardColumns { + id = 'id', + address = 'address', + blockTime = 'blockTime', + blockHeight = 'blockHeight', + amount = 'amount', +} From 972bfe30fdd06d6da308d56cc11530d21f357473 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Wed, 29 Nov 2023 19:21:41 -0500 Subject: [PATCH 2/7] nit --- .../packages/postgres/src/models/wallet-model.ts | 16 ---------------- .../postgres/src/stores/trading-reward-table.ts | 5 ++++- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/indexer/packages/postgres/src/models/wallet-model.ts b/indexer/packages/postgres/src/models/wallet-model.ts index 182abd3d25..cb06bb12bc 100644 --- a/indexer/packages/postgres/src/models/wallet-model.ts +++ b/indexer/packages/postgres/src/models/wallet-model.ts @@ -16,22 +16,6 @@ export default class WalletModel extends BaseModel { } static relationMappings = { - transferRecipientWallet: { - relation: Model.HasManyRelation, - modelClass: path.join(__dirname, 'transfer-model'), - join: { - from: 'wallets.address', - to: 'transfers.recipientWalletAddress', - }, - }, - transferSenderWallet: { - relation: Model.HasManyRelation, - modelClass: path.join(__dirname, 'transfer-model'), - join: { - from: 'wallets.address', - to: 'transfers.senderWalletAddress', - }, - }, tradingRewards: { relation: Model.HasManyRelation, modelClass: path.join(__dirname, 'trading-reward-model'), diff --git a/indexer/packages/postgres/src/stores/trading-reward-table.ts b/indexer/packages/postgres/src/stores/trading-reward-table.ts index fc00b761f0..50aedbfffb 100644 --- a/indexer/packages/postgres/src/stores/trading-reward-table.ts +++ b/indexer/packages/postgres/src/stores/trading-reward-table.ts @@ -68,7 +68,10 @@ export async function findAll( } else { baseQuery = baseQuery.orderBy( TradingRewardColumns.blockHeight, - Ordering.ASC, + Ordering.DESC, + ).orderBy( + TradingRewardColumns.address, + Ordering.DESC, ); } From 69004a085e121acfc4b77ff8e3a5ae1bcdd686bf Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Wed, 29 Nov 2023 19:39:12 -0500 Subject: [PATCH 3/7] fix tests --- indexer/packages/postgres/src/models/wallet-model.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/indexer/packages/postgres/src/models/wallet-model.ts b/indexer/packages/postgres/src/models/wallet-model.ts index cb06bb12bc..0417ae347f 100644 --- a/indexer/packages/postgres/src/models/wallet-model.ts +++ b/indexer/packages/postgres/src/models/wallet-model.ts @@ -15,16 +15,7 @@ export default class WalletModel extends BaseModel { return 'address'; } - static relationMappings = { - tradingRewards: { - relation: Model.HasManyRelation, - modelClass: path.join(__dirname, 'trading-reward-model'), - join: { - from: 'wallets.address', - to: 'trading_rewards.address', - }, - }, - }; + static relationMappings = {}; static get jsonSchema() { return { From 41f2954ed578229584d293aa622cbc055238455f Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Thu, 30 Nov 2023 10:33:47 -0500 Subject: [PATCH 4/7] fix tests and linter --- .../postgres/__tests__/stores/trading-rewards-table.test.ts | 4 ++-- indexer/packages/postgres/src/models/wallet-model.ts | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts index 1c2f47c79c..00c4ebce6d 100644 --- a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts +++ b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts @@ -36,11 +36,11 @@ describe('TradingReward store', () => { ); expect(tradingRewards.length).toEqual(2); - expect(tradingRewards[0]).toEqual(expect.objectContaining(defaultTradingReward)); - expect(tradingRewards[1]).toEqual(expect.objectContaining({ + expect(tradingRewards[0]).toEqual(expect.objectContaining({ ...defaultTradingReward, blockHeight: '20', })); + expect(tradingRewards[1]).toEqual(expect.objectContaining(defaultTradingReward)); }); it('Successfully finds a TradingReward', async () => { diff --git a/indexer/packages/postgres/src/models/wallet-model.ts b/indexer/packages/postgres/src/models/wallet-model.ts index 0417ae347f..09561b0f38 100644 --- a/indexer/packages/postgres/src/models/wallet-model.ts +++ b/indexer/packages/postgres/src/models/wallet-model.ts @@ -1,7 +1,3 @@ -import path from 'path'; - -import { Model } from 'objection'; - import { NonNegativeNumericPattern } from '../lib/validators'; import UpsertQueryBuilder from '../query-builders/upsert'; import BaseModel from './base-model'; From cc6c57398a83d5763c8da8100c4f368f9acf4b83 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Thu, 30 Nov 2023 12:36:18 -0500 Subject: [PATCH 5/7] fix tests --- .../20231129153017_create_trading_rewards_table.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts b/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts index 8e6283f60f..26aa229bc7 100644 --- a/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts +++ b/indexer/packages/postgres/src/db/migrations/migration_files/20231129153017_create_trading_rewards_table.ts @@ -11,7 +11,7 @@ export async function up(knex: Knex): Promise { table.decimal('amount').notNullable(); // Foreign - table.foreign('address').references('wallet.address'); + table.foreign('address').references('wallets.address'); // Indices table.index(['address']); From 560e615f2ec876a83ff11b03674f49ba044961b6 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Thu, 30 Nov 2023 13:23:07 -0500 Subject: [PATCH 6/7] fix foreign key --- .../__tests__/stores/trading-rewards-table.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts index 00c4ebce6d..87144ea3ff 100644 --- a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts +++ b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts @@ -1,13 +1,18 @@ import { TradingRewardFromDatabase } from '../../src/types'; import { clearData, migrate, teardown } from '../../src/helpers/db-helpers'; -import { defaultTradingReward } from '../helpers/constants'; +import { defaultTradingReward, defaultWallet } from '../helpers/constants'; import * as TradingRewardTable from '../../src/stores/trading-reward-table'; +import { WalletTable } from '../../src'; describe('TradingReward store', () => { beforeAll(async () => { await migrate(); }); + beforeEach(async () => { + await WalletTable.create(defaultWallet); + }) + afterEach(async () => { await clearData(); }); From 2c989d9e0058ad747e93715f9f676af2b9e16950 Mon Sep 17 00:00:00 2001 From: Christopher Li Date: Thu, 30 Nov 2023 13:24:53 -0500 Subject: [PATCH 7/7] lint --- .../postgres/__tests__/stores/trading-rewards-table.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts index 87144ea3ff..03504cbdd0 100644 --- a/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts +++ b/indexer/packages/postgres/__tests__/stores/trading-rewards-table.test.ts @@ -11,7 +11,7 @@ describe('TradingReward store', () => { beforeEach(async () => { await WalletTable.create(defaultWallet); - }) + }); afterEach(async () => { await clearData();