Skip to content

Commit

Permalink
Merge pull request #183 from ckb-cell/feat/rgbpp-assets-info
Browse files Browse the repository at this point in the history
feat: add /rgbpp/v1/assets/type for get assets type info
  • Loading branch information
Flouse authored Aug 8, 2024
2 parents b989f61 + e49c235 commit 7a6fc03
Show file tree
Hide file tree
Showing 14 changed files with 309 additions and 80 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@rgbpp-sdk/service": "^0.4.0",
"@sentry/node": "^7.102.1",
"@sentry/profiling-node": "^7.102.1",
"@spore-sdk/core": "^0.2.0-beta.9",
"async-retry": "^1.3.3",
"awilix": "^10.0.1",
"axios": "^1.6.7",
Expand Down
9 changes: 6 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 6 additions & 42 deletions src/routes/rgbpp/address.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { FastifyPluginCallback, FastifyRequest } from 'fastify';
import { FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import validateBitcoinAddress from '../../utils/validators';
import { ZodTypeProvider } from 'fastify-type-provider-zod';
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';
Expand All @@ -13,6 +12,7 @@ import { UTXO } from '../../services/bitcoin/schema';
import { Transaction as BTCTransaction } from '../bitcoin/types';
import { TransactionWithStatus } from '../../services/ckb';
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
import { filterCellsByTypeScript, getTypeScript } from '../../utils/typescript';

const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodTypeProvider> = (fastify, _, done) => {
const env: Env = fastify.container.resolve('env');
Expand All @@ -25,26 +25,6 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
}
});

/**
* Get type script from request query
*/
function getTypeScript(request: FastifyRequest) {
const { type_script } = request.query as { type_script: string | Script };
let typeScript: Script | undefined = undefined;
if (type_script) {
if (typeof type_script === 'string') {
if (type_script.startsWith('0x')) {
typeScript = blockchain.Script.unpack(type_script);
} else {
typeScript = JSON.parse(decodeURIComponent(type_script));
}
} else {
typeScript = type_script;
}
}
return typeScript;
}

/**
* Get UTXOs by btc address
*/
Expand Down Expand Up @@ -72,23 +52,6 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
return cells;
}

/**
* Filter cells by type script
*/
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 || typeScript.args === '0x') {
const script = { ...cell.cellOutput.type, args: '' };
return isScriptEqual(script, typeScript);
}
return isScriptEqual(cell.cellOutput.type, typeScript);
});
}

