diff --git a/packages/ckb-sdk-rpc/types/rpc/index.d.ts b/packages/ckb-sdk-rpc/types/rpc/index.d.ts index ec541071..12272f9a 100644 --- a/packages/ckb-sdk-rpc/types/rpc/index.d.ts +++ b/packages/ckb-sdk-rpc/types/rpc/index.d.ts @@ -33,7 +33,7 @@ declare module RPC { Committed = 'committed', } - export type ScriptHashType = 'data' | 'type' + export type ScriptHashType = CKBComponents.ScriptHashType export type DepType = 'code' | 'dep_group' diff --git a/packages/ckb-sdk-utils/README.md b/packages/ckb-sdk-utils/README.md index 21fffeee..1f467a5c 100644 --- a/packages/ckb-sdk-utils/README.md +++ b/packages/ckb-sdk-utils/README.md @@ -13,9 +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 of new version from script - [Utils](#utils) @@ -148,6 +149,19 @@ utils.addressToScript('ckb1qsvf96jqmq4483ncl7yrzfzshwchu9jd0glq4yy5r2jcsw04d7xly // } ``` +```js +/** + * @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": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args":"0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" +}) +// ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4 +``` + ### Utils ```plain diff --git a/packages/ckb-sdk-utils/__tests__/address/fixtures.json b/packages/ckb-sdk-utils/__tests__/address/fixtures.json index 6cd2b6b5..53fbc3fd 100644 --- a/packages/ckb-sdk-utils/__tests__/address/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/address/fixtures.json @@ -3,6 +3,22 @@ "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] + }, + "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" + }, + "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": { @@ -106,35 +122,47 @@ }, "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 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] }, "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 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 identifies the hash_type has invalid hash type": { + "params": ["ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqcqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgaxsc2r"], + "exception": "'ckb1qq6pngwqn6e9vlm92th84rk0l4jp2h8lurchjmnwv8kq3rt5psf4vqcqza8m903wt5xp5wuxjnurydg2x0qksh280gxqzqgaxsc2r' is not a valid address" } }, "addressToScript": { @@ -185,6 +213,106 @@ "hashType": "type", "args": "0x36c329ed630d6ce750712a477543672adab57f4c" } + }, + "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 hash_type = type": { + "params": [ + { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" + } + ], + "expected": "ckb1qzda0cr08m85hc8jlnfp3zer7xulejywt49kt2rr0vthywaa50xwsqdnnw7qkdnnclfkg59uzn8umtfd2kwxceqxwquc4" + }, + "full version testnet address identifies hash_type = type": { + "params": [ + { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xb39bbc0b3673c7d36450bc14cfcdad2d559c6c64" + }, + false + ], + "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": [ + { + "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..09ca82af 100644 --- a/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/exceptions/fixtures.json @@ -38,14 +38,28 @@ "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" } }, "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/__tests__/serialization/transaction/fixtures.json b/packages/ckb-sdk-utils/__tests__/serialization/transaction/fixtures.json index 9cf5bf2c..0f2fb440 100644 --- a/packages/ckb-sdk-utils/__tests__/serialization/transaction/fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/serialization/transaction/fixtures.json @@ -332,6 +332,64 @@ ] }, "expected": "0x120200000c000000c5010000b90100001c000000200000006e00000072000000a2000000a50100000000000002000000c12386705b5cbb312b693874f3edf45c43a274482e27b8df0fd80c8d3f5feb8b00000000010fb4945d52baf91e0dee2a686cdd9d84cad95b566a1d7409b970ee0a0f364f6002000000000000000001000000000000000000000031f695263423a4b05045dd25ce6692bb55d7bba2965d8be16b036e138e72cc6501000000030100000c000000a20000009600000010000000180000006100000000e87648170000004900000010000000300000003100000068d5438ac952d2f584abf879527946a537e82c7f3c1cbf6d8ebf9767437d8e88011400000059a27ef3ba84f061517d13f42cf44ed02061006135000000100000003000000031000000ece45e0979030e2f8909f76258631c42333b1e906fd9701ec3600a464a90b8f600000000006100000010000000180000006100000000506a41e15900004900000010000000300000003100000068d5438ac952d2f584abf879527946a537e82c7f3c1cbf6d8ebf9767437d8e88011400000059a27ef3ba84f061517d13f42cf44ed020610061140000000c0000001000000000000000000000004d000000080000004100000082df73581bcd08cb9aa270128d15e79996229ce8ea9e4f985b49fbf36762c5c37936caf3ea3784ee326f60b8992924fcf496f9503c907982525a3436f01ab32900" + }, + "transaction containing data1 lock script": { + "transaction": { + "version": "0x0", + "cellDeps": [ + { + "outPoint": { + "txHash": "0xace5ea83c478bb866edf122ff862085789158f5cbff155b7bb5f13058555b708", + "index": "0x0" + }, + "depType": "depGroup" + } + ], + "headerDeps": [], + "inputs": [ + { + "since": "0x0", + "previousOutput": { + "txHash": "0xa563884b3686078ec7e7677a5f86449b15cf2693f3c1241766c6996f206cc541", + "index": "0x7" + } + } + ], + "outputs": [ + { + "capacity": "0x2540be400", + "lock": { + "codeHash": "0x709f3fda12f561cfacf92273c57a98fede188a3f1a59b1f888d113f9cce08649", + "hashType": "data", + "args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7" + }, + "type": null + }, + { + "capacity": "0x2540be400", + "lock": { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7" + }, + "type": null + }, + { + "capacity": "0x2540be400", + "lock": { + "codeHash": "0x709f3fda12f561cfacf92273c57a98fede188a3f1a59b1f888d113f9cce08649", + "hashType": "data1", + "args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7" + }, + "type": null + } + ], + "outputsData": ["0x", "0x", "0x"], + "witnesses": [ + "0x550000001000000055000000550000004100000070b823564f7d1f814cc135ddd56fd8e8931b3a7040eaf1fb828adae29736a3cb0bc7f65021135b293d10a22da61fcc64f7cb660bf2c3276ad63630dad0b6099001" + ] + }, + "expected": "0x390200000c000000d8010000cc0100001c00000020000000490000004d0000007d000000b00100000000000001000000ace5ea83c478bb866edf122ff862085789158f5cbff155b7bb5f13058555b708000000000100000000010000000000000000000000a563884b3686078ec7e7677a5f86449b15cf2693f3c1241766c6996f206cc54107000000330100001000000071000000d20000006100000010000000180000006100000000e40b540200000049000000100000003000000031000000709f3fda12f561cfacf92273c57a98fede188a3f1a59b1f888d113f9cce086490014000000c8328aabcd9b9e8e64fbc566c4385c3bdeb219d76100000010000000180000006100000000e40b5402000000490000001000000030000000310000009bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce80114000000c8328aabcd9b9e8e64fbc566c4385c3bdeb219d76100000010000000180000006100000000e40b540200000049000000100000003000000031000000709f3fda12f561cfacf92273c57a98fede188a3f1a59b1f888d113f9cce086490214000000c8328aabcd9b9e8e64fbc566c4385c3bdeb219d71c000000100000001400000018000000000000000000000000000000610000000800000055000000550000001000000055000000550000004100000070b823564f7d1f814cc135ddd56fd8e8931b3a7040eaf1fb828adae29736a3cb0bc7f65021135b293d10a22da61fcc64f7cb660bf2c3276ad63630dad0b6099001" } } } diff --git a/packages/ckb-sdk-utils/__tests__/utils/rawTransactionToHash.fixtures.json b/packages/ckb-sdk-utils/__tests__/utils/rawTransactionToHash.fixtures.json index ce73d139..b9359359 100644 --- a/packages/ckb-sdk-utils/__tests__/utils/rawTransactionToHash.fixtures.json +++ b/packages/ckb-sdk-utils/__tests__/utils/rawTransactionToHash.fixtures.json @@ -60,6 +60,64 @@ "hash": "0x9d1bf801b235ce62812844f01381a070c0cc72876364861e00492eac1d8b54e7" }, "expected": "0xe765f9912b06c72552dae11779f6371309236e968aa045ae3b8f426d8ec8ca05" + }, + { + "rawTransaction": { + "version": "0x0", + "cellDeps": [ + { + "outPoint": { + "txHash": "0xace5ea83c478bb866edf122ff862085789158f5cbff155b7bb5f13058555b708", + "index": "0x0" + }, + "depType": "depGroup" + } + ], + "headerDeps": [], + "inputs": [ + { + "since": "0x0", + "previousOutput": { + "txHash": "0xa563884b3686078ec7e7677a5f86449b15cf2693f3c1241766c6996f206cc541", + "index": "0x7" + } + } + ], + "outputs": [ + { + "capacity": "0x2540be400", + "lock": { + "codeHash": "0x709f3fda12f561cfacf92273c57a98fede188a3f1a59b1f888d113f9cce08649", + "hashType": "data", + "args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7" + }, + "type": null + }, + { + "capacity": "0x2540be400", + "lock": { + "codeHash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", + "hashType": "type", + "args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7" + }, + "type": null + }, + { + "capacity": "0x2540be400", + "lock": { + "codeHash": "0x709f3fda12f561cfacf92273c57a98fede188a3f1a59b1f888d113f9cce08649", + "hashType": "data1", + "args": "0xc8328aabcd9b9e8e64fbc566c4385c3bdeb219d7" + }, + "type": null + } + ], + "outputsData": ["0x", "0x", "0x"], + "witnesses": [ + "0x550000001000000055000000550000004100000070b823564f7d1f814cc135ddd56fd8e8931b3a7040eaf1fb828adae29736a3cb0bc7f65021135b293d10a22da61fcc64f7cb660bf2c3276ad63630dad0b6099001" + ] + }, + "expected": "0x9110ca9266f89938f09ae6f93cc914b2c856cc842440d56fda6d16ee62543f5c" } ] } 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 f00dc8c9..bedd6abf 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, @@ -6,7 +6,18 @@ import { ANYONE_CAN_PAY_TESTNET, } from '../systemScripts' import { hexToBytes, bytesToHex } from '../convertors' -import { HexStringWithout0xException, AddressException, AddressPayloadException } from '../exceptions' +import { + HexStringWithout0xException, + AddressException, + AddressPayloadException, + CodeHashException, + HashTypeException, + ParameterRequiredException, +} from '../exceptions' + +const MAX_BECH32_LIMIT = 1023 + +// TODO: deprecate outdated methods export enum AddressPrefix { Mainnet = 'ckb', @@ -14,11 +25,55 @@ export enum AddressPrefix { } export enum AddressType { + 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' - 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 } +/** + * @description payload to a full address of new version + */ +const payloadToAddress = (payload: Uint8Array, isMainnet = true) => + 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')) { + throw new HexStringWithout0xException(args) + } + + if (!codeHash.startsWith('0x') || codeHash.length !== 66) { + throw new CodeHashException(codeHash) + } + + enum HashType { + data = '00', + type = '01', + data1 = '02', + } + + if (!HashType[hashType]) { + throw new HashTypeException(hashType) + } + + 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) + +/** + * 0x00 SECP256K1 + blake160 + * 0x01 SECP256k1 + multisig + * 0x02 anyone_can_pay + */ export type CodeHashIndex = '0x00' | '0x01' | '0x02' export interface AddressOptions { @@ -29,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. @@ -39,18 +95,44 @@ export const toAddressPayload = ( args: string | Uint8Array, type: AddressType = AddressType.HashIdx, codeHashOrCodeHashIndex: CodeHashIndex | CKBComponents.Hash256 = '0x00', + hashType?: CKBComponents.ScriptHashType, ): 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) + } + + if (!hashType) { + throw new ParameterRequiredException('hashType') + } + + return scriptToPayload({ + codeHash: codeHashOrCodeHashIndex, + hashType, + args: typeof args === 'string' ? args : bytesToHex(args), + }) } /** - * @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. @@ -61,11 +143,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, @@ -115,8 +198,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 +214,19 @@ 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)}`) + } + + break + } default: { throw new AddressPayloadException(payload) } @@ -148,12 +245,18 @@ 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 { - isValidPayload(payload) + 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) { - throw new AddressException(address, err.type) + throw new AddressException(address, err.stack, err.type) } return encode === 'binary' ? payload : bytesToHex(payload) } @@ -162,6 +265,20 @@ 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 args = `0x${p.substr(70)}` + return { codeHash, hashType, args } + } + if (type === +AddressType.HashIdx) { const codeHashIndices = [ SECP256K1_BLAKE160, 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, -} 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 145be75a..4b93b770 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,32 @@ 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 default { AddressPayloadException, AddressException, + CodeHashException, + HashTypeException, } 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/packages/ckb-sdk-utils/src/serialization/script.ts b/packages/ckb-sdk-utils/src/serialization/script.ts index 5caf4ecd..2ed1efe7 100644 --- a/packages/ckb-sdk-utils/src/serialization/script.ts +++ b/packages/ckb-sdk-utils/src/serialization/script.ts @@ -6,6 +6,7 @@ export const serializeCodeHash = (codeHash: CKBComponents.Hash256) => serializeA export const serializeHashType = (hashType: CKBComponents.ScriptHashType) => { if (hashType === 'data') return '0x00' if (hashType === 'type') return '0x01' + if (hashType === 'data1') return '0x02' throw new TypeError("Hash type must be either of 'data' or 'type'") } diff --git a/packages/ckb-types/index.d.ts b/packages/ckb-types/index.d.ts index 96209de1..c3b91417 100644 --- a/packages/ckb-types/index.d.ts +++ b/packages/ckb-types/index.d.ts @@ -30,7 +30,7 @@ declare namespace CKBComponents { Committed = 'committed', } - export type ScriptHashType = 'data' | 'type' + export type ScriptHashType = 'data' | 'type' | 'data1' export type DepType = 'code' | 'depGroup' 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"