@hashgraph/sdk
Version:
546 lines (512 loc) • 18.5 kB
JavaScript
Object.defineProperty(exports, "__esModule", {
value: true
});
exports._checksum = _checksum;
exports._parseAddress = _parseAddress;
exports.aliasToEvmAddress = aliasToEvmAddress;
exports.aliasToPublicKey = aliasToPublicKey;
exports.compare = compare;
exports.constructor = constructor;
exports.fromEvmAddress = fromEvmAddress;
exports.fromSolidityAddress = fromSolidityAddress;
exports.fromString = fromString;
exports.fromStringSplitter = fromStringSplitter;
exports.publicKeyToAlias = publicKeyToAlias;
exports.toEvmAddress = toEvmAddress;
exports.toSolidityAddress = toSolidityAddress;
exports.toStringWithChecksum = toStringWithChecksum;
exports.validateChecksum = validateChecksum;
var _long = _interopRequireDefault(require("long"));
var hex = _interopRequireWildcard(require("./encoding/hex.cjs"));
var _BadEntityIdError = _interopRequireDefault(require("./BadEntityIdError.cjs"));
var util = _interopRequireWildcard(require("./util.cjs"));
var _base = _interopRequireDefault(require("./base32.cjs"));
var HieroProto = _interopRequireWildcard(require("@hashgraph/proto"));
var _PublicKey = _interopRequireDefault(require("./PublicKey.cjs"));
var _bytes = require("@ethersproject/bytes");
var _EvmAddress = _interopRequireDefault(require("./EvmAddress.cjs"));
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
// SPDX-License-Identifier: Apache-2.0
/**
* @typedef {import("./client/Client.js").default<*, *>} Client
*/
/**
* @typedef {object} IEntityId
* @property {number | Long} num
* @property {(number | Long)=} shard
* @property {(number | Long)=} realm
*/
/**
* @typedef {object} IEntityIdResult
* @property {Long} shard
* @property {Long} realm
* @property {Long} num
*/
/**
* @typedef {object} IEntityIdParts
* @property {string?} shard
* @property {string?} realm
* @property {string} numOrHex
* @property {string?} checksum
*/
/**
* @typedef {object} IEntityIdResultWithChecksum
* @property {Long} shard
* @property {Long} realm
* @property {Long} num
* @property {string | null} checksum
*/
const regex = /"^(0|(?:[1-9]\\d*))\\.(0|(?:[1-9]\\d*))\\.(0|(?:[1-9]\\d*))(?:-([a-z]{5}))?$/;
/**
* This regex supports entity IDs
* - as stand alone nubmers
* - as shard.realm.num
* - as shard.realm.hex
* - can optionally provide checksum for any of the above
*/
const ENTITY_ID_REGEX = /^(\d+)(?:\.(\d+)\.([a-fA-F0-9]+))?(?:-([a-z]{5}))?$/;
/**
* @description The length of an EVM address in bytes.
*/
const EVM_ADDRESS_LENGTH = 20;
/**
* @description The length of the long-zero prefix in bytes.
*/
const LONG_ZERO_PREFIX_LENGTH = 12;
/**
* @description The offset of the entity number in the EVM address.
*/
const ENTITY_NUM_OFFSET = 16;
/**
* This method is called by most entity ID constructors. It's purpose is to
* deduplicate the constuctors.
*
* @param {number | Long | IEntityId} props
* @param {(number | null | Long)=} realmOrNull
* @param {(number | null | Long)=} numOrNull
* @returns {IEntityIdResult}
*/
function constructor(props, realmOrNull, numOrNull) {
// Make sure either both the second and third parameter are
// set or not set; we shouldn't have one set, but the other not set.
//
//NOSONAR
if (realmOrNull == null && numOrNull != null || realmOrNull != null && numOrNull == null) {
throw new Error("invalid entity ID");
}
// If the first parameter is a number then we need to convert the
// first, second, and third parameters into numbers. Otherwise,
// we should look at the fields `shard`, `realm`, and `num` on
// `props`
const [shard, realm, num] = typeof props === "number" || _long.default.isLong(props) ? [numOrNull != null ? _long.default.fromValue(/** @type {Long | number} */props) : _long.default.ZERO, realmOrNull != null ? _long.default.fromValue(realmOrNull) : _long.default.ZERO, numOrNull != null ? _long.default.fromValue(numOrNull) : _long.default.fromValue(/** @type {Long | number} */props)] : [props.shard != null ? _long.default.fromValue(props.shard) : _long.default.ZERO, props.realm != null ? _long.default.fromValue(props.realm) : _long.default.ZERO, _long.default.fromValue(props.num)];
// Make sure none of the numbers are negative
if (shard.isNegative() || realm.isNegative() || num.isNegative()) {
throw new Error("negative numbers are not allowed in IDs");
}
return {
shard,
realm,
num
};
}
/**
* A simple comparison function for comparing entity IDs
*
* @param {[Long, Long, Long]} a
* @param {[Long, Long, Long]} b
* @returns {number}
*/
function compare(a, b) {
let comparison = a[0].compare(b[0]);
if (comparison != 0) {
return comparison;
}
comparison = a[1].compare(b[1]);
if (comparison != 0) {
return comparison;
}
return a[2].compare(b[2]);
}
/**
* This type is part of the entity ID checksums feature which
* is responsible for checking if an entity ID was created on
* the same ledger ID as the client is currently using.
*
* @typedef {object} ParseAddressResult
* @property {number} status
* @property {Long} [num1]
* @property {Long} [num2]
* @property {Long} [num3]
* @property {string} [correctChecksum]
* @property {string} [givenChecksum]
* @property {string} [noChecksumFormat]
* @property {string} [withChecksumFormat]
*/
/**
* @param {string} text
* @returns {IEntityIdParts}
*/
function fromStringSplitter(text) {
const match = ENTITY_ID_REGEX.exec(text);
if (match == null) {
throw new Error(`failed to parse entity id: ${text}`);
}
if (match[2] == null && match[3] == null) {
return {
shard: "0",
realm: "0",
numOrHex: match[1],
checksum: match[4]
};
} else {
return {
shard: match[1],
realm: match[2],
numOrHex: match[3],
checksum: match[4]
};
}
}
/**
* @param {string} text
* @returns {IEntityIdResultWithChecksum}
*/
function fromString(text) {
const result = fromStringSplitter(text);
if (Number.isNaN(result.shard) || Number.isNaN(result.realm) || Number.isNaN(result.numOrHex)) {
throw new Error("invalid format for entity ID");
}
return {
shard: result.shard != null ? _long.default.fromString(result.shard) : _long.default.ZERO,
realm: result.realm != null ? _long.default.fromString(result.realm) : _long.default.ZERO,
num: _long.default.fromString(result.numOrHex),
checksum: result.checksum
};
}
/**
* Return the shard, realm, and num from a solidity address.
*
* Solidity addresses are 20 bytes long and hex encoded, where the first 4
* bytes represent the shard, the next 8 bytes represent the realm, and
* the last 8 bytes represent the num. All in Big Endian format
*
* @param {string} address
* @returns {[Long, Long, Long]}
*/
function fromSolidityAddress(address) {
const addr = address.startsWith("0x") ? hex.decode(address.slice(2)) : hex.decode(address);
if (addr.length !== EVM_ADDRESS_LENGTH) {
throw new Error(`Invalid hex encoded solidity address length:
expected length 40, got length ${address.length}`);
}
const shard = _long.default.fromBytesBE([0, 0, 0, 0, ...addr.slice(0, 4)]);
const realm = _long.default.fromBytesBE(Array.from(addr.slice(4, 12)));
const num = _long.default.fromBytesBE(Array.from(addr.slice(12, 20)));
return [shard, realm, num];
}
/**
* Parse an EVM address and return shard, realm, entity num, and optional EVM address.
*
* For long zero addresses (first 12 bytes are zeros): returns [shard, realm, entityNum, null]
* For regular EVM addresses: returns [shard, realm, 0, EvmAddress]
*
* @param {Long | number} shard - The shard number to use
* @param {Long | number} realm - The realm number to use
* @param {string} address - The EVM address to parse (with or without 0x prefix)
* @returns {[Long, Long, Long, EvmAddress | null]} - [shard, realm, entityNum, evmAddressOrNull]
*/
function fromEvmAddress(shard, realm, address) {
if (!hex.isHexString(address)) {
throw new Error(`Invalid EVM address hex string: ${address}`);
}
const addr = address.startsWith("0x") ? hex.decode(address.slice(2)) : hex.decode(address);
if (addr.length !== EVM_ADDRESS_LENGTH) {
throw new Error(`Invalid hex encoded evm address length:
expected length ${EVM_ADDRESS_LENGTH}, got length ${address.length}`);
}
let num = _long.default.ZERO;
if (util.isLongZeroAddress(addr)) {
num = _long.default.fromBytesBE(Array.from(addr.slice(LONG_ZERO_PREFIX_LENGTH, EVM_ADDRESS_LENGTH)));
}
let shardLong = shard instanceof _long.default ? shard : _long.default.fromNumber(shard);
let realmLong = realm instanceof _long.default ? realm : _long.default.fromNumber(realm);
return [shardLong, realmLong, num, num.isZero() ? _EvmAddress.default.fromBytes(addr) : null];
}
/**
* Convert shard, realm, and num into a solidity address.
*
* See `fromSolidityAddress()` for more documentation.
*
* @param {[Long,Long,Long] | [number,number,number]} address
* @returns {string}
*/
function toSolidityAddress(address) {
const buffer = new Uint8Array(20);
const view = util.safeView(buffer);
const [shard, realm, num] = address;
view.setUint32(0, util.convertToNumber(shard));
view.setUint32(8, util.convertToNumber(realm));
view.setUint32(16, util.convertToNumber(num));
return hex.encode(buffer);
}
/**
* @overload
* @param {Uint8Array} evmAddressBytes - EVM address bytes to convert to hex
* @returns {string}
*/
/**
* @overload
* @param {Long} accountNum - Account number to convert to long-zero EVM address
* @returns {string}
*/
/**
* Convert EVM address bytes to hex string or account num to long-zero EVM address.
*
* @param {Uint8Array | Long} evmAddressBytesOrAccountNum
* @returns {string}
*/
function toEvmAddress(evmAddressBytesOrAccountNum) {
if (evmAddressBytesOrAccountNum instanceof Uint8Array) {
return hex.encode(evmAddressBytesOrAccountNum);
}
const accountNum = evmAddressBytesOrAccountNum;
const buffer = new Uint8Array(EVM_ADDRESS_LENGTH);
const view = util.safeView(buffer);
view.setUint32(ENTITY_NUM_OFFSET, util.convertToNumber(accountNum));
return hex.encode(buffer);
}
/**
* Parse the address string addr and return an object with the results (8 fields).
* The first four fields are numbers, which could be implemented as signed 32 bit
* integers, and the last four are strings.
*
* status; //the status of the parsed address
* // 0 = syntax error
* // 1 = an invalid with-checksum address (bad checksum)
* // 2 = a valid no-checksum address
* // 3 = a valid with-checksum address
* num1; //the 3 numbers in the address, such as 1.2.3, with leading zeros removed
* num2;
* num3;
* correctchecksum; //the correct checksum
* givenChecksum; //the checksum in the address that was parsed
* noChecksumFormat; //the address in no-checksum format
* withChecksumFormat; //the address in with-checksum format
*
* @param {Uint8Array} ledgerId
* @param {string} addr
* @returns {ParseAddressResult}
*/
function _parseAddress(ledgerId, addr) {
let match = regex.exec(addr);
if (match === null) {
let result = {
status: 0
}; // When status == 0, the rest of the fields should be ignored
return result;
}
let a = [_long.default.fromString(match[1]), _long.default.fromString(match[2]), _long.default.fromString(match[3])];
let ad = `${a[0].toString()}.${a[1].toString()}.${a[2].toString()}`;
let c = _checksum(ledgerId, ad);
let s = match[4] === undefined ? 2 : c == match[4] ? 3 : 1; //NOSONAR
return {
status: s,
num1: a[0],
num2: a[1],
num3: a[2],
givenChecksum: match[4],
correctChecksum: c,
noChecksumFormat: ad,
withChecksumFormat: `${ad}-${c}`
};
}
/**
* Given an address like "0.0.123", return a checksum like "laujm"
*
* @param {Uint8Array} ledgerId
* @param {string} addr
* @returns {string}
*/
function _checksum(ledgerId, addr) {
let answer = "";
let d = []; // Digits with 10 for ".", so if addr == "0.0.123" then d == [0, 10, 0, 10, 1, 2, 3]
let s0 = 0; // Sum of even positions (mod 11)
let s1 = 0; // Sum of odd positions (mod 11)
let s = 0; // Weighted sum of all positions (mod p3)
let sh = 0; // Hash of the ledger ID
let c = 0; // The checksum, as a single number
const p3 = 26 * 26 * 26; // 3 digits in base 26
const p5 = 26 * 26 * 26 * 26 * 26; // 5 digits in base 26
const ascii_a = "a".charCodeAt(0); // 97
const m = 1000003; // Min prime greater than a million. Used for the final permutation.
const w = 31; // Sum s of digit values weights them by powers of w. Should be coprime to p5.
let h = new Uint8Array(ledgerId.length + 6);
h.set(ledgerId, 0);
h.set([0, 0, 0, 0, 0, 0], ledgerId.length);
for (let i = 0; i < addr.length; i++) {
//NOSONAR
d.push(addr[i] === "." ? 10 : parseInt(addr[i], 10));
}
for (let i = 0; i < d.length; i++) {
s = (w * s + d[i]) % p3;
if (i % 2 === 0) {
s0 = (s0 + d[i]) % 11;
} else {
s1 = (s1 + d[i]) % 11;
}
}
for (let i = 0; i < h.length; i++) {
sh = (w * sh + h[i]) % p5;
}
c = (((addr.length % 5 * 11 + s0) * 11 + s1) * p3 + s + sh) % p5;
c = c * m % p5;
for (let i = 0; i < 5; i++) {
answer = String.fromCharCode(ascii_a + c % 26) + answer;
c /= 26;
}
return answer;
}
/**
* Validate an entity ID checksum against a client
*
* @param {Long} shard
* @param {Long} realm
* @param {Long} num
* @param {string | null} checksum
* @param {Client} client
*/
function validateChecksum(shard, realm, num, checksum, client) {
if (client._network._ledgerId == null || checksum == null) {
return;
}
const expectedChecksum = _checksum(client._network._ledgerId._ledgerId, `${shard.toString()}.${realm.toString()}.${num.toString()}`);
if (checksum != expectedChecksum) {
throw new _BadEntityIdError.default(shard, realm, num, checksum, expectedChecksum);
}
}
/**
* Stringify the entity ID with a checksum.
*
* @param {string} string
* @param {Client} client
* @returns {string}
*/
function toStringWithChecksum(string, client) {
if (client == null) {
throw new Error("client cannot be null");
}
if (client._network._ledgerId == null) {
throw new Error("cannot calculate checksum with a client that does not contain a recognzied ledger ID");
}
const checksum = _checksum(client._network._ledgerId._ledgerId, string);
return `${string}-${checksum}`;
}
/**
* Append Buffers.
* @param {Uint8Array} buffer1
* @param {Uint8Array} buffer2
* @returns {Uint8Array}
*/
function appendBuffer(buffer1, buffer2) {
var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);
tmp.set(new Uint8Array(buffer1), 0);
tmp.set(new Uint8Array(buffer2), buffer1.byteLength);
return tmp;
}
/**
* Convert bytes to hex string.
* @param {Uint8Array} bytes
* @returns {string}
*/
function toHexString(bytes) {
var s = "0x";
bytes.forEach(function (byte) {
s += ("0" + (byte & 0xff).toString(16)).slice(-2);
});
return s;
}
/**
* Deserialize the alias to public key.
* Alias is created from ed25519 or ECDSASecp256k1 types of accounts. If hollow account is used, the alias is created from evm address.
* For hollow accounts, please use aliasToEvmAddress.
*
* @param {string} alias
* @returns {PublicKey | null}
*/
function aliasToPublicKey(alias) {
const bytes = _base.default.decode(alias);
if (!bytes) {
return null;
}
let key;
try {
key = HieroProto.proto.Key.decode(bytes);
} catch (e) {
throw new Error("The alias is created with hollow account. Please use aliasToEvmAddress!");
}
if (key.ed25519 != null && key.ed25519.byteLength > 0) {
return _PublicKey.default.fromBytes(key.ed25519);
}
if (key.ECDSASecp256k1 != null && key.ECDSASecp256k1.byteLength > 0) {
return _PublicKey.default.fromBytes(key.ECDSASecp256k1);
}
return null;
}
/**
* Deserialize the alias to evm address.
* Alias is created from hollow account.
* For ed25519 or ECDSASecp256k1 accounts, please use aliasToPublicKey.
*
* @param {string} alias
* @returns {string | null}
*/
function aliasToEvmAddress(alias) {
const bytes = _base.default.decode(alias);
if (!bytes) {
return null;
}
try {
HieroProto.proto.Key.decode(bytes);
throw new Error("The alias is created with ed25519 or ECDSASecp256k1 account. Please use aliasToPublicKey!");
} catch (e) {
return toHexString(bytes);
}
}
/**
* Serialize the public key to alias.
* Alias is created from ed25519 or ECDSASecp256k1 types of accounts. If hollow account is used, the alias is created from evm address.
*
* @param {string | PublicKey} publicKey
* @returns {string | null}
*/
function publicKeyToAlias(publicKey) {
if (typeof publicKey === "string" && (publicKey.startsWith("0x") && publicKey.length == 42 || publicKey.length == 40)) {
if (!publicKey.startsWith("0x")) {
publicKey = `0x${publicKey}`;
}
const bytes = (0, _bytes.arrayify)(publicKey);
if (!bytes) {
return null;
}
return _base.default.encode(bytes);
}
const publicKeyRaw = typeof publicKey === "string" ? _PublicKey.default.fromString(publicKey) : publicKey;
let publicKeyHex = publicKeyRaw.toStringRaw();
let leadingHex = "";
if (publicKeyRaw._key._type === "secp256k1") {
leadingHex = "0x3A21"; // LEADING BYTES FROM PROTOBUFS
}
if (publicKeyRaw._key._type === "ED25519") {
leadingHex = "0x1220"; // LEADING BYTES FROM PROTOBUFS
}
if (!publicKeyHex.startsWith("0x")) {
publicKeyHex = `0x${publicKeyHex}`;
}
const leadingBytes = (0, _bytes.arrayify)(leadingHex);
const publicKeyBytes = (0, _bytes.arrayify)(publicKeyHex);
const publicKeyInBytes = appendBuffer(leadingBytes, publicKeyBytes);
const alias = _base.default.encode(publicKeyInBytes);
return alias;
}
;