Skip to content

Commit ed47880

Browse files
committed
refactor: separate the xudt balance calculation of total/pending amount
1 parent 1c2f169 commit ed47880

File tree

5 files changed

+126
-43
lines changed

5 files changed

+126
-43
lines changed

src/routes/rgbpp/address.ts

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,23 @@ import { ZodTypeProvider } from 'fastify-type-provider-zod';
55
import { CKBTransaction, Cell, IsomorphicTransaction, Script, XUDTBalance } from './types';
66
import z from 'zod';
77
import { Env } from '../../env';
8-
import { buildPreLockArgs, getXudtTypeScript, isScriptEqual, isTypeAssetSupported } from '@rgbpp-sdk/ckb';
9-
import { groupBy } from 'lodash';
8+
import {
9+
isScriptEqual,
10+
buildPreLockArgs,
11+
getRgbppLockScript,
12+
getXudtTypeScript,
13+
isTypeAssetSupported,
14+
} from '@rgbpp-sdk/ckb';
15+
import { groupBy, uniq } from 'lodash';
1016
import { BI } from '@ckb-lumos/lumos';
1117
import { UTXO } from '../../services/bitcoin/schema';
1218
import { Transaction as BTCTransaction } from '../bitcoin/types';
1319
import { TransactionWithStatus } from '../../services/ckb';
1420
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
1521
import { filterCellsByTypeScript, getTypeScript } from '../../utils/typescript';
22+
import { unpackRgbppLockArgs } from '@rgbpp-sdk/btc/lib/ckb/molecule';
23+
import { TestnetTypeMap } from '../../constants';
24+
import { remove0x } from '@rgbpp-sdk/btc';
1625

1726
const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodTypeProvider> = (fastify, _, done) => {
1827
const env: Env = fastify.container.resolve('env');
@@ -52,6 +61,18 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
5261
return cells;
5362
}
5463

