Skip to content

Commit

Permalink
feat: support for batch transferring of RGBPP XUDT assets
Browse files Browse the repository at this point in the history
  • Loading branch information
ShookLyngs committed Aug 6, 2024
1 parent 25c8459 commit 3c0eb05
Show file tree
Hide file tree
Showing 17 changed files with 1,094 additions and 154 deletions.
10 changes: 8 additions & 2 deletions packages/rgbpp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"name": "rgbpp",
"version": "0.5.0",
"scripts": {
"test": "vitest",
"build": "tsup",
"lint": "tsc && eslint --ext .ts src/* && prettier --check 'src/*.ts'",
"lint:fix": "tsc && eslint --fix --ext .ts src/* && prettier --write 'src/*.ts'"
Expand Down Expand Up @@ -58,12 +59,17 @@
"dist"
],
"dependencies": {
"@ckb-lumos/base": "^0.22.2",
"@ckb-lumos/codec": "^0.22.2",
"@nervosnetwork/ckb-sdk-utils": "0.109.2",
"@rgbpp-sdk/btc": "workspace:*",
"@rgbpp-sdk/ckb": "workspace:*",
"@rgbpp-sdk/service": "workspace:*",
"@nervosnetwork/ckb-sdk-utils": "0.109.2"
"@rgbpp-sdk/service": "workspace:*"
},
"publishConfig": {
"access": "public"
},
"devDependencies": {
"zod": "^3.23.8"
}
}
12 changes: 10 additions & 2 deletions packages/rgbpp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,13 @@ export type { SendBtcProps, SendUtxosProps, SendRgbppUtxosProps } from '@rgbpp-s
/**
* RGB++
*/
export { buildRgbppTransferTx } from './rgbpp/xudt';
export type { RgbppTransferTxParams, RgbppTransferTxResult } from './rgbpp/types';
export type {
RgbppTransferTxParams,
RgbppTransferTxResult,
RgbppTransferAllTxsParams,
RgbppTransferAllTxsResult,
} from './rgbpp/types/xudt';
export { RgbppError, RgbppErrorCodes } from './rgbpp/error';
export { buildRgbppTransferTx } from './rgbpp/xudt/btc-transfer';
export { buildRgbppTransferAllTxs } from './rgbpp/xudt/btc-transfer-all';
export { sendRgbppTxGroups } from './rgbpp/utils/transaction';
27 changes: 27 additions & 0 deletions packages/rgbpp/src/rgbpp/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export enum RgbppErrorCodes {
UNKNOWN,

CANNOT_DECODE_UTXO_ID = 20,
UNEXPECTED_CKB_VTX_OUTPUTS_LENGTH,
}

export const RgbppErrorMessages = {
[RgbppErrorCodes.UNKNOWN]: 'Unknown error',

[RgbppErrorCodes.CANNOT_DECODE_UTXO_ID]: 'Cannot decode UtxoId',
[RgbppErrorCodes.UNEXPECTED_CKB_VTX_OUTPUTS_LENGTH]: 'Unexpected length of the CkbVirtualTx outputs',
};

export class RgbppError extends Error {
public code = RgbppErrorCodes.UNKNOWN;
constructor(code: RgbppErrorCodes, message = RgbppErrorMessages[code] || 'Unknown error') {
super(message);
this.code = code;
Object.setPrototypeOf(this, RgbppError.prototype);
}

static withComment(code: RgbppErrorCodes, comment?: string): RgbppError {
const message: string | undefined = RgbppErrorMessages[code];
return new RgbppError(code, comment ? `${message}: ${comment}` : message);
}
}
100 changes: 100 additions & 0 deletions packages/rgbpp/src/rgbpp/summary/asset-summarizer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Cell } from '@ckb-lumos/base';
import { Utxo } from '@rgbpp-sdk/btc';
import { leToU128 } from '@rgbpp-sdk/ckb';
import { encodeCellId } from '../utils/ckb';
import { encodeUtxoId } from '../utils/btc';

export interface AssetSummary {
amount: bigint;
utxos: number;
cells: number;
}

