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 12 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
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 @@ -16,13 +16,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 @@ -45,13 +45,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
40 changes: 40 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,44 @@ 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 > 0xffffffff
ShookLyngs marked this conversation as resolved.
Show resolved Hide resolved
) {
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