Skip to content

Commit

Permalink
feat: impl rgbpp assets holders resolve field
Browse files Browse the repository at this point in the history
  • Loading branch information
ahonn committed Jul 30, 2024
1 parent 3421728 commit fc86135
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 7 deletions.
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@nestjs/mercurius": "^12.2.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.3.10",
"@nestjs/schedule": "^4.1.0",
"@ntegral/nestjs-sentry": "^4.0.1",
"@prisma/client": "^5.16.2",
"@rgbpp-sdk/btc": "^0.0.0-snap-20240727021715",
Expand All @@ -51,6 +52,7 @@
"mercurius": "^14.1.0",
"mercurius-cache": "^6.0.1",
"nestjs-cacheable": "^1.0.0",
"p-limit": "^3.1.0",
"redis": "^4.6.7",
"reflect-metadata": "^0.2.0",
"rpc-websockets": "^7.11.2",
Expand Down
2 changes: 2 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Env, envSchema } from './env';
import { CoreModule } from './core/core.module';
import { ApiModule } from './modules/api.module';
import { CacheableModule } from 'nestjs-cacheable';
import { ScheduleModule } from '@nestjs/schedule';

@Module({
imports: [
Expand Down Expand Up @@ -50,6 +51,7 @@ import { CacheableModule } from 'nestjs-cacheable';
},
inject: [ConfigService],
}),
ScheduleModule.forRoot(),
CoreModule,
ApiModule,
],
Expand Down
26 changes: 22 additions & 4 deletions backend/src/modules/rgbpp/statistic/statistic.module.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import { Module } from '@nestjs/common';
import { Logger, Module } from '@nestjs/common';
import { CkbExplorerModule } from 'src/core/ckb-explorer/ckb-explorer.module';
import { RgbppStatisticResolver } from './statistic.resolver';
import { RgbppStatisticService } from './statistic.service';
import { CkbRpcModule } from 'src/core/ckb-rpc/ckb-rpc.module';
import { BitcoinApiModule } from 'src/core/bitcoin-api/bitcoin-api.module';
import { Cron, CronExpression } from '@nestjs/schedule';

@Module({
imports: [CkbExplorerModule],
providers: [RgbppStatisticResolver],
imports: [CkbExplorerModule, CkbRpcModule, BitcoinApiModule],
providers: [RgbppStatisticResolver, RgbppStatisticService],
exports: [RgbppStatisticService],
})
export class RgbppStatisticModule {}
export class RgbppStatisticModule {
private readonly logger = new Logger(RgbppStatisticModule.name);

constructor(
private rgbppStatisticService: RgbppStatisticService,
) {}

@Cron(CronExpression.EVERY_10_MINUTES)
public async collectRgbppAssetsHoldersCronTask() {
this.logger.log('Collecting RGBPP assets holders...');
const holders = await this.rgbppStatisticService.collectRgbppAssetsHolders();
await this.rgbppStatisticService.setRgbppAssetsHolders(holders);
}
}
10 changes: 7 additions & 3 deletions backend/src/modules/rgbpp/statistic/statistic.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { Float, Query, ResolveField, Resolver } from '@nestjs/graphql';
import { CkbExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service';
import { RgbppBaseStatistic, RgbppStatistic } from './statistic.model';
import { RgbppStatisticService } from './statistic.service';

@Resolver(() => RgbppStatistic)
export class RgbppStatisticResolver {
constructor(private ckbExplorerService: CkbExplorerService) {}
constructor(
private ckbExplorerService: CkbExplorerService,
private rgbppStatisticService: RgbppStatisticService,
) {}

@Query(() => RgbppStatistic, { name: 'rgbppStatistic' })
public async getRgbppStatistic(): Promise<RgbppBaseStatistic> {
Expand All @@ -19,7 +23,7 @@ export class RgbppStatisticResolver {

@ResolveField(() => Float)
public async holdersCount(): Promise<number> {
// TODO: implement this resolver
return 0;
const holders = await this.rgbppStatisticService.getRgbppAssetsHolders();
return holders.length;
}
}
104 changes: 104 additions & 0 deletions backend/src/modules/rgbpp/statistic/statistic.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { BI } from '@ckb-lumos/bi';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NetworkType } from '@rgbpp-sdk/btc';
import { getRgbppLockScript, remove0x, RGBPPLock } from '@rgbpp-sdk/ckb';
import { BtcNetworkTypeMap } from 'src/constants';
import { BitcoinApiService } from 'src/core/bitcoin-api/bitcoin-api.service';
import { CkbExplorerService } from 'src/core/ckb-explorer/ckb-explorer.service';
import { CkbRpcWebsocketService } from 'src/core/ckb-rpc/ckb-rpc-websocket.service';
import { Env } from 'src/env';
import * as pLimit from 'p-limit';
import { bytes } from '@ckb-lumos/lumos/codec';
import { CACHE_MANAGER, Cache } from '@nestjs/cache-manager';
import { TEN_MINUTES_MS } from 'src/common/date';

const limit = pLimit(100);

@Injectable()
export class RgbppStatisticService {
private holdersCacheKey = `RgbppStatisticService:holders`;

constructor(
private configService: ConfigService<Env>,
private bitcoinApiService: BitcoinApiService,
private ckbExplorerService: CkbExplorerService,
private ckbRpcService: CkbRpcWebsocketService,
@Inject(CACHE_MANAGER) protected cacheManager: Cache,
) { }

public async getRgbppAssetsHolders() {
const cached = await this.cacheManager.get<string[]>(this.holdersCacheKey);
if (cached) {
return cached;
}
const holders = await this.collectRgbppAssetsHolders();
await this.setRgbppAssetsHolders(holders);
return holders;
}

public async setRgbppAssetsHolders(holders: string[]) {
await this.cacheManager.set(this.holdersCacheKey, holders, TEN_MINUTES_MS);
};

public async collectRgbppAssetsHolders() {
const network = this.configService.get('NETWORK');
const rgbppLock = getRgbppLockScript(
network === NetworkType.MAINNET,
BtcNetworkTypeMap[network],
);
const rgbppTxs = await this.ckbExplorerService.getRgbppTransactions();
const cells = await this.ckbRpcService.getCells(
{
script: {
code_hash: rgbppLock.codeHash,
hash_type: rgbppLock.hashType,
args: '0x',
},
script_type: 'lock',
},
'desc',
BI.from(rgbppTxs.meta.total).toHexString(),
);

const utxos = cells.objects
.map((cell) => {
const { args } = cell.output.lock;
try {
const unpack = RGBPPLock.unpack(args);
const btcTxid = bytes.hexify(bytes.bytify(unpack.btcTxid).reverse());
return {
outIndex: unpack.outIndex,
btcTxid: remove0x(btcTxid),
};
} catch {
return null;
}
})
.filter((utxo) => utxo !== null);

const txidSet = new Set(utxos.map((utxo) => utxo.btcTxid));
const txs = await Promise.allSettled(
Array.from(txidSet).map((txid) =>
limit(async () => {
const tx = await this.bitcoinApiService.getTx({ txid });
return tx;
}),
),
);

const holdersSet = txs.reduce((set, tx) => {
if (tx.status === 'fulfilled') {
const { vout } = tx.value;
vout.forEach((output) => {
const { scriptpubkey } = output;
if (scriptpubkey) {
set.add(scriptpubkey);
}
});
}
return set;
}, new Set<string>());
return Array.from(holdersSet);
}
}
44 changes: 44 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit fc86135

Please sign in to comment.