Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[IND-494]: Create trading_rewards postgres table #822

Merged
merged 7 commits into from
Nov 30, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions indexer/packages/postgres/__tests__/helpers/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
SubaccountCreateObject,
TendermintEventCreateObject,
TimeInForce,
TradingRewardCreateObject,
TransactionCreateObject,
TransferCreateObject,
WalletCreateObject,
Expand Down Expand Up @@ -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',
};
Original file line number Diff line number Diff line change
@@ -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);
});
Comment on lines +24 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case 'Successfully creates a TradingReward' does not assert the result of the create operation. It is important to verify that the create method returns the expected result to ensure that the TradingReward was created successfully.


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,
blockHeight: '20',
}));
expect(tradingRewards[1]).toEqual(expect.objectContaining(defaultTradingReward));
});

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));
Comment on lines +51 to +58
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test case 'Successfully finds a TradingReward' correctly asserts the expected result. However, it would be beneficial to also verify that the findById method returns undefined when a non-existent ID is passed. This would ensure that the method behaves correctly in scenarios where the TradingReward does not exist.

});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as Knex from 'knex';

export async function up(knex: Knex): Promise<void> {
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider specifying the precision and scale for the 'amount' column if the decimal values have a fixed format, to ensure data consistency and avoid potential issues with arithmetic operations on these values.


// Foreign
table.foreign('address').references('wallets.address');

// Indices
table.index(['address']);
table.index(['blockTime']);
table.index(['blockHeight']);
});
}

export async function down(knex: Knex): Promise<void> {
return knex.schema.dropTableIfExists('trading_rewards');
}
1 change: 1 addition & 0 deletions indexer/packages/postgres/src/helpers/db-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const layer1Tables = [
'liquidity_tiers',
'wallets',
'compliance_data',
'trading_rewards',
];

/**
Expand Down
75 changes: 75 additions & 0 deletions indexer/packages/postgres/src/models/trading-reward-model.ts
Original file line number Diff line number Diff line change
@@ -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' },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The address property in the JSON schema does not specify a format or pattern. Consider adding validation to ensure that addresses are stored in a consistent and valid format.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment indicates a need to ensure that jsonSchema(), sqlToJsonConversions(), and model fields match. If this has been addressed, the comment should be removed to avoid confusion.

*/
static get sqlToJsonConversions() {
return {
id: 'string',
address: 'string',
blockTime: 'date-time',
blockHeight: 'string',
amount: 'string',
};
}

QueryBuilderType!: UpsertQueryBuilder<this>;

id!: string;

address!: string;

blockTime!: string;

blockHeight!: string;

amount!: string;
}
4 changes: 2 additions & 2 deletions indexer/packages/postgres/src/models/wallet-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NumericPattern } from '../lib/validators';
import { NonNegativeNumericPattern } from '../lib/validators';
import UpsertQueryBuilder from '../query-builders/upsert';
import BaseModel from './base-model';

Expand All @@ -22,7 +22,7 @@ export default class WalletModel extends BaseModel {
],
properties: {
address: { type: 'string' },
totalTradingRewards: { type: 'string', pattern: NumericPattern },
totalTradingRewards: { type: 'string', pattern: NonNegativeNumericPattern },
},
};
}
Expand Down
108 changes: 108 additions & 0 deletions indexer/packages/postgres/src/stores/trading-reward-table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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<TradingRewardFromDatabase[]> {
verifyAllRequiredFields(
{
address,
blockHeight,
blockTimeBeforeOrAt,
limit,
} as QueryConfig,
requiredFields,
);

let baseQuery: QueryBuilder<TradingRewardModel> = setupBaseQuery<TradingRewardModel>(
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.DESC,
).orderBy(
TradingRewardColumns.address,
Ordering.DESC,
);
}

if (limit) {
baseQuery = baseQuery.limit(limit);
}

return baseQuery.returning('*');
}

export async function create(
tradingRewardToCreate: TradingRewardCreateObject,
options: Options = { txId: undefined },
): Promise<TradingRewardFromDatabase> {
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<TradingRewardFromDatabase | undefined> {
const baseQuery: QueryBuilder<TradingRewardModel> = setupBaseQuery<TradingRewardModel>(
TradingRewardModel,
options,
);
return baseQuery
.findById(address)
.returning('*');
}
8 changes: 8 additions & 0 deletions indexer/packages/postgres/src/types/db-model-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand Down
1 change: 1 addition & 0 deletions indexer/packages/postgres/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
7 changes: 7 additions & 0 deletions indexer/packages/postgres/src/types/query-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}
16 changes: 16 additions & 0 deletions indexer/packages/postgres/src/types/trading-reward-types.ts
Original file line number Diff line number Diff line change
@@ -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',
}
Loading