Skip to content

Commit

Permalink
Merge pull request #48 from 0LNetworkCommunity/feat/accounts
Browse files Browse the repository at this point in the history
Feat: New Accounts Page
  • Loading branch information
minaxolone authored Jun 14, 2024
2 parents 2e7512b + 30a08af commit 00508e8
Show file tree
Hide file tree
Showing 72 changed files with 2,502 additions and 1,755 deletions.
556 changes: 278 additions & 278 deletions api/package-lock.json

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
},
"dependencies": {
"@apollo/server": "^4.10.4",
"@aptos-labs/ts-sdk": "^1.18.1",
"@aws-sdk/client-s3": "^3.592.0",
"@aptos-labs/ts-sdk": "^1.19.0",
"@aws-sdk/client-s3": "^3.596.0",
"@clickhouse/client": "^1.1.0",
"@nestjs/apollo": "^12.1.0",
"@nestjs/bullmq": "^10.1.1",
Expand All @@ -45,12 +45,12 @@
"axios": "^1.7.2",
"bluebird": "^3.7.2",
"bn.js": "^5.2.1",
"bullmq": "^5.7.15",
"bullmq": "^5.8.1",
"csv-stringify": "^6.5.0",
"d3-array": "^3.2.4",
"decimal.js": "^10.4.3",
"firebase-admin": "^12.1.1",
"graphql": "^16.8.1",
"graphql": "^16.8.2",
"graphql-ws": "^5.16.0",
"lodash": "^4.17.21",
"maxmind": "^4.3.20",
Expand All @@ -68,13 +68,13 @@
"@types/jest": "^29.5.12",
"@types/node": "^20.14.2",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.12.0",
"@typescript-eslint/parser": "^7.12.0",
"@typescript-eslint/eslint-plugin": "^7.13.0",
"@typescript-eslint/parser": "^7.13.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.7.0",
"prettier": "^3.3.1",
"prettier": "^3.3.2",
"prisma": "^5.15.0",
"source-map-support": "^0.5.21",
"supertest": "^7.0.0",
Expand Down
89 changes: 13 additions & 76 deletions api/src/ol/account.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,111 +6,48 @@ import {
Resolver,
Int,
} from "@nestjs/graphql";
import { ApiError } from "aptos";
import { Decimal } from "decimal.js";

import { OlService } from "./ol.service.js";
import { GqlAccount } from "./models/account.model.js";
import { GqlSlowWallet } from "./models/slow-wallet.model.js";
import { Account } from "./models/account.model.js";
import { SlowWallet } from "./models/slow-wallet.model.js";
import { OrderDirection } from "./models/Paginated.js";
import { PaginatedMovements } from "./models/PaginatedMovements.js";
import { MovementsService } from "./movements/movements.service.js";

export interface CoinStoreResource {
coin: {
value: string;
};
deposit_events: {
counter: string;
guid: {
id: {
addr: string;
creation_num: string;
};
};
};
withdraw_events: {
counter: string;
guid: {
id: {
addr: string;
creation_num: string;
};
};
};
}

export interface SlowWalletResource {
transferred: string;
unlocked: string;
}

@Resolver(GqlAccount)
@Resolver(Account)
export class AccountResolver {
public constructor(
private readonly olService: OlService,
private readonly movementsService: MovementsService,
) {}

@Query(() => GqlAccount, { nullable: true })
@Query(() => Account, { nullable: true })
public async account(
@Args({ name: "address", type: () => Buffer }) address: Buffer,
): Promise<GqlAccount | null> {
): Promise<Account | null> {
const accountExists = await this.olService.accountExists(address);
if (accountExists) {
return new GqlAccount(address);
return new Account(address);
}
return null;
}

@ResolveField(() => Decimal, { nullable: true })
public async balance(@Parent() account: GqlAccount): Promise<Decimal | null> {
try {
const res = await this.olService.aptosClient.getAccountResource(
`0x${account.address.toString("hex")}`,
"0x1::coin::CoinStore<0x1::libra_coin::LibraCoin>",
);
const balance = new Decimal(
(res.data as CoinStoreResource).coin.value,
).div(1e6);
return balance;
} catch (error) {
if (error instanceof ApiError) {
if (error.errorCode === "resource_not_found") {
return null;
}
}
throw error;
}
public async balance(@Parent() account: Account): Promise<Decimal | null> {
return this.olService.getAccountBalance(account.address);
}

@ResolveField(() => GqlSlowWallet, { nullable: true })
@ResolveField(() => SlowWallet, { nullable: true })
public async slowWallet(
@Parent() account: GqlAccount,
): Promise<GqlSlowWallet | null> {
try {
const res = await this.olService.aptosClient.getAccountResource(
`0x${account.address.toString("hex")}`,
"0x1::slow_wallet::SlowWallet",
);
const slowWallet = res.data as SlowWalletResource;
return new GqlSlowWallet({
unlocked: new Decimal(slowWallet.unlocked).div(1e6),
transferred: new Decimal(slowWallet.transferred).div(1e6),
});
} catch (error) {
if (error instanceof ApiError) {
if (error.errorCode === "resource_not_found") {
return null;
}
}
throw error;
}
@Parent() account: Account,
): Promise<SlowWallet | null> {
return this.olService.getSlowWallet(account.address);
}

@ResolveField(() => PaginatedMovements)
public async movements(
@Parent() account: GqlAccount,
@Parent() account: Account,

@Args({
name: "first",
Expand Down
54 changes: 54 additions & 0 deletions api/src/ol/accounts/accounts.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Field, ObjectType, Int, Float } from "@nestjs/graphql";

export interface CumulativeShareInput {
amount: number;
percentage: number;
}

@ObjectType()
export class CumulativeShare {
@Field(() => Float)
public amount: number;

@Field(() => Float)
public percentage: number;

public constructor(input: CumulativeShareInput) {
this.amount = input.amount;
this.percentage = input.percentage;
}
}

export interface TopAccountInput {
rank: number;
address: string;
publicName: string;
balance: number;
cumulativeShare: CumulativeShare;
}

@ObjectType()
export class TopAccount {
@Field(() => Int)
public rank: number;

@Field()
public address: string;

@Field()
public publicName: string;

@Field(() => Float)
public balance: number;

@Field(() => CumulativeShare)
public cumulativeShare: CumulativeShare;

public constructor(input: TopAccountInput) {
this.rank = input.rank;
this.address = input.address;
this.publicName = input.publicName;
this.balance = input.balance;
this.cumulativeShare = input.cumulativeShare;
}
}
49 changes: 49 additions & 0 deletions api/src/ol/accounts/accounts.processor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { InjectQueue, Processor, WorkerHost } from "@nestjs/bullmq";
import { OnModuleInit } from "@nestjs/common";
import { Job, Queue } from "bullmq";

import { AccountsService } from "./accounts.service.js";
import { redisClient } from "../../redis/redis.service.js";
import { TOP_BALANCE_ACCOUNTS_CACHE_KEY } from "../constants.js";

@Processor("accounts")
export class AccountsProcessor extends WorkerHost implements OnModuleInit {
public constructor(
@InjectQueue("accounts")
private readonly accountsQueue: Queue,

private readonly accountsService: AccountsService,
) {
super();
}

public async onModuleInit() {
await this.accountsQueue.add("updateAccountsCache", undefined, {
repeat: {
every: 60 * 60 * 1_000, // 1 hour
},
});

// Execute the job immediately on startup
await this.updateAccountsCache();
}

public async process(job: Job<void, any, string>) {
switch (job.name) {
case "updateAccountsCache":
await this.updateAccountsCache();
break;

default:
throw new Error(`invalid job name ${job.name}`);
}
}

private async updateAccountsCache() {
const accounts = await this.accountsService.getTopBalanceAccounts(100);
await redisClient.set(
TOP_BALANCE_ACCOUNTS_CACHE_KEY,
JSON.stringify(accounts),
);
}
}
61 changes: 61 additions & 0 deletions api/src/ol/accounts/accounts.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Query, Resolver, Args } from "@nestjs/graphql";
import { Inject } from "@nestjs/common";
import { ServiceUnavailableException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";

import { CumulativeShare, TopAccount } from "./accounts.model.js";
import { AccountsService } from "./accounts.service.js";
import { redisClient } from "../../redis/redis.service.js";
import { TOP_BALANCE_ACCOUNTS_CACHE_KEY } from "../constants.js";

@Resolver(() => TopAccount)
export class AccountsResolver {
private readonly cacheEnabled: boolean;

public constructor(
@Inject(AccountsService)
private readonly accountsService: AccountsService,
config: ConfigService,
) {
this.cacheEnabled = config.get<boolean>("cacheEnabled")!;
}

@Query(() => [TopAccount])
async getTopAccounts(
@Args("limit", { type: () => Number, defaultValue: 100 }) limit: number,
): Promise<TopAccount[]> {
// Check if caching is enabled and the query is not present
if (this.cacheEnabled) {
const cachedAccounts = await redisClient.get(
TOP_BALANCE_ACCOUNTS_CACHE_KEY,
);
if (cachedAccounts) {
const accounts = JSON.parse(cachedAccounts);
return accounts.slice(0, limit).map(
(account: any) =>
new TopAccount({
rank: account.rank,
address: account.address,
publicName: account.publicName,
balance: account.balance,
cumulativeShare: new CumulativeShare(account.cumulativeShare),
}),
);
}
throw new ServiceUnavailableException("Cache not ready");
}

// If cache is not enabled, get data from the service
const accounts = await this.accountsService.getTopBalanceAccounts(limit);
return accounts.map(
(account) =>
new TopAccount({
rank: account.rank,
address: account.address,
publicName: account.publicName,
balance: account.balance,
cumulativeShare: new CumulativeShare(account.cumulativeShare),
}),
);
}
}
Loading

0 comments on commit 00508e8

Please sign in to comment.