diff --git a/packages/coin-kaspa/CHANGELOG.md b/packages/coin-kaspa/CHANGELOG.md index 5b77527..c8a7d34 100644 --- a/packages/coin-kaspa/CHANGELOG.md +++ b/packages/coin-kaspa/CHANGELOG.md @@ -8,3 +8,10 @@ All notable changes to this project will be documented in this file. ### Bug Fixes - **coin-kaspa:** add index.ts ([23](https://github.com/okx/js-wallet-sdk/pull/23)) + +# [1.0.2](https://github.com/okx/js-wallet-sdk) (2023-11-17) + +### Bug Fixes + +- **coin-kaspa:** fix address validate ([26](https://github.com/okx/js-wallet-sdk/pull/26)) + diff --git a/packages/coin-kaspa/package.json b/packages/coin-kaspa/package.json index 09ef9c8..d02c301 100644 --- a/packages/coin-kaspa/package.json +++ b/packages/coin-kaspa/package.json @@ -1,6 +1,6 @@ { "name": "@okxweb3/coin-kaspa", - "version": "1.0.1", + "version": "1.0.2", "description": "", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -32,7 +32,6 @@ "typescript": "^4.6.2" }, "dependencies": { - "@kaspa/core-lib": "^1.6.5", "@okxweb3/coin-base": "^1.0.0", "@okxweb3/crypto-lib": "^1.0.0" } diff --git a/packages/coin-kaspa/src/KaspaWallet.ts b/packages/coin-kaspa/src/KaspaWallet.ts index deb2864..5ba0991 100644 --- a/packages/coin-kaspa/src/KaspaWallet.ts +++ b/packages/coin-kaspa/src/KaspaWallet.ts @@ -6,7 +6,7 @@ import { SignTxParams, NotImplementedError, } from "@okxweb3/coin-base"; -import { Address } from "@kaspa/core-lib"; +import { validateAddress } from "./address"; export class KaspaWallet extends BaseWallet { async getDerivedPath(param: GetDerivedPathParam): Promise { @@ -18,8 +18,7 @@ export class KaspaWallet extends BaseWallet { } async validAddress(param: ValidAddressParams): Promise { - // @ts-ignore - return Promise.resolve(Address.isValid(param.address, "kaspa")); + return Promise.resolve(validateAddress(param.address)); } async signTransaction(param: SignTxParams): Promise { diff --git a/packages/coin-kaspa/src/address.ts b/packages/coin-kaspa/src/address.ts new file mode 100644 index 0000000..8c0ea8d --- /dev/null +++ b/packages/coin-kaspa/src/address.ts @@ -0,0 +1,14 @@ +import { encodePubKeyAddress, decodeAddress } from "./lib/address"; + +export function addressFromPubKey(pubKey: string) { + return encodePubKeyAddress(pubKey, "kaspa"); +} + +export function validateAddress(address: string) { + try { + decodeAddress(address); + } catch (e) { + return false; + } + return true; +} diff --git a/packages/coin-kaspa/src/index.ts b/packages/coin-kaspa/src/index.ts index d5b4c75..621e492 100644 --- a/packages/coin-kaspa/src/index.ts +++ b/packages/coin-kaspa/src/index.ts @@ -1 +1,2 @@ export * from "./KaspaWallet"; +export * from "./address"; diff --git a/packages/coin-kaspa/src/lib/address.ts b/packages/coin-kaspa/src/lib/address.ts new file mode 100644 index 0000000..7a584b7 --- /dev/null +++ b/packages/coin-kaspa/src/lib/address.ts @@ -0,0 +1,137 @@ +import { validate } from "./validation"; +import { convert as convertBits } from "./convertBits"; +import * as base32 from "./base32"; +import { base } from "@okxweb3/crypto-lib"; + +export function encodePubKeyAddress(pubKey: string, prefix: string) { + const eight0 = [0,0,0,0,0,0,0,0]; + const prefixData = prefixToArray(prefix).concat([0]); + const versionByte = 0; + + const pubKeyArray = Array.prototype.slice.call(base.fromHex(pubKey), 0); + const payloadData = convertBits(new Uint8Array([versionByte].concat(pubKeyArray)), 8, 5, false); + const checksumData = new Uint8Array(prefixData.length + payloadData.length + eight0.length); + checksumData.set(prefixData); + checksumData.set(payloadData, prefixData.length); + checksumData.set(eight0, prefixData.length + payloadData.length); + const polymodData = checksumToArray(polymod(checksumData)); + + const payload = new Uint8Array(payloadData.length + polymodData.length); + payload.set(payloadData); + payload.set(polymodData, payloadData.length); + + return 'kaspa:' + base32.encode(payload); +} + +export function decodeAddress(address: string) { + validate(hasSingleCase(address), 'Mixed case'); + address = address.toLowerCase(); + + const pieces = address.split(':'); + validate(pieces.length === 2, 'Invalid format: ' + address); + + const prefix = pieces[0]; + validate(prefix === 'kaspa', 'Invalid prefix: ' + address); + const encodedPayload = pieces[1]; + const payload = base32.decode(encodedPayload); + validate(validChecksum(prefix, payload), 'Invalid checksum: ' + address); + + const convertedBits = convertBits(payload.slice(0, -8), 5, 8, true); + const versionByte = convertedBits[0]; + const hashOrPublicKey = convertedBits.slice(1); + + if (versionByte === 1) { + validate(264 === hashOrPublicKey.length * 8, 'Invalid hash size: ' + address); + } else { + validate(256 === hashOrPublicKey.length * 8, 'Invalid hash size: ' + address); + } + + const type = getType(versionByte); + + return { + payload: Buffer.from(hashOrPublicKey), + prefix, + type, + }; +} + +function hasSingleCase(string: string) { + return string === string.toLowerCase() || string === string.toUpperCase(); +} + +function prefixToArray(prefix: string) { + const result = []; + for (let i = 0; i < prefix.length; i++) { + result.push(prefix.charCodeAt(i) & 31); + } + return result; +} + +function checksumToArray(checksum: number) { + const result = []; + for (let i = 0; i < 8; ++i) { + result.push(checksum & 31); + checksum /= 32; + } + return result.reverse(); +} + +function validChecksum(prefix: string, payload: Uint8Array) { + const prefixData = prefixToArray(prefix); + const data = new Uint8Array(prefix.length + 1 + payload.length); + data.set(prefixData); + data.set([0], prefixData.length) + data.set(payload, prefixData.length + 1); + + return polymod(data) === 0; +} + +function getType(versionByte: number) { + switch (versionByte & 120) { + case 0: + return 'pubkey'; + case 8: + return 'scripthash'; + default: + throw new Error('Invalid address type in version byte:' + versionByte); + } +} + +const GENERATOR1 = [0x98, 0x79, 0xf3, 0xae, 0x1e]; +const GENERATOR2 = [0xf2bc8e61, 0xb76d99e2, 0x3e5fb3c4, 0x2eabe2a8, 0x4f43e470]; + +function polymod(data: Uint8Array) { + // Treat c as 8 bits + 32 bits + var c0 = 0, c1 = 1, C = 0; + for (var j = 0; j < data.length; j++) { + // Set C to c shifted by 35 + C = c0 >>> 3; + // 0x[07]ffffffff + c0 &= 0x07; + // Shift as a whole number + c0 <<= 5; + c0 |= c1 >>> 27; + // 0xffffffff >>> 5 + c1 &= 0x07ffffff; + c1 <<= 5; + // xor the last 5 bits + c1 ^= data[j]; + for (var i = 0; i < GENERATOR1.length; ++i) { + if (C & (1 << i)) { + c0 ^= GENERATOR1[i]; + c1 ^= GENERATOR2[i]; + } + } + } + c1 ^= 1; + // Negative numbers -> large positive numbers + if (c1 < 0) { + c1 ^= 1 << 31; + c1 += (1 << 30) * 2; + } + // Unless bitwise operations are used, + // numbers are consisting of 52 bits, except + // the sign bit. The result is max 40 bits, + // so it fits perfectly in one number! + return c0 * (1 << 30) * 4 + c1; +} diff --git a/packages/coin-kaspa/src/lib/base32.ts b/packages/coin-kaspa/src/lib/base32.ts new file mode 100644 index 0000000..abb1084 --- /dev/null +++ b/packages/coin-kaspa/src/lib/base32.ts @@ -0,0 +1,71 @@ +/** + * The following methods are based on `Emilio Almansi`, thanks for their work + * @license + * https://github.com/ealmansi/cashaddrjs + * Copyright (c) 2017-2020 Emilio Almansi + * Distributed under the MIT software license, see the accompanying + * file LICENSE or http://www.opensource.org/licenses/mit-license.php. + */ + +'use strict'; + +import {validate} from './validation'; +/** + * Base32 encoding and decoding. + * + * @module base32 + */ + +/** + * Charset containing the 32 symbols used in the base32 encoding. + * @private + */ +const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; + +/** + * Inverted index mapping each symbol into its index within the charset. + * @private + */ +const CHARSET_INVERSE_INDEX = { + 'q': 0, 'p': 1, 'z': 2, 'r': 3, 'y': 4, '9': 5, 'x': 6, '8': 7, + 'g': 8, 'f': 9, '2': 10, 't': 11, 'v': 12, 'd': 13, 'w': 14, '0': 15, + 's': 16, '3': 17, 'j': 18, 'n': 19, '5': 20, '4': 21, 'k': 22, 'h': 23, + 'c': 24, 'e': 25, '6': 26, 'm': 27, 'u': 28, 'a': 29, '7': 30, 'l': 31, +}; + +/** + * Encodes the given array of 5-bit integers as a base32-encoded string. + * + * @static + * @param {Uint8Array} data Array of integers between 0 and 31 inclusive. + * @returns {string} + * @throws {Error} + */ +export function encode(data: Uint8Array) { + var base32 = ''; + for (var i = 0; i < data.length; ++i) { + var value = data[i]; + validate(0 <= value && value < 32, 'Invalid value: ' + value + '.'); + base32 += CHARSET[value]; + } + return base32; +} + +/** + * Decodes the given base32-encoded string into an array of 5-bit integers. + * + * @static + * @returns {Uint8Array} + * @throws {Error} + * @param str + */ +export function decode(str: string) { + var data = new Uint8Array(str.length); + for (var i = 0; i < str.length; ++i) { + var value = str[i]; + validate(value in CHARSET_INVERSE_INDEX, 'Invalid value: ' + value + '.'); + // @ts-ignore + data[i] = CHARSET_INVERSE_INDEX[value]; + } + return data; +} \ No newline at end of file diff --git a/packages/coin-kaspa/src/lib/convertBits.ts b/packages/coin-kaspa/src/lib/convertBits.ts new file mode 100644 index 0000000..0ed50ba --- /dev/null +++ b/packages/coin-kaspa/src/lib/convertBits.ts @@ -0,0 +1,76 @@ +/** + * The following methods are based on `Emilio Almansi`, thanks for their work + * https://github.com/ealmansi/cashaddrjs + */ + +// Copyright (c) 2017-2018 Emilio Almansi +// Copyright (c) 2017 Pieter Wuille +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +'use strict'; + +import {validate} from './validation'; + +/** + * Converts an array of integers made up of 'from' bits into an + * array of integers made up of 'to' bits. The output array is + * zero-padded if necessary, unless strict mode is true. + * Throws a {@link Error} if input is invalid. + * Original by Pieter Wuille: https://github.com/sipa/bech32. + * + * @param {Uint8Array} data Array of integers made up of 'from' bits. + * @param {number} from Length in bits of elements in the input array. + * @param {number} to Length in bits of elements in the output array. + * @param {bool} strictMode Require the conversion to be completed without padding. + * @returns {Uint8Array} + */ +export function convert(data: Uint8Array, from: number, to: number, strictMode: boolean) { + var length = strictMode + ? Math.floor(data.length * from / to) + : Math.ceil(data.length * from / to); + var mask = (1 << to) - 1; + var result = new Uint8Array(length); + var index = 0; + var accumulator = 0; + var bits = 0; + for (var i = 0; i < data.length; ++i) { + var value = data[i]; + validate(0 <= value && (value >> from) === 0, 'Invalid value: ' + value + '.'); + accumulator = (accumulator << from) | value; + bits += from; + while (bits >= to) { + bits -= to; + result[index] = (accumulator >> bits) & mask; + ++index; + } + } + if (!strictMode) { + if (bits > 0) { + result[index] = (accumulator << (to - bits)) & mask; + ++index; + } + } else { + validate( + bits < from && ((accumulator << (to - bits)) & mask) === 0, + 'Input cannot be converted to ' + to + ' bits without padding, but strict mode was used.' + ); + } + return result; +} \ No newline at end of file diff --git a/packages/coin-kaspa/src/lib/validation.ts b/packages/coin-kaspa/src/lib/validation.ts new file mode 100644 index 0000000..17fcb77 --- /dev/null +++ b/packages/coin-kaspa/src/lib/validation.ts @@ -0,0 +1,30 @@ +/** + * The following methods are based on `Emilio Almansi`, thanks for their work + * @license + * https://github.com/ealmansi/cashaddrjs + * Copyright (c) 2017-2020 Emilio Almansi + * Distributed under the MIT software license, see the accompanying + * file LICENSE or http://www.opensource.org/licenses/mit-license.php. + */ + +'use strict'; + +/** + * Validation utility. + * + * @module validation + */ + +/** + * Validates a given condition, throwing a {@link Error} if + * the given condition does not hold. + * + * @static + * @param {boolean} condition Condition to validate. + * @param {string} message Error message in case the condition does not hold. + */ +export function validate(condition: any, message: string) { + if (!condition) { + throw new Error(message); + } +} \ No newline at end of file diff --git a/packages/coin-kaspa/tests/kaspa.test.ts b/packages/coin-kaspa/tests/kaspa.test.ts index b97c22e..8780136 100644 --- a/packages/coin-kaspa/tests/kaspa.test.ts +++ b/packages/coin-kaspa/tests/kaspa.test.ts @@ -1,18 +1,17 @@ -import { Address } from "@kaspa/core-lib" -import { KaspaWallet } from "../src/KaspaWallet"; +import { KaspaWallet, addressFromPubKey } from "../src"; const wallet = new KaspaWallet(); describe("kaspa", () => { test("address", async () => { - let isValid = await wallet.validAddress({ address: "kaspa:qrcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" }); - console.log(isValid); + expect(await wallet.validAddress({ address: "kaspa:qrcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" })).toBe(true); + expect(await wallet.validAddress({ address: "kaspa:qrcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2a" })).toBe(false); + expect(await wallet.validAddress({ address: "kaspa:prcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" })).toBe(false); + expect(await wallet.validAddress({ address: "kaspa1:qrcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" })).toBe(false); + expect(await wallet.validAddress({ address: "kaspa:1prcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" })).toBe(false); + expect(await wallet.validAddress({ address: "kaspa:rcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" })).toBe(false); - isValid = await wallet.validAddress({ address: "kaspa:prcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h" }); - console.log(isValid); - - // @ts-ignore - const address = new Address(Buffer.from("f13b0d6390568bac735b080b03a993f492afbb9bc330333fdad4cd8b47206a80", "hex"), "kaspa").toString(); - console.log(address); + const address = addressFromPubKey("f13b0d6390568bac735b080b03a993f492afbb9bc330333fdad4cd8b47206a80"); + expect(address).toBe("kaspa:qrcnkrtrjptghtrntvyqkqafj06f9tamn0pnqvelmt2vmz68yp4gqj5lnal2h"); }); });