Skip to content

Commit

Permalink
Merge pull request #270 from ckb-cell/feat/l1-transfer-all
Browse files Browse the repository at this point in the history
feat(rgbpp): support for batch transferring of RGBPP XUDT assets
  • Loading branch information
Flouse authored Aug 12, 2024
2 parents e24c526 + cd64417 commit 215dd98
Show file tree
Hide file tree
Showing 33 changed files with 12,354 additions and 333 deletions.
13 changes: 13 additions & 0 deletions .changeset/twenty-jeans-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@rgbpp-sdk/btc": minor
"@rgbpp-sdk/ckb": minor
"rgbpp": minor
---

Support for batch transferring of RGBPP XUDT assets

- Add `buildRgbppTransferAllTxs()` API in the rgbpp lib for generating 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 in the rgbpp lib for sending BTC/CKB transaction groups to the `BtcAssetsApi`
- Add `unpackRgbppLockArgs()` API in the ckb lib for unpacking the lock script args of an RGBPP Cell
- Add `encodeCellId()` and `decodeCellId()` APIs in the ckb lib for handling the ID of a CKB Cell
- Add `encodeUtxoId()` and `decodeUtxoId()` APIs in the btc lib for handling the ID of a BTC UTXO
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://testnet.ckb.dev/rpc
VITE_CKB_INDEXER_URL: https://testnet.ckb.dev/indexer
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
15 changes: 10 additions & 5 deletions packages/btc/src/api/sendRgbppUtxos.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { Collector, checkCkbTxInputsCapacitySufficient } from '@rgbpp-sdk/ckb';
import { isRgbppLockCell, isBtcTimeLockCell, calculateCommitment } from '@rgbpp-sdk/ckb';
import {
Collector,
isRgbppLockCell,
isBtcTimeLockCell,
calculateCommitment,
unpackRgbppLockArgs,
checkCkbTxInputsCapacitySufficient,
} from '@rgbpp-sdk/ckb';
import { bitcoin } from '../bitcoin';
import { BaseOutput, Utxo } from '../transaction/utxo';
import { AddressToPubkeyMap } from '../address';
Expand All @@ -8,7 +14,6 @@ import { NetworkType } from '../preset/types';
import { ErrorCodes, TxBuildError } from '../error';
import { InitOutput, TxAddressOutput, TxBuilder } from '../transaction/build';
import { networkTypeToConfig } from '../preset/config';
import { unpackRgbppLockArgs } from '../ckb/molecule';
import { createSendUtxosBuilder } from './sendUtxos';
import { limitPromiseBatchSize } from '../utils';

