Skip to content

Commit

Permalink
Merge pull request #13 from ckb-cell/l1-transfer-virtual
Browse files Browse the repository at this point in the history
feat(rgbpp-ckb): build btc transfer virtual ckb tx
  • Loading branch information
duanyytop authored Mar 9, 2024
2 parents 3bc092e + dabbb8a commit bcfc3fa
Show file tree
Hide file tree
Showing 22 changed files with 2,147 additions and 164 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ jobs:
- name: Install dependencies
run: pnpm i

- name: Run tests for packages
run: pnpm run test:packages
env:
Expand Down
6 changes: 4 additions & 2 deletions packages/ckb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
"lib"
],
"dependencies": {
"@ckb-lumos/base": "0.21.1",
"@ckb-lumos/codec": "0.0.0-canary-3f37c5b-20240228111419",
"@nervosnetwork/ckb-sdk-core": "^0.109.0",
"@nervosnetwork/ckb-sdk-utils": "^0.109.0",
"@nervosnetwork/ckb-types": "^0.109.0",
"camelcase-keys": "^7.0.2",
"axios": "^1.6.7",
"bignumber.js": "^9.1.1",
"axios": "^1.6.7"
"camelcase-keys": "^7.0.2",
"js-sha256": "^0.11.0"
},
"devDependencies": {
"@ckb-lumos/molecule": "0.0.0-canary-3f37c5b-20240228111419",
Expand Down
45 changes: 38 additions & 7 deletions packages/ckb/src/collector/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import axios from 'axios';
import CKB from '@nervosnetwork/ckb-sdk-core';
import { toCamelcase } from '../utils/case-parser';
import { CollectResult, IndexerCell } from '../types/collector';
import { CollectResult, CollectUdtResult, IndexerCell } from '../types/collector';
import { MIN_CAPACITY } from '../constants';
import { CapacityNotEnoughError, IndexerError } from '../error';
import { CapacityNotEnoughError, IndexerError, UdtAmountNotEnoughError } from '../error';
import { leToU128 } from '../utils';

const parseScript = (script: CKBComponents.Script) => ({
code_hash: script.codeHash,
Expand Down Expand Up @@ -91,7 +92,7 @@ export class Collector {
): CollectResult {
const changeCapacity = minCapacity ?? MIN_CAPACITY;
let inputs: CKBComponents.CellInput[] = [];
let sum = BigInt(0);
let sumInputsCapacity = BigInt(0);
for (let cell of liveCells) {
inputs.push({
previousOutput: {
Expand All @@ -100,15 +101,45 @@ export class Collector {
},
since: '0x0',
});
sum = sum + BigInt(cell.output.capacity);
if (sum >= needCapacity + changeCapacity + fee) {
sumInputsCapacity += BigInt(cell.output.capacity);
if (sumInputsCapacity >= needCapacity + changeCapacity + fee) {
break;
}
}
if (sum < needCapacity + changeCapacity + fee) {
if (sumInputsCapacity < needCapacity + changeCapacity + fee) {
const message = errMsg ?? 'Insufficient free CKB balance';
throw new CapacityNotEnoughError(message);
}
return { inputs, capacity: sum };
return { inputs, sumInputsCapacity };
}

collectUdtInputs(liveCells: IndexerCell[], needAmount: bigint): CollectUdtResult {
let inputs: CKBComponents.CellInput[] = [];
let sumInputsCapacity = BigInt(0);
let sumAmount = BigInt(0);
for (let cell of liveCells) {
inputs.push({
previousOutput: {
txHash: cell.outPoint.txHash,
index: cell.outPoint.index,
},
since: '0x0',
});
sumInputsCapacity = sumInputsCapacity + BigInt(cell.output.capacity);
sumAmount += leToU128(cell.outputData);
if (sumAmount >= needAmount) {
break;
}
}
if (sumAmount < needAmount) {
throw new UdtAmountNotEnoughError('Insufficient UDT balance');
}
return { inputs, sumInputsCapacity, sumAmount };
}

async getLiveCell(outPoint: CKBComponents.OutPoint): Promise<CKBComponents.LiveCell> {
const ckb = new CKB(this.ckbNodeUrl);
const { cell } = await ckb.rpc.getLiveCell(outPoint, true);
return cell;
}
}
61 changes: 60 additions & 1 deletion packages/ckb/src/constants/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export const CKB_UNIT = BigInt(10000_0000);
export const MAX_FEE = BigInt(2000_0000);
export const MIN_CAPACITY = BigInt(63) * BigInt(10000_0000);
export const MIN_CAPACITY = BigInt(61) * BigInt(10000_0000);
export const SECP256K1_WITNESS_LOCK_LEN = 65;

const TestnetInfo = {
Secp256k1LockDep: {
Expand All @@ -10,6 +11,31 @@ const TestnetInfo = {
},
depType: 'depGroup',
} as CKBComponents.CellDep,

RgbppLockScript: {
codeHash: '0xd23761b364210735c19c60561d213fb3beae2fd6172743719eff6920e020baac',
hashType: 'type',
args: '',
} as CKBComponents.Script,

RgbppLockDep: {
outPoint: { txHash: '0x437d4343c1eb5901c74ba34f6e9b1a1a25d72b441659d73bb1b40e9924bda6fb', index: '0x0' },
depType: 'depGroup',
} as CKBComponents.CellDep,

XUDTTypeScript: {
codeHash: '0x25c29dc317811a6f6f3985a7a9ebc4838bd388d19d0feeecf0bcd60f6c0975bb',
hashType: 'type',
args: '',
} as CKBComponents.Script,

XUDTTypeDep: {
outPoint: {
txHash: '0xbf6fb538763efec2a70a6a3dcb7242787087e1030c4e7d86585bc63a9d337f5f',
index: '0x0',
},
depType: 'code',
} as CKBComponents.CellDep,
};

const MainnetInfo = {
Expand All @@ -20,7 +46,40 @@ const MainnetInfo = {
},
depType: 'depGroup',
} as CKBComponents.CellDep,

RgbppLockScript: {
codeHash: '0xd23761b364210735c19c60561d213fb3beae2fd6172743719eff6920e020baac',
hashType: 'type',
args: '',
} as CKBComponents.Script,

RgbppLockDep: {
outPoint: { txHash: '0x437d4343c1eb5901c74ba34f6e9b1a1a25d72b441659d73bb1b40e9924bda6fb', index: '0x0' },
depType: 'depGroup',
} as CKBComponents.CellDep,

XUDTTypeScript: {
codeHash: '0x50bd8d6680b8b9cf98b73f3c08faf8b2a21914311954118ad6609be6e78a1b95',
hashType: 'data1',
args: '',
} as CKBComponents.Script,

XUDTTypeDep: {
outPoint: {
txHash: '0xc07844ce21b38e4b071dd0e1ee3b0e27afd8d7532491327f39b786343f558ab7',
index: '0x0',
},
depType: 'code',
} as CKBComponents.CellDep,
};

export const getSecp256k1CellDep = (isMainnet = false) =>
isMainnet ? MainnetInfo.Secp256k1LockDep : TestnetInfo.Secp256k1LockDep;

export const getXudtTypeScript = (isMainnet = false) =>
isMainnet ? MainnetInfo.XUDTTypeScript : TestnetInfo.XUDTTypeScript;
export const getXudtDep = (isMainnet = false) => (isMainnet ? MainnetInfo.XUDTTypeDep : TestnetInfo.XUDTTypeDep);

export const getRgbppLockScript = (isMainnet = false) =>
isMainnet ? MainnetInfo.RgbppLockScript : TestnetInfo.RgbppLockScript;
export const getRgbppLockDep = (isMainnet = false) => (isMainnet ? MainnetInfo.RgbppLockDep : TestnetInfo.RgbppLockDep);
18 changes: 18 additions & 0 deletions packages/ckb/src/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,21 @@ export class NoLiveCellError extends Error {
super(message);
}
}

export class NoRgbppLiveCellError extends Error {
constructor(message: string) {
super(message);
}
}

export class UdtAmountNotEnoughError extends Error {
constructor(message: string) {
super(message);
}
}

export class InputsCapacityNotEnoughError extends Error {
constructor(message: string) {
super(message);
}
}
1 change: 1 addition & 0 deletions packages/ckb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './collector';
export * from './error';
export * from './paymaster';
export * from './types';
export * from './rgbpp';
11 changes: 5 additions & 6 deletions packages/ckb/src/paymaster/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { AddressPrefix, addressToScript, getTransactionSize, privateKeyToAddress } from '@nervosnetwork/ckb-sdk-utils';
import { ConstructParams } from '../types/transfer';
import { ConstructPaymasterParams } from '../types/rgbpp';
import { NoLiveCellError } from '../error';
import { CKB_UNIT, MAX_FEE, getSecp256k1CellDep } from '../constants';
import { CKB_UNIT, MAX_FEE, SECP256K1_WITNESS_LOCK_LEN, getSecp256k1CellDep } from '../constants';
import { append0x, calculateTransactionFee } from '../utils';

const SECP256K1_MIN_CAPACITY = BigInt(61) * CKB_UNIT;
const SECP256K1_WITNESS_LOCK_LEN = 65;

export const splitMultiCellsWithSecp256k1 = async ({
masterPrivateKey,
collector,
receiverAddress,
capacityWithCKB,
cellAmount,
}: ConstructParams) => {
}: ConstructPaymasterParams) => {
const isMainnet = receiverAddress.startsWith('ckb');
const masterAddress = privateKeyToAddress(masterPrivateKey, {
prefix: isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet,
Expand All @@ -30,7 +29,7 @@ export const splitMultiCellsWithSecp256k1 = async ({
const cellCapacity = BigInt(capacityWithCKB) * CKB_UNIT;
const needCapacity = cellCapacity * BigInt(cellAmount);
let txFee = MAX_FEE;
const { inputs, capacity: emptyInputsCapacity } = collector.collectInputs(
const { inputs, sumInputsCapacity } = collector.collectInputs(
emptyCells,
needCapacity,
txFee,
Expand All @@ -42,7 +41,7 @@ export const splitMultiCellsWithSecp256k1 = async ({
capacity: append0x(cellCapacity.toString(16)),
});

const changeCapacity = emptyInputsCapacity - needCapacity - txFee;
const changeCapacity = sumInputsCapacity - needCapacity - txFee;
outputs.push({
lock: masterLock,
capacity: append0x(changeCapacity.toString(16)),
Expand Down
79 changes: 79 additions & 0 deletions packages/ckb/src/rgbpp/btc-transfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { BtcTransferCkbVirtualTxParams, BtcTransferCkbVirtualResult, BtcTransferCkbVirtualTx } from '../types/rgbpp';
import { blockchain } from '@ckb-lumos/base';
import { NoRgbppLiveCellError } from '../error';
import { append0x, calculateRgbppCellCapacity, u128ToLe, u32ToLe } from '../utils';
import { calculateCommitment, genRgbppLockScript } from '../utils/rgbpp';
import { IndexerCell } from '../types';
import { getRgbppLockDep, getSecp256k1CellDep, getXudtDep } from '../constants';

export const genBtcTransferCkbVirtualTx = async ({
collector,
xudtTypeBytes,
rgbppLockArgsList,
transferAmount,
isMainnet,
}: BtcTransferCkbVirtualTxParams): Promise<BtcTransferCkbVirtualResult> => {
const xudtType = blockchain.Script.unpack(xudtTypeBytes) as CKBComponents.Script;

const rgbppLocks = rgbppLockArgsList.map((args) => genRgbppLockScript(args, isMainnet));
let rgbppCells: IndexerCell[] = [];
for await (const rgbppLock of rgbppLocks) {
const cells = await collector.getCells({ lock: rgbppLock, type: xudtType });
if (!cells || cells.length === 0) {
throw new NoRgbppLiveCellError('No rgb++ cells found with the xudt type script and the rgbpp lock args');
}
rgbppCells = [...rgbppCells, ...cells];
}

const { inputs, sumInputsCapacity, sumAmount } = collector.collectUdtInputs(rgbppCells, transferAmount);

const rpbppCellCapacity = calculateRgbppCellCapacity(xudtType);
const outputsData = [append0x(u128ToLe(transferAmount))];
const outputs: CKBComponents.CellOutput[] = [
{
lock: genRgbppLockScript(u32ToLe(1)),
type: xudtType,
capacity: append0x(rpbppCellCapacity.toString(16)),
},
];

if (sumAmount > transferAmount) {
outputs.push({
lock: genRgbppLockScript(u32ToLe(2)),
type: xudtType,
capacity: append0x(rpbppCellCapacity.toString(16)),
});
outputsData.push(append0x(u128ToLe(sumAmount - transferAmount)));
}

const cellDeps = [getRgbppLockDep(isMainnet), getXudtDep(isMainnet)];
const needPaymasterCell = inputs.length < outputs.length;
if (needPaymasterCell) {
cellDeps.push(getSecp256k1CellDep(isMainnet));
}
const witnesses = inputs.map((_) => '0x');

const ckbRawTx: CKBComponents.RawTransaction = {
version: '0x0',
cellDeps,
headerDeps: [],
inputs,
outputs,
outputsData,
witnesses,
};

const virtualTx: BtcTransferCkbVirtualTx = {
inputs,
outputs,
outputsData,
};
const commitment = calculateCommitment(virtualTx);

return {
ckbRawTx,
commitment,
needPaymasterCell,
sumInputsCapacity,
};
};
Loading

1 comment on commit bcfc3fa

@github-actions
Copy link

Choose a reason for hiding this comment

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

[{"name":"@rgbpp-sdk/btc","version":"0.0.0-snap-20240309002658"},{"name":"@rgbpp-sdk/ckb","version":"0.0.0-snap-20240309002658"}]

Please sign in to comment.