Skip to content

Commit a13e5b3

Browse files
committed
refactor: improve code readability in activity-related routes and functions
1 parent acdaa58 commit a13e5b3

File tree

4 files changed

+148
-118
lines changed

4 files changed

+148
-118
lines changed

src/constants.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { isAdminMode } from './env';
1+
import { env, isAdminMode } from './env';
22
import { BTCTestnetType } from '@rgbpp-sdk/ckb';
33

44
export enum NetworkType {
@@ -41,3 +41,6 @@ export const BTC_SIGNET_SPV_START_BLOCK_HEIGHT = 199800;
4141
// estimate time: 2024-04-02 06:20:03
4242
// ref: https://mempool.space/block/0000000000000000000077d98a103858c7d7cbc5ba67a4135f348a436bec1748
4343
export const BTC_MAINNET_SPV_START_BLOCK_HEIGHT = 837300;
44+
45+
export const IS_MAINNET = env.NETWORK === NetworkType.mainnet.toString();
46+
export const TESTNET_TYPE = TestnetTypeMap[env.NETWORK];

src/services/rgbpp.ts

+65-114
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,15 @@ import {
55
IndexerCell,
66
leToU128,
77
isScriptEqual,
8-
buildPreLockArgs,
98
buildRgbppLockArgs,
109
genRgbppLockScript,
11-
getRgbppLockScript,
12-
genBtcTimeLockArgs,
13-
getBtcTimeLockScript,
1410
btcTxIdFromBtcTimeLockArgs,
15-
calculateCommitment,
16-
BTCTimeLock,
1711
RGBPP_TX_ID_PLACEHOLDER,
1812
RGBPP_TX_INPUTS_MAX_LENGTH,
1913
} from '@rgbpp-sdk/ckb';
2014
import { remove0x } from '@rgbpp-sdk/btc';
2115
import { unpackRgbppLockArgs } from '@rgbpp-sdk/btc/lib/ckb/molecule';
22-
import { groupBy, cloneDeep, uniq } from 'lodash';
16+
import { groupBy, uniq, findLastIndex } from 'lodash';
2317
import { z } from 'zod';
2418
import { Job } from 'bullmq';
2519
import { BI, RPC, Script } from '@ckb-lumos/lumos';
@@ -30,8 +24,9 @@ import { Transaction, UTXO } from './bitcoin/schema';
3024
import BaseQueueWorker from './base/queue-worker';
3125
import DataCache from './base/data-cache';
3226
import { Cradle } from '../container';
33-
import { TestnetTypeMap } from '../constants';
34-
import { tryGetCommitmentFromBtcTx } from '../utils/commitment';
27+
import { isCommitmentMatchToCkbTx, tryGetCommitmentFromBtcTx } from '../utils/commitment';
28+
import { getBtcTimeLock, isBtcTimeLock, isRgbppLock } from '../utils/lockscript';
29+
import { IS_MAINNET, TESTNET_TYPE } from '../constants';
3530

3631
type GetCellsParams = Parameters<RPC['getCells']>;
3732
export type SearchKey = GetCellsParams[0];
@@ -92,30 +87,6 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
9287
this.limit = pLimit(100);
9388
}
9489

