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: add rgbpp address activity api #182

Merged
merged 10 commits into from
Jul 10, 2024
169 changes: 166 additions & 3 deletions src/routes/rgbpp/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ import { FastifyPluginCallback, FastifyRequest } from 'fastify';
import { Server } from 'http';
import validateBitcoinAddress from '../../utils/validators';
import { ZodTypeProvider } from 'fastify-type-provider-zod';
import { Cell, Script, XUDTBalance } from './types';
import { CKBTransaction, Cell, IsomorphicTransaction, Script, XUDTBalance } from './types';
import { blockchain } from '@ckb-lumos/base';
import z from 'zod';
import { Env } from '../../env';
import { buildPreLockArgs, getXudtTypeScript, isScriptEqual, isTypeAssetSupported } from '@rgbpp-sdk/ckb';
import { groupBy } from 'lodash';
import { BI } from '@ckb-lumos/lumos';
import { UTXO } from '../../services/bitcoin/schema';
import { Transaction as BTCTransaction } from '../bitcoin/types';
import { tryGetCommitmentFromBtcTx } from '../../utils/commitment';
import { TransactionWithStatus } from '../../services/ckb';

const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodTypeProvider> = (fastify, _, done) => {
const env: Env = fastify.container.resolve('env');
Expand Down Expand Up @@ -72,7 +75,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
/**
* Filter cells by type script
*/
async function filterCellsByTypeScript(cells: Cell[], typeScript: Script) {
function filterCellsByTypeScript(cells: Cell[], typeScript: Script) {
return cells.filter((cell) => {
if (!cell.cellOutput.type) {
return false;
Expand Down Expand Up @@ -132,7 +135,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
{
schema: {
description: 'Get RGB++ balance by btc address, support xUDT only for now',
tags: ['RGB++@Beta'],
tags: ['RGB++'],
params: z.object({
btc_address: z.string(),
}),
Expand Down Expand Up @@ -238,6 +241,166 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
},
);

async function getIsomorphicTx(btcTx: BTCTransaction) {
const isomorphicTx: IsomorphicTransaction = {
ckbRawTx: undefined,
ckbTx: undefined,
status: { confirmed: false },
};
const setCkbTxAndStatus = (tx: TransactionWithStatus) => {
isomorphicTx.ckbTx = CKBTransaction.parse(tx.transaction);
isomorphicTx.status.confirmed = tx.txStatus.status === 'committed';
};

const job = await fastify.transactionProcessor.getTransactionRequest(btcTx.txid);
if (job) {
const { ckbRawTx } = job.data.ckbVirtualResult;
isomorphicTx.ckbRawTx = 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;
setCkbTxAndStatus(ckbTx);
}
return isomorphicTx;
}
const rgbppLockTx = await fastify.rgbppCollector.queryRgbppLockTxByBtcTx(btcTx);
if (rgbppLockTx) {
const ckbTx = await fastify.ckb.rpc.getTransaction(rgbppLockTx.txHash);
setCkbTxAndStatus(ckbTx);
} else {
// XXX: this is a performance bottleneck, need to optimize
const btcTimeLockTx = await fastify.rgbppCollector.queryBtcTimeLockTxByBtcTxId(btcTx.txid);
if (btcTimeLockTx) {
setCkbTxAndStatus(btcTimeLockTx as TransactionWithStatus);
}
}
return isomorphicTx;
}

fastify.get(
'/:btc_address/activity',
{
schema: {
description: 'Get RGB++ activity by btc address',
tags: ['RGB++'],
params: z.object({
btc_address: z.string(),
}),
querystring: z.object({
type_script: Script.or(z.string())
.describe(
`
type script to filter cells

two ways to provide:
- as a object: 'encodeURIComponent(JSON.stringify({"codeHash":"0x...", "args":"0x...", "hashType":"type"}))'
- as a hex string: '0x...' (You can pack by @ckb-lumos/codec blockchain.Script.pack({ "codeHash": "0x...", ... }))
`,
)
.default(getXudtTypeScript(env.NETWORK === 'mainnet')),
rgbpp_only: z
.enum(['true', 'false'])
.default('false')
.describe('Whether to get RGB++ only activity, default is false'),
after_btc_txid: z.string().optional().describe('Get activity after this btc txid'),
}),
response: {
200: z.object({
address: z.string(),
txs: z.array(
z
.object({
btcTx: BTCTransaction,
})
.and(
z.union([
z.object({
isRgbpp: z.literal(true),
isomorphicTx: IsomorphicTransaction,
}),
z.object({ isRgbpp: z.literal(false) }),
]),
),
),
cursor: z.string().optional(),
}),
},
},
},
async (request) => {
const { btc_address } = request.params;
const { rgbpp_only, after_btc_txid } = request.query;
const typeScript = getTypeScript(request);
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved

const btcTxs = await fastify.bitcoin.getAddressTxs({
address: btc_address,
after_txid: after_btc_txid,
});
const withCommitmentTxs = btcTxs.filter((btcTx) => tryGetCommitmentFromBtcTx(btcTx));

let txs = await Promise.all(
withCommitmentTxs.map(async (btcTx) => {
const isomorphicTx = await getIsomorphicTx(btcTx);
const isRgbpp = isomorphicTx.ckbRawTx || isomorphicTx.ckbTx;
if (!isRgbpp) {
return {
btcTx,
isRgbpp: false,
} as const;
}

const inputOutpoints = isomorphicTx.ckbRawTx?.inputs || isomorphicTx.ckbTx?.inputs || [];
const inputs = await fastify.ckb.getInputCellsByOutPoint(
inputOutpoints.map((input) => input.previousOutput) as CKBComponents.OutPoint[],
);
const outputs = isomorphicTx.ckbRawTx?.outputs || isomorphicTx.ckbTx?.outputs || [];

return {
btcTx,
isRgbpp: true,
isomorphicTx: {
...isomorphicTx,
inputs,
outputs,
},
} as const;
}),
);

if (rgbpp_only === 'true') {
txs = txs.filter((tx) => tx.isRgbpp);
}

if (typeScript) {
txs = txs.filter((tx) => {
if (!tx.isRgbpp) {
return false;
}
const cells = [...tx.isomorphicTx.inputs, ...tx.isomorphicTx.outputs];
const filteredCells = cells.filter((cell) => {
if (!cell.type) return false;
if (!typeScript.args) {
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
const script = { ...cell.type, args: '' };
return isScriptEqual(script, typeScript);
}
return isScriptEqual(cell.type, typeScript);
});
return filteredCells.length > 0;
});
}

const cursor = btcTxs.length > 0 ? btcTxs[btcTxs.length - 1].txid : undefined;
return {
address: btc_address,
txs,
cursor,
};
},
);

done();
};

Expand Down
66 changes: 9 additions & 57 deletions src/routes/rgbpp/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,7 @@ import { Server } from 'http';
import z from 'zod';
import { CKBVirtualResult } from './types';
import { Job } from 'bullmq';
import {
btcTxIdFromBtcTimeLockArgs,
buildRgbppLockArgs,
genRgbppLockScript,
getBtcTimeLockScript,
} from '@rgbpp-sdk/ckb';
import { remove0x } from '@rgbpp-sdk/btc';
import { CUSTOM_HEADERS } from '../../constants';
import { env } from '../../env';
import { JwtPayload } from '../../plugins/jwt';

const transactionRoute: FastifyPluginCallback<Record<never, never>, Server, ZodTypeProvider> = (fastify, _, done) => {
Expand Down Expand Up @@ -73,62 +65,22 @@ const transactionRoute: FastifyPluginCallback<Record<never, never>, Server, ZodT
},
async (request, reply) => {
const { btc_txid } = request.params;
const isMainnet = env.NETWORK === 'mainnet';

// get the transaction hash from the job if it exists
const job = await fastify.transactionProcessor.getTransactionRequest(btc_txid);
if (job?.returnvalue) {
return { txhash: job.returnvalue };
}

const transaction = await fastify.bitcoin.getTx({ txid: btc_txid });

// query CKB transaction hash by RGBPP_LOCK cells
for (let index = 0; index < transaction.vout.length; index++) {
const args = buildRgbppLockArgs(index, btc_txid);
const lock = genRgbppLockScript(args, isMainnet);

const txs = await fastify.ckb.indexer.getTransactions({
script: lock,
scriptType: 'lock',
});

if (txs.objects.length > 0) {
const [tx] = txs.objects;
reply.header(CUSTOM_HEADERS.ResponseCacheable, 'true');
return { txhash: tx.txHash };
}
const btcTx = await fastify.bitcoin.getTx({ txid: btc_txid });
const rgbppLockTx = await fastify.rgbppCollector.queryRgbppLockTxByBtcTx(btcTx);
if (rgbppLockTx) {
reply.header(CUSTOM_HEADERS.ResponseCacheable, 'true');
return { txhash: rgbppLockTx.txHash };
}

// XXX: unstable, need to be improved: https://github.com/ckb-cell/btc-assets-api/issues/45
// query CKB transaction hash by BTC_TIME_LOCK cells
const btcTimeLockScript = getBtcTimeLockScript(isMainnet);
const txs = await fastify.ckb.indexer.getTransactions({
script: {
...btcTimeLockScript,
args: '0x',
},
scriptType: 'lock',
});

if (txs.objects.length > 0) {
for (const { txHash } of txs.objects) {
const tx = await fastify.ckb.rpc.getTransaction(txHash);
const isBtcTimeLockTx = tx.transaction.outputs.some((output) => {
if (
output.lock.codeHash !== btcTimeLockScript.codeHash ||
output.lock.hashType !== btcTimeLockScript.hashType
) {
return false;
}
const btcTxid = btcTxIdFromBtcTimeLockArgs(output.lock.args);
return remove0x(btcTxid) === btc_txid;
});
if (isBtcTimeLockTx) {
reply.header(CUSTOM_HEADERS.ResponseCacheable, 'true');
return { txhash: txHash };
}
}
const btcTimeLockTx = await fastify.rgbppCollector.queryBtcTimeLockTxByBtcTxId(btc_txid);
if (btcTimeLockTx) {
reply.header(CUSTOM_HEADERS.ResponseCacheable, 'true');
return { txhash: btcTimeLockTx.transaction.hash };
}

reply.status(404);
Expand Down
11 changes: 11 additions & 0 deletions src/routes/rgbpp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,14 @@ export const XUDTBalance = z.object({
type_hash: z.string(),
});
export type XUDTBalance = z.infer<typeof XUDTBalance>;

export const IsomorphicTransaction = z.object({
ckbRawTx: CKBRawTransaction.optional(),
Flouse marked this conversation as resolved.
Show resolved Hide resolved
ckbTx: CKBTransaction.optional(),
inputs: z.array(OutputCell).optional(),
outputs: z.array(OutputCell).optional(),
Flouse marked this conversation as resolved.
Show resolved Hide resolved
status: z.object({
confirmed: z.boolean(),
}),
});
export type IsomorphicTransaction = z.infer<typeof IsomorphicTransaction>;
13 changes: 12 additions & 1 deletion src/services/ckb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
sendCkbTx,
} from '@rgbpp-sdk/ckb';
import { Cradle } from '../container';
import { Indexer, RPC, Script } from '@ckb-lumos/lumos';
import { BI, Indexer, RPC, Script } from '@ckb-lumos/lumos';
import { CKBRPC } from '@ckb-lumos/rpc';
import { z } from 'zod';
import * as Sentry from '@sentry/node';
Expand All @@ -21,6 +21,7 @@ import {
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
import DataCache from './base/data-cache';
import { scriptToHash } from '@nervosnetwork/ckb-sdk-utils';
import { OutputCell } from '../routes/rgbpp/types';

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

Expand Down Expand Up @@ -304,6 +305,16 @@ export default class CKBClient {
return null;
}

public async getInputCellsByOutPoint(outPoints: CKBComponents.OutPoint[]): Promise<OutputCell[]> {
const batchRequest = this.rpc.createBatchRequest(outPoints.map((outPoint) => ['getTransaction', outPoint.txHash]));
const txs = await batchRequest.exec();
const inputs = txs.map((tx: TransactionWithStatus, index: number) => {
const outPoint = outPoints[index];
return tx.transaction.outputs[BI.from(outPoint.index).toNumber()];
});
return inputs;
}

/**
* Wait for the ckb transaction to be confirmed
* @param txHash - the ckb transaction hash
Expand Down
Loading
Loading