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(rgbpp): support for batch transferring of RGBPP XUDT assets #270

Merged
merged 17 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3c0eb05
feat: support for batch transferring of RGBPP XUDT assets
ShookLyngs Aug 6, 2024
a31a376
chore: add changeset for 3c0eb05d
ShookLyngs Aug 6, 2024
7f84d39
test: update rgbpp tests for the btc-transfer-all feature
ShookLyngs Aug 7, 2024
1357c68
test: add a default value for IS_MAINNET in the rgbpp test env
ShookLyngs Aug 7, 2024
f7f1af4
chore: add ckb node/indexer url as env variables to the test workflow
ShookLyngs Aug 7, 2024
6441f92
docs: add a description of the buildRgbppTransferTx() API to the READ…
ShookLyngs Aug 7, 2024
2852645
fix: wrong ckb node/indexer url specified in the test workflow
ShookLyngs Aug 9, 2024
c492cf3
refactor: move btc/ckb utils from the rgbpp lib to sub-libs
ShookLyngs Aug 9, 2024
0f8f523
refactor: remove saveJson() util in the rgbpp tests
ShookLyngs Aug 9, 2024
37074c4
refactor: remove "sent" and "retry" in types the sendRgbppTxGroups() …
ShookLyngs Aug 10, 2024
699be77
chore: update changeset for c492cf3f
ShookLyngs Aug 10, 2024
b1865c2
refactor: remove deprecated error in rgbpp lib
ShookLyngs Aug 11, 2024
736d088
refactor: add "fromPubkey" to RgbppTransferBtcParams in the rgbpp lib
ShookLyngs Aug 11, 2024
75840f8
fix: missing check statement in decodeUtxoId() method
ShookLyngs Aug 11, 2024
1440fb8
refactor: rename some props with some fixes in the AssetSummarizer
ShookLyngs Aug 11, 2024
c7f37a3
docs: update rgbpp README with improved type descriptions
ShookLyngs Aug 11, 2024
cd64417
fix: if the target utxo is bound to any unsupported-type cells, mark …
ShookLyngs Aug 12, 2024
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
8 changes: 8 additions & 0 deletions .changeset/twenty-jeans-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'rgbpp': minor
---

Support for batch transferring of RGBPP XUDT assets

- Add `buildRgbppTransferAllTxs()` API to generate one or more BTC/CKB transaction groups for transferring the entire amount of a specific type of RGBPP XUDT asset from one or more BTC addresses to a recipient
- Add `sendRgbppTxGroups()` API for sending BTC/CKB transaction groups to the `BtcAssetsApi`
2 changes: 2 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ jobs:
- name: Run tests for packages
run: pnpm run test:packages
env:
VITE_CKB_NODE_URL: https://mainnet.ckb.dev/rpc
VITE_CKB_INDEXER_URL: https://mainnet.ckb.dev/indexer
Flouse marked this conversation as resolved.
Show resolved Hide resolved
VITE_BTC_SERVICE_URL: https://btc-assets-api.testnet.mibao.pro
VITE_BTC_SERVICE_TOKEN: ${{ secrets.TESTNET_SERVICE_TOKEN }}
VITE_BTC_SERVICE_ORIGIN: https://btc-assets-api.testnet.mibao.pro
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"tsx": "4.16.3",
"tsup": "^8.1.0",
"typescript": "^5.4.3",
"vitest": "1.6.0"
"vitest": "2.0.5"
},
"lint-staged": {
"{packages,apps,examples,tests}/**/*.{js,jsx,ts,tsx}": [
Expand Down
11 changes: 11 additions & 0 deletions packages/rgbpp/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Network
VITE_IS_MAINNET=false

# CKB
VITE_CKB_NODE_URL=https://testnet.ckb.dev/rpc
VITE_CKB_INDEXER_URL=https://testnet.ckb.dev/indexer

# BTC
VITE_BTC_SERVICE_URL=https://btc-assets-api.testnet.mibao.pro
VITE_BTC_SERVICE_TOKEN=
VITE_BTC_SERVICE_ORIGIN=
12 changes: 10 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,19 @@
"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": {
"@types/node": "^20.3.1",
"lodash": "^4.17.21",
"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;
Flouse marked this conversation as resolved.
Show resolved Hide resolved
cells: number;
}

export interface AssetGroupSummary {
utxoId: string;
cellIds: string[];
assets: Record<string, AssetSummary>;
}
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved

export interface TransactionGroupSummary {
utxos: number;
cells: number;
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
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'>> = {};
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
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 {
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
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;
Flouse marked this conversation as resolved.
Show resolved Hide resolved
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