diff --git a/packages/ckb-sdk-core/__tests__/index.test.js b/packages/ckb-sdk-core/__tests__/index.test.js index 64534542..f2b8bfd2 100644 --- a/packages/ckb-sdk-core/__tests__/index.test.js +++ b/packages/ckb-sdk-core/__tests__/index.test.js @@ -176,4 +176,131 @@ describe('ckb', () => { expect(tx).toEqual(expected) }) }) + + describe('calculate dao maximum withdraw', () => { + it('normal withdraw hash', async () => { + ckb.rpc = { + getTransaction: jest.fn().mockResolvedValueOnce({ + txStatus: { status: 'committed' }, + transaction: { + outputs: [ + { + "capacity":"0xe8d4a51000", + "lock":{ + "args":"0xf601cac75568afec3b9c9af1e1ff730062007685", + "codeHash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType":"type" + }, + "type":{ + "args":"0x", + "codeHash":"0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", + "hashType":"type" + } + } + ], + outputsData: ["0x0000000000000000","0x"] + } + }), + getHeader: jest.fn() + .mockResolvedValueOnce({ dao: '0x1aaf2ca6847c223c3ef9e8c069c9250020212a6311e2d30200609349396eb407' }) + .mockResolvedValueOnce({ dao: '0x9bafffa73e432e3c94c6f9db34cb25009f9e4efe4b5fd60200ea63c6d4ffb407' }) + } + const res = await ckb.calculateDaoMaximumWithdraw({ tx: '', index: '0x0' }, '') + expect(res).toBe('0xe8df95141e') + ckb.rpc = rpc + }) + it('another normal withdraw hash', async () => { + ckb.rpc = { + getTransaction: jest.fn().mockResolvedValueOnce({ + "transaction":{ + "outputs":[ + { + "capacity":"0x6fc23ac00", + "lock":{ + "args":"0x4cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6aa10000001000000030000000990000009cdfb2824302e0cd0ee1fb4ac9849c8c2348ab84f2e7d2c6e12e8b6f5f5f378d69000000100000003000000031000000deec13a7b8e100579541384ccaf4b5223733e4a5483c3aec95ddc4c1d5ea5b2201340000004cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6a153ab336340f7985b8b9e412b7968fedabd69a9cb0040000000000c0", + "codeHash":"0x5a2506bb68d81a11dcadad4cb7eae62a17c43c619fe47ac8037bc8ce2dd90360", + "hashType":"type" + }, + "type":null + }, + { + "capacity":"0x83e607de09", + "lock":{ + "args":"0x2718f00d61e6fb37eed98cfdf6b30bde38cad8f6", + "codeHash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType":"type" + }, + "type":null + } + ], + "outputsData":[ + "0x", + "0x" + ], + }, + "txStatus":{ + "blockHash":"0x8c14f374c5934613dbb0f9cbebb0edaabe259291a08e2dd3afbb291b1b8be359", + "reason":null, + "status":"committed" + } + }), + getHeader: jest.fn() + .mockResolvedValueOnce({ dao: '0x7e9a4d29f532433cb8dbe56b64ce2500b4c9c67a3fccda020098a1f224a4b707' }) + .mockResolvedValueOnce({ dao: '0xd87090655733433c2395d67a64ce2500d73e963e54ccda0200f244c504a4b707' }) + } + const res = await ckb.calculateDaoMaximumWithdraw({index: '0x0', txHash: '0xabd4f1b9e914cd859cb7ecf4f57009ef7cd2d84a799ed61acff904bdf5fea91a'}, '0x04914c83fa9ea4126279ebe2d2cdff74235f63227821882e4e16f6a908f43691') + expect(res).toBe('0x6fc23ac9b') + ckb.rpc = rpc + }) + it('normal withdraw outputpoint', async () => { + ckb.rpc = { + getTransaction: jest.fn().mockResolvedValue({ + "transaction":{ + "outputs":[ + { + "capacity":"0x6fc23ac00", + "lock":{ + "args":"0x4cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6aa10000001000000030000000990000009cdfb2824302e0cd0ee1fb4ac9849c8c2348ab84f2e7d2c6e12e8b6f5f5f378d69000000100000003000000031000000deec13a7b8e100579541384ccaf4b5223733e4a5483c3aec95ddc4c1d5ea5b2201340000004cc2e6526204ae6a2e8fcf12f7ad472f41a1606d5b9624beebd215d780809f6a153ab336340f7985b8b9e412b7968fedabd69a9cb0040000000000c0", + "codeHash":"0x5a2506bb68d81a11dcadad4cb7eae62a17c43c619fe47ac8037bc8ce2dd90360", + "hashType":"type" + }, + "type":null + }, + { + "capacity":"0x83e607de09", + "lock":{ + "args":"0x2718f00d61e6fb37eed98cfdf6b30bde38cad8f6", + "codeHash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType":"type" + }, + "type":null + } + ], + "outputsData":[ + "0x", + "0x" + ], + }, + "txStatus":{ + "blockHash":"0x8c14f374c5934613dbb0f9cbebb0edaabe259291a08e2dd3afbb291b1b8be359", + "reason":null, + "status":"committed" + } + }), + getHeader: jest.fn() + .mockResolvedValueOnce({ dao: '0x7e9a4d29f532433cb8dbe56b64ce2500b4c9c67a3fccda020098a1f224a4b707' }) + .mockResolvedValueOnce({ dao: '0xd87090655733433c2395d67a64ce2500d73e963e54ccda0200f244c504a4b707' }) + } + const res = await ckb.calculateDaoMaximumWithdraw({index: '0x0', txHash: '0xabd4f1b9e914cd859cb7ecf4f57009ef7cd2d84a799ed61acff904bdf5fea91a'}, '0x04914c83fa9ea4126279ebe2d2cdff74235f63227821882e4e16f6a908f43691') + expect(res).toBe('0x6fc23ac9b') + ckb.rpc = rpc + }) + it('exception', async () => { + ckb.rpc = { + getTransaction: jest.fn().mockResolvedValueOnce({ txStatus: {}}) + } + await expect(ckb.calculateDaoMaximumWithdraw({ tx: '', index: '0x0' }, '')).rejects.toThrow('Transaction is not committed yet') + ckb.rpc = rpc + }) + }) }) diff --git a/packages/ckb-sdk-core/src/index.ts b/packages/ckb-sdk-core/src/index.ts index 96662a38..fe42a28a 100644 --- a/packages/ckb-sdk-core/src/index.ts +++ b/packages/ckb-sdk-core/src/index.ts @@ -420,6 +420,37 @@ class CKB { } } + public calculateDaoMaximumWithdraw = async ( + depositOutPoint: CKBComponents.OutPoint, + withdraw: CKBComponents.Hash | CKBComponents.OutPoint + ): Promise => { + let tx = await this.rpc.getTransaction(depositOutPoint.txHash) + if (tx.txStatus.status !== 'committed') throw new Error('Transaction is not committed yet') + const depositBlockHash = tx.txStatus.blockHash + let celloutput = tx.transaction.outputs[+depositOutPoint.index] + let celloutputData = tx.transaction.outputsData[+depositOutPoint.index] + let withdrawBlockHash: CKBComponents.Hash + if (typeof withdraw === 'string') { + withdrawBlockHash = withdraw + } else { + tx = await this.rpc.getTransaction(withdraw.txHash) + if (tx.txStatus.status !== 'committed') throw new Error('Transaction is not committed yet') + withdrawBlockHash = tx.txStatus.blockHash + celloutput = tx.transaction.outputs[+withdraw.index] + celloutputData = tx.transaction.outputsData[+withdraw.index] + } + const [depositHeader, withDrawHeader] = await Promise.all([ + this.rpc.getHeader(depositBlockHash), + this.rpc.getHeader(withdrawBlockHash) + ]) + return utils.calculateMaximumWithdraw( + celloutput, + celloutputData, + depositHeader.dao, + withDrawHeader.dao + ) + } + #secp256k1DepsShouldBeReady = () => { if (!this.config.secp256k1Dep) { throw new ParameterRequiredException('Secp256k1 dep') diff --git a/packages/ckb-sdk-utils/__tests__/convertors/index.test.js b/packages/ckb-sdk-utils/__tests__/convertors/index.test.js index 7b5ec2b0..34a3407a 100644 --- a/packages/ckb-sdk-utils/__tests__/convertors/index.test.js +++ b/packages/ckb-sdk-utils/__tests__/convertors/index.test.js @@ -1,4 +1,4 @@ -const { toUint16Le, toUint32Le, toUint64Le, hexToBytes, bytesToHex } = require('../../lib/convertors') +const { toUint16Le, toUint32Le, toUint64Le, hexToBytes, bytesToHex, toBigEndian } = require('../../lib/convertors') const { HexStringWithout0xException } = require('../../lib/exceptions') const { @@ -68,3 +68,7 @@ describe('bytes to hex', () => { expect(bytesToHex(bytes)).toEqual(expected) }) }) + +describe('to big endian', () => { + expect(toBigEndian('0x3ef9e8c069c92500')).toBe('0x0025c969c0e8f93e') +}) \ No newline at end of file diff --git a/packages/ckb-sdk-utils/__tests__/utils/index.test.js b/packages/ckb-sdk-utils/__tests__/utils/index.test.js index c236a4b2..c818e614 100644 --- a/packages/ckb-sdk-utils/__tests__/utils/index.test.js +++ b/packages/ckb-sdk-utils/__tests__/utils/index.test.js @@ -1,4 +1,4 @@ -const { privateKeyToPublicKey, privateKeyToAddress, scriptToHash, rawTransactionToHash } = require('../..') +const { privateKeyToPublicKey, privateKeyToAddress, scriptToHash, rawTransactionToHash, calculateMaximumWithdraw, extractDAOData } = require('../..') const exceptions = require('../../lib/exceptions') const rawTransactionToHashFixtures = require('./rawTransactionToHash.fixtures.json') @@ -82,3 +82,34 @@ describe('privateKeyToAddress', () => { }), ).toBe(fixture.testnetAddress) }) + +describe('calculate-maximum-withdraw', () => { + const outputCell = { + "capacity":"0xe8d4a51000", + "lock":{ + "args":"0xf601cac75568afec3b9c9af1e1ff730062007685", + "codeHash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType":"type" + }, + "type":{ + "args":"0x", + "codeHash":"0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", + "hashType":"type" + } + } + expect( + calculateMaximumWithdraw( + outputCell, + '0x0000000000000000', + '0x1aaf2ca6847c223c3ef9e8c069c9250020212a6311e2d30200609349396eb407', + '0x9bafffa73e432e3c94c6f9db34cb25009f9e4efe4b5fd60200ea63c6d4ffb407' + )).toBe('0xe8df95141e') +}) + +describe('extract header dao', () => { + const DAOData = extractDAOData('0x1aaf2ca6847c223c3ef9e8c069c9250020212a6311e2d30200609349396eb407') + expect(DAOData.c).toBe('0x3c227c84a62caf1a') + expect(DAOData.ar).toBe('0x0025c969c0e8f93e') + expect(DAOData.s).toBe('0x02d3e211632a2120') + expect(DAOData.u).toBe('0x07b46e3949936000') +}) \ No newline at end of file diff --git a/packages/ckb-sdk-utils/__tests__/utils/occupiedCapacity.test.js b/packages/ckb-sdk-utils/__tests__/utils/occupiedCapacity.test.js new file mode 100644 index 00000000..7741fb25 --- /dev/null +++ b/packages/ckb-sdk-utils/__tests__/utils/occupiedCapacity.test.js @@ -0,0 +1,51 @@ +const { scriptOccupied, cellOccupied } = require('../..') + +describe('script occupied', () => { + it('no args', () => { + const occupied = scriptOccupied({ + args: '0x', + codeHash: '0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e', + hashType: 'type' + }) + expect(occupied).toBe(33) + }) + it('with args', () => { + const occupied = scriptOccupied({ + args: '0x00ffee', + codeHash: '0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e', + hashType: 'type' + }) + expect(occupied).toBe(36) + }) +}) + + +describe('cell occupied', () => { + it('no type', () => { + const occupied = cellOccupied({ + capacity: '0xe8d4a51000', + lock: { + args: '0x', + codeHash: '0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e', + hashType: 'type' + }, + }) + expect(occupied).toBe(41) + }) + it('with type', () => { + const occupied = cellOccupied({ + capacity: '0xe8d4a51000', + lock: { + args: '0x', + codeHash: '0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e', + hashType: 'type' + }, + type: { + args: '0x00ff', + codeHash: '0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e', + hashType: 'type' + } + }) + expect(occupied).toBe(76) + }) +}) \ No newline at end of file diff --git a/packages/ckb-sdk-utils/src/convertors/index.ts b/packages/ckb-sdk-utils/src/convertors/index.ts index bb3e9eaa..b80c850a 100644 --- a/packages/ckb-sdk-utils/src/convertors/index.ts +++ b/packages/ckb-sdk-utils/src/convertors/index.ts @@ -1,4 +1,4 @@ -import { assertToBeHexStringOrBigint } from '../validators' +import { assertToBeHexStringOrBigint, assertToBeHexString } from '../validators' import { HexStringWithout0xException } from '../exceptions' /** @@ -61,6 +61,19 @@ export const hexToBytes = (rawhex: string | number | bigint) => { return new Uint8Array(bytes) } +/** + * Converts a hex string in little endian into big endian + * + * @memberof convertors + * @param {string} le16 The hex string to convert + * @returns {string} Returns a big endian + */ + export const toBigEndian = (leHex: string) => { + assertToBeHexString(leHex) + const bytes = hexToBytes(leHex); + return `0x${bytes.reduceRight((pre, cur) => pre + cur.toString(16).padStart(2, '0'), '')}` +} + export const bytesToHex = (bytes: Uint8Array): string => `0x${[...bytes].map(b => b.toString(16).padStart(2, '0')).join('')}` @@ -70,4 +83,5 @@ export default { toUint64Le, hexToBytes, bytesToHex, + toBigEndian } diff --git a/packages/ckb-sdk-utils/src/index.ts b/packages/ckb-sdk-utils/src/index.ts index c8843628..3326cacb 100644 --- a/packages/ckb-sdk-utils/src/index.ts +++ b/packages/ckb-sdk-utils/src/index.ts @@ -1,10 +1,11 @@ import JSBI from 'jsbi' import ECPair from './ecpair' -import { hexToBytes } from './convertors' +import { hexToBytes, toBigEndian } from './convertors' import { pubkeyToAddress, AddressOptions } from './address' -import { ParameterRequiredException } from './exceptions' +import { ParameterRequiredException, HexStringWithout0xException } from './exceptions' import crypto from './crypto' -import { serializeScript } from './serialization/script' +import { serializeScript } from './serialization' +import { cellOccupied } from './occupiedCapacity' import { serializeRawTransaction, serializeTransaction, serializeWitnessArgs } from './serialization/transaction' import { PERSONAL } from './const' @@ -13,6 +14,7 @@ export * from './serialization' export * from './convertors' export * from './epochs' export * from './sizes' +export * from './occupiedCapacity' export * as systemScripts from './systemScripts' export * as reconcilers from './reconcilers' @@ -44,3 +46,43 @@ export const privateKeyToPublicKey = (privateKey: string) => { export const privateKeyToAddress = (privateKey: string, options: AddressOptions) => pubkeyToAddress(privateKeyToPublicKey(privateKey), options) + + +export const extractDAOData = (dao: CKBComponents.DAO) => { + if (!dao.startsWith('0x')) { + throw new HexStringWithout0xException(dao) + } + const value = dao.replace('0x', '') + return { + c: toBigEndian(`0x${value.slice(0, 16)}`), + ar: toBigEndian(`0x${value.slice(16, 32)}`), + s: toBigEndian(`0x${value.slice(32, 48)}`), + u: toBigEndian(`0x${value.slice(48, 64)}`) + } +} + +export const calculateMaximumWithdraw = ( + outputCell: CKBComponents.CellOutput, + outputDataCapacity: CKBComponents.Bytes, + depositDAO: CKBComponents.DAO, + withdrawDAO: CKBComponents.DAO +) => { + const depositCellSerialized = cellOccupied(outputCell) + outputDataCapacity.slice(2).length / 2 + const occupiedCapacity = JSBI.asUintN( + 128, + JSBI.multiply(JSBI.BigInt(100000000), JSBI.BigInt(depositCellSerialized)) + ) + return `0x${JSBI.add( + JSBI.divide( + JSBI.multiply( + JSBI.subtract( + JSBI.asUintN(128, JSBI.BigInt(outputCell.capacity)), + occupiedCapacity + ), + JSBI.asUintN(128, JSBI.BigInt(extractDAOData(withdrawDAO).ar)) + ), + JSBI.asUintN(128, JSBI.BigInt(extractDAOData(depositDAO).ar)) + ), + occupiedCapacity + ).toString(16)}` +} diff --git a/packages/ckb-sdk-utils/src/occupiedCapacity.ts b/packages/ckb-sdk-utils/src/occupiedCapacity.ts new file mode 100644 index 00000000..45729d92 --- /dev/null +++ b/packages/ckb-sdk-utils/src/occupiedCapacity.ts @@ -0,0 +1,10 @@ +const codeHashOccupied = 32 +const hashTypeOccupied = 1 + +export const scriptOccupied = (script: CKBComponents.Script) => { + return script.args.slice(2).length / 2 + codeHashOccupied + hashTypeOccupied +} + +export const cellOccupied = (cell: CKBComponents.CellOutput) => { + return 8 + scriptOccupied(cell.lock) + (cell.type ? scriptOccupied(cell.type) : 0) +} \ No newline at end of file