From a375abd8db7a7b87bcc7842296af39e831488b7e Mon Sep 17 00:00:00 2001 From: Keith Date: Tue, 5 Nov 2019 23:18:25 +0800 Subject: [PATCH] feat(core): add the method of generateDaoWithdrawTransaction --- packages/ckb-sdk-core/__tests__/fixtures.json | 122 ++++++++++++++++++ packages/ckb-sdk-core/__tests__/index.test.js | 42 ++++++ .../examples/depositAndWithdraw.js | 102 +++++++++++++++ .../src/generateRawTransaction.ts | 31 +++-- packages/ckb-sdk-core/src/index.ts | 118 ++++++++++++++++- packages/ckb-sdk-core/src/loadCells.ts | 1 + packages/ckb-sdk-core/types/global.d.ts | 1 + 7 files changed, 399 insertions(+), 18 deletions(-) create mode 100644 packages/ckb-sdk-core/examples/depositAndWithdraw.js diff --git a/packages/ckb-sdk-core/__tests__/fixtures.json b/packages/ckb-sdk-core/__tests__/fixtures.json index 9fe43e08..f09c9c3c 100644 --- a/packages/ckb-sdk-core/__tests__/fixtures.json +++ b/packages/ckb-sdk-core/__tests__/fixtures.json @@ -418,5 +418,127 @@ "witnesses": [] } } + }, + "generateDaoDepositTransaction": { + "params": { + "fromAddress": "ckt1qyqw975zuu9svtyxgjuq44lv7mspte0n2tmqa703cd", + "capacity": "0x174876e800", + "fee": "0x186a0", + "cells": [ + { + "blockHash": "0xf15ea2efcee00a41afadabbb71d4ac4b614e602628d6d542bde9084f1c8b7107", + "lock": { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + }, + "outPoint": { + "txHash": "0x2da368a15fc31bbf0108bfb119b10a50742d9607530c64ca6f6f41d79621772e", + "index": "0x0" + }, + "capacity": "0x2f766e9925", + "dataHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "status": "live", + "type": null + } + ] + }, + "expected": { + "version": "0x0", + "cellDeps": [ + { + "outPoint": { + "txHash": "0xcb77d6dd01abde6dde8cd3fffaa9811399309ae47e18162096b7ae45e5e69f14", + "index": "0x0" + }, + "depType": "depGroup" + }, + { + "outPoint": { + "txHash": "0xb5724acb4f5f82afb717c3ec3fe025d3b6e45ff48f4ffbb6162c950399cbcabe", + "index": "0x2" + }, + "depType": "code" + } + ], + "headerDeps": [], + "inputs": [ + { + "previousOutput": { + "txHash": "0x2da368a15fc31bbf0108bfb119b10a50742d9607530c64ca6f6f41d79621772e", + "index": "0x0" + }, + "since": "0x0" + } + ], + "outputs": [ + { + "capacity": "0x174876e800", + "lock": { + "hashType": "type", + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + }, + "type": { + "codeHash": "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", + "args": "0x", + "hashType": "type" + } + }, + { + "capacity": "0x182df62a85", + "lock": { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + } + ], + "witnesses": [{ "lock": "", "inputType": "", "outputType": "" }], + "outputsData": ["0x0000000000000000", "0x"] + } + }, + "generateDaoWithdrawStartTransaction": { + "params": { + "outPoint": { + "txHash": "0xe5f1f754d0f032edf073187d2b69c6d1b81cf91528f2aaf2d2aa658db4b556fd", + "index": "0x0" + }, + "fee": "1000", + "cells": [ + { + "blockHash": "0x7790fbe20bea5037c753caa8ee3c5a7937687b32bdcf73ebfa8abafb8108ffd4", + "lock": { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + }, + "outPoint": { + "txHash": "0xe5f1f754d0f032edf073187d2b69c6d1b81cf91528f2aaf2d2aa658db4b556fd", + "index": "0x1" + }, + "capacity": "0x182df62a85", + "dataHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "status": "live", + "type": null + }, + { + "blockHash": "0xb8171c6953e55f56b92935b554fe2923358452970a143bdcb539a19ee0b30447", + "lock": { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + }, + "outPoint": { + "txHash": "0x0e8d950607252dc0beaf879c2fd8ee989f28d8a5309d35061424212be1c07eb6", + "index": "0x0" + }, + "capacity": "0x2f765892a5", + "dataHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "status": "live", + "type": null + } + ] + } } } diff --git a/packages/ckb-sdk-core/__tests__/index.test.js b/packages/ckb-sdk-core/__tests__/index.test.js index 806b067c..4bca8749 100644 --- a/packages/ckb-sdk-core/__tests__/index.test.js +++ b/packages/ckb-sdk-core/__tests__/index.test.js @@ -130,4 +130,46 @@ describe('ckb-core', () => { } }) }) + + describe('nervos dao', () => { + beforeEach(() => { + core.config.secp256k1Dep = { + hashType: 'type', + codeHash: '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8', + outPoint: { + txHash: '0xcb77d6dd01abde6dde8cd3fffaa9811399309ae47e18162096b7ae45e5e69f14', + index: '0x0', + }, + } + core.config.daoDep = { + hashType: 'type', + codeHash: '0x516be0333273bbe12a723f3be583c524f0b6089326f89c49fc61e24d1f56be21', + typeHash: '0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e', + outPoint: { + txHash: '0xb5724acb4f5f82afb717c3ec3fe025d3b6e45ff48f4ffbb6162c950399cbcabe', + index: '0x2', + }, + } + }) + it('generate deposit transaction', async () => { + const { params, expected } = fixtures.generateDaoDepositTransaction + const tx = await core.generateDaoDepositTransaction({ + fromAddress: params.fromAddress, + capacity: BigInt(params.capacity), + fee: BigInt(params.fee), + cells: params.cells, + }) + expect(tx).toEqual(expected) + }) + + it.skip('generate start withdraw transaction', async () => { + const { params, expected } = fixtures.generateDaoWithdrawStartTransaction + const tx = await core.generateDaoWithdrawStartTransaction({ + outPoint: params.outPoint, + fee: BigInt(params.fee), + cells: params.cells, + }) + expect(tx).toEqual(expected) + }) + }) }) diff --git a/packages/ckb-sdk-core/examples/depositAndWithdraw.js b/packages/ckb-sdk-core/examples/depositAndWithdraw.js new file mode 100644 index 00000000..4a3cd217 --- /dev/null +++ b/packages/ckb-sdk-core/examples/depositAndWithdraw.js @@ -0,0 +1,102 @@ +/* eslint-disable */ +const Core = require('../lib').default +const nodeUrl = process.env.NODE_URL || 'http://localhost:8114' // example node url + +const core = new Core(nodeUrl) + +const privateKey = process.env.PRIV_KEY || '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' // example private key + +const address = core.utils.privateKeyToAddress(privateKey, { + prefix: 'ckb' +}) + +const depositTxHash = '' +const startWithdrawTxHash = '' + +/** + * @description deposit a cell to Nervos DAO + */ +const deposit = async () => { + const currentEpoch = await core.rpc.getCurrentEpoch() + console.log(`current epoch: ${JSON.stringify(currentEpoch, null, 2)}`) + + const rawDepositTx = await core.generateDaoDepositTransaction({ + fromAddress: address, + capacity: 600000000000n, + fee: 10000000n, + cells: [] + }) + + const signedDepositTx = core.signTransaction(privateKey)(rawDepositTx) + + const txHash = await core.rpc.sendTransaction(signedDepositTx) + console.log(`deposit tx hash: ${txHash}`) + return txHash +} + +/** + * @description inspect the deposit transaction + */ +const inspectDepositTx = async () => { + const tx = await core.rpc.getTransaction(depositTxHash) + console.log(JSON.stringify(tx, null, 2)) +} + +/** + * @description start to withdraw a Nervos DAO cell + */ +const startWithdraw = async () => { + const outPoint = { + txHash: depositTxHash, + index: '0x0' + } + const currentBlockNumber = await core.rpc.getTipBlockNumber() + const liveCell = await core.rpc.getLiveCell(outPoint, false) + const lockScript = liveCell.cell.output.lock + const lockHash = core.utils.scriptToHash(lockScript) + await core.loadCells({ + lockHash, + start: BigInt(+currentBlockNumber - 50), + end: currentBlockNumber, + save: true + }) + + const rawStartWithdrawTx = await core.generateDaoWithdrawStartTransaction({ + outPoint, + fee: BigInt(100000) + }) + const signedStartWithdrawTx = core.signTransaction(privateKey)(rawStartWithdrawTx) + const txHash = await core.rpc.sendTransaction(signedStartWithdrawTx) + console.log(`start withdraw tx hash: ${txHash}`) + return txHash +} + +/** + * @description withdraw a Nervos DAO cell + */ +const withdraw = async () => { + const depositOutPoint = { + txHash: depositTxHash, + index: '0x0' + } + const withdrawOutPoint = { + txHash: startWithdrawTxHash, + index: '0x0' + } + const rawWithdrawTx = await core.generateDaoWithdrawTransaction({ + depositOutPoint, + withdrawOutPoint, + fee: 1000, + }) + + const signedWithdrawTx = core.signTransaction(privateKey)(rawWithdrawTx) + const txHash = await core.rpc.sendTransaction(signedWithdrawTx) + console.log(`withdraw tx hash: ${txHash}`) + return txHash +} + + +// deposit() +// inspectDepositTx() +// startWithdraw() +// withdraw() diff --git a/packages/ckb-sdk-core/src/generateRawTransaction.ts b/packages/ckb-sdk-core/src/generateRawTransaction.ts index 6050c6fd..2133c11a 100644 --- a/packages/ckb-sdk-core/src/generateRawTransaction.ts +++ b/packages/ckb-sdk-core/src/generateRawTransaction.ts @@ -10,7 +10,8 @@ const generateRawTransaction = ({ safeMode = true, cells: unspentCells = [], deps, - cellCapacityThreshold = BigInt(61_00_000_000), + capacityThreshold = BigInt(61_00_000_000), + changeThreshold = BigInt(61_00_000_000), }: { fromPublicKeyHash: string toPublicKeyHash: string @@ -19,7 +20,8 @@ const generateRawTransaction = ({ safeMode: boolean cells?: CachedCell[] deps: DepCellInfo - cellCapacityThreshold?: bigint | string | number + capacityThreshold?: bigint | string | number + changeThreshold?: bigint | string | number }): CKBComponents.RawTransactionToSign => { if (!deps) { throw new Error('The deps is not loaded') @@ -29,19 +31,24 @@ const generateRawTransaction = ({ throw new HexStringShouldStartWith0x(capacity) } - if (typeof cellCapacityThreshold === 'string' && !cellCapacityThreshold.startsWith('0x')) { - throw new HexStringShouldStartWith0x(cellCapacityThreshold) + if (typeof capacityThreshold === 'string' && !capacityThreshold.startsWith('0x')) { + throw new HexStringShouldStartWith0x(capacityThreshold) + } + + if (typeof changeThreshold === 'string' && !changeThreshold.startsWith('0x')) { + throw new HexStringShouldStartWith0x(changeThreshold) } const targetCapacity = BigInt(capacity) - const realFee = BigInt(fee) - const threshold = BigInt(cellCapacityThreshold) + const targetFee = BigInt(fee) + const minCapacity = BigInt(capacityThreshold) + const minChange = BigInt(changeThreshold) - if (targetCapacity < threshold) { - throw new Error(`Capacity should not be less than ${threshold} shannon`) + if (targetCapacity < minCapacity) { + throw new Error(`Capacity should not be less than ${minCapacity} shannon`) } - const costCapacity = targetCapacity + realFee + threshold + const costCapacity = targetCapacity + targetFee + minChange const lockScript = { codeHash: deps.codeHash, @@ -79,7 +86,7 @@ const generateRawTransaction = ({ */ for (let i = 0; i < unspentCells.length; i++) { const unspentCell = unspentCells[i] - if (!safeMode || unspentCell.dataHash === EMPTY_DATA_HASH) { + if (!safeMode || (unspentCell.dataHash === EMPTY_DATA_HASH && !unspentCell.type)) { inputs.push({ previousOutput: unspentCell.outPoint, since: '0x0', @@ -93,8 +100,8 @@ const generateRawTransaction = ({ if (inputCapacity < costCapacity) { throw new Error('Input capacity is not enough') } - if (inputCapacity > targetCapacity + realFee) { - changeOutput.capacity = inputCapacity - targetCapacity - realFee + if (inputCapacity > targetCapacity + targetFee) { + changeOutput.capacity = inputCapacity - targetCapacity - targetFee } /** diff --git a/packages/ckb-sdk-core/src/index.ts b/packages/ckb-sdk-core/src/index.ts index a4e880a7..a0482a54 100644 --- a/packages/ckb-sdk-core/src/index.ts +++ b/packages/ckb-sdk-core/src/index.ts @@ -220,6 +220,8 @@ class Core { safeMode = true, cells = [], deps, + capacityThreshold, + changeThreshold, }: { fromAddress: string toAddress: string @@ -228,6 +230,8 @@ class Core { safeMode?: boolean cells?: CachedCell[] deps: DepCellInfo + capacityThreshold?: string | bigint + changeThreshold?: string | bigint }) => { const [fromPublicKeyHash, toPublicKeyHash] = [fromAddress, toAddress].map((addr: string) => { const addressPayload = this.utils.parseAddress(addr, 'hex') @@ -258,10 +262,12 @@ class Core { safeMode, cells: availableCells, deps, + capacityThreshold, + changeThreshold, }) } - public generateDaoDepositTx = async ({ + public generateDaoDepositTransaction = async ({ fromAddress, capacity, fee, @@ -270,7 +276,7 @@ class Core { fromAddress: string, capacity: bigint, fee: bigint, - cells: CachedCell[], + cells?: CachedCell[], }) => { if (!this.config.daoDep) { await this.loadDaoDep() @@ -301,16 +307,17 @@ class Core { rawTx.cellDeps.push({ outPoint: this.config.daoDep!.outPoint, - depType: 'depGroup', + depType: 'code', }) rawTx.witnesses.unshift({ lock: '', inputType: '', outputType: '' }) return rawTx } - public generateDaoWithdrawStartTx = async ({ outPoint, fee }: { + public generateDaoWithdrawStartTransaction = async ({ outPoint, fee, cells = [] }: { outPoint: CKBComponents.OutPoint, - fee: bigint | string + fee: bigint | string, + cells?: CachedCell[] }) => { if (!this.config.secp256k1Dep) { await this.loadSecp256k1Dep() @@ -338,11 +345,16 @@ class Core { fee, safeMode: true, deps: this.config.secp256k1Dep!, + capacityThreshold: BigInt(0), + cells, }) + rawTx.outputs.splice(0, 1) + rawTx.outputsData.splice(0, 1) + rawTx.inputs.unshift({ previousOutput: outPoint, since: '0x0' }) rawTx.outputs.unshift(tx.transaction.outputs[+outPoint.index]) - rawTx.cellDeps.push({ outPoint: this.config.daoDep!.outPoint, depType: 'depGroup' }) + rawTx.cellDeps.push({ outPoint: this.config.daoDep!.outPoint, depType: 'code' }) rawTx.headerDeps.push(depositBlockHeader.hash) rawTx.outputsData.unshift(encodedBlockNumber) rawTx.witnesses.unshift({ @@ -352,6 +364,100 @@ class Core { }) return rawTx } + + public generateDaoWithdrawTransaction = async ({ + depositOutPoint, + withdrawOutPoint, + fee, + }: { + depositOutPoint: CKBComponents.OutPoint + withdrawOutPoint: CKBComponents.OutPoint + fee: bigint | string + }): Promise => { + if (!this.config.secp256k1Dep) { + await this.loadSecp256k1Dep() + } + if (!this.config.daoDep) { + await this.loadDaoDep() + } + + const DAO_LOCK_PERIOD_EPOCHS = 180 + const cellStatus = await this.rpc.getLiveCell(withdrawOutPoint, true) + if (cellStatus.status !== 'live') throw new Error('Cell is not live yet') + + const tx = await this.rpc.getTransaction(withdrawOutPoint.txHash) + if (tx.txStatus.status !== 'committed') throw new Error('Transaction is not committed yet') + + /* eslint-disable */ + const depositBlockNumber = +this.utils.bytesToHex(this.utils.hexToBytes(cellStatus.cell.data?.content ?? '').reverse()) + /* eslint-enable */ + + const depositBlockHeader = await this.rpc.getBlockByNumber(BigInt(depositBlockNumber)).then(block => block.header) + const depositEpoch = this.utils.parseEpoch(depositBlockHeader.epoch) + + const withdrawBlockHeader = await this.rpc.getBlock(tx.txStatus.blockHash).then(block => block.header) + const withdrawEpoch = this.utils.parseEpoch(withdrawBlockHeader.epoch) + + const withdrawFraction = withdrawEpoch.index * withdrawEpoch.length + const depositFraction = depositEpoch.index * depositEpoch.length + let depositedEpochs = withdrawEpoch.number - depositEpoch.number + if (withdrawFraction > depositFraction) { + depositedEpochs += 1 + } + + const lockEpochs = Math.floor((depositedEpochs + DAO_LOCK_PERIOD_EPOCHS - 1) / 10) * 10 + const minimalSinceEpochNumber = depositEpoch.number + lockEpochs + const minimalSinceEpochIndex = depositEpoch.index + const minimalSinceEpochLength = depositEpoch.length + + const minimalSince = this.epochSince(minimalSinceEpochLength, minimalSinceEpochIndex, minimalSinceEpochNumber) + const outputCapacity = await this.rpc.calculateDaoMaximumWithdraw(depositOutPoint, withdrawBlockHeader.hash) + const targetCapacity = BigInt(outputCapacity) + const targetFee = BigInt(fee) + if (targetCapacity < targetFee) { + throw new Error(`The fee(${targetFee}) is too big that withdraw(${targetCapacity}) is not enough`) + } + + const outputs: CKBComponents.CellOutput[] = [ + { + capacity: `0x${(targetCapacity - targetFee).toString(16)}`, + lock: tx.transaction.outputs[+withdrawOutPoint.index].lock, + }, + ] + + const outputsData = ['0x'] + + return { + version: '0x0', + cellDeps: [ + { outPoint: this.config.secp256k1Dep!.outPoint, depType: 'depGroup' }, + { outPoint: this.config.daoDep!.outPoint, depType: 'code' }, + ], + headerDeps: [ + depositBlockHeader.hash, + withdrawBlockHeader.hash, + ], + inputs: [ + { + previousOutput: withdrawOutPoint, + since: minimalSince, + }, + ], + outputs, + outputsData, + witnesses: [{ + lock: '', + inputType: '0x0000000000000000', + outputType: '', + }], + } + } + + private epochSince = (length: number, index: number, number: number) => + '0x20' + + `${length.toString(16).padStart(4, '0')}` + + `${index.toString(16).padStart(4, '0')}` + + `${number.toString(16).padStart(6, '0')}` } export default Core diff --git a/packages/ckb-sdk-core/src/loadCells.ts b/packages/ckb-sdk-core/src/loadCells.ts index e37cd14d..994c56d4 100644 --- a/packages/ckb-sdk-core/src/loadCells.ts +++ b/packages/ckb-sdk-core/src/loadCells.ts @@ -64,6 +64,7 @@ const loadCells = async ({ ...digest, dataHash: cellDetailInRange[idx].cell.data!.hash, status: cellDetailInRange[idx].status, + type: cellDetailInRange[idx].cell.output.type, })) .filter(cell => cell.status === 'live') ) diff --git a/packages/ckb-sdk-core/types/global.d.ts b/packages/ckb-sdk-core/types/global.d.ts index 56cf3371..a9186262 100644 --- a/packages/ckb-sdk-core/types/global.d.ts +++ b/packages/ckb-sdk-core/types/global.d.ts @@ -8,6 +8,7 @@ interface DepCellInfo { interface CachedCell extends CKBComponents.CellIncludingOutPoint { status: string dataHash: string + type: CKBComponent.Script | null } interface TransactionBuilderInitParams {