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

feat(rgbpp): calculate the pending rgbpp balance #155

Merged
merged 11 commits into from
Jun 3, 2024
2 changes: 1 addition & 1 deletion devbox.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"packages": [
"nodejs@latest",
"nodejs@20",
"nodePackages.pnpm@latest",
"redis@latest"
]
Expand Down
87 changes: 66 additions & 21 deletions devbox.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,63 +2,108 @@
"lockfile_version": "1",
"packages": {
"nodePackages.pnpm@latest": {
"last_modified": "2024-03-08T13:51:52Z",
"resolved": "github:NixOS/nixpkgs/a343533bccc62400e8a9560423486a3b6c11a23b#nodePackages.pnpm",
"last_modified": "2024-05-22T06:18:38Z",
"resolved": "github:NixOS/nixpkgs/3f316d2a50699a78afe5e77ca486ad553169061e#nodePackages.pnpm",
"source": "devbox-search",
"version": "8.15.3",
"version": "8.15.5",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/m9p6q42njy1k33fdzf2axqyhy8vh4vn5-pnpm-8.15.3"
"store_path": "/nix/store/98b2ljlansqmkpkd5pqfgwhcwr5kvsha-pnpm-8.15.5"
},
"aarch64-linux": {
"store_path": "/nix/store/war7jm6ka7afc2v8a78hlq8v9m4pg38m-pnpm-8.15.3"
"store_path": "/nix/store/b0h4xyqwdk2zhyhw42bprjnj2i518sxn-pnpm-8.15.5"
},
"x86_64-darwin": {
"store_path": "/nix/store/3w3bs2cdqx7sivcldcf6drwrhilv511m-pnpm-8.15.3"
"store_path": "/nix/store/7xyc072k4g6430vijvylidqacjxy5dbb-pnpm-8.15.5"
},
"x86_64-linux": {
"store_path": "/nix/store/hcq09j80njlfghy65qmwhn5nq20nk8kl-pnpm-8.15.3"
"store_path": "/nix/store/77wblnm5dnmgnan3695j3mk4r7j75s5j-pnpm-8.15.5"
}
}
},
"nodejs@latest": {
"last_modified": "2024-03-09T07:11:56Z",
"resolved": "github:NixOS/nixpkgs/0e7f98a5f30166cbed344569426850b21e4091d4#nodejs_21",
"nodejs@20": {
"last_modified": "2024-05-22T06:18:38Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/3f316d2a50699a78afe5e77ca486ad553169061e#nodejs_20",
"source": "devbox-search",
"version": "21.7.1",
"version": "20.12.2",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/mh1db6rni4mlcr473sh3fy6jg2m38jvg-nodejs-21.7.1"
"outputs": [
{
"name": "out",
"path": "/nix/store/bzzs4kvjyvjjhs3rj08vqpvvzmfggvbv-nodejs-20.12.2",
"default": true
},
{
"name": "libv8",
"path": "/nix/store/c56874bxzncqwy58kif6wfnzy017v1sl-nodejs-20.12.2-libv8"
}
],
"store_path": "/nix/store/bzzs4kvjyvjjhs3rj08vqpvvzmfggvbv-nodejs-20.12.2"
},
"aarch64-linux": {
"store_path": "/nix/store/ziyb3ny5f9hcwp0lsb9gwdv1pqsn03x1-nodejs-21.7.1"
"outputs": [
{
"name": "out",
"path": "/nix/store/y50zafzgnnkrj4hvmk23icv2ggvys8r9-nodejs-20.12.2",
"default": true
},
{
"name": "libv8",
"path": "/nix/store/vc7y8h3c8pwbh4zbvjcyfqrd3fhdjhw6-nodejs-20.12.2-libv8"
}
],
"store_path": "/nix/store/y50zafzgnnkrj4hvmk23icv2ggvys8r9-nodejs-20.12.2"
},
"x86_64-darwin": {
"store_path": "/nix/store/0zaa3aarbsj38g62ihv94gz2hgvgh6bc-nodejs-21.7.1"
"outputs": [
{
"name": "out",
"path": "/nix/store/l53svh1nfrcb83qbqvrrkangrcl1rr25-nodejs-20.12.2",
"default": true
},
{
"name": "libv8",
"path": "/nix/store/q71hh22bfqjygd34gq16dv4dwfc33378-nodejs-20.12.2-libv8"
}
],
"store_path": "/nix/store/l53svh1nfrcb83qbqvrrkangrcl1rr25-nodejs-20.12.2"
},
"x86_64-linux": {
"store_path": "/nix/store/4arhczh30ychpx6h2wpj5nhx39wm0nla-nodejs-21.7.1"
"outputs": [
{
"name": "out",
"path": "/nix/store/6g9n96qf1yx139xklnmy3v4xhjvjgsji-nodejs-20.12.2",
"default": true
},
{
"name": "libv8",
"path": "/nix/store/s7b0dqga0311mvq48mirnlm0p3dr4gm3-nodejs-20.12.2-libv8"
}
],
"store_path": "/nix/store/6g9n96qf1yx139xklnmy3v4xhjvjgsji-nodejs-20.12.2"
}
}
},
"redis@latest": {
"last_modified": "2024-03-08T13:51:52Z",
"last_modified": "2024-05-22T06:18:38Z",
"plugin_version": "0.0.2",
"resolved": "github:NixOS/nixpkgs/a343533bccc62400e8a9560423486a3b6c11a23b#redis",
"resolved": "github:NixOS/nixpkgs/3f316d2a50699a78afe5e77ca486ad553169061e#redis",
"source": "devbox-search",
"version": "7.2.4",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/700kyznxcmlkqqabhwa64vmyg4aj6igj-redis-7.2.4"
"store_path": "/nix/store/f1hmasfrq0s1yag27kmdwv97cjxv85rm-redis-7.2.4"
},
"aarch64-linux": {
"store_path": "/nix/store/xlc976dh4nb2aa0gzs0jfgld4cc3x8si-redis-7.2.4"
"store_path": "/nix/store/5ay0vkvk8l2n8fgxnmxdci1y1ldafnx8-redis-7.2.4"
},
"x86_64-darwin": {
"store_path": "/nix/store/5v58fadclljqa2fmwq281bx5wma9cslb-redis-7.2.4"
"store_path": "/nix/store/5l40xjgzqsd56dx5zfwr2v6sfpk12lkl-redis-7.2.4"
},
"x86_64-linux": {
"store_path": "/nix/store/zp1lapdicjxnjhiz8l6j2j4nn6sa9fg5-redis-7.2.4"
"store_path": "/nix/store/xlvzg81dgimxfjxpxwr2w3q1ca3l5lwa-redis-7.2.4"
}
}
}
Expand Down
103 changes: 59 additions & 44 deletions src/routes/rgbpp/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { blockchain } from '@ckb-lumos/base';
import z from 'zod';
import { serializeScript } from '@nervosnetwork/ckb-sdk-utils';
import { Env } from '../../env';
import { getXudtTypeScript, isTypeAssetSupported, leToU128 } from '@rgbpp-sdk/ckb';
import { getXudtTypeScript, isTypeAssetSupported } from '@rgbpp-sdk/ckb';
import { BI } from '@ckb-lumos/lumos';
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
import { UTXO } from '../../services/bitcoin/schema';

