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/v1/assets/type for get assets type info #183

Merged
merged 8 commits into from
Aug 8, 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
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
Loading