Skip to content

Commit

Permalink
update kaspa address validate
Browse files Browse the repository at this point in the history
  • Loading branch information
jinguang.huang committed Nov 17, 2023
1 parent 744a835 commit 7690e14
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 15 deletions.
7 changes: 7 additions & 0 deletions packages/coin-kaspa/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))

3 changes: 1 addition & 2 deletions packages/coin-kaspa/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
}
Expand Down
5 changes: 2 additions & 3 deletions packages/coin-kaspa/src/KaspaWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
Expand All @@ -18,8 +18,7 @@ export class KaspaWallet extends BaseWallet {
}

async validAddress(param: ValidAddressParams): Promise<any> {
// @ts-ignore
return Promise.resolve(Address.isValid(param.address, "kaspa"));
return Promise.resolve(validateAddress(param.address));
}

async signTransaction(param: SignTxParams): Promise<any> {
Expand Down
14 changes: 14 additions & 0 deletions packages/coin-kaspa/src/address.ts
Original file line number Diff line number Diff line change
@@ -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;
}
1 change: 1 addition & 0 deletions packages/coin-kaspa/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./KaspaWallet";
export * from "./address";
137 changes: 137 additions & 0 deletions packages/coin-kaspa/src/lib/address.ts
Original file line number Diff line number Diff line change
@@ -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;
}
71 changes: 71 additions & 0 deletions packages/coin-kaspa/src/lib/base32.ts
Original file line number Diff line number Diff line change
@@ -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;
}
76 changes: 76 additions & 0 deletions packages/coin-kaspa/src/lib/convertBits.ts
Original file line number Diff line number Diff line change
@@ -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;
}
30 changes: 30 additions & 0 deletions packages/coin-kaspa/src/lib/validation.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 7690e14

Please sign in to comment.