Skip to content

Commit

Permalink
feat: added alias utils for parsing from/to public address
Browse files Browse the repository at this point in the history
  • Loading branch information
ochikov committed Dec 19, 2023
1 parent c78512b commit ead21ac
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 5 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"pino": "^8.14.1",
"pino-pretty": "^10.0.0",
"protobufjs": "^7.2.5",
"utf8": "^3.0.0"
"utf8": "^3.0.0",
"rfc4648": "^1.5.3"
},
"devDependencies": {
"@babel/cli": "^7.23.4",
Expand Down
7 changes: 7 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

115 changes: 115 additions & 0 deletions src/EntityIdHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import Long from "long";
import * as hex from "./encoding/hex.js";
import BadEntityIdError from "./BadEntityIdError.js";
import * as util from "./util.js";
import base32 from "./base32.js";
import * as HashgraphProto from "@hashgraph/proto";
import PublicKey from "./PublicKey.js";

/**
* @typedef {import("./client/Client.js").default<*, *>} Client
Expand Down Expand Up @@ -408,3 +411,115 @@ export function toStringWithChecksum(string, client) {

return `${string}-${checksum}`;
}

/**
* 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}
*/
export function aliasToPublicKey(alias) {
const bytes = base32.decode(alias);
if (!bytes) {
return null;
}

Check warning on line 440 in src/EntityIdHelper.js

View check run for this annotation

Codecov / codecov/patch

src/EntityIdHelper.js#L439-L440

Added lines #L439 - L440 were not covered by tests
let key;
try {
key = HashgraphProto.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.fromBytes(key.ed25519);
}

if (key.ECDSASecp256k1 != null && key.ECDSASecp256k1.byteLength > 0) {
return PublicKey.fromBytes(key.ECDSASecp256k1);
}

return null;

Check warning on line 458 in src/EntityIdHelper.js

View check run for this annotation

Codecov / codecov/patch

src/EntityIdHelper.js#L457-L458

Added lines #L457 - L458 were not covered by tests
}

/**
* 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}
*/
export function aliasToEvmAddress(alias) {
const bytes = base32.decode(alias);
if (!bytes) {
return null;
}

Check warning on line 473 in src/EntityIdHelper.js

View check run for this annotation

Codecov / codecov/patch

src/EntityIdHelper.js#L472-L473

Added lines #L472 - L473 were not covered by tests
try {
HashgraphProto.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}
*/
export function publicKeyToAlias(publicKey) {
if (
typeof publicKey === "string" &&
((publicKey.startsWith("0x") && publicKey.length == 42) ||
publicKey.length == 40)
) {
publicKey = publicKey.replace("0x", "");
const bytes = Buffer.from(publicKey, "hex");
if (!bytes) {
return null;
}

Check warning on line 501 in src/EntityIdHelper.js

View check run for this annotation

Codecov / codecov/patch

src/EntityIdHelper.js#L500-L501

Added lines #L500 - L501 were not covered by tests
return base32.encode(bytes);
}

const publicKeyRaw =
typeof publicKey === "string"
? PublicKey.fromString(publicKey)
: publicKey;
const publicKeyHex = publicKeyRaw.toStringRaw();
let leadingHex = "";

if (publicKeyRaw._key._type === "secp256k1") {
leadingHex = "3A21"; // LEADING BYTES FROM PROTOBUFS
}

if (publicKeyRaw._key._type === "ED25519") {
leadingHex = "1220"; // LEADING BYTES FROM PROTOBUFS
}

const leadingBytes = Buffer.from(leadingHex, "hex");
const publicKeyBytes = Buffer.from(publicKeyHex, "hex");
const publicKeyInBytes = [leadingBytes, publicKeyBytes];
const alias = base32.encode(Buffer.concat(publicKeyInBytes));
return alias;
}
43 changes: 43 additions & 0 deletions src/base32.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2019-2023 Hedera Hashgraph, LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

// HIP-32: https://hips.hedera.com/hip/hip-32
import { base32 } from "rfc4648";

const decodeOpts = { loose: true };
const encodeOpts = { pad: false };

/**
* Decodes the rfc4648 base32 string into a {@link Uint8Array}. If the input string is null, returns null.
* @param {string} str the base32 string.
* @returns {Uint8Array | ''}
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const decode = (str) => str && base32.parse(str, decodeOpts);

/**
* Encodes the byte array into a rfc4648 base32 string without padding. If the input is null, returns null. Note with
* the rfc4648 loose = true option, it allows lower case letters, padding, and auto corrects 0 -> O, 1 -> L, 8 -> B
* @param {Buffer|Uint8Array} data
* @returns {string}
*/
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
const encode = (data) => data && base32.stringify(data, encodeOpts);

export default {
decode,
encode,
};
4 changes: 2 additions & 2 deletions test/unit/EcdsaPrivateKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ describe("EcdsaPrivateKey", function () {

it("should return private key from bytes", async function () {
const privateKeyFromBytes = PrivateKey.fromBytesECDSA(
DER_PRIVATE_KEY_BYTES
DER_PRIVATE_KEY_BYTES,
);
const publicKeyDer = privateKeyFromBytes.toStringDer();
expect(publicKeyDer).to.be.equal(DER_PRIVATE_KEY);
Expand All @@ -375,7 +375,7 @@ describe("EcdsaPrivateKey", function () {
const aliasAccountId = publicKey.toAccountId(0, 0);

expect(aliasAccountId.toString()).to.be.equal(
`0.0.${publicKey.toString()}`
`0.0.${publicKey.toString()}`,
);
});

Expand Down
4 changes: 2 additions & 2 deletions test/unit/Ed25519PrivateKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ describe("Ed25519PrivateKey", function () {

it("should return private key from bytes", async function () {
const privateKeyFromBytes = PrivateKey.fromBytesED25519(
DER_PRIVATE_KEY_BYTES
DER_PRIVATE_KEY_BYTES,
);
const publicKeyDer = privateKeyFromBytes.toStringDer();
expect(publicKeyDer).to.be.equal(DER_PRIVATE_KEY);
Expand All @@ -290,7 +290,7 @@ describe("Ed25519PrivateKey", function () {
const aliasAccountId = publicKey.toAccountId(0, 0);

expect(aliasAccountId.toString()).to.be.equal(
`0.0.${publicKey.toString()}`
`0.0.${publicKey.toString()}`,
);
});

Expand Down
60 changes: 60 additions & 0 deletions test/unit/EntityIdHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,64 @@ describe("EntityIdHelper", function () {

expect(address).to.eql(arrayLong);
});

it("should deserialise ed25519 alias to public key", function () {
const alias = "CIQBOMQE74WV37E4XU7GJAJJUP727KUVABF7KY2QF5IC5JIEPZUFK3I";
const publicKey =
"173204ff2d5dfc9cbd3e648129a3ffafaa95004bf563502f502ea5047e68556d";
const result = EntityIdHelper.aliasToPublicKey(alias);
expect(result.toStringRaw()).to.eql(publicKey);
});

it("should deserialise ecdsa alias to public key", function () {
const alias =
"HIQQHWKEWBU4IMVRHQKA7ZWXRB5MTVOE3VVIZH75H7ASQSIKXUEDCEGU";
const publicKey =
"03d944b069c432b13c140fe6d7887ac9d5c4dd6a8c9ffd3fc128490abd083110d4";
const result = EntityIdHelper.aliasToPublicKey(alias);
expect(result.toStringRaw()).to.eql(publicKey);
});

it("should error on hollow account alias to public key", function () {
const alias = "ADYQKZW5EGPUZ63YPBMLTEE2I2ATDXAL";
let errorThrown = false;
try {
EntityIdHelper.aliasToPublicKey(alias);
} catch (_) {
errorThrown = true;
}

expect(errorThrown).to.be.true;
});

it("should deserialise alias to evm address", function () {
const alias = "ADYQKZW5EGPUZ63YPBMLTEE2I2ATDXAL";
const evmAddress = "0x00f10566dd219f4cfb787858b9909a468131dc0b";
const result = EntityIdHelper.aliasToEvmAddress(alias);
expect(result).to.eql(evmAddress);
});

it("should serialize ed25519 public key to alias", function () {
const alias = "CIQBOMQE74WV37E4XU7GJAJJUP727KUVABF7KY2QF5IC5JIEPZUFK3I";
const publicKey =
"173204ff2d5dfc9cbd3e648129a3ffafaa95004bf563502f502ea5047e68556d";
const result = EntityIdHelper.publicKeyToAlias(publicKey);
expect(result).to.eql(alias);
});

it("should serialize ecdsa public key to alias", function () {
const alias =
"HIQQHWKEWBU4IMVRHQKA7ZWXRB5MTVOE3VVIZH75H7ASQSIKXUEDCEGU";
const publicKey =
"03d944b069c432b13c140fe6d7887ac9d5c4dd6a8c9ffd3fc128490abd083110d4";
const result = EntityIdHelper.publicKeyToAlias(publicKey);
expect(result).to.eql(alias);
});

it("should serialize hollow account evmAddress key to alias", function () {
const alias = "ADYQKZW5EGPUZ63YPBMLTEE2I2ATDXAL";
const evmAddress = "0x00f10566dd219f4cfb787858b9909a468131dc0b";
const result = EntityIdHelper.publicKeyToAlias(evmAddress);
expect(result).to.eql(alias);
});
});

0 comments on commit ead21ac

Please sign in to comment.