export interface AssetGroupSummary {
utxoId: string;
cellIds: string[];
assets: Record<string, AssetSummary>;
}

export interface TransactionGroupSummary {
utxos: number;
cells: number;
utxoIds: string[];
cellIds: string[];
assets: Record<string, AssetSummary>;
}

export class AssetSummarizer {
groups: AssetGroupSummary[] = [];

constructor() {}

addGroup(utxo: Utxo, cells: Cell[]): AssetGroupSummary {
const utxoId = encodeUtxoId(utxo.txid, utxo.vout);
const assets: Record<string, Omit<AssetSummary, 'xudtTypeArgs'>> = {};
const cellIds: string[] = [];

for (const cell of cells) {
cellIds.push(encodeCellId(cell.outPoint!.txHash, cell.outPoint!.index));
const xudtTypeArgs = cell.cellOutput.type?.args ?? 'empty';
const amount = leToU128(cell.data.substring(0, 34));
if (assets[xudtTypeArgs] === undefined) {
assets[xudtTypeArgs] = {
utxos: 1,
cells: 0,
amount: 0n,
};
}

assets[xudtTypeArgs]!.cells += 1;
assets[xudtTypeArgs]!.amount += amount;
}

const result: AssetGroupSummary = {
utxoId,
cellIds,
assets,
};

this.groups.push(result);
return result;
}

addGroups(groups: { utxo: Utxo; cells: Cell[] }[]): TransactionGroupSummary {
const groupResults = groups.map((group) => this.addGroup(group.utxo, group.cells));
return this.summarizeGroups(groupResults);
}

summarizeGroups(groups?: AssetGroupSummary[]): TransactionGroupSummary {
const targetGroups = groups ?? this.groups;
const utxoIds = targetGroups.map((summary) => summary.utxoId);
const cellIds = targetGroups.flatMap((summary) => summary.cellIds);
const assets = targetGroups.reduce(
(result, summary) => {
for (const xudtTypeArgs in summary.assets) {
if (result[xudtTypeArgs] === undefined) {
result[xudtTypeArgs] = {
utxos: 0,
cells: 0,
amount: 0n,
};
}

result[xudtTypeArgs]!.utxos += summary.assets[xudtTypeArgs]!.utxos;
result[xudtTypeArgs]!.cells += summary.assets[xudtTypeArgs]!.cells;
result[xudtTypeArgs]!.amount += summary.assets[xudtTypeArgs]!.amount;
}
return result;
},
{} as Record<string, Omit<AssetSummary, 'xudtTypeArgs'>>,
);

return {
utxos: utxoIds.length,
cells: cellIds.length,
utxoIds,
cellIds,
assets,
};
}
}
42 changes: 0 additions & 42 deletions packages/rgbpp/src/rgbpp/types.ts

This file was deleted.

96 changes: 96 additions & 0 deletions packages/rgbpp/src/rgbpp/types/xudt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { BaseCkbVirtualTxResult, BTCTestnetType, BtcTransferVirtualTxResult, Collector, Hex } from '@rgbpp-sdk/ckb';
import { AddressToPubkeyMap, DataSource } from '@rgbpp-sdk/btc';
import { TransactionGroupSummary } from '../summary/asset-summarizer';

export interface RgbppTransferCkbParams {
// The collector that collects CKB live cells and transactions
collector: Collector;
// The transferred RGB++ xUDT type script args
xudtTypeArgs: Hex;
// The rgbpp assets cell lock script args array whose data structure is: out_index | bitcoin_tx_id
rgbppLockArgsList: Hex[];
// The XUDT amount to be transferred, if the noMergeOutputCells is true, the transferAmount will be ignored
transferAmount: bigint;
// The CKB transaction fee rate, default value is 1100
feeRate?: bigint;
}

export interface RgbppTransferBtcParams {
// The sender BTC address
fromAddress: string;
// The receiver BTC address
toAddress: string;
dataSource: DataSource;
// The public key of sender BTC address
fromPubkey?: Hex;
// The fee rate of the BTC transaction
feeRate?: number;
// The Bitcoin Testnet type including Testnet3 and Signet, default value is Testnet3
testnetType?: BTCTestnetType;
}

