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-sdk/ckb): Add spv client cell and btc tx proof #22

Merged
merged 14 commits into from
Mar 15, 2024
2 changes: 1 addition & 1 deletion packages/ckb/src/collector/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class Collector {
};
}
let payload = {
id: 1,
id: Math.floor(Math.random() * 100000),
jsonrpc: '2.0',
method: 'get_cells',
params: [param, 'asc', '0x3E8'],
Expand Down
3 changes: 3 additions & 0 deletions packages/ckb/src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ export const CKB_UNIT = BigInt(10000_0000);
export const MAX_FEE = BigInt(2000_0000);
export const MIN_CAPACITY = BigInt(61) * BigInt(10000_0000);
export const SECP256K1_WITNESS_LOCK_SIZE = 65;
export const BTC_JUMP_CONFIRMATION_BLOCKS = 6;

export const RGBPP_WITNESS_PLACEHOLDER = '0xFF';

const TestnetInfo = {
Secp256k1LockDep: {
Expand Down
6 changes: 6 additions & 0 deletions packages/ckb/src/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ export class InputsCapacityNotEnoughError extends Error {
super(message);
}
}

export class SpvRpcError extends Error {
constructor(message: string) {
super(message);
}
}
2 changes: 2 additions & 0 deletions packages/ckb/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export * from './error';
export * from './paymaster';
export * from './types';
export * from './rgbpp';
export * from './spv';
export * from './utils';
19 changes: 15 additions & 4 deletions packages/ckb/src/rgbpp/btc-jump-ckb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { RgbppCkbVirtualTx, BtcJumpCkbVirtualTxParams, BtcJumpCkbVirtualTxResult
import { blockchain } from '@ckb-lumos/base';
import { NoRgbppLiveCellError } from '../error';
import { append0x, calculateRgbppCellCapacity, u128ToLe, u32ToLe } from '../utils';
import { calculateCommitment, genBtcTimeLockScript, genRgbppLockScript } from '../utils/rgbpp';
import { IndexerCell } from '../types';
import { getRgbppLockDep, getSecp256k1CellDep, getXudtDep } from '../constants';
import { calculateCommitment, compareInputs, genBtcTimeLockScript, genRgbppLockScript } from '../utils/rgbpp';
import { Hex, IndexerCell } from '../types';
import { RGBPP_WITNESS_PLACEHOLDER, getRgbppLockDep, getSecp256k1CellDep, getXudtDep } from '../constants';
import { addressToScript } from '@nervosnetwork/ckb-sdk-utils';

/**
Expand Down Expand Up @@ -34,6 +34,7 @@ export const genBtcJumpCkbVirtualTx = async ({
}
rgbppCells = [...rgbppCells, ...cells];
}
rgbppCells = rgbppCells.sort(compareInputs);

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

Expand Down Expand Up @@ -63,7 +64,17 @@ export const genBtcJumpCkbVirtualTx = async ({
if (needPaymasterCell) {
cellDeps.push(getSecp256k1CellDep(isMainnet));
}
const witnesses = inputs.map((_) => '0x');

const witnesses: Hex[] = [];
const lockArgsSet: Set<string> = new Set();
for (const cell of rgbppCells) {
if (lockArgsSet.has(cell.output.lock.args)) {
witnesses.push('0x');
} else {
lockArgsSet.add(cell.output.lock.args);
witnesses.push(RGBPP_WITNESS_PLACEHOLDER);
}
}

const ckbRawTx: CKBComponents.RawTransaction = {
version: '0x0',
Expand Down
52 changes: 44 additions & 8 deletions packages/ckb/src/rgbpp/btc-time.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,65 @@
import { getBtcTimeLockDep, getXudtDep } from '../constants';
import { BtcTimeCellsParams } from '../types';
import { lockScriptFromBtcTimeLockArgs } from '../utils';
import { bytesToHex, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils';
import { BTC_JUMP_CONFIRMATION_BLOCKS, getBtcTimeLockDep, getXudtDep } from '../constants';
import { BTCTimeUnlock } from '../schemas/generated/rgbpp';
import { BtcTimeCellsParams, Hex } from '../types';
import {
append0x,
btcTxIdFromBtcTimeLockArgs,
calculateTransactionFee,
compareInputs,
lockScriptFromBtcTimeLockArgs,
} from '../utils';
import { buildSpvClientCellDep } from '../spv';

export const buildBtcTimeUnlockWitness = (btcTxProof: Hex): Hex => {
const btcTimeUnlock = BTCTimeUnlock.pack({ btcTxProof });
return append0x(bytesToHex(btcTimeUnlock));
};

/**
* Collect btc time cells and spend them to create xudt cells for the specific lock scripts in the btc time lock args
* The btc time lock args data structure is: lock_script | after | new_bitcoin_tx_id
* @param btcTimeCells The btc time cells which have met the block confirmations and can be spent
* @param spvService SPV RPC service
* @param isMainnet
*/
export const buildBtcTimeCellsSpentTx = async ({
btcTimeCells,
spvService,
isMainnet,
}: BtcTimeCellsParams): Promise<CKBComponents.RawTransaction> => {
const inputs: CKBComponents.CellInput[] = btcTimeCells.map((cell) => ({
const sortedBtcTimeCells = btcTimeCells.sort(compareInputs);
const inputs: CKBComponents.CellInput[] = sortedBtcTimeCells.map((cell) => ({
previousOutput: cell.outPoint,
since: '0x0',
}));

const outputs: CKBComponents.CellOutput[] = btcTimeCells.map((cell) => ({
const outputs: CKBComponents.CellOutput[] = sortedBtcTimeCells.map((cell) => ({
lock: lockScriptFromBtcTimeLockArgs(cell.output.lock.args),
type: cell.output.type,
capacity: cell.output.capacity,
}));

const outputsData = btcTimeCells.map((cell) => cell.outputData);
const outputsData = sortedBtcTimeCells.map((cell) => cell.outputData);

const cellDeps: CKBComponents.CellDep[] = [getBtcTimeLockDep(isMainnet), getXudtDep(isMainnet)];

// TODO: Wait for btc time unlock witness
const witnesses = inputs.map((_) => '0x');
const witnesses: Hex[] = [];

const lockArgsSet: Set<string> = new Set();
for await (const cell of sortedBtcTimeCells) {
if (lockArgsSet.has(cell.output.lock.args)) {
witnesses.push('0x');
continue;
}
lockArgsSet.add(cell.output.lock.args);
const { spvClient, proof } = await spvService.fetchSpvClientCellAndTxProof({
btcTxId: btcTxIdFromBtcTimeLockArgs(cell.output.lock.args),
confirmBlocks: BTC_JUMP_CONFIRMATION_BLOCKS,
});
cellDeps.push(buildSpvClientCellDep(spvClient));
witnesses.push(buildBtcTimeUnlockWitness(proof));
}

const ckbTx: CKBComponents.RawTransaction = {
version: '0x0',
Expand All @@ -40,5 +71,10 @@ export const buildBtcTimeCellsSpentTx = async ({
witnesses,
};

const txSize = getTransactionSize(ckbTx);
const estimatedTxFee = calculateTransactionFee(txSize);
const lastOutputCapacity = BigInt(outputs[outputs.length - 1].capacity) - estimatedTxFee;
ckbTx.outputs[outputs.length - 1].capacity = append0x(lastOutputCapacity.toString(16));

return ckbTx;
};
20 changes: 16 additions & 4 deletions packages/ckb/src/rgbpp/btc-transfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { BtcTransferVirtualTxParams, BtcTransferVirtualTxResult, RgbppCkbVirtual
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';
import { calculateCommitment, compareInputs, genRgbppLockScript } from '../utils/rgbpp';
import { Hex, IndexerCell } from '../types';
import { RGBPP_WITNESS_PLACEHOLDER, getRgbppLockDep, getSecp256k1CellDep, getXudtDep } from '../constants';

/**
* Generate the virtual ckb transaction for the btc transfer tx
Expand Down Expand Up @@ -32,11 +32,14 @@ export const genBtcTransferCkbVirtualTx = async ({
}
rgbppCells = [...rgbppCells, ...cells];
}
rgbppCells = rgbppCells.sort(compareInputs);

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

const rpbppCellCapacity = calculateRgbppCellCapacity(xudtType);
const outputsData = [append0x(u128ToLe(transferAmount))];

// The Vouts[0] for OP_RETURN and Vouts[1], Vouts[2] for RGBPP assets
const outputs: CKBComponents.CellOutput[] = [
{
lock: genRgbppLockScript(u32ToLe(1), isMainnet),
Expand All @@ -59,7 +62,16 @@ export const genBtcTransferCkbVirtualTx = async ({
if (needPaymasterCell) {
cellDeps.push(getSecp256k1CellDep(isMainnet));
}
const witnesses = inputs.map((_) => '0x');
const witnesses: Hex[] = [];
const lockArgsSet: Set<string> = new Set();
for (const cell of rgbppCells) {
if (lockArgsSet.has(cell.output.lock.args)) {
witnesses.push('0x');
} else {
lockArgsSet.add(cell.output.lock.args);
witnesses.push(RGBPP_WITNESS_PLACEHOLDER);
}
}

const ckbRawTx: CKBComponents.RawTransaction = {
version: '0x0',
Expand Down
50 changes: 42 additions & 8 deletions packages/ckb/src/rgbpp/ckb-builder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
bytesToHex,
getTransactionSize,
rawTransactionToHash,
scriptToHash,
Expand All @@ -8,30 +9,62 @@ import {
AppendBtcTxIdToLockArgsParams,
AppendPaymasterCellAndSignTxParams,
AppendWitnessesParams,
Hex,
SendCkbTxParams,
} from '../types';
import { SECP256K1_WITNESS_LOCK_SIZE, getRgbppLockScript } from '../constants';
import { append0x, calculateTransactionFee, isRgbppLockOrBtcTimeLock, remove0x } from '../utils';
import { RGBPP_WITNESS_PLACEHOLDER, SECP256K1_WITNESS_LOCK_SIZE, getRgbppLockScript } from '../constants';
import { append0x, calculateTransactionFee, isRgbppLockOrBtcTimeLock, remove0x, u8ToHex } from '../utils';
import { InputsCapacityNotEnoughError } from '../error';
import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses';
import { buildSpvClientCellDep } from '../spv';
import { RGBPPUnlock, Uint16 } from '../schemas/generated/rgbpp';

export const buildRgbppUnlockWitness = (
btcTxBytes: Hex,
btcTxProof: Hex,
ckbRawTx: CKBComponents.RawTransaction,
): Hex => {
const inputLen = append0x(u8ToHex(ckbRawTx.inputs.length));
const outputLen = append0x(u8ToHex(ckbRawTx.outputs.length));

const version = Uint16.pack([0, 0]);
const rgbppUnlock = RGBPPUnlock.pack({ version, extraData: { inputLen, outputLen }, btcTx: btcTxBytes, btcTxProof });
return append0x(bytesToHex(rgbppUnlock));
};

// TODO: waiting for SPV btc tx proof
/**
* Append RGBPP unlock witnesses to ckb tx and the tx can be sent to blockchain if the needPaymasterCell is false.
* And if the needPaymasterCell is true, appending paymaster cell to inputs and signing ckb tx are required
* And if the needPaymasterCell is true, appending paymaster cell to inputs and signing ckb tx are required.
* @param collector The collector that collects CKB live cells and transactions
* @param btcTxBytes The hex string of btc transaction, refer to https://github.com/bitcoinjs/bitcoinjs-lib/blob/master/ts_src/transaction.ts#L609
* @param spvService SPV RPC service
* @param spvClientCellTxProof The OutPoint of SPV client cell and btc tx proof that come from SPV RPC service
* @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee
* @param needPaymasterCell The needPaymasterCell indicates whether a paymaster cell is required
*/
export const appendCkbTxWitnesses = ({ ckbRawTx, sumInputsCapacity, needPaymasterCell }: AppendWitnessesParams) => {
const inputsCapacity = BigInt(sumInputsCapacity);
export const appendCkbTxWitnesses = async ({
ckbRawTx,
spvService,
btcTxBytes,
btcTxId,
sumInputsCapacity,
needPaymasterCell,
}: AppendWitnessesParams): Promise<CKBComponents.RawTransaction> => {
let rawTx = ckbRawTx;

const { spvClient, proof } = await spvService.fetchSpvClientCellAndTxProof({ btcTxId, confirmBlocks: 0 });
rawTx.cellDeps.push(buildSpvClientCellDep(spvClient));

const rgbppUnlock = buildRgbppUnlockWitness(btcTxBytes, proof, ckbRawTx);
rawTx.witnesses = rawTx.witnesses.map((witness) => (witness === RGBPP_WITNESS_PLACEHOLDER ? rgbppUnlock : witness));

if (!needPaymasterCell) {
let rawTx = ckbRawTx;
const partialOutputsCapacity = rawTx.outputs
.slice(0, rawTx.outputs.length - 1)
.map((output) => BigInt(output.capacity))
.reduce((prev, current) => prev + current, BigInt(0));

const inputsCapacity = BigInt(sumInputsCapacity);
if (inputsCapacity <= partialOutputsCapacity) {
throw new InputsCapacityNotEnoughError('The sum of inputs capacity is not enough');
}
Expand All @@ -41,8 +74,9 @@ export const appendCkbTxWitnesses = ({ ckbRawTx, sumInputsCapacity, needPaymaste

const changeCapacity = inputsCapacity - partialOutputsCapacity - estimatedTxFee;
rawTx.outputs[rawTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16));
return rawTx;
}

return rawTx;
};

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/ckb/src/schemas/generated/rgbpp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,9 @@ export const RGBPPUnlock = table(
version: Uint16,
extraData: ExtraCommitmentData,
btcTx: Bytes,
btcTxProof: Bytes,
},
['version', 'extraData', 'btcTx'],
['version', 'extraData', 'btcTx', 'btcTxProof'],
);

export const BTCTimeLock = table(
Expand Down
1 change: 1 addition & 0 deletions packages/ckb/src/schemas/schemas/rgbpp.mol
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ table RGBPPUnlock {
version: Uint16,
extra_data: ExtraCommitmentData,
btc_tx: Bytes,
btc_tx_proof: Bytes,
}

table BTCTimeLock {
Expand Down
55 changes: 55 additions & 0 deletions packages/ckb/src/spv/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import axios from 'axios';
import { SpvClientCellTxProofReq, SpvClientCellTxProofResponse } from '../types/spv';
import { SpvRpcError } from '../error';
import { append0x, toCamelcase, u32ToLe } from '../utils';
import { blockchain } from '@ckb-lumos/base';
import { Hex } from '../types';

export class SPVService {
private url: string;

constructor(url: string) {
this.url = url;
}

fetchSpvClientCellAndTxProof = async ({
btcTxId,
confirmBlocks,
}: SpvClientCellTxProofReq): Promise<SpvClientCellTxProofResponse> => {
let payload = {
id: Math.floor(Math.random() * 100000),
jsonrpc: '2.0',
method: 'getTxProof',
params: [btcTxId, confirmBlocks],
};
const body = JSON.stringify(payload, null, ' ');
const response = await axios({
method: 'post',
url: this.url,
headers: {
'Content-Type': 'application/json',
},
timeout: 20000,
data: body,
});
const data = response.data;
if (data.error) {
console.error(data.error);
throw new SpvRpcError('Fetch SPV client cell and tx proof error');
} else {
return toCamelcase(data.result);
}
};
}

export const buildSpvClientCellDep = (spvClient: Hex) => {
const outPoint = blockchain.OutPoint.unpack(spvClient);
const cellDep: CKBComponents.CellDep = {
outPoint: {
txHash: outPoint.txHash,
index: append0x(u32ToLe(outPoint.index)),
},
depType: 'code',
};
return cellDep;
};
1 change: 1 addition & 0 deletions packages/ckb/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './common';
export * from './collector';
export * from './rgbpp';
export * from './spv';
Loading
Loading