const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodTypeProvider> = (fastify, _, done) => {
const env: Env = fastify.container.resolve('env');
Expand Down Expand Up @@ -39,14 +39,20 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
}

/**
* Get RGB++ assets by btc address
* Get UTXOs by btc address
*/
async function getRgbppAssetsCells(btc_address: string, typeScript?: Script, no_cache?: string) {
async function getUxtos(btc_address: string, no_cache?: string) {
const utxos = await fastify.utxoSyncer.getUtxosByAddress(btc_address, no_cache === 'true');
if (env.UTXO_SYNC_DATA_CACHE_ENABLE) {
await fastify.utxoSyncer.enqueueSyncJob(btc_address);
}
return utxos;
}

/**
* Get RGB++ assets by btc address
*/
async function getRgbppAssetsCells(btc_address: string, utxos: UTXO[], no_cache?: string) {
const rgbppUtxoCellsPairs = await fastify.rgbppCollector.getRgbppUtxoCellsPairs(
btc_address,
utxos,
Expand All @@ -56,23 +62,26 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
await fastify.rgbppCollector.enqueueCollectJob(btc_address, utxos);
}
const cells = rgbppUtxoCellsPairs.map((pair) => pair.cells).flat();

if (typeScript) {
return cells.filter((cell) => {
if (!cell.cellOutput.type) {
return false;
}
// if typeScript.args is empty, only compare codeHash and hashType
if (!typeScript.args) {
const script = { ...cell.cellOutput.type, args: '' };
return serializeScript(script) === serializeScript(typeScript);
}
return serializeScript(cell.cellOutput.type) === serializeScript(typeScript);
});
}
return cells;
}

/**
* Filter cells by type script
*/
async function filterCellsByTypeScript(cells: Cell[], typeScript: Script) {
return cells.filter((cell) => {
if (!cell.cellOutput.type) {
return false;
}
// if typeScript.args is empty, only compare codeHash and hashType
if (!typeScript.args) {
const script = { ...cell.cellOutput.type, args: '' };
return serializeScript(script) === serializeScript(typeScript);
ahonn marked this conversation as resolved.
Show resolved Hide resolved
}
return serializeScript(cell.cellOutput.type) === serializeScript(typeScript);
});
}

fastify.get(
'/:btc_address/assets',
{
Expand Down Expand Up @@ -107,8 +116,10 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
async (request) => {
const { btc_address } = request.params;
const { no_cache } = request.query;
const utxos = await getUxtos(btc_address, no_cache);
const cells = await getRgbppAssetsCells(btc_address, utxos, no_cache);
const typeScript = getTypeScript(request);
return getRgbppAssetsCells(btc_address, typeScript, no_cache);
return typeScript ? filterCellsByTypeScript(cells, typeScript) : cells;
},
);

Expand Down Expand Up @@ -154,38 +165,42 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
if (!typeScript || !isTypeAssetSupported(typeScript, env.NETWORK === 'mainnet')) {
throw fastify.httpErrors.badRequest('Unsupported type asset');
}
const cells = await getRgbppAssetsCells(btc_address, typeScript, no_cache);

const scripts = fastify.ckb.getScripts();
if (serializeScript({ ...typeScript, args: '' }) !== serializeScript(scripts.XUDT)) {
throw fastify.httpErrors.badRequest('Unsupported type asset');
}

const infoCellDataMap = new Map();
const allInfoCellTxs = await fastify.ckb.getAllInfoCellTxs();
const utxos = await getUxtos(btc_address, no_cache);
const xudtBalances: Record<string, XUDTBalance> = {};

for await (const cell of cells) {
const type = cell.cellOutput.type!;
const typeHash = computeScriptHash(type);
if (!infoCellDataMap.has(typeHash)) {
const infoCellData = fastify.ckb.getInfoCellData(allInfoCellTxs, type);
infoCellDataMap.set(typeHash, infoCellData);
}
const infoCellData = infoCellDataMap.get(typeHash);
const amount = BI.from(leToU128(cell.data)).toHexString();
if (infoCellData) {
if (!xudtBalances[typeHash]) {
xudtBalances[typeHash] = {
...infoCellData,
typeHash,
amount,
};
} else {
xudtBalances[typeHash].amount = BI.from(xudtBalances[typeHash].amount).add(BI.from(amount)).toHexString();
}
}
}
let cells = await getRgbppAssetsCells(btc_address, utxos, no_cache);
cells = typeScript ? await filterCellsByTypeScript(cells, typeScript) : cells;
const availableXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(cells);
Object.keys(availableXudtBalances).forEach((key) => {
const { amount, ...xudtInfo } = availableXudtBalances[key];
xudtBalances[key] = {
...xudtInfo,
total_amount: amount,
available_amount: amount,
pending_amount: '0x0',
};
});

const pendingUtxos = utxos.filter((utxo) => !utxo.status.confirmed);
const pendingTxids = Array.from(new Set(pendingUtxos.map((utxo) => utxo.txid)));
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
let pendingOutputCells = await fastify.transactionProcessor.getPendingOuputCellsByTxids(pendingTxids);
pendingOutputCells = typeScript
? await filterCellsByTypeScript(pendingOutputCells, typeScript)
: pendingOutputCells;
const pendingXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(pendingOutputCells);
Object.values(pendingXudtBalances).forEach(({ amount, typeHash }) => {
xudtBalances[typeHash].pending_amount = BI.from(xudtBalances[typeHash].pending_amount)
Flouse marked this conversation as resolved.
Show resolved Hide resolved
.add(BI.from(amount))
.toHexString();
xudtBalances[typeHash].total_amount = BI.from(xudtBalances[typeHash].total_amount)
.add(BI.from(amount))
.toHexString();
});

return {
address: btc_address,
Expand Down
4 changes: 3 additions & 1 deletion src/routes/rgbpp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,9 @@ export const XUDTBalance = z.object({
name: z.string(),
decimal: z.number(),
symbol: z.string(),
amount: z.string(),
total_amount: z.string(),
available_amount: z.string(),
pending_amount: z.string(),
typeHash: z.string(),
});
export type XUDTBalance = z.infer<typeof XUDTBalance>;
8 changes: 4 additions & 4 deletions src/services/base/data-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,18 @@ export default class DataCache<T> {
this.expire = options.expire;
}

public async set(btcAddress: string, data: unknown) {
public async set(id: string, data: unknown) {
const parsed = this.schema.safeParse(data);
if (!parsed.success) {
throw new DataCacheError(parsed.error.message);
}
const key = `data-cache:${this.prefix}:${btcAddress}`;
const key = `data-cache:${this.prefix}:${id}`;
await this.redis.set(key, JSON.stringify(parsed.data), 'PX', this.expire);
return parsed.data;
}

public async get(btcAddress: string): Promise<T | null> {
const key = `data-cache:${this.prefix}:${btcAddress}`;
public async get(id: string): Promise<T | null> {
const key = `data-cache:${this.prefix}:${id}`;
const data = await this.redis.get(key);
if (data) {
const parsed = this.schema.safeParse(JSON.parse(data));
Expand Down
23 changes: 15 additions & 8 deletions src/services/ckb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ export class CKBRpcError extends Error {
export default class CKBClient {
public rpc: RPC;
public indexer: Indexer;
private dataCahe: DataCache<CKBComponents.TransactionWithStatus[]>;
private dataCache: DataCache<unknown>;

constructor(private cradle: Cradle) {
this.rpc = new RPC(cradle.env.CKB_RPC_URL);
this.indexer = new Indexer(cradle.env.CKB_RPC_URL);
this.dataCahe = new DataCache(cradle.redis, {
this.dataCache = new DataCache(cradle.redis, {
prefix: 'ckb-info-cell-txs',
expire: 60 * 1000,
});
Expand Down Expand Up @@ -215,9 +215,9 @@ export default class CKBClient {
* Get all transactions that have the xudt type cell and info cell
*/
public async getAllInfoCellTxs() {
const cachedTxs = await this.dataCahe.get('all');
const cachedTxs = await this.dataCache.get('all');
if (cachedTxs) {
return cachedTxs;
return cachedTxs as CKBComponents.TransactionWithStatus[];
}

const scripts = this.getScripts();
Expand Down Expand Up @@ -249,18 +249,23 @@ export default class CKBClient {
});
});
const txs: CKBComponents.TransactionWithStatus[] = await batchRequest.exec();
await this.dataCahe.set('all', txs);
await this.dataCache.set('all', txs);
return txs;
}

/**
* Get the unique cell of the given xudt type
* @param txs - the transactions that have the xudt type cell and unique cell
* @param script - the xudt type script
*/
public getInfoCellData(txs: CKBComponents.TransactionWithStatus[], script: Script) {
const isMainnet = this.cradle.env.NETWORK === 'mainnet';
public async getInfoCellData(script: Script) {
const typeHash = computeScriptHash(script);
const cachedData = await this.dataCache.get(`type:${typeHash}`);
if (cachedData) {
return cachedData as ReturnType<typeof decodeInfoCellData>;
}

const isMainnet = this.cradle.env.NETWORK === 'mainnet';
const txs = await this.getAllInfoCellTxs();
for (const tx of txs) {
// check if the unique cell is the info cell of the xudt type
const uniqueCellIndex = tx.transaction.outputs.findIndex((cell) => {
Expand All @@ -269,6 +274,7 @@ export default class CKBClient {
if (uniqueCellIndex !== -1) {
const infoCellData = this.getUniqueCellData(tx, uniqueCellIndex, script);
if (infoCellData) {
await this.dataCache.set(`type:${typeHash}`, infoCellData);
return infoCellData;
}
}
Expand All @@ -279,6 +285,7 @@ export default class CKBClient {
if (inscriptionCellIndex !== -1) {
const infoCellData = this.getInscriptionInfoCellData(tx, inscriptionCellIndex, script);
if (infoCellData) {
await this.dataCache.set(`type:${typeHash}`, infoCellData);
return infoCellData;
}
}
Expand Down
Loading
Loading