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

fix: duplicated bindings in the activity API #194

Merged
merged 5 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isAdminMode } from './env';
import { env, isAdminMode } from './env';
import { BTCTestnetType } from '@rgbpp-sdk/ckb';

export enum NetworkType {
Expand Down Expand Up @@ -41,3 +41,6 @@ export const BTC_SIGNET_SPV_START_BLOCK_HEIGHT = 199800;
// estimate time: 2024-04-02 06:20:03
// ref: https://mempool.space/block/0000000000000000000077d98a103858c7d7cbc5ba67a4135f348a436bec1748
export const BTC_MAINNET_SPV_START_BLOCK_HEIGHT = 837300;

export const IS_MAINNET = env.NETWORK === NetworkType.mainnet.toString();
export const TESTNET_TYPE = TestnetTypeMap[env.NETWORK];
12 changes: 6 additions & 6 deletions src/routes/rgbpp/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType

async function getIsomorphicTx(btcTx: BTCTransaction) {
const isomorphicTx: IsomorphicTransaction = {
ckbRawTx: undefined,
ckbVirtualTx: undefined,
ckbTx: undefined,
status: { confirmed: false },
};
Expand All @@ -293,13 +293,13 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
const job = await fastify.transactionProcessor.getTransactionRequest(btcTx.txid);
if (job) {
const { ckbRawTx } = job.data.ckbVirtualResult;
isomorphicTx.ckbRawTx = ckbRawTx;
isomorphicTx.ckbVirtualTx = ckbRawTx;
// if the job is completed, get the ckb tx hash and fetch the ckb tx
const state = await job.getState();
if (state === 'completed') {
const ckbTx = await fastify.ckb.rpc.getTransaction(job.returnvalue);
// remove ckbRawTx to reduce response size
isomorphicTx.ckbRawTx = undefined;
isomorphicTx.ckbVirtualTx = undefined;
setCkbTxAndStatus(ckbTx);
}
return isomorphicTx;
Expand Down Expand Up @@ -381,19 +381,19 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
let txs = await Promise.all(
btcTxs.map(async (btcTx) => {
const isomorphicTx = await getIsomorphicTx(btcTx);
const isRgbpp = isomorphicTx.ckbRawTx || isomorphicTx.ckbTx;
const isRgbpp = isomorphicTx.ckbVirtualTx || isomorphicTx.ckbTx;
if (!isRgbpp) {
return {
btcTx,
isRgbpp: false,
} as const;
}

const inputs = isomorphicTx.ckbRawTx?.inputs || isomorphicTx.ckbTx?.inputs || [];
const inputs = isomorphicTx.ckbVirtualTx?.inputs || isomorphicTx.ckbTx?.inputs || [];
const inputCells = await fastify.ckb.getInputCellsByOutPoint(inputs.map((input) => input.previousOutput!));
const inputCellOutputs = inputCells.map((cell) => cell.cellOutput);

const outputs = isomorphicTx.ckbRawTx?.outputs || isomorphicTx.ckbTx?.outputs || [];
const outputs = isomorphicTx.ckbVirtualTx?.outputs || isomorphicTx.ckbTx?.outputs || [];

return {
btcTx,
Expand Down
2 changes: 1 addition & 1 deletion src/routes/rgbpp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export const XUDTBalance = XUDTTypeInfo.merge(
export type XUDTBalance = z.infer<typeof XUDTBalance>;

export const IsomorphicTransaction = z.object({
ckbRawTx: CKBRawTransaction.optional(),
ckbVirtualTx: CKBRawTransaction.optional(),
ckbTx: CKBTransaction.optional(),
inputs: z.array(OutputCell).optional(),
outputs: z.array(OutputCell).optional(),
Expand Down
171 changes: 128 additions & 43 deletions src/services/rgbpp.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
import { Transaction, UTXO } from './bitcoin/schema';
import pLimit from 'p-limit';
import asyncRetry from 'async-retry';
import { Cradle } from '../container';
import * as Sentry from '@sentry/node';
import {
IndexerCell,
btcTxIdFromBtcTimeLockArgs,
leToU128,
isScriptEqual,
buildRgbppLockArgs,
genRgbppLockScript,
getBtcTimeLockScript,
isScriptEqual,
leToU128,
btcTxIdFromBtcTimeLockArgs,
RGBPP_TX_ID_PLACEHOLDER,
RGBPP_TX_INPUTS_MAX_LENGTH,
} from '@rgbpp-sdk/ckb';
import * as Sentry from '@sentry/node';
import { BI, RPC, Script } from '@ckb-lumos/lumos';
import { Job } from 'bullmq';
import { remove0x } from '@rgbpp-sdk/btc';
import { unpackRgbppLockArgs } from '@rgbpp-sdk/btc/lib/ckb/molecule';
import { groupBy, uniq, findLastIndex } from 'lodash';
import { z } from 'zod';
import { Job } from 'bullmq';
import { BI, RPC, Script } from '@ckb-lumos/lumos';
import { TransactionWithStatus } from '@ckb-lumos/base';
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
import { Cell, XUDTBalance } from '../routes/rgbpp/types';
import { Transaction, UTXO } from './bitcoin/schema';
import BaseQueueWorker from './base/queue-worker';
import DataCache from './base/data-cache';
import { groupBy } from 'lodash';
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
import { remove0x } from '@rgbpp-sdk/btc';
import { TestnetTypeMap } from '../constants';
import { TransactionWithStatus } from '@ckb-lumos/base';
import { Cradle } from '../container';
import { isCommitmentMatchToCkbTx, tryGetCommitmentFromBtcTx } from '../utils/commitment';
import { getBtcTimeLock, isBtcTimeLock, isRgbppLock } from '../utils/lockscript';
import { IS_MAINNET, TESTNET_TYPE } from '../constants';

type GetCellsParams = Parameters<RPC['getCells']>;
export type SearchKey = GetCellsParams[0];
Expand Down Expand Up @@ -59,10 +63,10 @@ class RgbppCollectorError extends Error {
/**
* RgbppCollector is used to collect the cells for the utxos.
* The cells are stored in the cache with the btc address as the key,
* will be recollect when the utxos are updated or new collect job is enqueued.
* will be recollected when the utxos are updated or new collect job is enqueued.
*/
export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest, IRgbppCollectJobReturn> {
private limit: pLimit.Limit;
private readonly limit: pLimit.Limit;
private dataCache: DataCache<IRgbppCollectJobReturn>;

constructor(private cradle: Cradle) {
Expand Down Expand Up @@ -151,21 +155,20 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
* @param typeScript - the type script to filter the cells
*/
public async getRgbppCellsByBatchRequest(utxos: UTXO[], typeScript?: Script) {
const network = this.cradle.env.NETWORK;
const batchRequest: CKBBatchRequest = this.cradle.ckb.rpc.createBatchRequest(
utxos.map((utxo: UTXO) => {
const { txid, vout } = utxo;
const args = buildRgbppLockArgs(vout, txid);
const searchKey: SearchKey = {
script: genRgbppLockScript(args, network === 'mainnet', TestnetTypeMap[network]),
script: genRgbppLockScript(args, IS_MAINNET, TESTNET_TYPE),
scriptType: 'lock',
};
if (typeScript) {
searchKey.filter = {
script: typeScript,
};
}
// TOOD: In extreme cases, the num of search target cells may be more than limit=0x64=100
// TODO: In extreme cases, the num of search target cells may be more than limit=0x64=100
// Priority: Low
const params: GetCellsParams = [searchKey, 'desc', '0x64'];
return ['getCells', ...params];
Expand Down Expand Up @@ -240,55 +243,60 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
}

public async queryRgbppLockTxByBtcTx(btcTx: Transaction) {
const network = this.cradle.env.NETWORK;
const isMainnet = this.cradle.env.NETWORK === 'mainnet';

// Only query the first RGBPP_TX_INPUTS_MAX_LENGTH transactions for performance reasons
const maxRecords = `0x${RGBPP_TX_INPUTS_MAX_LENGTH.toString(16)}`;
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(
btcTx.vout.map((_, index) => {
const args = buildRgbppLockArgs(index, btcTx.txid);
const lock = genRgbppLockScript(args, isMainnet, TestnetTypeMap[network]);
const lock = genRgbppLockScript(args, IS_MAINNET, TESTNET_TYPE);
const searchKey: SearchKey = {
script: lock,
scriptType: 'lock',
};
return ['getTransactions', searchKey, 'desc', '0x1'];
return ['getTransactions', searchKey, 'asc', maxRecords];
}),
);
type getTransactionsResult = ReturnType<typeof this.cradle.ckb.rpc.getTransactions<false>>;
const transactions: Awaited<getTransactionsResult>[] = await batchRequest.exec();
for (const { objects } of transactions) {
if (objects.length > 0) {
const [tx] = objects;
return tx;
for (const tx of transactions) {
for (const indexerTx of tx.objects) {
const ckbTx = await this.cradle.ckb.rpc.getTransaction(indexerTx.txHash);
const isIsomorphic = await this.isIsomorphicTx(btcTx, ckbTx.transaction);
if (isIsomorphic) {
return indexerTx;
}
}
}
return null;
}

public async queryBtcTimeLockTxByBtcTxId(btcTxId: string) {
const isMainnet = this.cradle.env.NETWORK === 'mainnet';
// XXX: unstable, need to be improved: https://github.com/ckb-cell/btc-assets-api/issues/45
const btcTimeLockScript = getBtcTimeLockScript(isMainnet);
const btcTimeLockTxs = await this.cradle.ckb.indexer.getTransactions({
script: {
...btcTimeLockScript,
args: '0x',
const btcTimeLockTxs = await this.cradle.ckb.indexer.getTransactions(
{
script: {
...getBtcTimeLock(),
args: '0x',
},
scriptType: 'lock',
groupByTransaction: true,
},
{
order: 'asc',
},
scriptType: 'lock',
});

const batchRequest = this.cradle.ckb.rpc.createBatchRequest(
btcTimeLockTxs.objects.map(({ txHash }) => ['getTransaction', txHash]),
);

const txHashes = uniq(btcTimeLockTxs.objects.map(({ txHash }) => txHash));
const batchRequest = this.cradle.ckb.rpc.createBatchRequest(txHashes.map((txHash) => ['getTransaction', txHash]));
const transactions: TransactionWithStatus[] = await batchRequest.exec();
if (transactions.length > 0) {
for (const tx of transactions) {
const isBtcTimeLockTx = tx.transaction.outputs.some((output) => {
if (!isScriptEqual(output.lock, btcTimeLockScript)) {
if (!isScriptEqual(output.lock, getBtcTimeLock())) {
return false;
}
const btcTxid = btcTxIdFromBtcTimeLockArgs(output.lock.args);
return remove0x(btcTxid) === btcTxId;
const outputBtcTxId = btcTxIdFromBtcTimeLockArgs(output.lock.args);
return remove0x(outputBtcTxId) === btcTxId;
});
if (isBtcTimeLockTx) {
return tx;
Expand All @@ -298,9 +306,86 @@ export default class RgbppCollector extends BaseQueueWorker<IRgbppCollectRequest
return null;
}

async isIsomorphicTx(
btcTx: Transaction,
ckbTx: CKBComponents.RawTransaction,
validateCommitment?: boolean,
): Promise<boolean> {
// Find the commitment from the btc_tx
const btcTxCommitment = tryGetCommitmentFromBtcTx(btcTx);
if (!btcTxCommitment) {
return false;
}

// Check inputs:
// 1. Find the last index of the type inputs
// 2. Check if all rgbpp_lock inputs can be found in the btc_tx.vin (regardless the position)
// 3. Check if the inputs contain at least one rgbpp_lock cell (as L1-L1 and L1-L2 transactions should have)
const inputs = await this.cradle.ckb.getInputCellsByOutPoint(ckbTx.inputs.map((input) => input.previousOutput!));
const lastTypeInputIndex = findLastIndex(inputs, (input) => !!input.cellOutput.type);
const anyRgbppLockInput = inputs.some((input) => isRgbppLock(input.cellOutput.lock));
if (!anyRgbppLockInput) {
return false;
}
const allInputsValid = inputs.every((input) => {
if (!input.cellOutput.type) {
return true;
}
if (!isRgbppLock(input.cellOutput.lock)) {
return true;
}
const rgbppLockArgs = unpackRgbppLockArgs(input.cellOutput.lock.args);
const matchingBtcInput = btcTx.vin.find(
(btcInput) => btcInput.txid === remove0x(rgbppLockArgs.btcTxid) && btcInput.vout === rgbppLockArgs.outIndex,
);
return !!matchingBtcInput;
});
if (!allInputsValid) {
return false;
}

ahonn marked this conversation as resolved.
Show resolved Hide resolved
// Check outputs:
// 1. Find the last index of the type outputs
// 2. Check if all type outputs are rgbpp_lock or btc_time_lock cells
// 4. Check if each rgbpp_lock cell has an isomorphic UTXO in the btc_tx.vout
// 5. Check if each btc_time_lock cell contains the corresponding btc_txid in the lock args
Flouse marked this conversation as resolved.
Show resolved Hide resolved
const lastTypeOutputIndex = findLastIndex(ckbTx.outputs, (output) => !!output.type);
const allOutputsValid = ckbTx.outputs.every((output) => {
if (!output.type) {
return true;
}
if (isRgbppLock(output.lock)) {
const rgbppLockArgs = unpackRgbppLockArgs(output.lock.args);
const btcTxId = remove0x(rgbppLockArgs.btcTxid);
if (btcTxId === RGBPP_TX_ID_PLACEHOLDER) {
return true;
}
if (btcTxId === btcTx.txid && btcTx.vout[rgbppLockArgs.outIndex] !== undefined) {
return true;
}
}
if (isBtcTimeLock(output.lock)) {
const btcTxId = remove0x(btcTxIdFromBtcTimeLockArgs(output.lock.args));
if (btcTxId === RGBPP_TX_ID_PLACEHOLDER || btcTx.txid === btcTxId) {
return true;
}
}
return false;
});
if (!allOutputsValid) {
return false;
}

ahonn marked this conversation as resolved.
Show resolved Hide resolved
// Compare commitment between btc_tx and ckb_tx
if (!validateCommitment) {
return true;
}
const btcTxCommitmentHex = btcTxCommitment.toString('hex');
return isCommitmentMatchToCkbTx(btcTxCommitmentHex, ckbTx, lastTypeInputIndex, lastTypeOutputIndex);
}

ahonn marked this conversation as resolved.
Show resolved Hide resolved
/**
* Enqueue a collect job to the queue
* @param utxos - the utxos to collect
*/
public async enqueueCollectJob(btcAddress: string, allowDuplicate?: boolean): Promise<Job<IRgbppCollectRequest>> {
let jobId = btcAddress;
Expand Down
Loading
Loading