95-
private get isMainnet() {
96-
return this.cradle.env.NETWORK === 'mainnet';
97-
}
98-
99-
private get testnetType() {
100-
return TestnetTypeMap[this.cradle.env.NETWORK];
101-
}
102-
103-
private get rgbppLockScript() {
104-
return getRgbppLockScript(this.isMainnet, this.testnetType);
105-
}
106-
107-
private get btcTimeLockScript() {
108-
return getBtcTimeLockScript(this.isMainnet, this.testnetType);
109-
}
110-
111-
private isRgbppLock(lock: CKBComponents.Script) {
112-
return lock.codeHash === this.rgbppLockScript.codeHash && lock.hashType === this.rgbppLockScript.hashType;
113-
}
114-
115-
private isBtcTimeLock(lock: CKBComponents.Script) {
116-
return lock.codeHash === this.btcTimeLockScript.codeHash && lock.hashType === this.btcTimeLockScript.hashType;
117-
}
118-
11990
/**
12091
* Capture the exception to the sentry scope with the btc address and utxos
12192
* @param job - the job that failed
@@ -189,7 +160,7 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
189160
const { txid, vout } = utxo;
190161
const args = buildRgbppLockArgs(vout, txid);
191162
const searchKey: SearchKey = {
192-
script: genRgbppLockScript(args, this.isMainnet, this.testnetType),
163+
script: genRgbppLockScript(args, IS_MAINNET, TESTNET_TYPE),
193164
scriptType: 'lock',
194165
};
195166
if (typeScript) {
@@ -277,7 +248,7 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
277248
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(
278249
btcTx.vout.map((_, index) => {
279250
const args = buildRgbppLockArgs(index, btcTx.txid);
280-
const lock = genRgbppLockScript(args, this.isMainnet, this.testnetType);
251+
const lock = genRgbppLockScript(args, IS_MAINNET, TESTNET_TYPE);
281252
const searchKey: SearchKey = {
282253
script: lock,
283254
scriptType: 'lock',
@@ -291,7 +262,6 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
291262
for (const indexerTx of tx.objects) {
292263
const ckbTx = await this.cradle.ckb.rpc.getTransaction(indexerTx.txHash);
293264
const isIsomorphic = await this.isIsomorphicTx(btcTx, ckbTx.transaction);
294-
// console.log('isIsomorphic', btcTx.txid, ckbTx.transaction.hash, isIsomorphic);
295265
if (isIsomorphic) {
296266
return indexerTx;
297267
}
@@ -302,21 +272,27 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
302272

303273
public async queryBtcTimeLockTxByBtcTxId(btcTxId: string) {
304274
// XXX: unstable, need to be improved: https://github.com/ckb-cell/btc-assets-api/issues/45
305-
const btcTimeLockTxs = await this.cradle.ckb.indexer.getTransactions({
306-
script: {
307-
...this.btcTimeLockScript,
308-
args: '0x',
275+
const btcTimeLockTxs = await this.cradle.ckb.indexer.getTransactions(
276+
{
277+
script: {
278+
...getBtcTimeLock(),
279+
args: '0x',
280+
},
281+
scriptType: 'lock',
282+
groupByTransaction: true,
309283
},
310-
scriptType: 'lock',
311-
});
284+
{
285+
order: 'asc',
286+
},
287+
);
312288

313289
const txHashes = uniq(btcTimeLockTxs.objects.map(({ txHash }) => txHash));
314290
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(txHashes.map((txHash) => ['getTransaction', txHash]));
315291
const transactions: TransactionWithStatus[] = await batchRequest.exec();
316292
if (transactions.length > 0) {
317293
for (const tx of transactions) {
318294
const isBtcTimeLockTx = tx.transaction.outputs.some((output) => {
319-
if (!isScriptEqual(output.lock, this.btcTimeLockScript)) {
295+
if (!isScriptEqual(output.lock, getBtcTimeLock())) {
320296
return false;
321297
}
322298
const outputBtcTxId = btcTxIdFromBtcTimeLockArgs(output.lock.args);
@@ -330,18 +306,11 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
330306
return null;
331307
}
332308

333-
async isIsomorphicTx(btcTx: Transaction, ckbTx: CKBComponents.RawTransaction, validateCommitment?: boolean) {
334-
const replaceLockArgsWithPlaceholder = (cell: CKBComponents.CellOutput, index: number) => {
335-
if (this.isRgbppLock(cell.lock)) {
336-
cell.lock.args = buildPreLockArgs(index + 1);
337-
}
338-
if (this.isBtcTimeLock(cell.lock)) {
339-
const lockArgs = BTCTimeLock.unpack(cell.lock.args);
340-
cell.lock.args = genBtcTimeLockArgs(lockArgs.lockScript, RGBPP_TX_ID_PLACEHOLDER, lockArgs.after);
341-
}
342-
return cell;
343-
};
344-
309+
async isIsomorphicTx(
310+
btcTx: Transaction,
311+
ckbTx: CKBComponents.RawTransaction,
312+
validateCommitment?: boolean,
313+
): Promise<boolean> {
345314
// Find the commitment from the btc_tx
346315
const btcTxCommitment = tryGetCommitmentFromBtcTx(btcTx);
347316
if (!btcTxCommitment) {
@@ -352,86 +321,68 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
352321
// 1. Find the last index of the type inputs
353322
// 2. Check if all rgbpp_lock inputs can be found in the btc_tx.vin
354323
// 3. Check if the inputs contain at least one rgbpp_lock cell (as L1-L1 and L1-L2 transactions should have)
355-
let lastTypeInputIndex = -1;
356-
let foundRgbppLockInput = false;
357-
const outPoints = ckbTx.inputs.map((input) => input.previousOutput!);
358-
const inputs = await this.cradle.ckb.getInputCellsByOutPoint(outPoints);
359-
for (let i = 0; i < inputs.length; i++) {
360-
if (inputs[i].type) {
361-
lastTypeInputIndex = i;
362-
const isRgbppLock = this.isRgbppLock(inputs[i].lock);
363-
if (isRgbppLock) {
364-
foundRgbppLockInput = true;
365-
const btcInput = btcTx.vin[i];
366-
const rgbppLockArgs = unpackRgbppLockArgs(inputs[i].lock.args);
367-
if (
368-
!btcInput ||
369-
btcInput.txid !== remove0x(rgbppLockArgs.btcTxid) ||
370-
btcInput.vout !== rgbppLockArgs.outIndex
371-
) {
372-
return false;
373-
}
374-
}
375-
}
324+
const inputs = await this.cradle.ckb.getInputCellsByOutPoint(ckbTx.inputs.map((input) => input.previousOutput!));
325+
const lastTypeInputIndex = findLastIndex(inputs, (input) => !!input.cellOutput.type);
326+
const anyRgbppLockInput = inputs.some((input) => isRgbppLock(input.cellOutput.lock));
327+
if (!anyRgbppLockInput) {
328+
return false;
376329
}
377-
// XXX: In some type of RGB++ transactions, the inputs may not contain any rgbpp_lock cells
378-
// We add this check to ensure this function only validates for L1-L1 and L1-L2 transactions
379-
if (!foundRgbppLockInput) {
330+
const allInputsValid = inputs.every((input, index) => {
331+
if (!input.cellOutput.type) {
332+
return true;
333+
}
334+
if (!isRgbppLock(input.cellOutput.lock)) {
335+
return true;
336+
}
337+
const btcInput = btcTx.vin[index];
338+
const rgbppLockArgs = unpackRgbppLockArgs(input.cellOutput.lock.args);
339+
return btcInput && btcInput.txid === remove0x(rgbppLockArgs.btcTxid) && btcInput.vout === rgbppLockArgs.outIndex;
340+
});
341+
if (!allInputsValid) {
380342
return false;
381343
}
382344

383345
// Check outputs:
384-
// 1. Find the last index of the type outputs
385-
// 2. Check if all type outputs are rgbpp_lock/btc_time_lock cells
386-
// 3. Check if each rgbpp_lock cell has an isomorphic UTXO in the btc_tx.vout
387-
// 4. Check if each btc_time_lock cell contains the corresponding btc_txid in the lock args
388-
// 5. Check if the outputs contain at least one rgbpp_lock/btc_time_lock cell
389-
let lastTypeOutputIndex = -1;
390-
for (let i = 0; i < ckbTx.outputs.length; i++) {
391-
const ckbOutput = ckbTx.outputs[i];
392-
const isRgbppLock = this.isRgbppLock(ckbOutput.lock);
393-
const isBtcTimeLock = this.isBtcTimeLock(ckbOutput.lock);
394-
if (isRgbppLock) {
395-
const rgbppLockArgs = unpackRgbppLockArgs(ckbOutput.lock.args);
346+
// 1. Find the last index of the type outputs, and check if at least one type output exists
347+
// 2. Check if all type outputs are rgbpp_lock or btc_time_lock cells
348+
// 4. Check if each rgbpp_lock cell has an isomorphic UTXO in the btc_tx.vout
349+
// 5. Check if each btc_time_lock cell contains the corresponding btc_txid in the lock args
350+
const lastTypeOutputIndex = findLastIndex(ckbTx.outputs, (output) => !!output.type);
351+
if (lastTypeOutputIndex < 0) {
352+
return false;
353+
}
354+
const anyRelatedLockToTypeOutput = ckbTx.outputs.some(
355+
(output) => output.type && (isRgbppLock(output.lock) || isBtcTimeLock(output.lock)),
356+
);
357+
if (!anyRelatedLockToTypeOutput) {
358+
return false;
359+
}
360+
const allOutputsValid = ckbTx.outputs.every((output) => {
361+
if (isRgbppLock(output.lock)) {
362+
const rgbppLockArgs = unpackRgbppLockArgs(output.lock.args);
396363
const btcTxId = remove0x(rgbppLockArgs.btcTxid);
397364
if (btcTxId !== RGBPP_TX_ID_PLACEHOLDER && (btcTxId !== btcTx.txid || !btcTx.vout[rgbppLockArgs.outIndex])) {
398365
return false;
399366
}
400367
}
401-
if (isBtcTimeLock) {
402-
const btcTxId = remove0x(btcTxIdFromBtcTimeLockArgs(ckbOutput.lock.args));
368+
if (isBtcTimeLock(output.lock)) {
369+
const btcTxId = remove0x(btcTxIdFromBtcTimeLockArgs(output.lock.args));
403370
if (btcTxId !== RGBPP_TX_ID_PLACEHOLDER && btcTx.txid !== btcTxId) {
404371
return false;
405372
}
406373
}
407-
if (ckbOutput.type) {
408-
lastTypeOutputIndex = i;
409-
}
410-
}
411-
if (lastTypeOutputIndex < 0) {
374+
return true;
375+
});
376+
if (!allOutputsValid) {
412377
return false;
413378
}
414379

415-
// Cut the ckb_tx to simulate how the ckb_virtual_tx looks like
416-
const ckbVirtualTx = cloneDeep(ckbTx);
417-
ckbVirtualTx.inputs = ckbVirtualTx.inputs.slice(0, Math.max(lastTypeInputIndex, 0) + 1);
418-
ckbVirtualTx.outputs = ckbVirtualTx.outputs.slice(0, lastTypeOutputIndex + 1).map(replaceLockArgsWithPlaceholder);
419-
420-
// Copy ckb_tx and change output lock args to placeholder args
421-
const ckbPlaceholderTx = cloneDeep(ckbTx);
422-
ckbPlaceholderTx.outputs = ckbPlaceholderTx.outputs.map(replaceLockArgsWithPlaceholder);
380+
// Compare commitment between btc_tx and ckb_tx
423381
if (!validateCommitment) {
424382
return true;
425383
}
426-
427-
// Generate commitment with the ckb_tx/ckb_virtual_tx, then compare it with the btc_tx commitment.
428-
// If both commitments don't match the btc_tx commitment:
429-
// 1. The ckb_tx is not the isomorphic transaction of the btc_tx (this is the usual case)
430-
// 2. The commitment calculation logic differs from the one used in the btc_tx/ckb_tx
431-
const ckbTxCommitment = calculateCommitment(ckbPlaceholderTx);
432-
const ckbVirtualTxCommitment = calculateCommitment(ckbVirtualTx);
433384
const btcTxCommitmentHex = btcTxCommitment.toString('hex');
434-
return btcTxCommitmentHex === ckbVirtualTxCommitment || btcTxCommitmentHex === ckbTxCommitment;
385+
return isCommitmentMatchToCkbTx(btcTxCommitmentHex, ckbTx, lastTypeInputIndex, lastTypeOutputIndex);
435386
}
436387

437388
/**

src/utils/commitment.ts

+59-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import { cloneDeep } from 'lodash';
12
import { opReturnScriptPubKeyToData } from '@rgbpp-sdk/btc';
3+
import { calculateCommitment } from '@rgbpp-sdk/ckb/lib/utils/rgbpp';
4+
import { RGBPP_TX_ID_PLACEHOLDER, BTCTimeLock, buildPreLockArgs, genBtcTimeLockArgs } from '@rgbpp-sdk/ckb';
25
import { Transaction } from '../routes/bitcoin/types';
6+
import { isBtcTimeLock, isRgbppLock } from './lockscript';
37

48
export class OpReturnNotFoundError extends Error {
59
constructor(txid: string) {
@@ -9,11 +13,11 @@ export class OpReturnNotFoundError extends Error {
913
}
1014

1115
/**
12-
* Get commitment from Bitcoin transactions
16+
* Get commitment from the Bitcoin transaction
1317
* depended on @rgbpp-sdk/btc opReturnScriptPubKeyToData method
1418
* @param tx - Bitcoin transaction
1519
*/
16-
export function getCommitmentFromBtcTx(tx: Transaction) {
20+
export function getCommitmentFromBtcTx(tx: Transaction): Buffer {
1721
const opReturn = tx.vout.find((vout) => vout.scriptpubkey_type === 'op_return');
1822
if (!opReturn) {
1923
throw new OpReturnNotFoundError(tx.txid);
@@ -22,7 +26,12 @@ export function getCommitmentFromBtcTx(tx: Transaction) {
2226
return opReturnScriptPubKeyToData(buffer);
2327
}
2428

25-
export function tryGetCommitmentFromBtcTx(tx: Transaction) {
29+
/**
30+
* Try to get commitment from the Bitcoin transactions, returns null if OP_RETURN output not found
31+
* depended on @rgbpp-sdk/btc opReturnScriptPubKeyToData method
32+
* @param tx - Bitcoin transaction
33+
*/
34+
export function tryGetCommitmentFromBtcTx(tx: Transaction): Buffer | null {
2635
try {
2736
return getCommitmentFromBtcTx(tx);
2837
} catch (error) {
@@ -32,3 +41,50 @@ export function tryGetCommitmentFromBtcTx(tx: Transaction) {
3241
throw error;
3342
}
3443
}
44+
45+
/**
46+
* Validate if the commitment matches the CKB transaction
47+
* @param commitment - The expected commitment from a Bitcoin transaction
48+
* @param ckbTx - The target CKB transaction or RawTransaction to compare with
49+
* @param lastTypeInputIndex - The last index of type script input in the ckbTx
50+
* @param lastTypeOutputIndex - The last index of type script output in the ckbTx
51+
*/
52+
export function isCommitmentMatchToCkbTx(
53+
commitment: string,
54+
ckbTx: CKBComponents.RawTransaction,
55+
lastTypeInputIndex: number,
56+
lastTypeOutputIndex: number,
57+
) {
58+
function replaceLockArgsWithPlaceholder(cell: CKBComponents.CellOutput, outputIndex: number) {
59+
if (isRgbppLock(cell.lock)) {
60+
cell.lock.args = buildPreLockArgs(outputIndex + 1);
61+
}
62+
if (isBtcTimeLock(cell.lock)) {
63+
const lockArgs = BTCTimeLock.unpack(cell.lock.args);
64+
cell.lock.args = genBtcTimeLockArgs(lockArgs.lockScript, RGBPP_TX_ID_PLACEHOLDER, lockArgs.after);
65+
}
66+
return cell;
67+
}
68+
69+
// Use the ckb_tx to compare with the btc_tx commitment directly
70+
const finalTx = cloneDeep(ckbTx);
71+
finalTx.outputs = finalTx.outputs.map(replaceLockArgsWithPlaceholder);
72+
const finalTxCommitment = calculateCommitment(finalTx);
73+
if (commitment === finalTxCommitment) {
74+
return true;
75+
}
76+
77+
// Slice inputs and outputs of the ckb_tx to simulate how the original ckb_virtual_result looks like
78+
const slicedTx = cloneDeep(ckbTx);
79+
slicedTx.inputs = slicedTx.inputs.slice(0, Math.max(lastTypeInputIndex, 0) + 1);
80+
slicedTx.outputs = slicedTx.outputs.slice(0, lastTypeOutputIndex + 1).map(replaceLockArgsWithPlaceholder);
81+
const slicedTxCommitment = calculateCommitment(slicedTx);
82+
if (commitment === slicedTxCommitment) {
83+
return true;
84+
}
85+
86+
// If both commitments don't match the btc_tx commitment:
87+
// 1. The ckb_tx does not match to the commitment from the btc_tx (the usual case)
88+
// 2. The provided btc_tx commitment calculation is different from this function
89+
return false;
90+
}

0 commit comments

Comments
 (0)