64+
/**
65+
* Filter RgbppLock cells by cells
66+
*/
67+
function getRgbppLockCellsByCells(cells: Cell[]): Cell[] {
68+
const rgbppLockScript = getRgbppLockScript(env.NETWORK === 'mainnet', TestnetTypeMap[env.NETWORK]);
69+
return cells.filter(
70+
(cell) =>
71+
rgbppLockScript.codeHash === cell.cellOutput.lock.codeHash &&
72+
rgbppLockScript.hashType === cell.cellOutput.lock.hashType,
73+
);
74+
}
75+
5576
fastify.get(
5677
'/:btc_address/assets',
5778
{
@@ -147,13 +168,14 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
147168
throw fastify.httpErrors.badRequest('Unsupported type asset');
148169
}
149170

150-
const utxos = await getUxtos(btc_address, no_cache);
151171
const xudtBalances: Record<string, XUDTBalance> = {};
172+
const utxos = await getUxtos(btc_address, no_cache);
152173

153-
let cells = await getRgbppAssetsCells(btc_address, utxos, no_cache);
154-
cells = typeScript ? filterCellsByTypeScript(cells, typeScript) : cells;
155-
156-
const availableXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(cells);
174+
// Find confirmed RgbppLock Xudt assets
175+
const confirmedUtxos = utxos.filter((utxo) => utxo.status.confirmed);
176+
const confirmedCells = await getRgbppAssetsCells(btc_address, confirmedUtxos, no_cache);
177+
const confirmedTargetCells = filterCellsByTypeScript(confirmedCells, typeScript);
178+
const availableXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(confirmedTargetCells);
157179
Object.keys(availableXudtBalances).forEach((key) => {
158180
const { amount, ...xudtInfo } = availableXudtBalances[key];
159181
xudtBalances[key] = {
@@ -164,6 +186,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
164186
};
165187
});
166188

189+
// Find all unconfirmed RgbppLock Xudt outputs
167190
const pendingUtxos = utxos.filter(
168191
(utxo) =>
169192
!utxo.status.confirmed ||
@@ -172,19 +195,14 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
172195
);
173196
const pendingUtxosGroup = groupBy(pendingUtxos, (utxo) => utxo.txid);
174197
const pendingTxids = Object.keys(pendingUtxosGroup);
175-
176198
const pendingOutputCellsGroup = await Promise.all(
177199
pendingTxids.map(async (txid) => {
178200
const cells = await fastify.transactionProcessor.getPendingOutputCellsByTxid(txid);
179201
const lockArgsSet = new Set(pendingUtxosGroup[txid].map((utxo) => buildPreLockArgs(utxo.vout)));
180202
return cells.filter((cell) => lockArgsSet.has(cell.cellOutput.lock.args));
181203
}),
182204
);
183-
let pendingOutputCells = pendingOutputCellsGroup.flat();
184-
if (typeScript) {
185-
pendingOutputCells = filterCellsByTypeScript(pendingOutputCells, typeScript);
186-
}
187-
205+
const pendingOutputCells = filterCellsByTypeScript(pendingOutputCellsGroup.flat(), typeScript);
188206
const pendingXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(pendingOutputCells);
189207
Object.values(pendingXudtBalances).forEach(({ amount, type_hash, ...xudtInfo }) => {
190208
if (!xudtBalances[type_hash]) {
@@ -200,6 +218,49 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
200218
xudtBalances[type_hash].pending_amount = BI.from(xudtBalances[type_hash].pending_amount)
201219
.add(BI.from(amount))
202220
.toHexString();
221+
});
222+
223+
// Find spent RgbppLock Xudt assets in unconfirmed transactions' inputs
224+
const allTxs = await fastify.bitcoin.getAddressTxs({ address: btc_address });
225+
const unconfirmedTxids = allTxs.filter((tx) => !tx.status.confirmed).map((tx) => tx.txid);
226+
const spendingInputCellsGroup = await Promise.all(
227+
unconfirmedTxids.map(async (txid) => {
228+
const inputCells = await fastify.transactionProcessor.getPendingInputCellsByTxid(txid);
229+
const inputRgbppCells = getRgbppLockCellsByCells(filterCellsByTypeScript(inputCells, typeScript));
230+
const inputCellLockArgs = inputRgbppCells.map((cell) => unpackRgbppLockArgs(cell.cellOutput.lock.args));
231+
232+
const txids = uniq(inputCellLockArgs.map((args) => remove0x(args.btcTxid)));
233+
const txs = await Promise.all(txids.map((txid) => fastify.bitcoin.getTx({ txid })));
234+
const txsMap = txs.reduce(
235+
(sum, tx, index) => {
236+
const txid = txids[index];
237+
sum[txid] = tx ?? null;
238+
return sum;
239+
},
240+
{} as Record<string, BTCTransaction | null>,
241+
);
242+
243+
return inputRgbppCells.filter((cell, index) => {
244+
const lockArgs = inputCellLockArgs[index];
245+
const tx = txsMap[remove0x(lockArgs.btcTxid)];
246+
const utxo = tx?.vout[lockArgs.outIndex];
247+
return utxo?.scriptpubkey_address === btc_address;
248+
});
249+
}),
250+
);
251+
const spendingInputCells = spendingInputCellsGroup.flat();
252+
const spendingXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(spendingInputCells);
253+
Object.values(spendingXudtBalances).forEach(({ amount, type_hash, ...xudtInfo }) => {
254+
if (!xudtBalances[type_hash]) {
255+
xudtBalances[type_hash] = {
256+
...xudtInfo,
257+
type_hash,
258+
total_amount: '0x0',
259+
available_amount: '0x0',
260+
pending_amount: '0x0',
261+
};
262+
}
263+
203264
xudtBalances[type_hash].total_amount = BI.from(xudtBalances[type_hash].total_amount)
204265
.add(BI.from(amount))
205266
.toHexString();
@@ -322,18 +383,18 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
322383
} as const;
323384
}
324385

325-
const inputOutpoints = isomorphicTx.ckbRawTx?.inputs || isomorphicTx.ckbTx?.inputs || [];
326-
const inputs = await fastify.ckb.getInputCellsByOutPoint(
327-
inputOutpoints.map((input) => input.previousOutput) as CKBComponents.OutPoint[],
328-
);
386+
const inputs = isomorphicTx.ckbRawTx?.inputs || isomorphicTx.ckbTx?.inputs || [];
387+
const inputCells = await fastify.ckb.getInputCellsByOutPoint(inputs.map((input) => input.previousOutput!));
388+
const inputCellOutputs = inputCells.map((cell) => cell.cellOutput);
389+
329390
const outputs = isomorphicTx.ckbRawTx?.outputs || isomorphicTx.ckbTx?.outputs || [];
330391

331392
return {
332393
btcTx,
333394
isRgbpp: true,
334395
isomorphicTx: {
335396
...isomorphicTx,
336-
inputs,
397+
inputs: inputCellOutputs,
337398
outputs,
338399
},
339400
} as const;

src/services/ckb.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ import {
2222
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
2323
import DataCache from './base/data-cache';
2424
import { scriptToHash } from '@nervosnetwork/ckb-sdk-utils';
25-
import { OutputCell } from '../routes/rgbpp/types';
25+
import { Cell } from '../routes/rgbpp/types';
26+
import { uniq } from 'lodash';
2627

2728
export type TransactionWithStatus = Awaited<ReturnType<CKBRPC['getTransaction']>>;
2829

@@ -326,14 +327,27 @@ export default class CKBClient {
326327
return null;
327328
}
328329

329-
public async getInputCellsByOutPoint(outPoints: CKBComponents.OutPoint[]): Promise<OutputCell[]> {
330-
const batchRequest = this.rpc.createBatchRequest(outPoints.map((outPoint) => ['getTransaction', outPoint.txHash]));
331-
const txs = await batchRequest.exec();
332-
const inputs = txs.map((tx: TransactionWithStatus, index: number) => {
333-
const outPoint = outPoints[index];
334-
return tx.transaction.outputs[BI.from(outPoint.index).toNumber()];
330+
public async getInputCellsByOutPoint(outPoints: CKBComponents.OutPoint[]): Promise<Cell[]> {
331+
const txHashes = uniq(outPoints.map((outPoint) => outPoint.txHash));
332+
const batchRequest = this.rpc.createBatchRequest(txHashes.map((txHash) => ['getTransaction', txHash]));
333+
const txs: TransactionWithStatus[] = await batchRequest.exec();
334+
const txsMap = txs.reduce(
335+
(acc, tx: TransactionWithStatus) => {
336+
acc[tx.transaction.hash] = tx;
337+
return acc;
338+
},
339+
{} as Record<string, TransactionWithStatus>,
340+
);
341+
return outPoints.map((outPoint) => {
342+
const tx = txsMap[outPoint.txHash];
343+
const outPointIndex = BI.from(outPoint.index).toNumber();
344+
return Cell.parse({
345+
cellOutput: tx.transaction.outputs[outPointIndex],
346+
data: tx.transaction.outputsData[outPointIndex],
347+
blockHash: tx.txStatus.blockHash,
348+
outPoint,
349+
});
335350
});
336-
return inputs;
337351
}
338352

339353
/**

src/services/transaction.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -608,14 +608,34 @@ export default class TransactionProcessor
608608
const { ckbVirtualResult } = job.data;
609609
const outputs = ckbVirtualResult.ckbRawTx.outputs;
610610
return outputs.map((output, index) => {
611-
const cell: Cell = {
611+
return Cell.parse({
612612
cellOutput: output,
613613
data: ckbVirtualResult.ckbRawTx.outputsData[index],
614-
};
615-
return cell;
614+
});
616615
});
617616
}
618617

618+
/**
619+
* get pending input cells by txid, get ckb input cells from the uncompleted job
620+
* @param txid - the transaction id
621+
*/
622+
public async getPendingInputCellsByTxid(txid: string): Promise<Cell[]> {
623+
const job = await this.getTransactionRequest(txid);
624+
if (!job) {
625+
return [];
626+
}
627+
628+
// get ckb input cells from the uncompleted job only
629+
const state = await job.getState();
630+
if (state === 'completed' || state === 'failed') {
631+
return [];
632+
}
633+
634+
const { ckbVirtualResult } = job.data;
635+
const inputOutPoints = ckbVirtualResult.ckbRawTx.inputs.map((input) => input.previousOutput!);
636+
return await this.cradle.ckb.getInputCellsByOutPoint(inputOutPoints);
637+
}
638+
619639
/**
620640
* Retry all failed jobs in the queue
621641
* @param maxAttempts - the max attempts to retry

test/routes/__snapshots__/token.test.ts.snap

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,5 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3-
exports[`\`/token/generate\` - 400 1`] = `
4-
"[
5-
{
6-
"code": "invalid_type",
7-
"expected": "object",
8-
"received": "null",
9-
"path": [],
10-
"message": "Expected object, received null"
11-
}
12-
]"
13-
`;
14-
153
exports[`\`/token/generate\` - without params 1`] = `
164
"[
175
{

test/routes/rgbpp/__snapshots__/address.test.ts.snap

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6889,7 +6889,7 @@ exports[`/:btc_address/balance - with pending_amount 1`] = `
68896889
"name": "Unique BBQ",
68906890
"pending_amount": "0x5f5e100",
68916891
"symbol": "",
6892-
"total_amount": "0xbebc200",
6892+
"total_amount": "0x5f5e100",
68936893
"type_hash": "0x78e21efcf107e7886eadeadecd1a01cfb88f1e5617f4438685db55b3a540d202",
68946894
"type_script": {
68956895
"args": "0x30d3fbec9ceba691770d57c6d06bdb98cf0f82bef0ca6e87687a118d6ce1e7b7",
@@ -6903,7 +6903,7 @@ exports[`/:btc_address/balance - with pending_amount 1`] = `
69036903
"name": "XUDT Test Token",
69046904
"pending_amount": "0x5f5e100",
69056905
"symbol": "PDD",
6906-
"total_amount": "0x5f5e100",
6906+
"total_amount": "0x0",
69076907
"type_hash": "0x10f511f2efb0027191b97ac5b4bd77374ffdac7399e8527d76f5f9bd32e7d35b",
69086908
"type_script": {
69096909
"args": "0x8c556e92974a8dd8237719020a259d606359ac2cc958cb8bda77a1c3bb3cd93b",

0 commit comments

Comments
 (0)