Expand Down Expand Up @@ -65,7 +70,7 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P
rgbppLockArgsList.map((rgbppLockArgs) => {
if (rgbppLockArgs) {
return limitPromiseBatchSize(() =>
props.source.getUtxo(rgbppLockArgs.btcTxid, rgbppLockArgs.outIndex, props.onlyConfirmedUtxos),
props.source.getUtxo(rgbppLockArgs.btcTxId, rgbppLockArgs.outIndex, props.onlyConfirmedUtxos),
);
}
return undefined;
Expand All @@ -85,7 +90,7 @@ export async function createSendRgbppUtxosBuilder(props: SendRgbppUtxosProps): P
if (!utxo) {
throw TxBuildError.withComment(
ErrorCodes.CANNOT_FIND_UTXO,
`hash: ${rgbppLockArgs.btcTxid}, index: ${rgbppLockArgs.outIndex}`,
`hash: ${rgbppLockArgs.btcTxId}, index: ${rgbppLockArgs.outIndex}`,
);
}

Expand Down
21 changes: 0 additions & 21 deletions packages/btc/src/ckb/molecule.ts

This file was deleted.

4 changes: 2 additions & 2 deletions packages/btc/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ export enum ErrorCodes {
UNSUPPORTED_OP_RETURN_SCRIPT,
INVALID_FEE_RATE,
PAYMASTER_MISMATCH,
INVALID_UTXO_ID,

CKB_CANNOT_FIND_OUTPOINT = 40,
CKB_INVALID_CELL_LOCK,
CKB_INVALID_INPUTS,
CKB_INVALID_OUTPUTS,
CKB_UNMATCHED_COMMITMENT,
CKB_RGBPP_LOCK_UNPACK_ERROR,

MEMPOOL_API_RESPONSE_ERROR = 60,
}
Expand All @@ -48,13 +48,13 @@ export const ErrorMessages = {
[ErrorCodes.UNSUPPORTED_OP_RETURN_SCRIPT]: 'Unsupported OP_RETURN script format',
[ErrorCodes.INVALID_FEE_RATE]: 'Invalid fee rate provided or recommended',
[ErrorCodes.PAYMASTER_MISMATCH]: 'Paymaster mismatched',
[ErrorCodes.INVALID_UTXO_ID]: 'Invalid UtxoId',

[ErrorCodes.CKB_CANNOT_FIND_OUTPOINT]: 'Cannot find CKB cell by OutPoint, it may not exist or is not live',
[ErrorCodes.CKB_INVALID_CELL_LOCK]: 'Invalid CKB cell lock, it should be RgbppLock, RgbppTimeLock or null',
[ErrorCodes.CKB_INVALID_INPUTS]: 'Invalid input(s) found in the CKB VirtualTx',
[ErrorCodes.CKB_INVALID_OUTPUTS]: 'Invalid output(s) found in the CKB VirtualTx',
[ErrorCodes.CKB_UNMATCHED_COMMITMENT]: 'Invalid commitment found in the CKB VirtualTx',
[ErrorCodes.CKB_RGBPP_LOCK_UNPACK_ERROR]: 'Failed to unpack RgbppLockArgs from the CKB cell lock',

[ErrorCodes.MEMPOOL_API_RESPONSE_ERROR]: 'Mempool.space API returned an error',
};
Expand Down
41 changes: 41 additions & 0 deletions packages/btc/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import limitPromiseConcurrency from 'p-limit';
import { bitcoin, ecc, ECPair } from './bitcoin';
import { bytes } from '@ckb-lumos/codec';
import { BaseOutput } from './transaction/utxo';
import { ErrorCodes, TxBuildError } from './error';

interface TweakableSigner extends bitcoin.Signer {
privateKey?: Buffer;
Expand Down Expand Up @@ -77,6 +79,45 @@ export function transactionToHex(tx: bitcoin.Transaction, withWitness?: boolean)
return buffer.toString('hex');
}

/**
* Encode a UTXO's txid and vout to a string ID of "{txid}:{vout}".
*/
export function encodeUtxoId(txid: string, vout: number): string {
if (!txid || remove0x(txid).length !== 64) {
throw TxBuildError.withComment(ErrorCodes.INVALID_UTXO_ID, `txid=${txid}`);
}
if (vout < 0 || vout > 0xffffffff) {
throw TxBuildError.withComment(ErrorCodes.INVALID_UTXO_ID, `vout=${vout}`);
}

return `${remove0x(txid)}:${vout}`;
}

/**
* Decode a string ID of "{txid}:{vout}" format to a BaseOutput object.
*/
export function decodeUtxoId(utxoId: string): BaseOutput {
const parts = utxoId.split(':');
const txid = parts[0];
const vout = parts[1] ? parseInt(parts[1]) : undefined;
if (
!txid ||
txid.startsWith('0x') ||
txid.length !== 64 ||
typeof vout !== 'number' ||
isNaN(vout) ||
vout < 0 ||
vout > 0xffffffff
) {
throw TxBuildError.withComment(ErrorCodes.INVALID_UTXO_ID, utxoId);
}

return {
txid,
vout,
};
}

/**
* Limits the batch size of promises when querying with Promise.all().
* @example
Expand Down
34 changes: 32 additions & 2 deletions packages/btc/tests/Utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import { bitcoin, transactionToHex } from '../src';
import { bitcoin, decodeUtxoId, encodeUtxoId, transactionToHex } from '../src';

describe('Utils', () => {
it('Convert transaction to hex', () => {
it('transactionToHex()', () => {
const originalHex =
'02000000000101177e673414fb4a393f0e1faf27a317d92e9f1a7b9a3ff36713d46ef5b7a1a6190100000000ffffffff020000000000000000226a20849f5b17209de17af5a94f0111e2ba03d1409da87a0f06894abb85b3b5024726df3c0f000000000016001462fc12a35b779f0cf7edcb9690be19b0386e0f9a024830450221009d869f20ef22864e02603571ce40da0586c03f20f5b8fb6295a4d636141d39dc02207082fdef40b34f6189491cba98c861ddfc8889d91c48f11f4660f11e93b1153b012103e1c38cf06691d449961d2b8f261a9a238c53da91d3a1e948497f7b1fe717968000000000';
const tx = bitcoin.Transaction.fromHex(originalHex);
Expand All @@ -19,4 +19,34 @@ describe('Utils', () => {
'0200000001177e673414fb4a393f0e1faf27a317d92e9f1a7b9a3ff36713d46ef5b7a1a6190100000000ffffffff020000000000000000226a20849f5b17209de17af5a94f0111e2ba03d1409da87a0f06894abb85b3b5024726df3c0f000000000016001462fc12a35b779f0cf7edcb9690be19b0386e0f9a00000000',
);
});
it('encodeUtxoId()', () => {
expect(encodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0)).toEqual(
'0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0',
);
expect(encodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0)).toEqual(
'0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0',
);
expect(encodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0xffffffff)).toEqual(
'0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:4294967295',
);
expect(() => encodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b22', 0)).toThrowError();
expect(() =>
encodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222', 0xffffffff01),
).toThrowError();
});
it('decodeUtxoId()', () => {
expect(decodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0')).toStrictEqual({
txid: '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222',
vout: 0,
});
expect(decodeUtxoId('0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:4294967295')).toStrictEqual({
txid: '0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222',
vout: 4294967295,
});

expect(() => decodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:0')).toThrowError();
expect(() =>
decodeUtxoId('0x0da44932270292fd3a4165f1f7ab81abf69b951e1e7d3b5c012e00a291c6b222:42949672951'),
).toThrowError();
});
});
8 changes: 8 additions & 0 deletions packages/ckb/src/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ enum ErrorCode {
RgbppCkbTxInputsExceeded = 109,
RgbppUtxoBindMultiTypeAssets = 110,
RgbppSporeTypeMismatch = 111,
InvalidCellId = 112,
}

export class CapacityNotEnoughError extends Error {
Expand Down Expand Up @@ -96,3 +97,10 @@ export class RgbppSporeTypeMismatchError extends Error {
super(message);
}
}

export class InvalidCellIdError extends Error {
code = ErrorCode.InvalidCellId;
constructor(message: string) {
super(message);
}
}
45 changes: 45 additions & 0 deletions packages/ckb/src/utils/id.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import { encodeCellId, decodeCellId } from './id';

describe('cell id', () => {
it('encodeCellId', () => {
expect(encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0x0')).toBe(
'0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0x0',
);
expect(encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0xffffffff')).toBe(
'0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0xffffffff',
);

expect(() =>
encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e6', '0x0'),
).toThrowError();
expect(() =>
encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0xffffffff01'),
).toThrowError();
expect(() =>
encodeCellId('7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0x0'),
).toThrowError();
expect(() =>
encodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65', '0'),
).toThrowError();
});
it('decodeCellId', () => {
expect(decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0x0')).toStrictEqual({
txHash: '0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65',
index: '0x0',
});
expect(decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0xffffffff')).toStrictEqual(
{
txHash: '0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65',
index: '0xffffffff',
},
);

expect(() => decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e6:0x0')).toThrowError();
expect(() =>
decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0xffffffff01'),
).toThrowError();
expect(() => decodeCellId('7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0x0')).toThrowError();
expect(() => decodeCellId('0x7610efaec3b9ce66349909fea88a1ae78cd488de3128bc6f71afc068306e0e65:0')).toThrowError();
});
});
37 changes: 37 additions & 0 deletions packages/ckb/src/utils/id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Hash, HexNumber, OutPoint, blockchain } from '@ckb-lumos/base';
import { InvalidCellIdError } from '../error';
import { append0x } from './hex';

export const encodeCellId = (txHash: Hash, index: HexNumber): string => {
if (!txHash.startsWith('0x') || !index.startsWith('0x')) {
throw new InvalidCellIdError(`Cannot encode CellId due to valid format: txHash=${txHash}, index=${index}`);
}
try {
blockchain.OutPoint.pack({
txHash,
index,
});
return `${txHash}:${index}`;
} catch {
throw new InvalidCellIdError(`Cannot encode CellId due to valid format: txHash=${txHash}, index=${index}`);
}
};

export const decodeCellId = (cellId: string): OutPoint => {
const [txHash, index] = cellId.split(':');
if (!txHash.startsWith('0x') || !index.startsWith('0x')) {
throw new InvalidCellIdError(`Cannot decode CellId: ${cellId}`);
}
try {
blockchain.OutPoint.pack({
txHash,
index,
});
return {
txHash: append0x(txHash),
index: append0x(index),
};
} catch {
throw new InvalidCellIdError(`Cannot decode CellId due to valid format: ${cellId}`);
}
};
1 change: 1 addition & 0 deletions packages/ckb/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './ckb-tx';
export * from './rgbpp';
export * from './spore';
export * from './cell-dep';
export * from './id';
7 changes: 7 additions & 0 deletions packages/ckb/src/utils/rgbpp.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
throwErrorWhenTxInputsExceeded,
throwErrorWhenRgbppCellsInvalid,
isRgbppCapacitySufficientForChange,
unpackRgbppLockArgs,
} from './rgbpp';
import { getXudtTypeScript } from '../constants';
import { IndexerCell, RgbppCkbVirtualTx } from '../types';
Expand Down Expand Up @@ -221,6 +222,12 @@ describe('rgbpp tests', () => {
expect('0x020000000000000000000000000000000000000000000000000000000000000000000000').toBe(buildPreLockArgs(2));
});

it('unpackRgbppLockArgs', () => {
const unpacked = unpackRgbppLockArgs('0x0200000006ec22c2def100bba3e295a1ff279c490d227151bf3166a4f3f008906c849399');
expect('0x9993846c9008f0f3a46631bf5171220d499c27ffa195e2a3bb00f1dec222ec06').toBe(unpacked.btcTxId);
expect(2).toBe(unpacked.outIndex);
});

it('replaceRealBtcTxId', () => {
const rgbppLockArgs = '0x020000000000000000000000000000000000000000000000000000000000000000000000';
const realBtcTxId = '0x9993846c9008f0f3a46631bf5171220d499c27ffa195e2a3bb00f1dec222ec06';
Expand Down
15 changes: 14 additions & 1 deletion packages/ckb/src/utils/rgbpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import {
getBtcTimeLockScript,
getRgbppLockScript,
} from '../constants';
import { RGBPPLock } from '../schemas/generated/rgbpp';
import { BTCTimeLock } from '../schemas/generated/rgbpp';
import { Script } from '../schemas/generated/blockchain';
import { bytes } from '@ckb-lumos/codec';
import { bytes, BytesLike } from '@ckb-lumos/codec';
import { toCamelcase } from './case-parser';
import {
InputsOrOutputsLenError,
Expand Down Expand Up @@ -142,6 +143,18 @@ export const buildPreLockArgs = (outIndex: number) => {
return buildRgbppLockArgs(outIndex, RGBPP_TX_ID_PLACEHOLDER);
};

export interface RgbppLockArgs {
btcTxId: Hex;
outIndex: number;
}
export const unpackRgbppLockArgs = (source: BytesLike): RgbppLockArgs => {
const unpacked = RGBPPLock.unpack(source);
return {
btcTxId: reverseHex(unpacked.btcTxid),
outIndex: unpacked.outIndex,
};
};

export const compareInputs = (a: IndexerCell, b: IndexerCell) => {
if (a.output.lock.args < b.output.lock.args) {
return -1;
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=
Loading

1 comment on commit 215dd98

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New snapshot version of the rgbpp-sdk packages have been released:

Name Version
@rgbpp-sdk/btc 0.0.0-snap-20240812163646
@rgbpp-sdk/ckb 0.0.0-snap-20240812163646
rgbpp 0.0.0-snap-20240812163646
@rgbpp-sdk/service 0.0.0-snap-20240812163646

Please sign in to comment.