From 7e2941a7cdeb85bde23b404272e5d8f59115f826 Mon Sep 17 00:00:00 2001 From: Keith Date: Wed, 1 Sep 2021 17:01:40 +0800 Subject: [PATCH 1/5] feat(utils): add full version address identified the hash_type and vm_version Add a new format of full address, which identifies the hash_type and vm_version ref https://github.com/nervosnetwork/rfcs/pull/239/ --- packages/ckb-sdk-utils/README.md | 14 ++ .../__tests__/address/fixtures.json | 126 +++++++++++++++-- .../__tests__/address/index.test.js | 19 +++ .../__tests__/exceptions/fixtures.json | 27 +++- .../__tests__/exceptions/index.test.js | 2 +- packages/ckb-sdk-utils/src/address/index.ts | 131 ++++++++++++++++-- .../ckb-sdk-utils/src/exceptions/address.ts | 34 ++++- 7 files changed, 325 insertions(+), 28 deletions(-) diff --git a/packages/ckb-sdk-utils/README.md b/packages/ckb-sdk-utils/README.md index 21fffeee..ab6b1bd7 100644 --- a/packages/ckb-sdk-utils/README.md +++ b/packages/ckb-sdk-utils/README.md @@ -16,6 +16,7 @@ See [Full Doc](https://github.com/nervosnetwork/ckb-sdk-js/blob/develop/README.m - `utils.fullPayloadToAddress`: script to full version address - `utils.parseAddress`: get address payload - `utils.addressToScript`: get lock script from address + - `utils.scriptToAddress`: get full address from script - [Utils](#utils) @@ -148,6 +149,19 @@ utils.addressToScript('ckb1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xly // } ``` +```js +/** + * @description generate full version address from script, the address conforms to format type 0x00 + * @tutorial https://github.com/nervosnetwork/rfcs/pull/239/ + */ +utils.scriptToAddress({ + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" +}) +// ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqg0yp5wq +``` + ### Utils ```plain diff --git a/packages/ckb-sdk-utils/__tests__/address/fixtures.json b/packages/ckb-sdk-utils/__tests__/address/fixtures.json index 6cd2b6b5..e725ca0a 100644 --- a/packages/ckb-sdk-utils/__tests__/address/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/address/fixtures.json @@ -3,6 +3,14 @@ "basic": { "params": ["0x36c329ed630d6ce750712a477543672adab57f4c"], "expected": [1, 0, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76] + }, + "should throw an error when its a full version address identified the hash_type and vm_version but code hash doesn't start with 0x":{ + "params": ["0x36c329ed630d6ce750712a477543672adab57f4c", "0x00", "3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356"], + "exception": "'3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356' is not a valid code hash" + }, + "should throw an error when its a full version address identified the hash_type and vm_version but code hash has invalid length":{ + "params": ["0x36c329ed630d6ce750712a477543672adab57f4c", "0x00", "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135"], + "exception": "'0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135' is not a valid code hash" } }, "fullPayloadToAddress": { @@ -28,6 +36,17 @@ ], "expected": "ckt1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xlydkr98kkxrtvuag8z2j8w4pkw2k6k4l5c02auef" }, + "data1 hash type":{ + "params": [ + { + "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101", + "type": "0x00", + "prefix": "ckt", + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356" + } + ], + "expected": "ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqsqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqg8gw2zh" + }, "default type = 0x02 and default prefix = ckb": { "params": [ { @@ -106,35 +125,55 @@ }, "data hash type full version address": { "params": ["ckb1q2da0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xw3vumhs9nvu786dj9p0q5elx66t24n3kxgdwd2q8"], - "expected": [ 2, 155, 215, 224, 111, 62, 207, 75, 224, 242, 252, 210, 24, 139, 35, 241, 185, 252, 200, 142, 93, 75, 101, 168, 99, 123, 23, 114, 59, 189, 163, 204, 232, 179, 155, 188, 11, 54, 115, 199, 211, 100, 80, 188, 20, 207, 205, 173, 45, 85, 156, 108, 100 ] + "expected": [2, 155, 215, 224, 111, 62, 207, 75, 224, 242, 252, 210, 24, 139, 35, 241, 185, 252, 200, 142, 93, 75, 101, 168, 99, 123, 23, 114, 59, 189, 163, 204, 232, 179, 155, 188, 11, 54, 115, 199, 211, 100, 80, 188, 20, 207, 205, 173, 45, 85, 156, 108, 100 ] }, "type hash type full version address": { "params": ["ckb1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xlydkr98kkxrtvuag8z2j8w4pkw2k6k4l5czfy37k"], - "expected": [ 4, 24, 146, 234, 64, 216, 43, 83, 198, 120, 255, 136, 49, 36, 80, 187, 177, 126, 22, 77, 122, 62, 10, 144, 148, 26, 165, 136, 57, 245, 111, 141, 242, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76 ] + "expected": [4, 24, 146, 234, 64, 216, 43, 83, 198, 120, 255, 136, 49, 36, 80, 187, 177, 126, 22, 77, 122, 62, 10, 144, 148, 26, 165, 136, 57, 245, 111, 141, 242, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76 ] + }, + "full version address identified the hash_type and vm_version": { + "params": ["ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgutrqyp"], + "expected": [0, 52, 25, 161, 192, 158, 178, 86, 127, 101, 82, 238, 122, 142, 207, 253, 100, 21, 92, 255, 224, 241, 121, 110, 110, 97, 236, 8, 141, 116, 12, 19, 86, 1, 0, 23, 79, 178, 190, 46, 93, 12, 26, 59, 134, 148, 248, 50, 53, 10, 51, 193, 104, 93, 71, 122, 12, 1, 1] }, "should throw an error when short version address has invalid payload size": { "params": ["ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqqm65l9j"], - "exception": "ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqqm65l9j is not a valid short version address" + "exception": "'ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqqm65l9j' is not a valid short version address" }, "should throw an error when anyone can pay address has invalid payload size": { "params": ["ckt1qyprdsefa43s6m882pcj53m4gdnj4k440axqqfmyd9c"], - "exception": "ckt1qyprdsefa43s6m882pcj53m4gdnj4k440axqqfmyd9c is not a valid short version address" + "exception": "'ckt1qyprdsefa43s6m882pcj53m4gdnj4k440axqqfmyd9c' is not a valid short version address" }, "should throw an error when address type is invalid": { "params": ["ckt1qwn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvdkr98kkxrtvuag8z2j8w4pkw2k6k4l5ctv25r2"], - "exception": "ckt1qwn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvdkr98kkxrtvuag8z2j8w4pkw2k6k4l5ctv25r2 is not a valid address" + "exception": "'ckt1qwn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvdkr98kkxrtvuag8z2j8w4pkw2k6k4l5ctv25r2' is not a valid address" }, "should throw an error when hash type is invalid": { "params": ["ckt1qwn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvdkr98kkxrtvuag8z2j8w4pkw2k6k4l5ctv25r2"], - "exception": "ckt1qwn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvdkr98kkxrtvuag8z2j8w4pkw2k6k4l5ctv25r2 is not a valid address" + "exception": "'ckt1qwn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvdkr98kkxrtvuag8z2j8w4pkw2k6k4l5ctv25r2' is not a valid address" }, "should throw an error when code hash index is invalid": { "params": ["ckt1qyzndsefa43s6m882pcj53m4gdnj4k440axqcth0hp"], - "exception": "ckt1qyzndsefa43s6m882pcj53m4gdnj4k440axqcth0hp is not a valid short version address" + "exception": "'ckt1qyzndsefa43s6m882pcj53m4gdnj4k440axqcth0hp' is not a valid short version address" }, "should throw an error when full version address has invalid size": { "params": ["ckb1qsqcjt4ypkpt20r83lugxyj9pwa30cty6737p2gfgx493qul2cgvrxhw"], - "exception": "ckb1qsqcjt4ypkpt20r83lugxyj9pwa30cty6737p2gfgx493qul2cgvrxhw is not a valid full version address" + "exception": "'ckb1qsqcjt4ypkpt20r83lugxyj9pwa30cty6737p2gfgx493qul2cgvrxhw' is not a valid full version address" + }, + "should throw an error when full version address identified the hash_type and vm_version has invalid code hash": { + "params": ["ckb1qqv6rsy7kft87e2jaeaganlavs24ellq79ukumnpasyg6aqvzdtqzukxep"], + "exception": "'ckb1qqv6rsy7kft87e2jaeaganlavs24ellq79ukumnpasyg6aqvzdtqzukxep' is not a valid address" + }, + "should throw an error when full version address identified the hash_type and vm_version has invalid hash type": { + "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqcqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgaxsc2r"], + "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqcqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgaxsc2r' is not a valid address" + }, + "should throw an error when full version address identified the hash_type and vm_version has invalid args length": { + "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqghy4uyeq"], + "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqghy4uyeq' is not a valid address" + }, + "should throw an error when full version address identified the hash_type and vm_version has args not matching args len": { + "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqz7etutjapsdrhp55lqer2z3nc95963m6psqszx2kx5s"], + "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqz7etutjapsdrhp55lqer2z3nc95963m6psqszx2kx5s' is not a valid address" } }, "addressToScript": { @@ -185,6 +224,77 @@ "hashType": "type", "args": "0x36c329ed630d6ce750712a477543672adab57f4c" } + }, + "full version address identified the hash_type and vm_version": { + "params": ["ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgutrqyp"], + "expected": { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + } + } + }, + "scriptToAddress": { + "full version mainnet address identified the hash_type and vm_version": { + "params": [ + { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + } + ], + "expected": "ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqg0yp5wq" + }, + "full version testnet address identified the hash_type and vm_version": { + "params": [ + { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + }, + false + ], + "expected": "ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgutrqyp" + }, + "should throw an error when args doesn't start with 0x": { + "params": [ + { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args":"4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + } + ], + "exception": "Hex string 4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101 should start with 0x" + }, + "should throw an error when code hash doesn't start with 0x": { + "params": [ + { + "codeHash": "3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + } + ], + "exception": "'3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356' is not a valid code hash" + }, + "should throw an error when code hash has invalid length": { + "params": [ + { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135", + "hashType": "type", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + } + ], + "exception": "'0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135' is not a valid code hash" + }, + "should throw an error when hash type is inavlid": { + "params": [ + { + "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "hashType": "type1", + "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + } + ], + "exception": "'type1' is not a valid hash type" } } } diff --git a/packages/ckb-sdk-utils/__tests__/address/index.test.js b/packages/ckb-sdk-utils/__tests__/address/index.test.js index f1adcbd2..cfb25da8 100644 --- a/packages/ckb-sdk-utils/__tests__/address/index.test.js +++ b/packages/ckb-sdk-utils/__tests__/address/index.test.js @@ -8,6 +8,7 @@ const { parseAddress, fullPayloadToAddress, addressToScript, + scriptToAddress, } = ckbUtils describe('Test address module', () => { @@ -120,4 +121,22 @@ describe('Test address module', () => { } }) }) + + describe('scriptToAddress', () => { + const fixtureTable = Object.entries(fixtures.scriptToAddress).map(([title, { params, expected, exception }]) => [ + title, + params, + expected, + exception, + ]) + test.each(fixtureTable)(`%s`, (_title, params, expected, exception) => { + expect.assertions(1) + try { + const actual = scriptToAddress(...params) + expect(actual).toEqual(expected) + } catch (err) { + expect(err).toEqual(new Error(exception)) + } + }) + }) }) diff --git a/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json b/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json index 1aa66a4a..dc484bb9 100644 --- a/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json @@ -38,14 +38,35 @@ "params": ["Invalid Payload", "short"], "expected": { "code": 104, - "message": "Invalid Payload is not a valid short version address payload" + "message": "'Invalid Payload' is not a valid short version address payload" } }, "AddressException": { - "params": ["Invalid Address", "full"], + "params": ["Invalid Address", "", "full"], "expected": { "code": 104, - "message": "Invalid Address is not a valid full version address" + "message": "'Invalid Address' is not a valid full version address" + } + }, + "CodeHashException": { + "params": ["0x"], + "expected": { + "code": 104, + "message": "'0x' is not a valid code hash" + } + }, + "HashTypeException": { + "params": ["0x03"], + "expected": { + "code": 104, + "message": "'0x03' is not a valid hash type" + } + }, + "ArgsLenException": { + "params": [""], + "expected": { + "code": 104, + "message": "'' is not a valid args length" } }, "OutLenTooSmallException": { diff --git a/packages/ckb-sdk-utils/__tests__/exceptions/index.test.js b/packages/ckb-sdk-utils/__tests__/exceptions/index.test.js index e2cd38c2..7b9577b2 100644 --- a/packages/ckb-sdk-utils/__tests__/exceptions/index.test.js +++ b/packages/ckb-sdk-utils/__tests__/exceptions/index.test.js @@ -1,7 +1,7 @@ const exceptions = require('../../lib/exceptions') const fixtures = require('./fixtures.json') -describe.only('Test exceptions', () => { +describe('Test exceptions', () => { const fixtureTable = Object.entries(fixtures).map(([exceptionName, { params, expected }]) => [ exceptionName, params, diff --git a/packages/ckb-sdk-utils/src/address/index.ts b/packages/ckb-sdk-utils/src/address/index.ts index f00dc8c9..e0964d75 100644 --- a/packages/ckb-sdk-utils/src/address/index.ts +++ b/packages/ckb-sdk-utils/src/address/index.ts @@ -6,7 +6,16 @@ import { ANYONE_CAN_PAY_TESTNET, } from '../systemScripts' import { hexToBytes, bytesToHex } from '../convertors' -import { HexStringWithout0xException, AddressException, AddressPayloadException } from '../exceptions' +import { + HexStringWithout0xException, + AddressException, + AddressPayloadException, + CodeHashException, + HashTypeException, + ArgsLenException, +} from '../exceptions' + +// TODO: deprecate outdated methods export enum AddressPrefix { Mainnet = 'ckb', @@ -14,11 +23,48 @@ export enum AddressPrefix { } export enum AddressType { + FullVersion = '0x00', // full version identified the hash_type and vm_version HashIdx = '0x01', // short version for locks with popular codehash - DataCodeHash = '0x02', // full version with hash type 'Data' - TypeCodeHash = '0x04', // full version with hash type 'Type' + DataCodeHash = '0x02', // full version with hash type 'Data', deprecated + TypeCodeHash = '0x04', // full version with hash type 'Type', deprecated +} + +const payloadToAddress = (payload: Uint8Array, isMainnet = true) => + bech32.encode(isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, bech32.toWords(payload)) + +const scriptToPayload = ({ codeHash, hashType, args }: CKBComponents.Script): Uint8Array => { + enum HashType { + data = '00', + type = '01', + data1 = '02', + } + + if (!args.startsWith('0x')) { + throw new HexStringWithout0xException(args) + } + + if (!codeHash.startsWith('0x') || codeHash.length !== 66) { + throw new CodeHashException(codeHash) + } + + if (!HashType[hashType]) { + throw new HashTypeException(hashType) + } + const argsLen = args.length / 2 - 1 + + return hexToBytes( + `0x00${codeHash.slice(2)}${HashType[hashType]}${argsLen.toString(16).padStart(4, '0')}${args.slice(2)}`, + ) } +export const scriptToAddress = (script: CKBComponents.Script, isMainnet = true) => + payloadToAddress(scriptToPayload(script), isMainnet) + +/** + * 0x00 SECP256K1 + blake160 + * 0x01 SECP256k1 + multisig + * 0x02 anyone_can_pay + */ export type CodeHashIndex = '0x00' | '0x01' | '0x02' export interface AddressOptions { @@ -40,13 +86,34 @@ export const toAddressPayload = ( type: AddressType = AddressType.HashIdx, codeHashOrCodeHashIndex: CodeHashIndex | CKBComponents.Hash256 = '0x00', ): Uint8Array => { - if (typeof args === 'string') { - if (!args.startsWith('0x')) { - throw new HexStringWithout0xException(args) - } - return new Uint8Array([...hexToBytes(type), ...hexToBytes(codeHashOrCodeHashIndex), ...hexToBytes(args)]) + if (typeof args === 'string' && !args.startsWith('0x')) { + throw new HexStringWithout0xException(args) } - return new Uint8Array([...hexToBytes(type), ...hexToBytes(codeHashOrCodeHashIndex), ...args]) + + if ([AddressType.DataCodeHash, AddressType.TypeCodeHash].includes(type)) { + /* eslint-disable max-len */ + console.warn( + `Address of 'AddressType.DataCodeHash' or 'AddressType.TypeCodeHash' is deprecated, please use address of AddressPrefix.FullVersion`, + ) + } + + if (type !== AddressType.FullVersion) { + return new Uint8Array([ + ...hexToBytes(type), + ...hexToBytes(codeHashOrCodeHashIndex), + ...(typeof args === 'string' ? hexToBytes(args) : args), + ]) + } + + if (!codeHashOrCodeHashIndex.startsWith('0x') || codeHashOrCodeHashIndex.length !== 66) { + throw new CodeHashException(codeHashOrCodeHashIndex) + } + + return scriptToPayload({ + codeHash: codeHashOrCodeHashIndex, + hashType: 'data1', + args: typeof args === 'string' ? args : bytesToHex(args), + }) } /** @@ -115,8 +182,9 @@ const isValidShortVersionPayload = (payload: Uint8Array) => { /* eslint-enable indent */ } -const isValidPayload = (payload: Uint8Array) => { - const [type, ...data] = payload +const isPayloadValid = (payload: Uint8Array) => { + const type = payload[0] + const data = payload.slice(1) /* eslint-disable indent */ switch (type) { case +AddressType.HashIdx: { @@ -130,6 +198,28 @@ const isValidPayload = (payload: Uint8Array) => { } break } + case +AddressType.FullVersion: { + const codeHash = data.slice(0, 32) + if (codeHash.length < 32) { + throw new CodeHashException(bytesToHex(codeHash)) + } + + const hashType = parseInt(data[32].toString(), 16) + if (hashType > 2) { + throw new HashTypeException(`0x${hashType.toString(16)}`) + } + + const argsLen = data.slice(33, 35) + if (argsLen.length < 2) { + throw new ArgsLenException(bytesToHex(argsLen)) + } + + if (data.slice(35).length !== +bytesToHex(argsLen)) { + throw new ArgsLenException(bytesToHex(argsLen)) + } + + break + } default: { throw new AddressPayloadException(payload) } @@ -151,9 +241,9 @@ export const parseAddress: ParseAddress = (address: string, encode: 'binary' | ' const decoded = bech32.decode(address) const payload = bech32.fromWords(new Uint8Array(decoded.words)) try { - isValidPayload(payload) + isPayloadValid(payload) } catch (err) { - throw new AddressException(address, err.type) + throw new AddressException(address, err.stack, err.type) } return encode === 'binary' ? payload : bytesToHex(payload) } @@ -162,6 +252,21 @@ export const addressToScript = (address: string): CKBComponents.Script => { const payload = parseAddress(address) const type = payload[0] + if (type === +AddressType.FullVersion) { + const HASH_TYPE: Record = { + '00': 'data', + '01': 'type', + '02': 'data1', + } + const p = bytesToHex(payload) + + const codeHash = `0x${p.substr(4, 64)}` + const hashType = HASH_TYPE[p.substr(68, 2)] + const argLen = parseInt(p.substr(70, 4), 16) + const args = `0x${p.substr(74, argLen * 2)}` + return { codeHash, hashType, args } + } + if (type === +AddressType.HashIdx) { const codeHashIndices = [ SECP256K1_BLAKE160, diff --git a/packages/ckb-sdk-utils/src/exceptions/address.ts b/packages/ckb-sdk-utils/src/exceptions/address.ts index 145be75a..4b838fba 100644 --- a/packages/ckb-sdk-utils/src/exceptions/address.ts +++ b/packages/ckb-sdk-utils/src/exceptions/address.ts @@ -6,7 +6,7 @@ export class AddressPayloadException extends Error { type: 'short' | 'full' | undefined constructor(payload: Uint8Array, type?: 'short' | 'full') { - super(`${payload} is not a valid ${type ? `${type} version ` : ''}address payload`) + super(`'${payload}' is not a valid ${type ? `${type} version ` : ''}address payload`) this.type = type } } @@ -16,13 +16,41 @@ export class AddressException extends Error { type: 'short' | 'full' | undefined - constructor(addr: string, type?: 'short' | 'full') { - super(`${addr} is not a valid ${type ? `${type} version ` : ''}address`) + constructor(addr: string, stack: string, type?: 'short' | 'full') { + super(`'${addr}' is not a valid ${type ? `${type} version ` : ''}address`) this.type = type + this.stack = stack + } +} + +export class CodeHashException extends Error { + code = ErrorCode.AddressInvalid + + constructor(codeHash: string) { + super(`'${codeHash}' is not a valid code hash`) + } +} + +export class HashTypeException extends Error { + code = ErrorCode.AddressInvalid + + constructor(hashType: string) { + super(`'${hashType}' is not a valid hash type`) + } +} + +export class ArgsLenException extends Error { + code = ErrorCode.AddressInvalid + + constructor(argsLen: string) { + super(`'${argsLen}' is not a valid args length`) } } export default { AddressPayloadException, AddressException, + CodeHashException, + HashTypeException, + ArgsLenException, } From ecf619d6b40b941620c075c3c28f09a9a56bc2b5 Mon Sep 17 00:00:00 2001 From: Keith Date: Wed, 29 Sep 2021 19:21:47 +0800 Subject: [PATCH 2/5] feat: update methods about full address of new version 1. import bech32, bech32m from an external pacakge 2. update format of the new full address --- packages/ckb-sdk-utils/README.md | 12 ++-- .../__tests__/address/fixtures.json | 65 +++++++------------ .../__tests__/exceptions/fixtures.json | 7 -- packages/ckb-sdk-utils/package.json | 1 + packages/ckb-sdk-utils/src/address/index.ts | 65 ++++++++++--------- packages/ckb-sdk-utils/src/crypto/index.ts | 4 +- .../ckb-sdk-utils/src/exceptions/address.ts | 9 --- packages/ckb-sdk-utils/src/index.ts | 2 +- yarn.lock | 5 ++ 9 files changed, 74 insertions(+), 96 deletions(-) diff --git a/packages/ckb-sdk-utils/README.md b/packages/ckb-sdk-utils/README.md index ab6b1bd7..1f467a5c 100644 --- a/packages/ckb-sdk-utils/README.md +++ b/packages/ckb-sdk-utils/README.md @@ -13,10 +13,10 @@ See [Full Doc](https://github.com/nervosnetwork/ckb-sdk-js/blob/develop/README.m - `utils.privateKeyToAddress`: get address from private key - `utils.pubkeyToAddress`: get address from public key - `utils.bech32Address`: args to short/full version address - - `utils.fullPayloadToAddress`: script to full version address + - `utils.fullPayloadToAddress`: script to full version address of obselete version, **deprecated and use `utils.scriptToAddress` instead** - `utils.parseAddress`: get address payload - `utils.addressToScript`: get lock script from address - - `utils.scriptToAddress`: get full address from script + - `utils.scriptToAddress`: get full address of new version from script - [Utils](#utils) @@ -151,15 +151,15 @@ utils.addressToScript('ckb1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xly ```js /** - * @description generate full version address from script, the address conforms to format type 0x00 + * @description generate full address of new version from script, the address conforms to format type 0x00 * @tutorial https://github.com/nervosnetwork/rfcs/pull/239/ */ utils.scriptToAddress({ - "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", "hashType": "type", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args":"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" }) -// ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqg0yp5wq +// ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4 ``` ### Utils diff --git a/packages/ckb-sdk-utils/__tests__/address/fixtures.json b/packages/ckb-sdk-utils/__tests__/address/fixtures.json index e725ca0a..0581716f 100644 --- a/packages/ckb-sdk-utils/__tests__/address/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/address/fixtures.json @@ -4,11 +4,11 @@ "params": ["0x36c329ed630d6ce750712a477543672adab57f4c"], "expected": [1, 0, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76] }, - "should throw an error when its a full version address identified the hash_type and vm_version but code hash doesn't start with 0x":{ + "should throw an error when its a full version address identifies the hash_type but code hash doesn't start with 0x": { "params": ["0x36c329ed630d6ce750712a477543672adab57f4c", "0x00", "3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356"], "exception": "'3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356' is not a valid code hash" }, - "should throw an error when its a full version address identified the hash_type and vm_version but code hash has invalid length":{ + "should throw an error when its a full version address identifies the hash_type but code hash has invalid length": { "params": ["0x36c329ed630d6ce750712a477543672adab57f4c", "0x00", "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135"], "exception": "'0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135' is not a valid code hash" } @@ -36,17 +36,6 @@ ], "expected": "ckt1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xlydkr98kkxrtvuag8z2j8w4pkw2k6k4l5c02auef" }, - "data1 hash type":{ - "params": [ - { - "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101", - "type": "0x00", - "prefix": "ckt", - "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356" - } - ], - "expected": "ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqsqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqg8gw2zh" - }, "default type = 0x02 and default prefix = ckb": { "params": [ { @@ -125,13 +114,13 @@ }, "data hash type full version address": { "params": ["ckb1q2da0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xw3vumhs9nvu786dj9p0q5elx66t24n3kxgdwd2q8"], - "expected": [2, 155, 215, 224, 111, 62, 207, 75, 224, 242, 252, 210, 24, 139, 35, 241, 185, 252, 200, 142, 93, 75, 101, 168, 99, 123, 23, 114, 59, 189, 163, 204, 232, 179, 155, 188, 11, 54, 115, 199, 211, 100, 80, 188, 20, 207, 205, 173, 45, 85, 156, 108, 100 ] + "expected": [2, 155, 215, 224, 111, 62, 207, 75, 224, 242, 252, 210, 24, 139, 35, 241, 185, 252, 200, 142, 93, 75, 101, 168, 99, 123, 23, 114, 59, 189, 163, 204, 232, 179, 155, 188, 11, 54, 115, 199, 211, 100, 80, 188, 20, 207, 205, 173, 45, 85, 156, 108, 100] }, "type hash type full version address": { "params": ["ckb1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xlydkr98kkxrtvuag8z2j8w4pkw2k6k4l5czfy37k"], - "expected": [4, 24, 146, 234, 64, 216, 43, 83, 198, 120, 255, 136, 49, 36, 80, 187, 177, 126, 22, 77, 122, 62, 10, 144, 148, 26, 165, 136, 57, 245, 111, 141, 242, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76 ] + "expected": [4, 24, 146, 234, 64, 216, 43, 83, 198, 120, 255, 136, 49, 36, 80, 187, 177, 126, 22, 77, 122, 62, 10, 144, 148, 26, 165, 136, 57, 245, 111, 141, 242, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76] }, - "full version address identified the hash_type and vm_version": { + "full version address identifies the hash_type": { "params": ["ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgutrqyp"], "expected": [0, 52, 25, 161, 192, 158, 178, 86, 127, 101, 82, 238, 122, 142, 207, 253, 100, 21, 92, 255, 224, 241, 121, 110, 110, 97, 236, 8, 141, 116, 12, 19, 86, 1, 0, 23, 79, 178, 190, 46, 93, 12, 26, 59, 134, 148, 248, 50, 53, 10, 51, 193, 104, 93, 71, 122, 12, 1, 1] }, @@ -159,21 +148,13 @@ "params": ["ckb1qsqcjt4ypkpt20r83lugxyj9pwa30cty6737p2gfgx493qul2cgvrxhw"], "exception": "'ckb1qsqcjt4ypkpt20r83lugxyj9pwa30cty6737p2gfgx493qul2cgvrxhw' is not a valid full version address" }, - "should throw an error when full version address identified the hash_type and vm_version has invalid code hash": { + "should throw an error when full version address identifies the hash_type has invalid code hash": { "params": ["ckb1qqv6rsy7kft87e2jaeaganlavs24ellq79ukumnpasyg6aqvzdtqzukxep"], "exception": "'ckb1qqv6rsy7kft87e2jaeaganlavs24ellq79ukumnpasyg6aqvzdtqzukxep' is not a valid address" }, - "should throw an error when full version address identified the hash_type and vm_version has invalid hash type": { + "should throw an error when full version address identifies the hash_type has invalid hash type": { "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqcqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgaxsc2r"], "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqcqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgaxsc2r' is not a valid address" - }, - "should throw an error when full version address identified the hash_type and vm_version has invalid args length": { - "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqghy4uyeq"], - "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqghy4uyeq' is not a valid address" - }, - "should throw an error when full version address identified the hash_type and vm_version has args not matching args len": { - "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqz7etutjapsdrhp55lqer2z3nc95963m6psqszx2kx5s"], - "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqz7etutjapsdrhp55lqer2z3nc95963m6psqszx2kx5s' is not a valid address" } }, "addressToScript": { @@ -225,43 +206,43 @@ "args": "0x36c329ed630d6ce750712a477543672adab57f4c" } }, - "full version address identified the hash_type and vm_version": { - "params": ["ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgutrqyp"], + "full version address identifies the hash_type": { + "params": ["ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4"], "expected": { - "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", "hashType": "type", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" } } }, "scriptToAddress": { - "full version mainnet address identified the hash_type and vm_version": { + "full version mainnet address identifies the hash_type": { "params": [ { - "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", "hashType": "type", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" } ], - "expected": "ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqg0yp5wq" + "expected": "ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4" }, - "full version testnet address identified the hash_type and vm_version": { + "full version testnet address identifies the hash_type": { "params": [ { - "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", "hashType": "type", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" }, false ], - "expected": "ckt1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqgqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgutrqyp" + "expected": "ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqgutnjd" }, "should throw an error when args doesn't start with 0x": { "params": [ { "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", "hashType": "type", - "args":"4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" } ], "exception": "Hex string 4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101 should start with 0x" @@ -271,7 +252,7 @@ { "codeHash": "3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", "hashType": "type", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" } ], "exception": "'3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356' is not a valid code hash" @@ -281,7 +262,7 @@ { "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135", "hashType": "type", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" } ], "exception": "'0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135' is not a valid code hash" @@ -291,7 +272,7 @@ { "codeHash": "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356", "hashType": "type1", - "args":"0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" + "args": "0x4fb2be2e5d0c1a3b8694f832350a33c1685d477a0c0101" } ], "exception": "'type1' is not a valid hash type" diff --git a/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json b/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json index dc484bb9..09ca82af 100644 --- a/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json @@ -62,13 +62,6 @@ "message": "'0x03' is not a valid hash type" } }, - "ArgsLenException": { - "params": [""], - "expected": { - "code": 104, - "message": "'' is not a valid args length" - } - }, "OutLenTooSmallException": { "params": [16, 32], "expected": { diff --git a/packages/ckb-sdk-utils/package.json b/packages/ckb-sdk-utils/package.json index a0271e6a..4d918565 100644 --- a/packages/ckb-sdk-utils/package.json +++ b/packages/ckb-sdk-utils/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@nervosnetwork/ckb-types": "0.43.0", + "bech32": "2.0.0", "elliptic": "6.5.4", "jsbi": "3.1.3", "tslib": "2.3.1" diff --git a/packages/ckb-sdk-utils/src/address/index.ts b/packages/ckb-sdk-utils/src/address/index.ts index e0964d75..a8d81f8b 100644 --- a/packages/ckb-sdk-utils/src/address/index.ts +++ b/packages/ckb-sdk-utils/src/address/index.ts @@ -1,4 +1,4 @@ -import { bech32, blake160 } from '..' +import { blake160, bech32, bech32m } from '..' import { SECP256K1_BLAKE160, SECP256K1_MULTISIG, @@ -12,9 +12,10 @@ import { AddressPayloadException, CodeHashException, HashTypeException, - ArgsLenException, } from '../exceptions' +const MAX_BECH32_LIMIT = 1023 + // TODO: deprecate outdated methods export enum AddressPrefix { @@ -23,22 +24,19 @@ export enum AddressPrefix { } export enum AddressType { - FullVersion = '0x00', // full version identified the hash_type and vm_version + FullVersion = '0x00', // full version identifies the hash_type HashIdx = '0x01', // short version for locks with popular codehash DataCodeHash = '0x02', // full version with hash type 'Data', deprecated TypeCodeHash = '0x04', // full version with hash type 'Type', deprecated } +/** + * @description payload to a full address of new version + */ const payloadToAddress = (payload: Uint8Array, isMainnet = true) => - bech32.encode(isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, bech32.toWords(payload)) + bech32m.encode(isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, bech32.toWords(payload), MAX_BECH32_LIMIT) const scriptToPayload = ({ codeHash, hashType, args }: CKBComponents.Script): Uint8Array => { - enum HashType { - data = '00', - type = '01', - data1 = '02', - } - if (!args.startsWith('0x')) { throw new HexStringWithout0xException(args) } @@ -47,16 +45,26 @@ const scriptToPayload = ({ codeHash, hashType, args }: CKBComponents.Script): Ui throw new CodeHashException(codeHash) } + enum HashType { + data = '00', + type = '01', + data1 = '02', + } + if (!HashType[hashType]) { throw new HashTypeException(hashType) } - const argsLen = args.length / 2 - 1 - return hexToBytes( - `0x00${codeHash.slice(2)}${HashType[hashType]}${argsLen.toString(16).padStart(4, '0')}${args.slice(2)}`, - ) + return hexToBytes(`0x00${codeHash.slice(2)}${HashType[hashType]}${args.slice(2)}`) } +/** + * @function scriptToAddress + * @description The only way recommended to generated a full address of new version + * @param {object} script + * @param {booealn} isMainnet + * @returns {string} address + */ export const scriptToAddress = (script: CKBComponents.Script, isMainnet = true) => payloadToAddress(scriptToPayload(script), isMainnet) @@ -117,7 +125,7 @@ export const toAddressPayload = ( } /** - * @name bech32Address + * @function bech32Address * @description generate the address by bech32 algorithm * @param args, used as the identifier of an address, usually the public key hash is used. * @param {[string]} prefix, the prefix precedes the address, default to be ckb. @@ -128,11 +136,12 @@ export const toAddressPayload = ( export const bech32Address = ( args: Uint8Array | string, { prefix = AddressPrefix.Mainnet, type = AddressType.HashIdx, codeHashOrCodeHashIndex = '0x00' }: AddressOptions = {}, -) => bech32.encode(prefix, bech32.toWords(toAddressPayload(args, type, codeHashOrCodeHashIndex))) +) => bech32.encode(prefix, bech32.toWords(toAddressPayload(args, type, codeHashOrCodeHashIndex)), MAX_BECH32_LIMIT) /** + * @deprecated * @name fullPayloadToAddress - * @description generate the address with payload in full version format. + * @description deprecated method to generate the address with payload in full version format. Use scriptToAddress instead. * @param {string} args, used as the identifier of an address. * @param {[string]} prefix, the prefix precedes the address, default to be ckb. * @param {[string]} type, used to indicate which format the address conforms to, default to be 0x02, @@ -209,15 +218,6 @@ const isPayloadValid = (payload: Uint8Array) => { throw new HashTypeException(`0x${hashType.toString(16)}`) } - const argsLen = data.slice(33, 35) - if (argsLen.length < 2) { - throw new ArgsLenException(bytesToHex(argsLen)) - } - - if (data.slice(35).length !== +bytesToHex(argsLen)) { - throw new ArgsLenException(bytesToHex(argsLen)) - } - break } default: { @@ -238,8 +238,14 @@ export declare interface ParseAddress { * e.g. 0x | 01 | 00 | e2fa82e70b062c8644b80ad7ecf6e015e5f352f6 */ export const parseAddress: ParseAddress = (address: string, encode: 'binary' | 'hex' = 'binary'): any => { - const decoded = bech32.decode(address) - const payload = bech32.fromWords(new Uint8Array(decoded.words)) + let payload: Uint8Array = new Uint8Array() + try { + const decoded = bech32.decode(address, MAX_BECH32_LIMIT) + payload = new Uint8Array(bech32.fromWords(new Uint8Array(decoded.words))) + } catch { + const decoded = bech32m.decode(address, MAX_BECH32_LIMIT) + payload = new Uint8Array(bech32m.fromWords(new Uint8Array(decoded.words))) + } try { isPayloadValid(payload) } catch (err) { @@ -262,8 +268,7 @@ export const addressToScript = (address: string): CKBComponents.Script => { const codeHash = `0x${p.substr(4, 64)}` const hashType = HASH_TYPE[p.substr(68, 2)] - const argLen = parseInt(p.substr(70, 4), 16) - const args = `0x${p.substr(74, argLen * 2)}` + const args = `0x${p.substr(70)}` return { codeHash, hashType, args } } diff --git a/packages/ckb-sdk-utils/src/crypto/index.ts b/packages/ckb-sdk-utils/src/crypto/index.ts index 40a6d188..784cbc4f 100644 --- a/packages/ckb-sdk-utils/src/crypto/index.ts +++ b/packages/ckb-sdk-utils/src/crypto/index.ts @@ -1,15 +1,17 @@ +import { bech32, bech32m } from 'bech32' import blake2b from './blake2b' -import bech32 from './bech32' import blake160 from './blake160' module.exports = { blake2b, blake160, bech32, + bech32m, } export default { blake2b, blake160, bech32, + bech32m, } diff --git a/packages/ckb-sdk-utils/src/exceptions/address.ts b/packages/ckb-sdk-utils/src/exceptions/address.ts index 4b838fba..4b93b770 100644 --- a/packages/ckb-sdk-utils/src/exceptions/address.ts +++ b/packages/ckb-sdk-utils/src/exceptions/address.ts @@ -39,18 +39,9 @@ export class HashTypeException extends Error { } } -export class ArgsLenException extends Error { - code = ErrorCode.AddressInvalid - - constructor(argsLen: string) { - super(`'${argsLen}' is not a valid args length`) - } -} - export default { AddressPayloadException, AddressException, CodeHashException, HashTypeException, - ArgsLenException, } diff --git a/packages/ckb-sdk-utils/src/index.ts b/packages/ckb-sdk-utils/src/index.ts index d444b7f8..c8843628 100644 --- a/packages/ckb-sdk-utils/src/index.ts +++ b/packages/ckb-sdk-utils/src/index.ts @@ -17,7 +17,7 @@ export * as systemScripts from './systemScripts' export * as reconcilers from './reconcilers' export { serializeScript, serializeRawTransaction, serializeTransaction, serializeWitnessArgs, JSBI, PERSONAL } -export const { blake2b, bech32, blake160 } = crypto +export const { blake2b, bech32, bech32m, blake160 } = crypto export const scriptToHash = (script: CKBComponents.Script) => { if (!script) throw new ParameterRequiredException('Script') diff --git a/yarn.lock b/yarn.lock index 39dc9e49..94cd22a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2045,6 +2045,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bech32@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355" + integrity sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg== + bech32@^1.1.2: version "1.1.4" resolved "https://registry.npmjs.org/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" From 542dfbd8cfb756427e51a233b2a70b846d2aaaaf Mon Sep 17 00:00:00 2001 From: Keith Date: Wed, 29 Sep 2021 21:49:17 +0800 Subject: [PATCH 3/5] test(utils): add more test cases about new full address format --- .../__tests__/address/fixtures.json | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/ckb-sdk-utils/__tests__/address/fixtures.json b/packages/ckb-sdk-utils/__tests__/address/fixtures.json index 0581716f..019e828d 100644 --- a/packages/ckb-sdk-utils/__tests__/address/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/address/fixtures.json @@ -206,17 +206,25 @@ "args": "0x36c329ed630d6ce750712a477543672adab57f4c" } }, - "full version address identifies the hash_type": { + "full version address identifies hash_type = type": { "params": ["ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4"], "expected": { "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", "hashType": "type", "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" } + }, + "full version address identifies hash_type = data1": { + "params": ["ckt1qzn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvq4nnw7qkdnnclfkg59uzn8umtfd2kwxceq225jvu"], + "expected": { + "codeHash": "0xa656f172b6b45c245307aeb5a7a37a176f002f6f22e92582c58bf7ba362e4176", + "hashType": "data1", + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" + } } }, "scriptToAddress": { - "full version mainnet address identifies the hash_type": { + "full version mainnet address identifies hash_type = type": { "params": [ { "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", @@ -226,7 +234,7 @@ ], "expected": "ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4" }, - "full version testnet address identifies the hash_type": { + "full version testnet address identifies hash_type = type": { "params": [ { "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", @@ -237,6 +245,27 @@ ], "expected": "ckt1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqgutnjd" }, + "full version mainnet address identifies hash_type = data1": { + "params": [ + { + "codeHash": "0xa656f172b6b45c245307aeb5a7a37a176f002f6f22e92582c58bf7ba362e4176", + "hashType": "data1", + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" + } + ], + "expected": "ckb1qzn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvq4nnw7qkdnnclfkg59uzn8umtfd2kwxceqyclaxy" + }, + "full version testnet address identifies hash_type = data1": { + "params": [ + { + "codeHash": "0xa656f172b6b45c245307aeb5a7a37a176f002f6f22e92582c58bf7ba362e4176", + "hashType": "data1", + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" + }, + false + ], + "expected": "ckt1qzn9dutjk669cfznq7httfar0gtk7qp0du3wjfvzck9l0w3k9eqhvq4nnw7qkdnnclfkg59uzn8umtfd2kwxceq225jvu" + }, "should throw an error when args doesn't start with 0x": { "params": [ { From 542d3450b5320b30196dac62954804618ebd1e39 Mon Sep 17 00:00:00 2001 From: Keith Date: Wed, 29 Sep 2021 21:54:37 +0800 Subject: [PATCH 4/5] refactor(utils): remove bech32 module and use an external package --- packages/ckb-sdk-utils/src/crypto/bech32.ts | 160 -------------------- 1 file changed, 160 deletions(-) delete mode 100644 packages/ckb-sdk-utils/src/crypto/bech32.ts diff --git a/packages/ckb-sdk-utils/src/crypto/bech32.ts b/packages/ckb-sdk-utils/src/crypto/bech32.ts deleted file mode 100644 index c7aaaf13..00000000 --- a/packages/ckb-sdk-utils/src/crypto/bech32.ts +++ /dev/null @@ -1,160 +0,0 @@ -const ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' - -const SEPARATOR = '1' - -const alphabetMap = new Map() - -for (let i = 0; i < ALPHABET.length; i++) { - const char = ALPHABET.charAt(i) - - if (alphabetMap.get(char) !== undefined) { - throw new TypeError(`${char} is ambiguous`) - } - - alphabetMap.set(char, i) -} - -const polymodStep = (values: any) => { - const b = values >> 25 - return ( - ((values & 0x1ffffff) << 5) ^ - (-((b >> 0) & 1) & 0x3b6a57b2) ^ - (-((b >> 1) & 1) & 0x26508e6d) ^ - (-((b >> 2) & 1) & 0x1ea119fa) ^ - (-((b >> 3) & 1) & 0x3d4233dd) ^ - (-((b >> 4) & 1) & 0x2a1462b3) - ) -} - -const prefixChecksum = (prefix: string) => { - let checksum = 1 - - for (let i = 0; i < prefix.length; ++i) { - const c = prefix.charCodeAt(i) - if (c < 33 || c > 126) throw new Error(`Invalid prefix (${prefix})`) - checksum = polymodStep(checksum) ^ (c >> 5) - } - - checksum = polymodStep(checksum) - - for (let i = 0; i < prefix.length; ++i) { - const v = prefix.charCodeAt(i) - checksum = polymodStep(checksum) ^ (v & 0x1f) - } - - return checksum -} - -export const encode = (prefix: string, words: Uint8Array) => { - const formattedPrefix = prefix.toLowerCase() - - // determine checksum mod - let checksum = prefixChecksum(formattedPrefix) - - let result = `${formattedPrefix}${SEPARATOR}` - - for (let i = 0; i < words.length; ++i) { - const x = words[i] - if (x >> 5 !== 0) throw new Error('Non 5-bit word') - - checksum = polymodStep(checksum) ^ x - - result += ALPHABET.charAt(x) - } - - for (let i = 0; i < 6; ++i) { - checksum = polymodStep(checksum) - } - - checksum ^= 1 - - for (let i = 0; i < 6; ++i) { - const v = (checksum >> ((5 - i) * 5)) & 0x1f - result += ALPHABET.charAt(v) - } - - return result -} - -export const decode = (encoded: string) => { - const lowered = encoded.toLowerCase() - - const uppered = encoded.toUpperCase() - - if (encoded !== lowered && encoded !== uppered) throw new Error(`Mixed-case string ${encoded}`) - - const str = lowered - - if (str.length < 8) throw new TypeError(`${str} too short`) - - const split = str.lastIndexOf(SEPARATOR) - - if (split === -1) throw new Error(`No separator character for ${str}`) - - if (split === 0) throw new Error(`Missing prefix for ${str}`) - - const prefix = str.slice(0, split) - - const wordChars = str.slice(split + 1) - - if (wordChars.length < 6) throw new Error('Data too short') - - let checksum = prefixChecksum(prefix) - - const words: number[] = [] - - wordChars.split('').forEach((_, i) => { - const c = wordChars.charAt(i) - const v = alphabetMap.get(c) - if (v === undefined) throw new Error(`Unknown character ${c}`) - checksum = polymodStep(checksum) ^ v - if (i + 6 < wordChars.length) { - words.push(v) - } - }) - - if (checksum !== 1) throw new Error(`Invalid checksum for ${str}`) - return { - prefix, - words, - } -} - -const convert = (data: Uint8Array, inBits: number, outBits: number, pad: boolean): Uint8Array => { - let value = 0 - let bits = 0 - const maxV = (1 << outBits) - 1 - - const result = [] - for (let i = 0; i < data.length; ++i) { - value = (value << inBits) | data[i] - bits += inBits - - while (bits >= outBits) { - bits -= outBits - result.push((value >> bits) & maxV) - } - } - - if (pad) { - if (bits > 0) { - result.push((value << (outBits - bits)) & maxV) - } - } else { - if (bits >= inBits) throw new Error('Excess padding') - if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding') - } - - return new Uint8Array(result) -} - -export const toWords = (bytes: Uint8Array) => convert(bytes, 8, 5, true) - -export const fromWords = (words: Uint8Array) => convert(words, 5, 8, false) - -export default { - decode, - encode, - toWords, - fromWords, -} From 949b8b531d12148b1fee9a40f19118b4f5a138f0 Mon Sep 17 00:00:00 2001 From: Keith Date: Thu, 30 Sep 2021 02:08:17 +0800 Subject: [PATCH 5/5] fix: hashType is required in toAddressPayload when address type is 0x00 --- .../ckb-sdk-utils/__tests__/address/fixtures.json | 8 ++++++++ packages/ckb-sdk-utils/src/address/index.ts | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/ckb-sdk-utils/__tests__/address/fixtures.json b/packages/ckb-sdk-utils/__tests__/address/fixtures.json index 019e828d..53fbc3fd 100644 --- a/packages/ckb-sdk-utils/__tests__/address/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/address/fixtures.json @@ -4,6 +4,10 @@ "params": ["0x36c329ed630d6ce750712a477543672adab57f4c"], "expected": [1, 0, 54, 195, 41, 237, 99, 13, 108, 231, 80, 113, 42, 71, 117, 67, 103, 42, 218, 181, 127, 76] }, + "full address of new version specifies hash_type = type": { + "params": ["0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64", "0x00", "0xa656f172b6b45c245307aeb5a7a37a176f002f6f22e92582c58bf7ba362e4176", "data1"], + "expected": [0, 166, 86, 241, 114, 182, 180, 92, 36, 83, 7, 174, 181, 167, 163, 122, 23, 111, 0, 47, 111, 34, 233, 37, 130, 197, 139, 247, 186, 54, 46, 65, 118, 2, 179, 155, 188, 11, 54, 115, 199, 211, 100, 80, 188, 20, 207, 205, 173, 45, 85, 156, 108, 100] + }, "should throw an error when its a full version address identifies the hash_type but code hash doesn't start with 0x": { "params": ["0x36c329ed630d6ce750712a477543672adab57f4c", "0x00", "3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356"], "exception": "'3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c1356' is not a valid code hash" @@ -11,6 +15,10 @@ "should throw an error when its a full version address identifies the hash_type but code hash has invalid length": { "params": ["0x36c329ed630d6ce750712a477543672adab57f4c", "0x00", "0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135"], "exception": "'0x3419a1c09eb2567f6552ee7a8ecffd64155cffe0f1796e6e61ec088d740c135' is not a valid code hash" + }, + "should throw an error when its a full version address identifies the hash_type but hash_type is missing": { + "params": ["0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64", "0x00", "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8"], + "exception": "hashType is required" } }, "fullPayloadToAddress": { diff --git a/packages/ckb-sdk-utils/src/address/index.ts b/packages/ckb-sdk-utils/src/address/index.ts index a8d81f8b..bedd6abf 100644 --- a/packages/ckb-sdk-utils/src/address/index.ts +++ b/packages/ckb-sdk-utils/src/address/index.ts @@ -12,6 +12,7 @@ import { AddressPayloadException, CodeHashException, HashTypeException, + ParameterRequiredException, } from '../exceptions' const MAX_BECH32_LIMIT = 1023 @@ -34,7 +35,7 @@ export enum AddressType { * @description payload to a full address of new version */ const payloadToAddress = (payload: Uint8Array, isMainnet = true) => - bech32m.encode(isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, bech32.toWords(payload), MAX_BECH32_LIMIT) + bech32m.encode(isMainnet ? AddressPrefix.Mainnet : AddressPrefix.Testnet, bech32m.toWords(payload), MAX_BECH32_LIMIT) const scriptToPayload = ({ codeHash, hashType, args }: CKBComponents.Script): Uint8Array => { if (!args.startsWith('0x')) { @@ -83,7 +84,8 @@ export interface AddressOptions { /** * @function toAddressPayload - * @description payload = type(01) | code hash index(00) | args(blake160-formatted pubkey) + * @description obsolete payload = type(01) | code hash index(00) | args(blake160-formatted pubkey) + * new payload = type(00) | code hash | hash type(00|01|02) | args * @see https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md * @param {string | Uint8Array} args, use as the identifier of an address, usually the public key hash is used. * @param {string} type, used to indicate which format is adopted to compose the address. @@ -93,6 +95,7 @@ export const toAddressPayload = ( args: string | Uint8Array, type: AddressType = AddressType.HashIdx, codeHashOrCodeHashIndex: CodeHashIndex | CKBComponents.Hash256 = '0x00', + hashType?: CKBComponents.ScriptHashType, ): Uint8Array => { if (typeof args === 'string' && !args.startsWith('0x')) { throw new HexStringWithout0xException(args) @@ -117,9 +120,13 @@ export const toAddressPayload = ( throw new CodeHashException(codeHashOrCodeHashIndex) } + if (!hashType) { + throw new ParameterRequiredException('hashType') + } + return scriptToPayload({ codeHash: codeHashOrCodeHashIndex, - hashType: 'data1', + hashType, args: typeof args === 'string' ? args : bytesToHex(args), }) }