export interface RgbppTransferTxParams {
ckb: RgbppTransferCkbParams;
btc: RgbppTransferBtcParams;
// True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet
isMainnet: boolean;
}

export interface RgbppTransferTxResult {
ckbVirtualTxResult: BtcTransferVirtualTxResult;
// The BTC PSBT hex string which can be used to construct Bitcoin PSBT
btcPsbtHex: Hex;
}

export interface RgbppTransferAllTxsParams {
ckb: {
// The collector that collects CKB live cells and transactions
collector: Collector;
// The transferred RGB++ xUDT type script args
xudtTypeArgs: Hex;
// The CKB transaction fee rate, default value is 1100
feeRate?: bigint;
};
btc: {
// The BTC addresses to transfer all the RGB++ assets from
assetAddresses: string[];
// The BTC address for paying all the transaction fees
fromAddress: string;
// The BTC address for receiving all the RGB++ assets
toAddress: string;
// The data source for collecting Bitcoin-related info
dataSource: DataSource;
// The map helps find the corresponding public key of a BTC address,
// note that you must specify a pubkey for each P2TR address in assetAddresses/fromAddress
pubkeyMap?: AddressToPubkeyMap;
// The BTC address to return change satoshi, default value is fromAddress
changeAddress?: string;
// The fee rate of the BTC transactions
feeRate?: number;
// The BTC Testnet to use, supports "Testnet3" and "Signet", default value is "Testnet3",
// the param helps find the targeting version of rgbpp-lock script on CKB Testnet
testnetType?: BTCTestnetType;
};
// True is for BTC and CKB Mainnet, false is for BTC Testnet3/Signet and CKB Testnet
isMainnet: boolean;
}

export interface RgbppTransferAllTxsResult {
transactions: RgbppTransferAllTxGroup[];
summary: {
included: TransactionGroupSummary;
excluded: TransactionGroupSummary;
};
}

export interface RgbppTransferAllTxGroup {
ckb: {
virtualTxResult: BaseCkbVirtualTxResult;
};
btc: {
psbtHex: string;
feeRate: number;
fee: number;
};
summary: TransactionGroupSummary;
}
18 changes: 18 additions & 0 deletions packages/rgbpp/src/rgbpp/utils/btc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BaseOutput, remove0x } from '@rgbpp-sdk/btc';
import { RgbppError, RgbppErrorCodes } from '../error';

export function encodeUtxoId(txid: string, vout: number): string {
return `${remove0x(txid)}:${vout}`;
}

export function decodeUtxoId(utxoId: string): BaseOutput {
const [txid, vout] = utxoId.split(':');
if (!txid || txid.length !== 64 || !vout || isNaN(parseInt(vout))) {
throw RgbppError.withComment(RgbppErrorCodes.CANNOT_DECODE_UTXO_ID, utxoId);
}

return {
txid,
vout: parseInt(vout),
};
}
25 changes: 25 additions & 0 deletions packages/rgbpp/src/rgbpp/utils/ckb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { bytes, BytesLike, UnpackResult } from '@ckb-lumos/codec';
import { getXudtTypeScript, RGBPPLock } from '@rgbpp-sdk/ckb';
import { blockchain } from '@ckb-lumos/base';

export function unpackRgbppLockArgs(source: BytesLike): UnpackResult<typeof RGBPPLock> {
const unpacked = RGBPPLock.unpack(source);
const reversedTxId = bytes.bytify(unpacked.btcTxid).reverse();
return {
btcTxid: bytes.hexify(reversedTxId),
outIndex: unpacked.outIndex,
};
}

export function buildXudtTypeScriptHex(xudtTypeArgs: string, isMainnet: boolean): string {
return bytes.hexify(
blockchain.Script.pack({
...getXudtTypeScript(isMainnet),
args: xudtTypeArgs,
}),
);
}

export function encodeCellId(txHash: string, index: string): string {
return `${txHash}:${index}`;
}
Loading

0 comments on commit 3c0eb05

Please sign in to comment.