diff --git a/packages/ckb-sdk-core/__tests__/ckb-core.test.js b/packages/ckb-sdk-core/__tests__/ckb-core.test.js index dc34d0be..a7ff5607 100644 --- a/packages/ckb-sdk-core/__tests__/ckb-core.test.js +++ b/packages/ckb-sdk-core/__tests__/ckb-core.test.js @@ -1,35 +1,86 @@ +const successFixtures = require('./successFixtures.json') +const exceptionFixtures = require('./exceptionFixtures.json') + const Core = require('../lib').default const url = 'http://localhost:8114' const core = new Core(url) describe('ckb-core', () => { - const fixture = { - empty: { - codeHash: '', - outPoint: { - blockHash: '', - cell: { - txHash: '', - index: '0', - }, - }, - }, - target: { - codeHash: '9e3b3557f11b2b3532ce352bfe8017e9fd11d154c4c7f9b7aaaa1e621b539a08', - outPoint: { - blockHash: 'aad9b82caa07f5989dfb8caa44927f0bab515a96ccaaceba82c7bea609fec205', - cell: { - txHash: 'bffab7ee0a050e2cb882de066d3dbf3afdd8932d6a26eda44f06e4b23f0f4b5a', - index: '1', - }, - }, - }, - } - it('load the system cell', async () => { - expect(core.config.systemCellInfo).toEqual(fixture.empty) - - const systemCellInfo = await core.loadSystemCell() - expect(systemCellInfo).toEqual(fixture.target) + describe('success', () => { + it('load the system cell', async () => { + const fixture = successFixtures.loadSystemCell + expect(core.config.systemCellInfo).toEqual(fixture.emptyInfo) + + const systemCellInfo = await core.loadSystemCell() + expect(systemCellInfo).toEqual(fixture.target) + }) + + it('sign witnesses', () => { + const fixture = successFixtures.signWitnesses + const signedWitnessesByPrivateKey = core.signWitnesses(fixture.privateKey)(fixture.message) + expect(signedWitnessesByPrivateKey).toEqual(fixture.target) + + const signedWitnessesByAddressObject = core.signWitnesses(core.generateAddress(fixture.privateKey))( + fixture.message + ) + expect(signedWitnessesByAddressObject).toEqual(fixture.target) + }) + + it('sign transaction', async () => { + const fixture = successFixtures.signTransaction + const signedTransactionWithPrivateKey = await core.signTransaction(fixture.privateKey)(fixture.transaction) + const signedTransactionWithAddressObj = await core.signTransaction(core.generateAddress(fixture.privateKey))( + fixture.transaction + ) + expect(signedTransactionWithPrivateKey).toEqual(fixture.target) + expect(signedTransactionWithAddressObj).toEqual(fixture.target) + }) + }) + + describe('exceptions', () => { + describe('sign witneses', () => { + it('throw an error when key is missing', () => { + const fixture = exceptionFixtures.signWitnessesWithoutKey + expect(() => core.signWitnesses(fixture.privateKey)(fixture.message)).toThrowError(fixture.exception) + }) + + it('throw an error when transaction hash is missing', () => { + const fixture = exceptionFixtures.signWitnessesWithoutTransactionHash + expect(() => core.signWitnesses(fixture.privateKey)(fixture.message)).toThrowError(fixture.exception) + }) + }) + + describe('sign transaction', () => { + it('throw an error when key is missing', () => { + const fixture = exceptionFixtures.signTransactionWithoutKey + expect(core.signTransaction(fixture.privateKey)(fixture.transaction)).rejects.toEqual( + new Error(fixture.exception) + ) + }) + + it('throw an error when trasnaction is missing', () => { + const fixture = exceptionFixtures.signTransactionWithoutTransaction + expect(core.signTransaction(fixture.privateKey)(fixture.transaction)).rejects.toEqual( + new Error(fixture.exception) + ) + }) + + it('throw an error when witnesses is missing', () => { + const fixture = exceptionFixtures.signTransactionWithoutWitnesses + console.log(!fixture.transaction.witnesses) + expect(core.signTransaction(fixture.privateKey)(fixture.transaction)).rejects.toEqual( + new Error(fixture.exception) + ) + }) + + it('throw an error with invalid cound of witnesses', () => { + const fixture = exceptionFixtures.signTransactionWithInvalidCountOfWitnesses + console.log(!fixture.transaction.witnesses) + expect(core.signTransaction(fixture.privateKey)(fixture.transaction)).rejects.toEqual( + new Error(fixture.exception) + ) + }) + }) }) }) diff --git a/packages/ckb-sdk-core/__tests__/exceptionFixtures.json b/packages/ckb-sdk-core/__tests__/exceptionFixtures.json new file mode 100644 index 00000000..4f7cc657 --- /dev/null +++ b/packages/ckb-sdk-core/__tests__/exceptionFixtures.json @@ -0,0 +1,156 @@ +{ + "signWitnessesWithoutKey": { + "privateKey": null, + "message": { + "transactionHash": "0xac1bb95455cdfb89b6e977568744e09b6b80e08cab9477936a09c4ca07f5b8ab", + "witnesses": [ + { + "data": [] + } + ] + }, + "exception": "Private key or address object is required" + }, + + "signWitnessesWithoutTransactionHash": { + "privateKey": "0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3", + "message": { + "transactionHash": null, + "witnesses": [ + { + "data": [] + } + ] + }, + "exception": "Transaction hash is required" + }, + + "signTransactionWithoutKey": { + "privateKey": null, + "transaction": { + "deps": [ + { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "inputs": [ + { + "previousOutput": { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "since": "0" + } + ], + "outputs": [ + { + "capacity": "5000000000000", + "data": "0x", + "lock": { + "args": [], + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "type": null + } + ], + "version": "0", + "witnesses": [{ "data": [] }] + }, + "exception": "Private key or address object is required" + }, + + "signTransactionWithoutTransaction": { + "privateKey": "0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3", + "transaction": null, + "exception": "Transaction is required" + }, + + "signTransactionWithoutWitnesses": { + "privateKey": "0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3", + "transaction": { + "deps": [ + { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "inputs": [ + { + "previousOutput": { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "since": "0" + } + ], + "outputs": [ + { + "capacity": "5000000000000", + "data": "0x", + "lock": { + "args": [], + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "type": null + } + ], + "version": "0", + "witnesses": null + }, + "exception": "Witnesses is required" + }, + + "signTransactionWithInvalidCountOfWitnesses": { + "privateKey": "0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3", + "transaction": { + "deps": [ + { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "inputs": [ + { + "previousOutput": { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "since": "0" + } + ], + "outputs": [ + { + "capacity": "5000000000000", + "data": "0x", + "lock": { + "args": [], + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "type": null + } + ], + "version": "0", + "witnesses": [] + }, + "exception": "Invalid count of witnesses" + } +} diff --git a/packages/ckb-sdk-core/__tests__/successFixtures.json b/packages/ckb-sdk-core/__tests__/successFixtures.json new file mode 100644 index 00000000..68fa8913 --- /dev/null +++ b/packages/ckb-sdk-core/__tests__/successFixtures.json @@ -0,0 +1,127 @@ +{ + "loadSystemCell": { + "emptyInfo": { + "codeHash": "", + "outPoint": { + "blockHash": "", + "cell": { + "txHash": "", + "index": "0" + } + } + }, + "target": { + "codeHash": "f1951123466e4479842387a66fabfd6b65fc87fd84ae8e6cd3053edb27fff2fd", + "outPoint": { + "blockHash": "fc3ad90e38598032598c90b4ad4fefb420bacabaa7c5b40111daca7dfcc1f9d4", + "cell": { + "txHash": "3f09b95f8886723cc850db0beb9c153169151c663f3e8f832dc04421fbb1f382", + "index": "1" + } + } + } + }, + + "signWitnesses": { + "privateKey": "0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3", + "message": { + "transactionHash": "0xac1bb95455cdfb89b6e977568744e09b6b80e08cab9477936a09c4ca07f5b8ab", + "witnesses": [ + { + "data": [] + } + ] + }, + "target": [ + { + "data": [ + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ead3d901330bc01", + "0x304402202c643579e47045be050d3842ed9270151af8885e33954bddad0e53e81d1c2dbe02202dc637877a8302110846ebc6a16d9148c106e25f945063ad1c4d4db2b6952408" + ] + } + ] + }, + + "signTransaction": { + "privateKey": "0xe79f3207ea4980b7fed79956d5934249ceac4751a4fae01a0f7c4a96884bc4e3", + "transaction": { + "deps": [ + { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "inputs": [ + { + "previousOutput": { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "since": "0" + } + ], + "outputs": [ + { + "capacity": "5000000000000", + "data": "0x", + "lock": { + "args": [], + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "type": null + } + ], + "version": "0", + "witnesses": [{ "data": [] }] + }, + "target": { + "deps": [ + { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "inputs": [ + { + "previousOutput": { + "cell": { + "txHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "index": "4294967295" + }, + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + }, + "since": "0" + } + ], + "outputs": [ + { + "capacity": "5000000000000", + "data": "0x", + "lock": { + "args": [], + "codeHash": "0x0000000000000000000000000000000000000000000000000000000000000001" + }, + "type": null + } + ], + "version": "0", + "witnesses": [ + { + "data": [ + "0x024a501efd328e062c8675f2365970728c859c592beeefd6be8ead3d901330bc01", + "0x3045022100aa659c1d2920d144b76e4d03eb4d0c56b22e66501c68bb330bc58b5af1ba411c02203728e526386d4186955049405a669a9ce63ec0e3b233ca7696f9a5d95651a256" + ] + } + ] + } + } +} diff --git a/packages/ckb-sdk-core/examples/sendTransaction.js b/packages/ckb-sdk-core/examples/sendTransaction.js index 84215173..5d00178f 100644 --- a/packages/ckb-sdk-core/examples/sendTransaction.js +++ b/packages/ckb-sdk-core/examples/sendTransaction.js @@ -37,6 +37,7 @@ const bootstrap = async () => { * - value, the address string * - privateKey, the private key in hex string format * - publicKey, the public key in hex string format + * - identifier, the identifier of the public key, a blake160-ed public key is use here * - sign(msg): signature string * - verify(msg, signature): boolean */ @@ -48,19 +49,14 @@ const bootstrap = async () => { /** * calculate the lockhash by the address - * 1. a blake160-ed public key is required in the args field of lock script + * 1. the identifier of the address is required in the args field of lock script * 2. compose the lock script with SYSTEM_ENCRYPTION_CODE_HASH, and args * 3. calculate the hash of lock script */ - const blake160edPublicKey = core.utils.blake160(myAddressObj.publicKey, 'hex') - /** - * to see the blake160-ed public key - */ - // console.log(blake160edPublicKey) const script = { codeHash: SYSTEM_ENCRYPTION_CODE_HASH, - args: [blake160edPublicKey], + args: [`0x${myAddressObj.idenfitier}`], } /** * to see the lock script @@ -130,11 +126,11 @@ const bootstrap = async () => { // .then(console.log) /** - * @notice fill the blaked160ed public key in the output's args, + * @notice fill the blaked160ed public key as the identifier of the target address in the output's args, * which is used to specify the next owner of the output, namely the fresh cell. * @notice use bigint or big number to handle the capacity for safety */ - const generateTransaction = async (targetBlake160edPublicKey, capacity) => { + const generateTransaction = async (targetIdentifier, capacity) => { const targetCapacity = BigInt(capacity) /** @@ -144,7 +140,7 @@ const bootstrap = async () => { capacity: targetCapacity, lock: { codeHash: SYSTEM_ENCRYPTION_CODE_HASH, - args: [targetBlake160edPublicKey], + args: [targetIdentifier], }, data: '0x', } @@ -156,7 +152,7 @@ const bootstrap = async () => { capacity: 0n, lock: { codeHash: SYSTEM_ENCRYPTION_CODE_HASH, - args: [`0x${blake160edPublicKey}`], + args: [`0x${myAddressObj.idenfitier}`], }, data: '0x', } @@ -193,27 +189,21 @@ const bootstrap = async () => { version: '0', deps: [SYSTEM_ENCRYPTION_OUT_POINT], inputs, - outputs: - changeOutput.capacity > 0n ? [ - { - ...targetOutput, - capacity: targetOutput.capacity.toString(), - }, - { - ...changeOutput, - capacity: changeOutput.capacity.toString(), - }, - ] : [ - { - ...targetOutput, - capacity: targetOutput.capacity.toString(), - }, - ], - witnesses: [ + outputs: changeOutput.capacity > 0n ? [{ + ...targetOutput, + capacity: targetOutput.capacity.toString(), + }, { - data: [], + ...changeOutput, + capacity: changeOutput.capacity.toString(), }, - ], + ] : [{ + ...targetOutput, + capacity: targetOutput.capacity.toString(), + }, ], + witnesses: [{ + data: [], + }, ], } return tx } @@ -225,38 +215,17 @@ const bootstrap = async () => { // console.log(JSON.stringify(tx, null, 2)) // }) - /** - * sign the transaction hash and then compute the witness which will fill the witnesses field in the transaction - * to know more about witness and segwit - * @link https://www.wikiwand.com/en/SegWit - */ - const fillTransactionWithWitnesses = async () => { - const tx = await generateTransaction(`0x${blake160edPublicKey}`, 6000000000) // generate the raw transaction with empty witnesses - const txHash = await core.rpc.computeTransactionHash(tx) // get transaction hash - const signedWitnesses = tx.witnesses.map(witness => { // sign witnesses - const oldData = witness.data || [] - const s = blake2b(32, null, null, core.utils.PERSONAL) - s.update(core.utils.hexToBytes(txHash.replace(/^0x/, ''))) - oldData.forEach(datum => { - s.update(core.utils.hexToBytes(datum)) - }) - const message = s.digest('hex') - const data = [myAddressObj.publicKey, myAddressObj.sign(message), ...oldData] - return data - }) - tx.witnesses = signedWitnesses - return tx - } - /** * send transaction */ - const tx = await fillTransactionWithWitnesses() + const tx = await generateTransaction(`0x${myAddressObj.idenfitier}`, 6000000000) // generate the raw transaction with empty witnesses + const signedTx = await core.signTransaction(myAddressObj)(tx) /** - * to see the real transaction, (slightly differs from the previous one which used to calculate the transaction hash) + * to see the signed transaction */ - // console.log(JSON.stringify(tx, null, 2)) - const realTxHash = await core.rpc.sendTransaction(tx) + // console.log(JSON.stringify(signedTx, null, 2)) + + const realTxHash = await core.rpc.sendTransaction(signedTx) /** * to see the real transaction hash */ diff --git a/packages/ckb-sdk-core/src/index.ts b/packages/ckb-sdk-core/src/index.ts index 2680a0b7..0dbc64d0 100644 --- a/packages/ckb-sdk-core/src/index.ts +++ b/packages/ckb-sdk-core/src/index.ts @@ -96,6 +96,52 @@ class Core { } return this.config.systemCellInfo } + + public signWitnesses = (key: string | Address) => ({ + transactionHash, + witnesses = [], + }: { + transactionHash: string + witnesses: CKBComponents.Witness[] + }) => { + if (!key) throw new Error('Private key or address object is required') + if (!transactionHash) throw new Error('Transaction hash is required') + + const addrObj = typeof key === 'string' ? this.generateAddress(key) : key + const signedWitnesses = witnesses.map(witness => { + const oldData = witness.data || [] + const s = this.utils.blake2b(32, null, null, this.utils.PERSONAL) + s.update(this.utils.hexToBytes(transactionHash.replace(/^0x/, ''))) + oldData.forEach(datum => { + s.update(this.utils.hexToBytes(datum)) + }) + const message = s.digest('hex') + const data = [`0x${addrObj.publicKey}`, `0x${addrObj.sign(message)}`, ...oldData] + return { + data, + } + }) + return signedWitnesses + } + + public signTransaction = (key: string | Address) => async (transaction: CKBComponents.RawTransaction) => { + if (!key) throw new Error('Private key or address object is required') + if (!transaction) throw new Error('Transaction is required') + if (!transaction.witnesses) throw new Error('Witnesses is required') + if (transaction.witnesses.length < transaction.inputs.length) throw new Error('Invalid count of witnesses') + + const transactionHash = await (this.rpc as RPC & { computeTransactionHash: Function }).computeTransactionHash( + transaction + ) + const signedWitnesses = await this.signWitnesses(key)({ + transactionHash, + witnesses: transaction.witnesses, + }) + return { + ...transaction, + witnesses: signedWitnesses, + } + } } export default Core