From 3a14846bb95254b5121461f2768b7c83137453fe Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Sun, 26 Jan 2025 16:11:07 +0800 Subject: [PATCH 1/9] feat: get balances from batch addresses --- config.mainnet/config.yaml | 3 + config/config.yaml | 3 + libs/asset/src/asset.controller.ts | 40 ++++++++++++- libs/asset/src/asset.service.ts | 17 ++++++ libs/cell/src/cell.controller.ts | 85 +++++++++++++++++++-------- libs/cell/src/cell.service.ts | 45 +++++++++++++- libs/commons/src/ormUtils/index.ts | 3 +- libs/commons/src/rest/index.ts | 14 +++++ libs/commons/src/utils/index.ts | 13 ++++ libs/udt/src/repos/udtBalance.repo.ts | 74 +++++++++++++++-------- libs/udt/src/udt.controller.ts | 43 +++++++++++++- libs/udt/src/udt.service.ts | 21 ++++++- 12 files changed, 304 insertions(+), 57 deletions(-) diff --git a/config.mainnet/config.yaml b/config.mainnet/config.yaml index 5027877..06d012d 100644 --- a/config.mainnet/config.yaml +++ b/config.mainnet/config.yaml @@ -20,6 +20,9 @@ sync: rgbppBtcCodeHash: "0xbc6c568a1a0d0a09f6844dc9d74ddb4343c32143ff25f727c59edf4fb72d6936" rgbppBtcHashType: "type" + rgbppBtcTimelockCodeHash: "0x70d64497a075bd651e98ac030455ea200637ee325a12ad08aff03f1a117e5a62" + rgbppBtcTimelockHashType: "type" + udtTypes: # sUDT - codeHash: "0x5e7a36a77e68eecc013dfa2fe6a23f3b6c344b04005808694ae6dd45eea4cfd5" diff --git a/config/config.yaml b/config/config.yaml index fbede97..d5a17e8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -20,6 +20,9 @@ sync: rgbppBtcCodeHash: "0xd07598deec7ce7b5665310386b4abd06a6d48843e953c5cc2112ad0d5a220364" rgbppBtcHashType: "type" + rgbppBtcTimelockCodeHash: "0x80a09eca26d77cea1f5a69471c59481be7404febf40ee90f886c36a948385b55" + rgbppBtcTimelockHashType: "type" + udtTypes: # sUDT - codeHash: "0x48dbf59b4c7ee1547238021b4869bceedf4eea6b43772e5d66ef8865b6ae7212" diff --git a/libs/asset/src/asset.controller.ts b/libs/asset/src/asset.controller.ts index 22cf741..fc7050c 100644 --- a/libs/asset/src/asset.controller.ts +++ b/libs/asset/src/asset.controller.ts @@ -6,6 +6,7 @@ import { EventType, extractIsomorphicInfo, IsomorphicBinding, + LeapType, NormalizedReturn, RpcError, ScriptMode, @@ -25,7 +26,7 @@ import { AssetService } from "./asset.service"; export class AssetController { constructor(private readonly service: AssetService) {} - async cellDetialWithoutAssets( + async cellDetailWithoutAssets( cell: ccc.Cell, index: number, eventType: EventType, @@ -42,6 +43,7 @@ export class AssetController { chain: Chain.Btc, txHash: ccc.hexFrom(isomorphicInfo.txHash), vout: Number(isomorphicInfo.index), + leapType: LeapType.None, // default is none }; } break; @@ -50,6 +52,7 @@ export class AssetController { chain: Chain.Doge, txHash: ccc.hexFrom(isomorphicInfo.txHash), vout: Number(isomorphicInfo.index), + leapType: LeapType.None, // default is none }; } } @@ -70,7 +73,7 @@ export class AssetController { index: number, eventType: EventType, ): Promise { - const cellAsset = await this.cellDetialWithoutAssets( + const cellAsset = await this.cellDetailWithoutAssets( cell, index, eventType, @@ -78,9 +81,10 @@ export class AssetController { const token = await this.service.getTokenFromCell(cell); if (token) { - const { tokenInfo, balance } = token; + const { tokenInfo, balance, mintable } = token; cellAsset.tokenData = { tokenId: ccc.hexFrom(tokenInfo.hash), + mintable, name: tokenInfo.name ?? undefined, symbol: tokenInfo.symbol ?? undefined, decimal: tokenInfo.decimals ?? undefined, @@ -136,6 +140,7 @@ export class AssetController { }; output: { totalBalance: ccc.Num; + mintable: boolean; indices: Array; }; } @@ -168,6 +173,7 @@ export class AssetController { }, output: { totalBalance: ccc.numFrom(0), + mintable: cellAsset.tokenData.mintable, indices: [], }, }; @@ -215,6 +221,7 @@ export class AssetController { }, output: { totalBalance: cellAsset.tokenData.amount, + mintable: cellAsset.tokenData.mintable, indices: [txAssetData.outputs.length], }, }; @@ -275,6 +282,7 @@ export class AssetController { const tokenId = ccc.hexFrom(firstTokenId); txAssetData.outputs[index].tokenData = { tokenId, + mintable: tokenGroups[tokenId].output.mintable, name: tokenMetadata.name ?? undefined, symbol: tokenMetadata.symbol ?? undefined, decimal: tokenMetadata.decimals ?? undefined, @@ -286,6 +294,32 @@ export class AssetController { } } + return this.filterAndChangeLeapTypes(txAssetData); + } + + filterAndChangeLeapTypes(txAssetData: TxAssetCellData): TxAssetCellData { + for (const rgbppChain of [Chain.Btc, Chain.Doge]) { + const hasRgbppModeInInputs = txAssetData.inputs.some( + (input) => input.rgbppBinding?.chain === rgbppChain, + ); + const hasRgbppModeInOutputs = txAssetData.outputs.some( + (output) => output.rgbppBinding?.chain === rgbppChain, + ); + if (hasRgbppModeInInputs && !hasRgbppModeInOutputs) { + for (let i = 0; i < txAssetData.inputs.length; i++) { + if (txAssetData.inputs[i].rgbppBinding?.chain === rgbppChain) { + txAssetData.inputs[i].rgbppBinding!.leapType = LeapType.FromUtxo; + } + } + } + if (!hasRgbppModeInInputs && hasRgbppModeInOutputs) { + for (let i = 0; i < txAssetData.outputs.length; i++) { + if (txAssetData.outputs[i].rgbppBinding?.chain === rgbppChain) { + txAssetData.outputs[i].rgbppBinding!.leapType = LeapType.ToUtxo; + } + } + } + } return txAssetData; } diff --git a/libs/asset/src/asset.service.ts b/libs/asset/src/asset.service.ts index c9dc882..d6801aa 100644 --- a/libs/asset/src/asset.service.ts +++ b/libs/asset/src/asset.service.ts @@ -1,5 +1,6 @@ import { assertConfig, + mintableScriptMode, parseBtcAddress, parseScriptMode, ScriptMode, @@ -17,6 +18,8 @@ export class AssetService { private readonly client: ccc.Client; private readonly rgbppBtcCodeHash: ccc.Hex; private readonly rgbppBtcHashType: ccc.HashType; + private readonly rgbppBtcTimelockCodeHash: ccc.Hex; + private readonly rgbppBtcTimelockHashType: ccc.HashType; private readonly udtTypes: { codeHash: ccc.HexLike; hashType: ccc.HashTypeLike; @@ -41,6 +44,12 @@ export class AssetService { this.rgbppBtcHashType = ccc.hashTypeFrom( assertConfig(configService, "sync.rgbppBtcHashType"), ); + this.rgbppBtcTimelockCodeHash = ccc.hexFrom( + assertConfig(configService, "sync.rgbppBtcTimelockCodeHash"), + ); + this.rgbppBtcTimelockHashType = ccc.hashTypeFrom( + assertConfig(configService, "sync.rgbppBtcTimelockHashType"), + ); const udtTypes = configService.get< @@ -60,6 +69,11 @@ export class AssetService { hashType: this.rgbppBtcHashType, mode: ScriptMode.RgbppBtc, }); + extension.push({ + codeHash: this.rgbppBtcTimelockCodeHash, + hashType: this.rgbppBtcTimelockHashType, + mode: ScriptMode.RgbppBtcTimelock, + }); return await parseScriptMode(script, this.client, extension); } @@ -199,6 +213,7 @@ export class AssetService { async getTokenFromCell(cell: ccc.Cell): Promise< | { tokenInfo: UdtInfo; + mintable: boolean; balance: ccc.Num; } | undefined @@ -220,8 +235,10 @@ export class AssetService { typeArgs: cell.cellOutput.type.args, }); const tokenAmount = ccc.udtBalanceFrom(cell.outputData); + const lockMode = await this.scriptMode(cell.cellOutput.lock); return { tokenInfo, + mintable: mintableScriptMode(lockMode), balance: tokenAmount, }; } diff --git a/libs/cell/src/cell.controller.ts b/libs/cell/src/cell.controller.ts index 0c021a3..2e7ee8d 100644 --- a/libs/cell/src/cell.controller.ts +++ b/libs/cell/src/cell.controller.ts @@ -5,6 +5,7 @@ import { Chain, extractIsomorphicInfo, IsomorphicBinding, + LeapType, NormalizedReturn, PagedTokenResult, RpcError, @@ -24,34 +25,71 @@ import { CellService } from "./cell.service"; export class CellController { constructor(private readonly service: CellService) {} + async parseIsomorphicBinding( + cell: ccc.Cell, + ): Promise { + let isomorphicBinding: IsomorphicBinding | undefined = undefined; + const lockScriptMode = await this.service.scriptMode(cell.cellOutput.lock); + const tx = assert( + await this.service.getTxByCell(cell), + RpcError.TxNotFound, + ); + // rgbpp lock related modes + for (const { mode, chain } of [ + { mode: ScriptMode.RgbppBtc, chain: Chain.Btc }, + { mode: ScriptMode.RgbppDoge, chain: Chain.Doge }, + ]) { + if (lockScriptMode === mode) { + const isomorphicInfo = assert( + extractIsomorphicInfo(cell.cellOutput.lock), + RpcError.IsomorphicBindingNotFound, + ); + const rgbppMode = await this.service.getScriptByModeFromTxInputs( + tx, + mode, + ); + isomorphicBinding = { + chain, + txHash: ccc.hexFrom(isomorphicInfo.txHash), + vout: Number(isomorphicInfo.index), + leapType: rgbppMode ? LeapType.None : LeapType.ToUtxo, + }; + break; + } + } + // rgbpp timelock related modes + for (const { mode, chain } of [ + { mode: ScriptMode.RgbppBtcTimelock, chain: Chain.Btc }, + { mode: ScriptMode.RgbppDogeTimelock, chain: Chain.Doge }, + ]) { + if (lockScriptMode === mode) { + const rgbppScript = assert( + await this.service.getScriptByModeFromTxInputs(tx, mode), + RpcError.RgbppCellNotFound, + ); + const isomorphicInfo = assert( + extractIsomorphicInfo(rgbppScript), + RpcError.IsomorphicBindingNotFound, + ); + isomorphicBinding = { + chain, + txHash: ccc.hexFrom(isomorphicInfo.txHash), + vout: Number(isomorphicInfo.index), + leapType: LeapType.FromUtxo, + }; + break; + } + } + return isomorphicBinding; + } + async cellToTokenCell( cell: ccc.Cell, spender?: ccc.OutPoint, ): Promise { const address = await this.service.scriptToAddress(cell.cellOutput.lock); const lockScriptMode = await this.service.scriptMode(cell.cellOutput.lock); - const isomorphicInfo = extractIsomorphicInfo(cell.cellOutput.lock); - let isomorphicBinding: IsomorphicBinding | undefined = undefined; - if (isomorphicInfo) { - switch (lockScriptMode) { - case ScriptMode.RgbppBtc: { - isomorphicBinding = { - chain: Chain.Btc, - txHash: ccc.hexFrom(isomorphicInfo.txHash), - vout: Number(isomorphicInfo.index), - }; - break; - } - case ScriptMode.RgbppDoge: { - isomorphicBinding = { - chain: Chain.Doge, - txHash: ccc.hexFrom(isomorphicInfo.txHash), - vout: Number(isomorphicInfo.index), - }; - break; - } - } - } + const isomorphicBinding = await this.parseIsomorphicBinding(cell); const typeScript = assert(cell.cellOutput.type, RpcError.CellNotAsset); const typeScriptType = await this.service.scriptMode(typeScript); return { @@ -87,10 +125,11 @@ export class CellController { async getCellByOutpoint( @Param("txHash") txHash: string, @Param("index") index: number, + @Query("containSpender") containSpender: boolean, ): Promise> { try { const { cell, spender } = assert( - await this.service.getCellByOutpoint(txHash, index), + await this.service.getCellByOutpoint(txHash, index, containSpender), RpcError.CkbCellNotFound, ); assert(cell.cellOutput.type, RpcError.CellNotAsset); diff --git a/libs/cell/src/cell.service.ts b/libs/cell/src/cell.service.ts index 5eea504..014be7f 100644 --- a/libs/cell/src/cell.service.ts +++ b/libs/cell/src/cell.service.ts @@ -18,6 +18,9 @@ export class CellService { private readonly client: ccc.Client; private readonly rgbppBtcCodeHash: ccc.Hex; private readonly rgbppBtcHashType: ccc.HashType; + private readonly rgbppBtcTimelockCodeHash: ccc.Hex; + private readonly rgbppBtcTimelockHashType: ccc.HashType; + private readonly btcRequesters: AxiosInstance[]; private readonly udtTypes: { codeHash: ccc.HexLike; hashType: ccc.HashTypeLike; @@ -41,6 +44,16 @@ export class CellService { assertConfig(configService, "sync.rgbppBtcHashType"), ); + this.rgbppBtcTimelockCodeHash = ccc.hexFrom( + assertConfig(configService, "sync.rgbppBtcTimelockCodeHash"), + ); + this.rgbppBtcTimelockHashType = ccc.hashTypeFrom( + assertConfig(configService, "sync.rgbppBtcTimelockHashType"), + ); + + const btcRpcUris = assertConfig(configService, "sync.btcRpcUris"); + this.btcRequesters = btcRpcUris.map((baseURL) => axios.create({ baseURL })); + const udtTypes = configService.get< { codeHash: ccc.HexLike; hashType: ccc.HashTypeLike }[] @@ -59,6 +72,11 @@ export class CellService { hashType: this.rgbppBtcHashType, mode: ScriptMode.RgbppBtc, }); + extension.push({ + codeHash: this.rgbppBtcTimelockCodeHash, + hashType: this.rgbppBtcTimelockHashType, + mode: ScriptMode.RgbppBtcTimelock, + }); return await parseScriptMode(script, this.client, extension); } @@ -77,9 +95,33 @@ export class CellService { return ccc.Address.fromScript(script, this.client).toString(); } + async getTxByCell(cell: ccc.Cell): Promise { + return await this.client + .getTransaction(cell.outPoint.txHash) + .then((tx) => tx?.transaction); + } + + async getScriptByModeFromTxInputs( + tx: ccc.Transaction, + scriptMode: ScriptMode, + ): Promise { + for (const input of tx.inputs) { + const script = await this.client + .getCellLive(input.previousOutput) + .then((cell) => cell?.cellOutput.lock); + if (script) { + const lockScriptMode = await this.scriptMode(script); + if (scriptMode === lockScriptMode) { + return script; + } + } + } + } + async getCellByOutpoint( txHash: ccc.HexLike, index: number, + containSpender: boolean, ): Promise< | { cell: ccc.Cell; @@ -90,7 +132,7 @@ export class CellService { const cell = await this.client.getCell({ txHash, index }); if (cell) { // If the cell is not an asset, skip finding the spender - if (cell.cellOutput.type === undefined) { + if (cell.cellOutput.type === undefined || !containSpender) { return { cell, }; @@ -137,7 +179,6 @@ export class CellService { }; } } - console.log("no spender found"); return { cell, }; diff --git a/libs/commons/src/ormUtils/index.ts b/libs/commons/src/ormUtils/index.ts index 2349a5e..0de07d3 100644 --- a/libs/commons/src/ormUtils/index.ts +++ b/libs/commons/src/ormUtils/index.ts @@ -30,7 +30,8 @@ export async function foreachInRepo({ while (true) { const entities = await repo.find({ where: lastId - ? ({ ...(criteria ?? {}), id: MoreThan(lastId) } as any) + ? /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + ({ ...(criteria ?? {}), id: MoreThan(lastId) } as any) : criteria, order, take: chunkSize ?? 100, diff --git a/libs/commons/src/rest/index.ts b/libs/commons/src/rest/index.ts index 1f41a0a..9523216 100644 --- a/libs/commons/src/rest/index.ts +++ b/libs/commons/src/rest/index.ts @@ -10,6 +10,8 @@ export enum Chain { export enum ScriptMode { RgbppBtc = "rgbppBtc", RgbppDoge = "rgbppDoge", + RgbppBtcTimelock = "rgbppBtcTimelock", + RgbppDogeTimelock = "rgbppDogeTimelock", SingleUseLock = "singleUseLock", OmniLock = "omniLock", Udt = "udt", @@ -29,6 +31,12 @@ enum ApiHashType { Data2 = "data2", } +export enum LeapType { + None = 0, + FromUtxo = 1, + ToUtxo = 2, +} + export class NormalizedReturn { code: number; msg?: string; @@ -86,6 +94,8 @@ export class TokenBalance { address: string; @ApiProperty({ type: Number }) balance: ccc.Num; + @ApiProperty({ type: Number }) + height: ccc.Num; } export class BlockHeader { @@ -211,6 +221,8 @@ export class TokenData { tokenId: ccc.Hex; @ApiProperty({ type: Number }) amount: ccc.Num; + @ApiProperty() + mintable: boolean; @ApiPropertyOptional() name?: string; @ApiPropertyOptional() @@ -226,6 +238,8 @@ export class IsomorphicBinding { txHash: ccc.Hex; @ApiProperty({ type: Number }) vout: number; + @ApiProperty({ enum: LeapType }) + leapType: LeapType; } export class TxAssetCellDetail { diff --git a/libs/commons/src/utils/index.ts b/libs/commons/src/utils/index.ts index 41204b5..8d0def2 100644 --- a/libs/commons/src/utils/index.ts +++ b/libs/commons/src/utils/index.ts @@ -67,6 +67,7 @@ export enum RpcError { CellNotAsset, ClusterNotFound, SporeNotFound, + IsomorphicBindingNotFound, } export const RpcErrorMessage: Record = { @@ -78,6 +79,7 @@ export const RpcErrorMessage: Record = { [RpcError.CellNotAsset]: "Cell is not an asset", [RpcError.ClusterNotFound]: "Cluster not found", [RpcError.SporeNotFound]: "Spore not found", + [RpcError.IsomorphicBindingNotFound]: "Isomorphic binding not found", }; export class ApiError { @@ -280,3 +282,14 @@ export function extractIsomorphicInfo( const { outIndex, txId } = decoded; return { txHash: txId, index: outIndex }; } + +export function mintableScriptMode(scriptMode: ScriptMode): boolean { + const unmintable = [ + ScriptMode.SingleUseLock, + ScriptMode.RgbppBtc, + ScriptMode.RgbppDoge, + ScriptMode.RgbppBtcTimelock, + ScriptMode.RgbppDogeTimelock, + ].includes(scriptMode); + return !unmintable; +} diff --git a/libs/udt/src/repos/udtBalance.repo.ts b/libs/udt/src/repos/udtBalance.repo.ts index 4c62e51..16d9553 100644 --- a/libs/udt/src/repos/udtBalance.repo.ts +++ b/libs/udt/src/repos/udtBalance.repo.ts @@ -2,7 +2,7 @@ import { formatSortable } from "@app/commons"; import { UdtBalance } from "@app/schemas"; import { ccc } from "@ckb-ccc/shell"; import { Injectable } from "@nestjs/common"; -import { EntityManager, MoreThan, Repository } from "typeorm"; +import { EntityManager, In, MoreThan, Repository } from "typeorm"; @Injectable() export class UdtBalanceRepo extends Repository { @@ -11,32 +11,56 @@ export class UdtBalanceRepo extends Repository { } async getTokenItemsByAddress( - address: string, + addresses: string[], tokenHash?: ccc.HexLike, + height?: ccc.Num, ): Promise { - const addressHash = ccc.hashCkb(ccc.bytesFrom(address, "utf8")); + const addressHashes = addresses.map((address) => + ccc.hashCkb(ccc.bytesFrom(address, "utf8")), + ); if (tokenHash) { - return await this.find({ - where: { - addressHash, - tokenHash: ccc.hexFrom(tokenHash), - balance: MoreThan(formatSortable(0)), - }, - order: { updatedAtHeight: "DESC" }, - take: 1, - }); + if (height) { + return await this.find({ + where: { + addressHash: In(addressHashes), + tokenHash: ccc.hexFrom(tokenHash), + balance: MoreThan(formatSortable(0)), + updatedAtHeight: formatSortable(height), + }, + }); + } else { + return await this.find({ + where: { + addressHash: In(addressHashes), + tokenHash: ccc.hexFrom(tokenHash), + balance: MoreThan(formatSortable(0)), + }, + order: { updatedAtHeight: "DESC" }, + take: 1, + }); + } } else { - const rawSql = ` - SELECT ub.* - FROM udt_balance AS ub - WHERE ub.id IN ( - SELECT MAX(id) - FROM udt_balance - WHERE addressHash = ? AND balance > 0 - GROUP BY tokenHash - ); - `; - return await this.manager.query(rawSql, [addressHash]); + if (height) { + return await this.find({ + where: { + addressHash: In(addressHashes), + balance: MoreThan(formatSortable(0)), + updatedAtHeight: formatSortable(height), + }, + }); + } else { + const rawSql = ` + SELECT ub.* + FROM udt_balance AS ub + WHERE ub.updatedAtHeight IN ( + SELECT MAX(updatedAtHeight) + FROM udt_balance + WHERE addressHash = IN (?) AND balance > 0 + GROUP BY tokenHash + ); + `; + return await this.manager.query(rawSql, [addressHashes]); + } } } @@ -48,8 +72,8 @@ export class UdtBalanceRepo extends Repository { const rawSql = ` SELECT ub.* FROM udt_balance AS ub - WHERE ub.id IN ( - SELECT MAX(ub_inner.id) + WHERE ub.updatedAtHeight IN ( + SELECT MAX(ub_inner.updatedAtHeight) FROM udt_balance AS ub_inner WHERE ub_inner.tokenHash = ? AND ub_inner.balance > 0 GROUP BY ub_inner.addressHash diff --git a/libs/udt/src/udt.controller.ts b/libs/udt/src/udt.controller.ts index 4d2b8e0..833aa1d 100644 --- a/libs/udt/src/udt.controller.ts +++ b/libs/udt/src/udt.controller.ts @@ -39,6 +39,7 @@ export class UdtController { decimal: udtInfo.decimals ?? undefined, address: udtBalance.address, balance: ccc.numFrom(udtBalance.balance), + height: parseSortableInt(udtBalance.updatedAtHeight), }; } @@ -119,12 +120,52 @@ export class UdtController { required: false, description: "The ID of the token to filter balances (optional)", }) + @ApiQuery({ + name: "height", + required: false, + description: "The height of the block to query (optional)", + }) @Get("/tokens/balances/:address") async getTokenBalances( @Param("address") address: string, @Query("tokenId") tokenId?: string, + @Query("height") height?: number, ): Promise> { - const udtBalances = await this.service.getTokenBalance(address, tokenId); + const udtBalances = await this.service.getTokenBalanceByAddress( + address, + tokenId, + height ? ccc.numFrom(height) : undefined, + ); + return { + code: 0, + data: await asyncMap( + udtBalances, + this.udtBalanceToTokenBalance.bind(this), + ), + }; + } + + @ApiOkResponse({ + type: [TokenBalance], + description: + "Get detailed token balances under a token id, filtered by addresses", + }) + @ApiQuery({ + name: "height", + required: false, + description: "The height of the block to query (optional)", + }) + @Get("/tokens/balances/:tokenId/:addresses") + async batchGetTokenBalances( + @Param("tokenId") tokenId: string, + @Param("addresses") addresses: string, + @Query("height") height?: number, + ): Promise> { + const udtBalances = await this.service.getTokenBalanceByTokenId( + tokenId, + addresses.split(","), + height ? ccc.numFrom(height) : undefined, + ); return { code: 0, data: await asyncMap( diff --git a/libs/udt/src/udt.service.ts b/libs/udt/src/udt.service.ts index 2e69f45..0394c27 100644 --- a/libs/udt/src/udt.service.ts +++ b/libs/udt/src/udt.service.ts @@ -97,11 +97,28 @@ export class UdtService { return this.udtBalanceRepo.getItemCountByTokenHash(tokenId); } - async getTokenBalance( + async getTokenBalanceByAddress( address: string, tokenId?: ccc.HexLike, + height?: ccc.Num, ): Promise { - return await this.udtBalanceRepo.getTokenItemsByAddress(address, tokenId); + return await this.udtBalanceRepo.getTokenItemsByAddress( + [address], + tokenId, + height, + ); + } + + async getTokenBalanceByTokenId( + tokenId: ccc.HexLike, + addresses: string[], + height?: ccc.Num, + ): Promise { + return await this.udtBalanceRepo.getTokenItemsByAddress( + addresses, + tokenId, + height, + ); } async getTokenAllBalances( From 2f5916930a827571de364f5538b08b4495ecda8d Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Tue, 28 Jan 2025 10:40:44 +0800 Subject: [PATCH 2/9] chore: complete remaining parameters of cell module interfaces --- libs/cell/src/cell.controller.ts | 17 +++++-- libs/cell/src/cell.service.ts | 6 +-- libs/udt/src/repos/udtBalance.repo.ts | 64 +++++++++++++++++++++------ 3 files changed, 66 insertions(+), 21 deletions(-) diff --git a/libs/cell/src/cell.controller.ts b/libs/cell/src/cell.controller.ts index 2e7ee8d..570eaea 100644 --- a/libs/cell/src/cell.controller.ts +++ b/libs/cell/src/cell.controller.ts @@ -58,13 +58,22 @@ export class CellController { } } // rgbpp timelock related modes - for (const { mode, chain } of [ - { mode: ScriptMode.RgbppBtcTimelock, chain: Chain.Btc }, - { mode: ScriptMode.RgbppDogeTimelock, chain: Chain.Doge }, + for (const { mode, premode, chain } of [ + { + mode: ScriptMode.RgbppBtcTimelock, + premode: ScriptMode.RgbppBtc, + chain: Chain.Btc, + }, + { + mode: ScriptMode.RgbppDogeTimelock, + premode: ScriptMode.RgbppDoge, + chain: Chain.Doge, + }, ]) { if (lockScriptMode === mode) { + console.log("premode = ", premode); const rgbppScript = assert( - await this.service.getScriptByModeFromTxInputs(tx, mode), + await this.service.getScriptByModeFromTxInputs(tx, premode), RpcError.RgbppCellNotFound, ); const isomorphicInfo = assert( diff --git a/libs/cell/src/cell.service.ts b/libs/cell/src/cell.service.ts index 014be7f..ef9ebfa 100644 --- a/libs/cell/src/cell.service.ts +++ b/libs/cell/src/cell.service.ts @@ -106,11 +106,11 @@ export class CellService { scriptMode: ScriptMode, ): Promise { for (const input of tx.inputs) { - const script = await this.client - .getCellLive(input.previousOutput) - .then((cell) => cell?.cellOutput.lock); + await input.completeExtraInfos(this.client); + const script = input.cellOutput?.lock; if (script) { const lockScriptMode = await this.scriptMode(script); + console.log("lockScriptMode = ", lockScriptMode); if (scriptMode === lockScriptMode) { return script; } diff --git a/libs/udt/src/repos/udtBalance.repo.ts b/libs/udt/src/repos/udtBalance.repo.ts index 16d9553..81d382d 100644 --- a/libs/udt/src/repos/udtBalance.repo.ts +++ b/libs/udt/src/repos/udtBalance.repo.ts @@ -15,6 +15,9 @@ export class UdtBalanceRepo extends Repository { tokenHash?: ccc.HexLike, height?: ccc.Num, ): Promise { + if (addresses.length === 0) { + return []; + } const addressHashes = addresses.map((address) => ccc.hashCkb(ccc.bytesFrom(address, "utf8")), ); @@ -49,15 +52,32 @@ export class UdtBalanceRepo extends Repository { }, }); } else { + // const rawSql = ` + // SELECT ub.* + // FROM udt_balance AS ub + // WHERE ub.updatedAtHeight IN ( + // SELECT MAX(updatedAtHeight) + // FROM udt_balance + // WHERE addressHash IN (?) AND balance > 0 + // GROUP BY tokenHash + // ); + // `; const rawSql = ` - SELECT ub.* - FROM udt_balance AS ub - WHERE ub.updatedAtHeight IN ( - SELECT MAX(updatedAtHeight) + WITH LatestRecords AS ( + SELECT + *, + ROW_NUMBER() OVER ( + PARTITION BY tokenHash + ORDER BY updatedAtHeight DESC + ) AS rn FROM udt_balance - WHERE addressHash = IN (?) AND balance > 0 - GROUP BY tokenHash - ); + WHERE + addressHash IN (?) + AND balance > 0 + ) + SELECT * + FROM LatestRecords + WHERE rn = 1; `; return await this.manager.query(rawSql, [addressHashes]); } @@ -88,15 +108,31 @@ export class UdtBalanceRepo extends Repository { } async getItemCountByTokenHash(tokenHash: ccc.HexLike): Promise { + // const rawSql = ` + // SELECT COUNT(*) AS holderCount + // FROM ( + // SELECT addressHash + // FROM udt_balance + // WHERE tokenHash = ? AND balance > 0 + // GROUP BY addressHash, tokenHash + // HAVING MAX(updatedAtHeight) + // ) AS grouped_holders; + // `; const rawSql = ` - SELECT COUNT(*) AS holderCount - FROM ( - SELECT addressHash + WITH LatestBalances AS ( + SELECT + addressHash, + ROW_NUMBER() OVER ( + PARTITION BY addressHash + ORDER BY updatedAtHeight DESC + ) AS rn, + balance FROM udt_balance - WHERE tokenHash = ? AND balance > 0 - GROUP BY addressHash, tokenHash - HAVING MAX(updatedAtHeight) - ) AS grouped_holders; + WHERE tokenHash = ? + ) + SELECT COUNT(DISTINCT addressHash) AS holderCount + FROM LatestBalances + WHERE rn = 1 AND balance > 0; `; const result = await this.manager.query(rawSql, [ccc.hexFrom(tokenHash)]); return parseInt(result[0].holderCount, 10) || 0; From 46b393053af1b18b1cdf763cbc26e78bc473154b Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Tue, 28 Jan 2025 10:55:46 +0800 Subject: [PATCH 3/9] chore: solve conflicts --- libs/asset/src/asset.service.ts | 2 +- libs/cell/src/cell.service.ts | 6 +----- libs/spore/src/spore.service.ts | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/libs/asset/src/asset.service.ts b/libs/asset/src/asset.service.ts index d6801aa..aa6874f 100644 --- a/libs/asset/src/asset.service.ts +++ b/libs/asset/src/asset.service.ts @@ -10,7 +10,7 @@ import { ccc } from "@ckb-ccc/shell"; import { cccA } from "@ckb-ccc/shell/advanced"; import { Inject, Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import axios, { AxiosInstance } from "axios"; +import { AxiosInstance } from "axios"; import { ClusterRepo, SporeRepo, UdtInfoRepo } from "./repos"; @Injectable() diff --git a/libs/cell/src/cell.service.ts b/libs/cell/src/cell.service.ts index ef9ebfa..3b581c7 100644 --- a/libs/cell/src/cell.service.ts +++ b/libs/cell/src/cell.service.ts @@ -10,7 +10,7 @@ import { import { ccc } from "@ckb-ccc/shell"; import { Inject, Injectable } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import axios, { AxiosInstance } from "axios"; +import { AxiosInstance } from "axios"; import { UdtInfoRepo } from "./repos"; @Injectable() @@ -20,7 +20,6 @@ export class CellService { private readonly rgbppBtcHashType: ccc.HashType; private readonly rgbppBtcTimelockCodeHash: ccc.Hex; private readonly rgbppBtcTimelockHashType: ccc.HashType; - private readonly btcRequesters: AxiosInstance[]; private readonly udtTypes: { codeHash: ccc.HexLike; hashType: ccc.HashTypeLike; @@ -51,9 +50,6 @@ export class CellService { assertConfig(configService, "sync.rgbppBtcTimelockHashType"), ); - const btcRpcUris = assertConfig(configService, "sync.btcRpcUris"); - this.btcRequesters = btcRpcUris.map((baseURL) => axios.create({ baseURL })); - const udtTypes = configService.get< { codeHash: ccc.HexLike; hashType: ccc.HashTypeLike }[] diff --git a/libs/spore/src/spore.service.ts b/libs/spore/src/spore.service.ts index 99bdac1..b9664d9 100644 --- a/libs/spore/src/spore.service.ts +++ b/libs/spore/src/spore.service.ts @@ -3,7 +3,7 @@ import { Cluster, Spore } from "@app/schemas"; import { ccc } from "@ckb-ccc/shell"; import { Inject, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import axios, { AxiosInstance } from "axios"; +import { AxiosInstance } from "axios"; import { ClusterRepo, SporeRepo } from "./repos"; @Injectable() From 3e99802d5fc502ffc78b42cfe5980b24bd20178a Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Tue, 28 Jan 2025 11:07:10 +0800 Subject: [PATCH 4/9] chore: eliminate console.log --- libs/cell/src/cell.controller.ts | 1 - libs/cell/src/cell.service.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/libs/cell/src/cell.controller.ts b/libs/cell/src/cell.controller.ts index 570eaea..82e6155 100644 --- a/libs/cell/src/cell.controller.ts +++ b/libs/cell/src/cell.controller.ts @@ -71,7 +71,6 @@ export class CellController { }, ]) { if (lockScriptMode === mode) { - console.log("premode = ", premode); const rgbppScript = assert( await this.service.getScriptByModeFromTxInputs(tx, premode), RpcError.RgbppCellNotFound, diff --git a/libs/cell/src/cell.service.ts b/libs/cell/src/cell.service.ts index 3b581c7..ce6d652 100644 --- a/libs/cell/src/cell.service.ts +++ b/libs/cell/src/cell.service.ts @@ -106,7 +106,6 @@ export class CellService { const script = input.cellOutput?.lock; if (script) { const lockScriptMode = await this.scriptMode(script); - console.log("lockScriptMode = ", lockScriptMode); if (scriptMode === lockScriptMode) { return script; } From 81a55ac2a1b6bea3345c64e0379a49fdb5ab4dfe Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Tue, 28 Jan 2025 11:10:53 +0800 Subject: [PATCH 5/9] chore: complete containSpender query description --- libs/cell/src/cell.controller.ts | 14 ++++++++++++-- libs/sync/src/sporeParser.ts | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/libs/cell/src/cell.controller.ts b/libs/cell/src/cell.controller.ts index 82e6155..c99a47c 100644 --- a/libs/cell/src/cell.controller.ts +++ b/libs/cell/src/cell.controller.ts @@ -129,15 +129,25 @@ export class CellController { type: TokenCell, description: "Get an on-chain cell by CKB OutPoint", }) + @ApiQuery({ + name: "containSpender", + required: false, + description: + "Whether to include the spender information of the cell, default is false (optional)", + }) @Get("/cells/by-outpoint/:txHash/:index") async getCellByOutpoint( @Param("txHash") txHash: string, @Param("index") index: number, - @Query("containSpender") containSpender: boolean, + @Query("containSpender") containSpender?: boolean, ): Promise> { try { const { cell, spender } = assert( - await this.service.getCellByOutpoint(txHash, index, containSpender), + await this.service.getCellByOutpoint( + txHash, + index, + containSpender ?? false, + ), RpcError.CkbCellNotFound, ); assert(cell.cellOutput.type, RpcError.CellNotAsset); diff --git a/libs/sync/src/sporeParser.ts b/libs/sync/src/sporeParser.ts index 7e348d7..572e81a 100644 --- a/libs/sync/src/sporeParser.ts +++ b/libs/sync/src/sporeParser.ts @@ -11,7 +11,7 @@ import { cccA } from "@ckb-ccc/shell/advanced"; import { spore } from "@ckb-ccc/spore"; import { Inject, Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; -import axios, { AxiosInstance } from "axios"; +import { AxiosInstance } from "axios"; import { EntityManager } from "typeorm"; import { ClusterRepo } from "./repos/cluster.repo"; import { SporeRepo } from "./repos/spore.repo"; From dcc622aca90d4ece849f29fa40cd1a6cc9024d4f Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Tue, 4 Feb 2025 21:45:18 +0800 Subject: [PATCH 6/9] chore: optimize getTokenItemsByTokenId --- libs/udt/src/repos/udtBalance.repo.ts | 46 +++++++++++++++++++-------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/libs/udt/src/repos/udtBalance.repo.ts b/libs/udt/src/repos/udtBalance.repo.ts index 81d382d..69d4999 100644 --- a/libs/udt/src/repos/udtBalance.repo.ts +++ b/libs/udt/src/repos/udtBalance.repo.ts @@ -89,22 +89,42 @@ export class UdtBalanceRepo extends Repository { offset: number, limit: number, ): Promise { + // const rawSql = ` + // SELECT ub.* + // FROM udt_balance AS ub + // WHERE ub.updatedAtHeight IN ( + // SELECT MAX(ub_inner.updatedAtHeight) + // FROM udt_balance AS ub_inner + // WHERE ub_inner.tokenHash = ? AND ub_inner.balance > 0 + // GROUP BY ub_inner.addressHash + // ) + // LIMIT ? OFFSET ?; + // `; + // return await this.manager.query(rawSql, [ + // ccc.hexFrom(tokenHash), + // limit, + // offset, + // ]); + const hexToken = ccc.hexFrom(tokenHash); const rawSql = ` - SELECT ub.* - FROM udt_balance AS ub - WHERE ub.updatedAtHeight IN ( - SELECT MAX(ub_inner.updatedAtHeight) - FROM udt_balance AS ub_inner - WHERE ub_inner.tokenHash = ? AND ub_inner.balance > 0 - GROUP BY ub_inner.addressHash + WITH LatestAddresses AS ( + SELECT + addressHash, + MAX(updatedAtHeight) AS max_height + FROM udt_balance + WHERE tokenHash = ? AND balance > 0 + GROUP BY addressHash + ORDER BY max_height DESC + LIMIT ? OFFSET ? ) - LIMIT ? OFFSET ?; + SELECT ub.* + FROM udt_balance ub + JOIN LatestAddresses la + ON ub.addressHash = la.addressHash + AND ub.updatedAtHeight = la.max_height + WHERE ub.tokenHash = ?; `; - return await this.manager.query(rawSql, [ - ccc.hexFrom(tokenHash), - limit, - offset, - ]); + return this.manager.query(rawSql, [hexToken, limit, offset, hexToken]); } async getItemCountByTokenHash(tokenHash: ccc.HexLike): Promise { From ef5ffb15ebf92ff5a5cfad00103a5dce593bbbfe Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Fri, 7 Feb 2025 13:53:28 +0800 Subject: [PATCH 7/9] chore: transparent fromDb parameter for block module APIs --- libs/block/src/block.controller.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/libs/block/src/block.controller.ts b/libs/block/src/block.controller.ts index d6d8b5c..6a470f5 100644 --- a/libs/block/src/block.controller.ts +++ b/libs/block/src/block.controller.ts @@ -7,8 +7,8 @@ import { RpcError, } from "@app/commons"; import { ccc } from "@ckb-ccc/shell"; -import { Controller, Get, Param } from "@nestjs/common"; -import { ApiOkResponse } from "@nestjs/swagger"; +import { Controller, Get, Param, Query } from "@nestjs/common"; +import { ApiOkResponse, ApiQuery } from "@nestjs/swagger"; import { BlockService } from "./block.service"; (BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function () { @@ -23,12 +23,21 @@ export class BlockController { type: BlockHeader, description: "Get tip block", }) + @ApiQuery({ + name: "fromDb", + required: false, + default: true, + description: + "Determine whether to get the block from the database or from the CKB node", + }) @Get("/blocks/latest") - async getLatestBlock(): Promise> { + async getLatestBlock( + @Query("fromDb") fromDb: boolean = true, + ): Promise> { try { const tipHeader = assert( await this.service.getBlockHeader({ - fromDb: false, + fromDb, }), RpcError.BlockNotFound, ); @@ -57,15 +66,23 @@ export class BlockController { type: BlockHeader, description: "Get block by block number", }) + @ApiQuery({ + name: "fromDb", + required: false, + default: true, + description: + "Determine whether to get the block from the database or from the CKB node", + }) @Get("/blocks/by-number/:blockNumber") async getBlockHeaderByNumber( @Param("blockNumber") blockNumber: number, + @Query("fromDb") fromDb: boolean = true, ): Promise> { try { const blockHeader = assert( await this.service.getBlockHeader({ blockNumber, - fromDb: false, + fromDb, }), RpcError.BlockNotFound, ); From df3c9e8dc8a813e7e69a9a53c056e52e07e347c5 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Sat, 8 Feb 2025 15:06:15 +0800 Subject: [PATCH 8/9] chore: report error if result of udt balances is empty --- libs/udt/src/udt.controller.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/libs/udt/src/udt.controller.ts b/libs/udt/src/udt.controller.ts index 833aa1d..79d5f00 100644 --- a/libs/udt/src/udt.controller.ts +++ b/libs/udt/src/udt.controller.ts @@ -136,6 +136,12 @@ export class UdtController { tokenId, height ? ccc.numFrom(height) : undefined, ); + if (udtBalances.length === 0) { + return { + code: -1, + msg: "No token balances found", + }; + } return { code: 0, data: await asyncMap( @@ -166,6 +172,12 @@ export class UdtController { addresses.split(","), height ? ccc.numFrom(height) : undefined, ); + if (udtBalances.length === 0) { + return { + code: -1, + msg: "No token balances found", + }; + } return { code: 0, data: await asyncMap( @@ -201,6 +213,12 @@ export class UdtController { isNaN(offset) ? 0 : offset, isNaN(limit) ? 10 : limit, ); + if (udtBalances.length === 0) { + return { + code: -1, + msg: "No token balances found", + }; + } const udtBalanceTotal = await this.service.getTokenHoldersCount(tokenId); return { code: 0, From 4951bf9d9be76f744700a1d874896f876e81f865 Mon Sep 17 00:00:00 2001 From: ashuralyk Date: Sat, 8 Feb 2025 16:16:26 +0800 Subject: [PATCH 9/9] bug: use || to filter valid tx assets from block --- libs/asset/src/asset.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/asset/src/asset.controller.ts b/libs/asset/src/asset.controller.ts index fc7050c..cb08f3e 100644 --- a/libs/asset/src/asset.controller.ts +++ b/libs/asset/src/asset.controller.ts @@ -373,7 +373,7 @@ export class AssetController { block.header.number, ); if ( - txAssetCellData.inputs.length > 0 && + txAssetCellData.inputs.length > 0 || txAssetCellData.outputs.length > 0 ) { txAssetCellDataList.push(txAssetCellData);