diff --git a/.changeset/tough-foxes-add.md b/.changeset/tough-foxes-add.md new file mode 100644 index 00000000..d7d02d67 --- /dev/null +++ b/.changeset/tough-foxes-add.md @@ -0,0 +1,10 @@ +--- +'@rgbpp-sdk/ckb': minor +--- + +Encapsulate 3 methods for supplementing CKB transaction fees: +- `appendIssuerCellToBtcBatchTransferToSign` for supplementing fees in RGB++ xUDT transactions; +- `prepareBtcTimeCellSpentUnsignedTx` for BTC timelock unlock transaction building and fee supplementation; +- `appendIssuerCellToSporesCreateUnsignedTx` for supplementing fees in Spores creation transactions; + +Encapsulate `signCkbTransaction` method for signing CKB transactions. \ No newline at end of file diff --git a/examples/rgbpp/xudt/offline/1-prepare-launch.ts b/examples/rgbpp/xudt/offline/1-prepare-launch.ts index 6bb6cd8f..ea9ea695 100644 --- a/examples/rgbpp/xudt/offline/1-prepare-launch.ts +++ b/examples/rgbpp/xudt/offline/1-prepare-launch.ts @@ -10,6 +10,7 @@ import { calculateTransactionFee, genRgbppLockScript, getSecp256k1CellDep, + signCkbTransaction, } from 'rgbpp/ckb'; import { RGBPP_TOKEN_INFO } from './0-rgbpp-token-info'; import { @@ -75,15 +76,15 @@ const prepareLaunchCell = async ({ changeCapacity -= estimatedTxFee; unsignedTx.outputs[unsignedTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); - const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); + const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); console.info(`Launch cell has been created and the CKB tx hash ${txHash}`); }; prepareLaunchCell({ - outIndex: 6, - btcTxId: '692a88b83d7dee2a77b5ab3372115a311aa503b1632d3ecdd2e77ffbafd94bdf', + outIndex: 3, + btcTxId: 'd6cbc8c4418cb1c4cab200c60e653ee886fd67d1c839197b1ac73a88a6360473', rgbppTokenInfo: RGBPP_TOKEN_INFO, }); diff --git a/examples/rgbpp/xudt/offline/2-launch-rgbpp.ts b/examples/rgbpp/xudt/offline/2-launch-rgbpp.ts index a6d5f5c1..11afca99 100644 --- a/examples/rgbpp/xudt/offline/2-launch-rgbpp.ts +++ b/examples/rgbpp/xudt/offline/2-launch-rgbpp.ts @@ -61,7 +61,7 @@ const launchRgppAsset = async ({ ownerRgbppLockArgs, launchAmount, rgbppTokenInf from: btcAccount.from, fromPubkey: btcAccount.fromPubkey, source: btcOfflineDataSource, - feeRate: 4096, + feeRate: 256, }); const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService); @@ -96,7 +96,7 @@ const launchRgppAsset = async ({ ownerRgbppLockArgs, launchAmount, rgbppTokenInf // rgbppLockArgs: outIndexU32 + btcTxId launchRgppAsset({ - ownerRgbppLockArgs: buildRgbppLockArgs(6, '692a88b83d7dee2a77b5ab3372115a311aa503b1632d3ecdd2e77ffbafd94bdf'), + ownerRgbppLockArgs: buildRgbppLockArgs(3, 'd6cbc8c4418cb1c4cab200c60e653ee886fd67d1c839197b1ac73a88a6360473'), rgbppTokenInfo: RGBPP_TOKEN_INFO, // The total issuance amount of RGBPP Token, the decimal is determined by RGBPP Token info launchAmount: BigInt(2100_0000) * BigInt(10 ** RGBPP_TOKEN_INFO.decimal), diff --git a/examples/rgbpp/xudt/offline/3-distribute-rgbpp.ts b/examples/rgbpp/xudt/offline/3-distribute-rgbpp.ts index efa8b290..23fec444 100644 --- a/examples/rgbpp/xudt/offline/3-distribute-rgbpp.ts +++ b/examples/rgbpp/xudt/offline/3-distribute-rgbpp.ts @@ -16,12 +16,14 @@ import { import { RgbppBtcAddressReceiver, appendCkbTxWitnesses, - appendIssuerCellToBtcBatchTransfer, buildRgbppLockArgs, getXudtTypeScript, sendCkbTx, updateCkbTxWithRealBtcTxId, genRgbppLockScript, + appendIssuerCellToBtcBatchTransferToSign, + addressToScriptHash, + signCkbTransaction, } from 'rgbpp/ckb'; import { saveCkbVirtualTxResult } from '../../shared/utils'; import { signAndSendPsbt } from '../../shared/btc-account'; @@ -78,7 +80,7 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers, xudtTyp from: btcAccount.from, fromPubkey: btcAccount.fromPubkey, source: btcOfflineDataSource, - feeRate: 4096, + feeRate: 256, }); const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService); @@ -97,8 +99,7 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers, xudtTyp rgbppApiSpvProof, }); - const signedTx = await appendIssuerCellToBtcBatchTransfer({ - secp256k1PrivateKey: CKB_PRIVATE_KEY, + const { ckbRawTx: unsignedTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({ issuerAddress: ckbAddress, ckbRawTx: ckbTx, collector: offlineCollector, @@ -106,6 +107,10 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers, xudtTyp isMainnet, }); + const keyMap = new Map(); + keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY); + const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true); + const txHash = await sendCkbTx({ collector, signedTx }); console.info(`RGB++ Asset has been distributed and CKB tx hash is ${txHash}`); } catch (error) { @@ -123,9 +128,9 @@ const distributeRgbppAssetOnBtc = async ({ rgbppLockArgsList, receivers, xudtTyp // rgbppLockArgs: outIndexU32 + btcTxId distributeRgbppAssetOnBtc({ // Warning: If rgbpp assets are distributed continuously, then the position of the current rgbpp asset utxo depends on the position of the previous change utxo distributed - rgbppLockArgsList: [buildRgbppLockArgs(1, 'a2bcca7807f8543d71e85e772335fa7eec2d812ca3250fed96a9d406aa1a9827')], + rgbppLockArgsList: [buildRgbppLockArgs(1, '65e0574dfdbed4809736f3ec5a73aa191f147873d083ddb9978aecb969dd1900')], // The xudtTypeArgs comes from the logs "RGB++ Asset type script args" of 2-launch-rgbpp.ts - xudtTypeArgs: '0x13ce1d60ec65d693724006086568645aa24c019510ebc9af7cf6b993c2d7bffb', + xudtTypeArgs: '0xe402314a4b31223afe00a9c69c0b872863b990219525e1547ec05d9d88434b24', receivers: [ { toBtcAddress: 'tb1qeq27se73d0e6zkh53e3xrj90vqzv8g7ja3nm85', diff --git a/examples/rgbpp/xudt/offline/4-btc-leap-ckb.ts b/examples/rgbpp/xudt/offline/4-btc-leap-ckb.ts index 993742bc..04d85f01 100644 --- a/examples/rgbpp/xudt/offline/4-btc-leap-ckb.ts +++ b/examples/rgbpp/xudt/offline/4-btc-leap-ckb.ts @@ -2,7 +2,9 @@ import { buildRgbppLockArgs, getXudtTypeScript, genRgbppLockScript, - appendIssuerCellToBtcBatchTransfer, + appendIssuerCellToBtcBatchTransferToSign, + signCkbTransaction, + addressToScriptHash, appendCkbTxWitnesses, updateCkbTxWithRealBtcTxId, sendCkbTx, @@ -71,7 +73,7 @@ const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, fromPubkey: btcAccount.fromPubkey, source: btcOfflineDataSource, needPaymaster: false, - feeRate: 6000, + feeRate: 256, }); const { txId: btcTxId, rawTxHex: btcTxBytes } = await signAndSendPsbt(psbt, btcAccount, btcService); @@ -90,9 +92,7 @@ const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, rgbppApiSpvProof, }); - // pay tx fee - const signedTx = await appendIssuerCellToBtcBatchTransfer({ - secp256k1PrivateKey: CKB_PRIVATE_KEY, + const { ckbRawTx: unsignedTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({ issuerAddress: ckbAddress, ckbRawTx: ckbTx, collector: offlineCollector, @@ -100,6 +100,10 @@ const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, isMainnet, }); + const keyMap = new Map(); + keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY); + const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true); + const txHash = await sendCkbTx({ collector, signedTx }); console.info(`Rgbpp asset has been jumped from BTC to CKB and the related CKB tx hash is ${txHash}`); } catch (error) { @@ -116,11 +120,11 @@ const leapFromBtcToCKB = async ({ rgbppLockArgsList, toCkbAddress, xudtTypeArgs, // rgbppLockArgs: outIndexU32 + btcTxId leapFromBtcToCKB({ - rgbppLockArgsList: [buildRgbppLockArgs(5, '316267e33808ae437c9870c3538ec5d46361e6474ace0e150442b10c41a1cb21')], + rgbppLockArgsList: [buildRgbppLockArgs(1, 'dfb3be075b831ce7605ab3f56b7dda39ba7438e015ded3332db1f54cfac161de')], toCkbAddress: 'ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqfpu7pwavwf3yang8khrsklumayj6nyxhqpmh7fq', // Please use your own RGB++ xudt asset's xudtTypeArgs - xudtTypeArgs: '0x13ce1d60ec65d693724006086568645aa24c019510ebc9af7cf6b993c2d7bffb', - transferAmount: BigInt(233_0000_0000), + xudtTypeArgs: '0xe402314a4b31223afe00a9c69c0b872863b990219525e1547ec05d9d88434b24', + transferAmount: BigInt(100_0000_0000), }); /* diff --git a/examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts b/examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts index 176738bc..0f818e5f 100644 --- a/examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts +++ b/examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts @@ -1,5 +1,12 @@ -import { buildBtcTimeCellsSpentTx, signBtcTimeCellSpentTx } from 'rgbpp'; -import { sendCkbTx, getBtcTimeLockScript, btcTxIdAndAfterFromBtcTimeLockArgs } from 'rgbpp/ckb'; +import { buildBtcTimeCellsSpentTx } from 'rgbpp'; +import { + sendCkbTx, + getBtcTimeLockScript, + btcTxIdAndAfterFromBtcTimeLockArgs, + prepareBtcTimeCellSpentUnsignedTx, + addressToScriptHash, + signCkbTransaction, +} from 'rgbpp/ckb'; import { BTC_TESTNET_TYPE, CKB_PRIVATE_KEY, btcService, ckbAddress, collector, isMainnet } from '../../env'; import { OfflineBtcAssetsDataSource, SpvProofEntry } from 'rgbpp/service'; @@ -38,14 +45,17 @@ const unlockBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string btcTestnetType: BTC_TESTNET_TYPE, }); - const signedTx = await signBtcTimeCellSpentTx({ - secp256k1PrivateKey: CKB_PRIVATE_KEY, + const { ckbRawTx: unsignedTx, inputCells } = await prepareBtcTimeCellSpentUnsignedTx({ collector, masterCkbAddress: ckbAddress, ckbRawTx, isMainnet, }); + const keyMap = new Map(); + keyMap.set(addressToScriptHash(ckbAddress), CKB_PRIVATE_KEY); + const signedTx = signCkbTransaction(keyMap, unsignedTx, inputCells, true); + const txHash = await sendCkbTx({ collector, signedTx }); console.info(`BTC time cell has been spent and CKB tx hash is ${txHash}`); }; @@ -53,5 +63,9 @@ const unlockBtcTimeCell = async ({ btcTimeCellArgs }: { btcTimeCellArgs: string // The btcTimeCellArgs is from the outputs[0].lock.args(BTC Time lock args) of the 3-btc-leap-ckb.ts CKB transaction unlockBtcTimeCell({ btcTimeCellArgs: - '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000021e782eeb1c9893b341ed71c2dfe6fa496a6435c0600000045241651e435d786d0ba8a1280d4bb3283eca10db728e2ba0a3978c136f5bb19', + '0x7d00000010000000590000005d000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8011400000021e782eeb1c9893b341ed71c2dfe6fa496a6435c06000000f2aa85670171e5727da232e041e194508b0beced672e731f638ff84abf8d5ff8', }); + +/* +npx tsx examples/rgbpp/xudt/offline/5-unlock-btc-time-cell.ts +*/ diff --git a/examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts b/examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts index 1731b1e5..d0650294 100644 --- a/examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts +++ b/examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts @@ -1,6 +1,6 @@ import { addressToScript, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; import { genCkbJumpBtcVirtualTx } from 'rgbpp'; -import { getSecp256k1CellDep, buildRgbppLockArgs, getXudtTypeScript } from 'rgbpp/ckb'; +import { getSecp256k1CellDep, buildRgbppLockArgs, getXudtTypeScript, signCkbTransaction } from 'rgbpp/ckb'; import { CKB_PRIVATE_KEY, isMainnet, @@ -49,8 +49,7 @@ const leapFromCkbToBtc = async ({ outIndex, btcTxId, xudtTypeArgs, transferAmoun witnesses: [emptyWitness, ...ckbRawTx.witnesses.slice(1)], }; - const signedTx = collector.getCkb().signTransaction(CKB_PRIVATE_KEY)(unsignedTx); - + const signedTx = signCkbTransaction(CKB_PRIVATE_KEY, unsignedTx); const txHash = await collector.getCkb().rpc.sendTransaction(signedTx, 'passthrough'); console.info(`Rgbpp asset has been jumped from CKB to BTC and CKB tx hash is ${txHash}`); }; @@ -62,10 +61,10 @@ leapFromCkbToBtc({ outIndex: 0, btcTxId: 'c1db31abe6bab345b5d5ab4a19c8f34c8cfe23efa4ec6bfa7b05c8e7b4f965b8', // Please use your own RGB++ xudt asset's xudtTypeArgs - xudtTypeArgs: '0x13ce1d60ec65d693724006086568645aa24c019510ebc9af7cf6b993c2d7bffb', + xudtTypeArgs: '0xe402314a4b31223afe00a9c69c0b872863b990219525e1547ec05d9d88434b24', transferAmount: BigInt(10_0000_0000), }); /* -npx tsx examples/rgbpp/xudt/offline/5-ckb-leap-btc.ts +npx tsx examples/rgbpp/xudt/offline/6-ckb-leap-btc.ts */ diff --git a/packages/ckb/src/rgbpp/btc-time.ts b/packages/ckb/src/rgbpp/btc-time.ts index 97d8b882..58a89676 100644 --- a/packages/ckb/src/rgbpp/btc-time.ts +++ b/packages/ckb/src/rgbpp/btc-time.ts @@ -2,8 +2,6 @@ import { addressToScript, bytesToHex, getTransactionSize, - rawTransactionToHash, - scriptToHash, serializeOutPoint, serializeWitnessArgs, } from '@nervosnetwork/ckb-sdk-utils'; @@ -27,8 +25,9 @@ import { buildSpvClientCellDep, isStandardUDTTypeSupported, isCompatibleUDTTypesSupported, + signCkbTransaction, + addressToScriptHash, } from '../utils'; -import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses'; export const buildBtcTimeUnlockWitness = (btcTxProof: Hex): Hex => { const btcTimeUnlock = BTCTimeUnlock.pack({ btcTxProof }); @@ -117,25 +116,17 @@ export const buildBtcTimeCellsSpentTx = async ({ return ckbTx; }; -/** - * Sign the BTC time cells spent transaction with Secp256k1 private key - * @param secp256k1PrivateKey The Secp256k1 private key of the master address - * @param ckbRawTx The CKB raw transaction to be signed - * @param collector The collector that collects CKB live cells and transactions - * @param masterCkbAddress The master CKB address - * @param outputCapacityRange(Optional) [u64; 2], filter cells by output capacity range, [inclusive, exclusive] - * @param ckbFeeRate(Optional) The CKB transaction fee rate, default value is 1100 - * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet - */ -export const signBtcTimeCellSpentTx = async ({ - secp256k1PrivateKey, +export const prepareBtcTimeCellSpentUnsignedTx = async ({ ckbRawTx, collector, masterCkbAddress, isMainnet, outputCapacityRange, ckbFeeRate, -}: SignBtcTimeCellsTxParams): Promise => { +}: Omit): Promise<{ + ckbRawTx: CKBComponents.RawTransactionToSign; + inputCells: { outPoint: CKBComponents.OutPoint; lock: CKBComponents.Script }[]; +}> => { const masterLock = addressToScript(masterCkbAddress); let emptyCells = await collector.getCells({ lock: masterLock, @@ -166,30 +157,45 @@ export const signBtcTimeCellSpentTx = async ({ const changeCapacity = BigInt(emptyCells[0].output.capacity) - estimatedTxFee; rawTx.outputs[0].capacity = append0x(changeCapacity.toString(16)); - const keyMap = new Map(); - keyMap.set(scriptToHash(masterLock), secp256k1PrivateKey); - const cells = rawTx.inputs.map((input, index) => ({ - outPoint: input.previousOutput, + outPoint: input.previousOutput as CKBComponents.OutPoint, lock: index === 0 ? masterLock : getBtcTimeLockScript(isMainnet), })); - const transactionHash = rawTransactionToHash(rawTx); - const signedWitnesses = signWitnesses(keyMap)({ - transactionHash, - witnesses: rawTx.witnesses, - inputCells: cells, - skipMissingKeys: true, - }); + return { ckbRawTx: rawTx, inputCells: cells }; +}; - const signedTx = { - ...rawTx, - witnesses: signedWitnesses.map((witness) => - typeof witness !== 'string' ? serializeWitnessArgs(witness) : witness, - ), - } as CKBComponents.RawTransaction; +/** + * Sign the BTC time cells spent transaction with Secp256k1 private key + * @param secp256k1PrivateKey The Secp256k1 private key of the master address + * @param ckbRawTx The CKB raw transaction to be signed + * @param collector The collector that collects CKB live cells and transactions + * @param masterCkbAddress The master CKB address + * @param outputCapacityRange(Optional) [u64; 2], filter cells by output capacity range, [inclusive, exclusive] + * @param ckbFeeRate(Optional) The CKB transaction fee rate, default value is 1100 + * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet + */ +export const signBtcTimeCellSpentTx = async ({ + secp256k1PrivateKey, + ckbRawTx, + collector, + masterCkbAddress, + isMainnet, + outputCapacityRange, + ckbFeeRate, +}: SignBtcTimeCellsTxParams): Promise => { + const { ckbRawTx: rawTx, inputCells } = await prepareBtcTimeCellSpentUnsignedTx({ + ckbRawTx, + collector, + masterCkbAddress, + isMainnet, + outputCapacityRange, + ckbFeeRate, + }); - return signedTx; + const keyMap = new Map(); + keyMap.set(addressToScriptHash(masterCkbAddress), secp256k1PrivateKey); + return signCkbTransaction(keyMap, rawTx, inputCells, true); }; /** diff --git a/packages/ckb/src/rgbpp/btc-transfer.ts b/packages/ckb/src/rgbpp/btc-transfer.ts index a50617e7..78a081ef 100644 --- a/packages/ckb/src/rgbpp/btc-transfer.ts +++ b/packages/ckb/src/rgbpp/btc-transfer.ts @@ -25,6 +25,8 @@ import { throwErrorWhenRgbppCellsInvalid, throwErrorWhenTxInputsExceeded, isStandardUDTTypeSupported, + signCkbTransaction, + addressToScriptHash, } from '../utils'; import { Hex, IndexerCell } from '../types'; import { @@ -36,14 +38,7 @@ import { getSecp256k1CellDep, } from '../constants'; import { blockchain } from '@ckb-lumos/base'; -import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses'; -import { - addressToScript, - getTransactionSize, - rawTransactionToHash, - scriptToHash, - serializeWitnessArgs, -} from '@nervosnetwork/ckb-sdk-utils'; +import { addressToScript, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; /** * Generate the virtual ckb transaction for the btc transfer tx @@ -348,28 +343,30 @@ export const genBtcBatchTransferCkbVirtualTx = async ({ }; /** - * Append paymaster cell to the ckb transaction inputs and sign the transaction with paymaster cell's secp256k1 private key - * @param secp256k1PrivateKey The Secp256k1 private key of the paymaster cells maintainer - * @param issuerAddress The issuer ckb address + * Appends cells from the issuer address to cover transaction fees and ensures sufficient capacity + * @param issuerAddress The address that provides cells for transaction fees * @param collector The collector that collects CKB live cells and transactions - * @param ckbRawTx CKB raw transaction + * @param ckbRawTx The raw transaction to append cells to * @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee - * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet + * @param sumInputsCapacity The total capacity of existing inputs * @param ckbFeeRate The CKB transaction fee rate, default value is 1100 + * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet + * @returns The updated transaction and input cells information */ -export const appendIssuerCellToBtcBatchTransfer = async ({ - secp256k1PrivateKey, +export const appendIssuerCellToBtcBatchTransferToSign = async ({ issuerAddress, collector, ckbRawTx, sumInputsCapacity, - isMainnet, ckbFeeRate, -}: AppendIssuerCellToBtcBatchTransfer): Promise => { + isMainnet, +}: Omit): Promise<{ + ckbRawTx: CKBComponents.RawTransactionToSign; + inputCells: { outPoint: CKBComponents.OutPoint; lock: CKBComponents.Script }[]; +}> => { + const rgbppInputsLength = ckbRawTx.inputs.length; const rawTx = ckbRawTx as CKBComponents.RawTransactionToSign; - const rgbppInputsLength = rawTx.inputs.length; - const sumOutputsCapacity: bigint = rawTx.outputs .map((output) => BigInt(output.capacity)) .reduce((prev, current) => prev + current, BigInt(0)); @@ -403,12 +400,9 @@ export const appendIssuerCellToBtcBatchTransfer = async ({ changeCapacity -= estimatedTxFee; rawTx.outputs[rawTx.outputs.length - 1].capacity = append0x(changeCapacity.toString(16)); - const keyMap = new Map(); - keyMap.set(scriptToHash(issuerLock), secp256k1PrivateKey); - const issuerCellIndex = rgbppInputsLength; const cells = rawTx.inputs.map((input, index) => ({ - outPoint: input.previousOutput, + outPoint: input.previousOutput as CKBComponents.OutPoint, lock: index >= issuerCellIndex ? issuerLock : getRgbppLockScript(isMainnet), })); @@ -416,19 +410,39 @@ export const appendIssuerCellToBtcBatchTransfer = async ({ const issuerWitnesses = rawTx.inputs.slice(rgbppInputsLength).map((_, index) => (index === 0 ? emptyWitness : '0x')); rawTx.witnesses = [...rawTx.witnesses, ...issuerWitnesses]; - const transactionHash = rawTransactionToHash(rawTx); - const signedWitnesses = signWitnesses(keyMap)({ - transactionHash, - witnesses: rawTx.witnesses, - inputCells: cells, - skipMissingKeys: true, + return { ckbRawTx: rawTx, inputCells: cells }; +}; + +/** + * Append paymaster cell to the ckb transaction inputs and sign the transaction with paymaster cell's secp256k1 private key + * @param secp256k1PrivateKey The Secp256k1 private key of the paymaster cells maintainer + * @param issuerAddress The issuer ckb address + * @param collector The collector that collects CKB live cells and transactions + * @param ckbRawTx CKB raw transaction + * @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee + * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet + * @param ckbFeeRate The CKB transaction fee rate, default value is 1100 + */ +export const appendIssuerCellToBtcBatchTransfer = async ({ + secp256k1PrivateKey, + issuerAddress, + collector, + ckbRawTx, + sumInputsCapacity, + isMainnet, + ckbFeeRate, +}: AppendIssuerCellToBtcBatchTransfer): Promise => { + const { ckbRawTx: rawTx, inputCells } = await appendIssuerCellToBtcBatchTransferToSign({ + issuerAddress, + collector, + ckbRawTx, + sumInputsCapacity, + ckbFeeRate, + isMainnet, }); - const signedTx = { - ...rawTx, - witnesses: signedWitnesses.map((witness) => - typeof witness !== 'string' ? serializeWitnessArgs(witness) : witness, - ), - }; - return signedTx; + const keyMap = new Map(); + keyMap.set(addressToScriptHash(issuerAddress), secp256k1PrivateKey); + + return signCkbTransaction(keyMap, rawTx, inputCells, true); }; diff --git a/packages/ckb/src/spore/spore.ts b/packages/ckb/src/spore/spore.ts index 60364ad0..4ad76222 100644 --- a/packages/ckb/src/spore/spore.ts +++ b/packages/ckb/src/spore/spore.ts @@ -13,6 +13,8 @@ import { generateSporeId, generateSporeTransferCoBuild, throwErrorWhenSporeCellsInvalid, + addressToScriptHash, + signCkbTransaction, } from '../utils'; import { AppendIssuerCellToSporeCreate, @@ -41,15 +43,7 @@ import { RgbppUtxoBindMultiTypeAssetsError, TypeAssetNotSupportedError, } from '../error'; -import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses'; -import { - addressToScript, - bytesToHex, - getTransactionSize, - rawTransactionToHash, - scriptToHash, - serializeWitnessArgs, -} from '@nervosnetwork/ckb-sdk-utils'; +import { addressToScript, bytesToHex, getTransactionSize } from '@nervosnetwork/ckb-sdk-utils'; /** * Generate the virtual ckb transaction for creating spores @@ -211,25 +205,17 @@ export const buildAppendingIssuerCellToSporesCreateTx = async ({ return rawTx; }; -/** - * Append paymaster cell to the ckb transaction inputs and sign the transaction with paymaster cell's secp256k1 private key - * @param secp256k1PrivateKey The Secp256k1 private key of the paymaster cells maintainer - * @param issuerAddress The issuer ckb address - * @param collector The collector that collects CKB live cells and transactions - * @param ckbRawTx CKB raw transaction - * @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee - * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet(see btcTestnetType for details about BTC Testnet) - * @param ckbFeeRate(Optional) The CKB transaction fee rate, default value is 1100 - */ -export const appendIssuerCellToSporesCreate = async ({ - secp256k1PrivateKey, +export const appendIssuerCellToSporesCreateUnsignedTx = async ({ + ckbRawTx, + isMainnet, issuerAddress, collector, - ckbRawTx, sumInputsCapacity, - isMainnet, ckbFeeRate, -}: AppendIssuerCellToSporeCreate): Promise => { +}: Omit): Promise<{ + ckbRawTx: CKBComponents.RawTransactionToSign; + inputCells: { outPoint: CKBComponents.OutPoint; lock: CKBComponents.Script }[]; +}> => { const rgbppInputsLength = ckbRawTx.inputs.length; const rawTx = await buildAppendingIssuerCellToSporesCreateTx({ @@ -244,12 +230,9 @@ export const appendIssuerCellToSporesCreate = async ({ const issuerLock = addressToScript(issuerAddress); - const keyMap = new Map(); - keyMap.set(scriptToHash(issuerLock), secp256k1PrivateKey); - const issuerCellIndex = rgbppInputsLength; const cells = rawTx.inputs.map((input, index) => ({ - outPoint: input.previousOutput, + outPoint: input.previousOutput as CKBComponents.OutPoint, lock: index >= issuerCellIndex ? issuerLock : getRgbppLockScript(isMainnet), })); @@ -264,21 +247,40 @@ export const appendIssuerCellToSporesCreate = async ({ rawTx.witnesses[lastRawTxWitnessIndex], ]; - const transactionHash = rawTransactionToHash(rawTx); - const signedWitnesses = signWitnesses(keyMap)({ - transactionHash, - witnesses: rawTx.witnesses, - inputCells: cells, - skipMissingKeys: true, + return { ckbRawTx: rawTx, inputCells: cells }; +}; + +/** + * Append paymaster cell to the ckb transaction inputs and sign the transaction with paymaster cell's secp256k1 private key + * @param secp256k1PrivateKey The Secp256k1 private key of the paymaster cells maintainer + * @param issuerAddress The issuer ckb address + * @param collector The collector that collects CKB live cells and transactions + * @param ckbRawTx CKB raw transaction + * @param sumInputsCapacity The sum capacity of ckb inputs which is to be used to calculate ckb tx fee + * @param isMainnet True is for BTC and CKB Mainnet, false is for BTC and CKB Testnet(see btcTestnetType for details about BTC Testnet) + * @param ckbFeeRate(Optional) The CKB transaction fee rate, default value is 1100 + */ +export const appendIssuerCellToSporesCreate = async ({ + secp256k1PrivateKey, + issuerAddress, + collector, + ckbRawTx, + sumInputsCapacity, + isMainnet, + ckbFeeRate, +}: AppendIssuerCellToSporeCreate): Promise => { + const { ckbRawTx: rawTx, inputCells } = await appendIssuerCellToSporesCreateUnsignedTx({ + ckbRawTx, + isMainnet, + issuerAddress, + collector, + sumInputsCapacity, + ckbFeeRate, }); - const signedTx = { - ...rawTx, - witnesses: signedWitnesses.map((witness) => - typeof witness !== 'string' ? serializeWitnessArgs(witness) : witness, - ), - }; - return signedTx; + const keyMap = new Map(); + keyMap.set(addressToScriptHash(issuerAddress), secp256k1PrivateKey); + return signCkbTransaction(keyMap, rawTx, inputCells, true); }; /** diff --git a/packages/ckb/src/utils/ckb-tx.ts b/packages/ckb/src/utils/ckb-tx.ts index 8aca55d3..d205751b 100644 --- a/packages/ckb/src/utils/ckb-tx.ts +++ b/packages/ckb/src/utils/ckb-tx.ts @@ -1,4 +1,15 @@ -import { PERSONAL, blake2b, hexToBytes, serializeInput, serializeScript } from '@nervosnetwork/ckb-sdk-utils'; +import { + PERSONAL, + addressToScript, + blake2b, + hexToBytes, + rawTransactionToHash, + scriptToHash, + serializeInput, + serializeScript, + serializeWitnessArgs, +} from '@nervosnetwork/ckb-sdk-utils'; +import signWitnesses from '@nervosnetwork/ckb-sdk-core/lib/signWitnesses'; import { RawClusterData, packRawClusterData, SporeDataProps, packRawSporeData } from '@spore-sdk/core'; import { remove0x, u64ToLe } from './hex'; import { @@ -234,3 +245,35 @@ export const checkCkbTxInputsCapacitySufficient = async ( .reduce((prev, current) => prev + current, BigInt(0)); return sumInputsCapacity > sumOutputsCapacity; }; + +export function signCkbTransaction( + key: string | Map, + ckbTx: CKBComponents.RawTransactionToSign, + inputCells: { outPoint: CKBComponents.OutPoint; lock: CKBComponents.Script }[] = [], + skipMissingKeys = false, +) { + if (key instanceof Map && inputCells.length === 0) { + throw new Error('inputCells must not be empty when using Map of keys'); + } + + const transactionHash = rawTransactionToHash(ckbTx); + const signedWitnesses = signWitnesses(key)({ + transactionHash, + witnesses: ckbTx.witnesses, + inputCells, + skipMissingKeys, + }); + + // Serialize the witness args if needed to ensure all witnesses are consistently in string format + return { + ...ckbTx, + witnesses: signedWitnesses.map((witness) => + typeof witness !== 'string' ? serializeWitnessArgs(witness) : witness, + ), + }; +} + +export const addressToScriptHash = (address: string) => { + const script = addressToScript(address); + return scriptToHash(script); +};