fastify.get(
'/:btc_address/assets',
{
Expand Down Expand Up @@ -125,7 +88,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
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);
const typeScript = getTypeScript(request.query.type_script);
const assetCells = typeScript ? filterCellsByTypeScript(cells, typeScript) : cells;
return assetCells.map((cell) => {
const typeHash = cell.cellOutput.type ? computeScriptHash(cell.cellOutput.type) : undefined;
Expand Down Expand Up @@ -175,7 +138,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
const { btc_address } = request.params;
const { no_cache } = request.query;

const typeScript = getTypeScript(request);
const typeScript = getTypeScript(request.query.type_script);
if (!typeScript || !isTypeAssetSupported(typeScript, env.NETWORK === 'mainnet')) {
throw fastify.httpErrors.badRequest('Unsupported type asset');
}
Expand All @@ -189,6 +152,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType

let cells = await getRgbppAssetsCells(btc_address, utxos, no_cache);
cells = typeScript ? filterCellsByTypeScript(cells, typeScript) : cells;

const availableXudtBalances = await fastify.rgbppCollector.getRgbppBalanceByCells(cells);
Object.keys(availableXudtBalances).forEach((key) => {
const { amount, ...xudtInfo } = availableXudtBalances[key];
Expand Down Expand Up @@ -340,7 +304,7 @@ const addressRoutes: FastifyPluginCallback<Record<never, never>, Server, ZodType
async (request) => {
const { btc_address } = request.params;
const { rgbpp_only, after_btc_txid } = request.query;
const typeScript = getTypeScript(request);
const typeScript = getTypeScript(request.query.type_script);

const btcTxs = await fastify.bitcoin.getAddressTxs({
address: btc_address,
Expand Down
111 changes: 110 additions & 1 deletion src/routes/rgbpp/assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@ import { FastifyPluginCallback } from 'fastify';
import { ZodTypeProvider } from 'fastify-type-provider-zod';
import { Server } from 'http';
import z from 'zod';
import { Cell } from './types';
import { Cell, Script, SporeTypeInfo, XUDTTypeInfo } from './types';
import { UTXO } from '../../services/bitcoin/schema';
import { getTypeScript } from '../../utils/typescript';
import { Env } from '../../env';
import { IndexerCell, isSporeTypeSupported, isUDTTypeSupported } from '@rgbpp-sdk/ckb';
import { computeScriptHash } from '@ckb-lumos/lumos/utils';
import { getSporeConfig, unpackToRawClusterData, unpackToRawSporeData } from '../../utils/spore';
import { SearchKey } from '../../services/rgbpp';

const assetsRoute: FastifyPluginCallback<Record<never, never>, Server, ZodTypeProvider> = (fastify, _, done) => {
const env: Env = fastify.container.resolve('env');

fastify.get(
'/:btc_txid',
{
Expand Down Expand Up @@ -97,6 +104,108 @@ const assetsRoute: FastifyPluginCallback<Record<never, never>, Server, ZodTypePr
},
);

fastify.get(
'/type',
{
schema: {
description: 'Get RGB++ assets type info by typescript',
tags: ['RGB++@Unstable'],
querystring: z.object({
type_script: Script.or(z.string())
.optional()
.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...", ... }))
`,
),
}),
response: {
200: z
.union([
z
.object({
type: z.literal('xudt'),
})
.merge(XUDTTypeInfo),
z
.object({
type: z.literal('spore'),
})
.merge(SporeTypeInfo),
])
.nullable(),
},
},
},
async (request) => {
const isMainnet = env.NETWORK === 'mainnet';
const typeScript = getTypeScript(request.query.type_script);
if (!typeScript) {
return null;
}
if (isUDTTypeSupported(typeScript, isMainnet)) {
const infoCell = await fastify.ckb.getInfoCellData(typeScript);
const typeHash = computeScriptHash(typeScript);
if (!infoCell) {
return null;
}
return {
type: 'xudt' as const,
type_hash: typeHash,
type_script: typeScript,
...infoCell,
};
}
if (isSporeTypeSupported(typeScript, isMainnet)) {
const searchKey: SearchKey = {
script: typeScript,
scriptType: 'type',
withData: true,
};
const result = await fastify.ckb.rpc.getCells(searchKey, 'desc', '0x1');
const [sporeCell] = result.objects;
const sporeData = unpackToRawSporeData(sporeCell.outputData!);
const sporeInfo: SporeTypeInfo = {
contentType: sporeData.contentType,
};
if (sporeData.clusterId) {
const sporeConfig = getSporeConfig(isMainnet);
const batchRequest = fastify.ckb.rpc.createBatchRequest(
sporeConfig.scripts.Cluster.versions.map((version) => {
const clusterScript = {
...version.script,
args: sporeData.clusterId!,
};
const searchKey: SearchKey = {
script: clusterScript,
scriptType: 'type',
withData: true,
};
return ['getCells', searchKey, 'desc', '0x1'];
}),
);
const cells = await batchRequest.exec();
const [cell] = cells.map(({ objects }: { objects: IndexerCell[] }) => objects).flat();
const clusterData = unpackToRawClusterData(cell.outputData!);
sporeInfo.cluster = {
id: sporeData.clusterId,
name: clusterData.name,
description: clusterData.description,
};
}
return {
type: 'spore' as const,
...sporeInfo,
};
}
return null;
},
);

done();
};

Expand Down
28 changes: 23 additions & 5 deletions src/routes/rgbpp/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,34 @@ export const CKBVirtualResult = z.object({
});
export type CKBVirtualResult = z.infer<typeof CKBVirtualResult>;

export const XUDTBalance = z.object({
export const XUDTTypeInfo = z.object({
symbol: z.string(),
name: z.string(),
decimal: z.number(),
symbol: z.string(),
total_amount: z.string(),
available_amount: z.string(),
pending_amount: z.string(),
type_hash: z.string(),
type_script: Script,
});
export type XUDTTypeInfo = z.infer<typeof XUDTTypeInfo>;

export const SporeTypeInfo = z.object({
contentType: z.string(),
cluster: z
.object({
id: z.string(),
name: z.string(),
description: z.string(),
})
.optional(),
});
export type SporeTypeInfo = z.infer<typeof SporeTypeInfo>;

export const XUDTBalance = XUDTTypeInfo.merge(
z.object({
total_amount: z.string(),
available_amount: z.string(),
pending_amount: z.string(),
}),
);
export type XUDTBalance = z.infer<typeof XUDTBalance>;

export const IsomorphicTransaction = z.object({
Expand Down
45 changes: 24 additions & 21 deletions src/services/ckb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,31 +253,33 @@ export default class CKBClient {
});
type getTransactionsResult = ReturnType<typeof this.rpc.getTransactions<false>>;
const infoCellTxs: Awaited<getTransactionsResult>[] = await batchRequest.exec();
const allIndexerTxs = infoCellTxs.reduce(
(acc, txs) => acc.concat(txs.objects.filter(({ ioType }: UngroupedIndexerTransaction) => ioType === 'output')),
[] as UngroupedIndexerTransaction[],
);

// get all transactions that have the xudt type cell and info cell
batchRequest = this.rpc.createBatchRequest();
infoCellTxs.forEach((txs) => {
txs.objects
.filter(({ ioType }: UngroupedIndexerTransaction) => ioType === 'output')
allIndexerTxs
.sort((txA: UngroupedIndexerTransaction, txB: UngroupedIndexerTransaction) => {
// make sure `infoCellTxs` are asc-ordered
.sort((txA: UngroupedIndexerTransaction, txB: UngroupedIndexerTransaction) => {
const aBlockNumber = BI.from(txA.blockNumber).toNumber();
const bBlockNumber = BI.from(txB.blockNumber).toNumber();
if (aBlockNumber < bBlockNumber) return -1;
else if (aBlockNumber > bBlockNumber) return 1;
else if (aBlockNumber === bBlockNumber) {
const aTxIndex = BI.from(txA.txIndex).toNumber();
const bTxIndex = BI.from(txB.txIndex).toNumber();
if (aTxIndex < bTxIndex) return -1;
else if (aTxIndex > bTxIndex) return 1;
}
// unreachable: aBlockNumber === bBlockNumber && aTxIndex === bTxIndex
return 0;
})
.forEach((tx: UngroupedIndexerTransaction) => {
batchRequest.add('getTransaction', tx.txHash);
});
});
// related issue: https://github.com/nervosnetwork/ckb/issues/4549
const aBlockNumber = BI.from(txA.blockNumber).toNumber();
const bBlockNumber = BI.from(txB.blockNumber).toNumber();
if (aBlockNumber < bBlockNumber) return -1;
else if (aBlockNumber > bBlockNumber) return 1;
else if (aBlockNumber === bBlockNumber) {
const aTxIndex = BI.from(txA.txIndex).toNumber();
const bTxIndex = BI.from(txB.txIndex).toNumber();
if (aTxIndex < bTxIndex) return -1;
else if (aTxIndex > bTxIndex) return 1;
}
// unreachable: aBlockNumber === bBlockNumber && aTxIndex === bTxIndex
return 0;
})
.forEach((tx: UngroupedIndexerTransaction) => {
batchRequest.add('getTransaction', tx.txHash);
});
const txs: TransactionWithStatus[] = await batchRequest.exec();
await this.dataCache.set('all', txs);
return txs;
Expand Down Expand Up @@ -315,6 +317,7 @@ export default class CKBClient {
if (inscriptionCellIndex !== -1) {
const infoCellData = this.getInscriptionInfoCellData(tx, inscriptionCellIndex, script);
if (infoCellData) {
// TODO: `type:${typeHash}` could be cached for a longer time
await this.dataCache.set(`type:${typeHash}`, infoCellData);
return infoCellData;
}
Expand Down
4 changes: 2 additions & 2 deletions src/services/rgbpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ import { TestnetTypeMap } from '../constants';
import { TransactionWithStatus } from '@ckb-lumos/base';

type GetCellsParams = Parameters<RPC['getCells']>;
type SearchKey = GetCellsParams[0];
type CKBBatchRequest = { exec: () => Promise<{ objects: IndexerCell[] }[]> };
export type SearchKey = GetCellsParams[0];
export type CKBBatchRequest = { exec: () => Promise<{ objects: IndexerCell[] }[]> };

export type RgbppUtxoCellsPair = {
utxo: UTXO;
Expand Down
8 changes: 8 additions & 0 deletions src/utils/spore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { unpackToRawSporeData, unpackToRawClusterData, predefinedSporeConfigs } from '@spore-sdk/core';

export { unpackToRawSporeData, unpackToRawClusterData };

export function getSporeConfig(isMainnet: boolean) {
const config = predefinedSporeConfigs[isMainnet ? 'Mainnet' : 'Testnet'];
return config;
}
Loading

0 comments on commit 7a6fc03

Please sign in to comment.