@arcblock/did
Version:
Javascript lib to work with ArcBlock DID
177 lines (175 loc) • 5.82 kB
JavaScript
import { toBits, toBytes, toStrictHex } from "./util.mjs";
import { Hasher, types } from "@ocap/mcrypto";
import { BN, isHex, numberToHex, stripHexPrefix, toAddress } from "@ocap/util";
import isEqual from "lodash/isEqual.js";
import pick from "lodash/pick.js";
import upperFirst from "lodash/upperFirst.js";
//#region src/type.ts
const mapping = {
pk: "key",
address: "encoding"
};
const DID_TYPE_ARCBLOCK = {
role: types.RoleType.ROLE_ACCOUNT,
pk: types.KeyType.ED25519,
hash: types.HashType.SHA3,
address: types.EncodingType.BASE58
};
const DID_TYPE_PASSKEY = {
role: types.RoleType.ROLE_PASSKEY,
pk: types.KeyType.PASSKEY,
hash: types.HashType.SHA2,
address: types.EncodingType.BASE58
};
const DID_TYPE_ETHEREUM = {
role: types.RoleType.ROLE_ACCOUNT,
pk: types.KeyType.ETHEREUM,
hash: types.HashType.KECCAK,
address: types.EncodingType.BASE16
};
const isEthereumType = (type) => {
const props = [
"pk",
"hash",
"address"
];
return isEqual(pick(type, props), pick(DID_TYPE_ETHEREUM, props));
};
/**
* Checks if the given string is an address
*
* @method isEthereumDid
* @param {String} address the given HEX address
* @return {Boolean}
*/
const isEthereumDid = (did) => {
const address = toAddress(did);
if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) return false;
if (/^(0x|0X)?[0-9a-f]{40}$/.test(address) || /^(0x|0X)?[0-9A-F]{40}$/.test(address)) return true;
return checkAddressChecksum(address);
};
/**
* Checks if the given string is a checksummed address
*
* @method checkAddressChecksum
* @param {String} address the given HEX address
* @return {Boolean}
*/
const checkAddressChecksum = (address) => {
const origin = address.replace(/^0x/i, "");
const addressHash = Hasher.Keccak.hash256(origin.toLowerCase()).replace(/^0x/i, "");
for (let i = 0; i < 40; i++) if (Number.parseInt(addressHash[i], 16) > 7 && origin[i].toUpperCase() !== origin[i] || Number.parseInt(addressHash[i], 16) <= 7 && origin[i].toLowerCase() !== origin[i]) return false;
return true;
};
/**
* Converts to a checksum address
*
* @method toChecksumAddress
* @param {String} address the given HEX address
* @return {String}
*/
const toChecksumAddress = (address) => {
if (typeof address === "undefined") return "";
if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) throw new Error(`Given address "${address}" is not a valid Ethereum address.`);
const lower = address.toLowerCase().replace(/^0x/i, "");
const addressHash = Hasher.Keccak.hash256(lower).replace(/^0x/i, "");
let checksumAddress = "0x";
for (let i = 0; i < lower.length; i++) if (Number.parseInt(addressHash[i], 16) > 7) checksumAddress += lower[i].toUpperCase();
else checksumAddress += lower[i];
return checksumAddress;
};
/**
* Create an wallet type object that be used as param to create a new wallet
*/
function DidType(type = {}) {
let input = {};
if (["default", "arcblock"].includes(type)) input = DID_TYPE_ARCBLOCK;
else if (["eth", "ethereum"].includes(type)) input = DID_TYPE_ETHEREUM;
else if (["passkey"].includes(type)) input = DID_TYPE_PASSKEY;
else input = {
...DID_TYPE_ARCBLOCK,
...type
};
const { role, pk, hash, address } = input;
Object.keys(input).forEach((x) => {
const key = upperFirst(`${mapping[x] || x}Type`);
if (Object.values(types[key]).includes(input[x]) === false) throw new Error(`Unsupported ${x} type: ${input[x]}`);
});
const output = {
role,
pk,
hash,
address
};
if (isEthereumType(output) === false) {
if ([
types.RoleType.ROLE_NODE,
types.RoleType.ROLE_VALIDATOR,
types.RoleType.ROLE_TETHER,
types.RoleType.ROLE_SWAP
].includes(output.role)) output.hash = types.HashType.SHA2;
}
return output;
}
DidType.toJSON = (type) => Object.keys(type).reduce((acc, x) => {
const key = upperFirst(`${mapping[x] || x}Type`);
acc[x] = Object.keys(types[key])[Object.values(types[key]).indexOf(type[x])];
return acc;
}, {});
DidType.fromJSON = (json) => Object.keys(json).reduce((acc, x) => {
const key = upperFirst(`${mapping[x] || x}Type`);
const typeStr = Object.keys(types[key]);
acc[x] = Object.values(types[key])[typeStr.indexOf(json[x])];
return acc;
}, {});
/**
* Convert type info object to hex string
*
* @public
* @static
* @param {object} type - wallet type, {@see @arcblock/did#DidType}
* @returns string
*/
const fromTypeInfo = (type) => {
const info = DidType(type);
const infoBits = `${toBits(info.role, 6)}${toBits(info.pk, 5)}${toBits(info.hash, 5)}`;
return toStrictHex(stripHexPrefix(numberToHex(Number.parseInt(infoBits, 2))), 4);
};
/**
* Get type info from did
*/
const toTypeInfo = (did) => {
let type = {};
try {
if (isEthereumDid(did)) type = DID_TYPE_ETHEREUM;
else {
const typeBytes = toBytes(did).slice(0, 2);
const typeBits = toBits(new BN(toStrictHex(Buffer.from(Uint8Array.from(typeBytes)).toString("hex")), 16), 16);
const roleBits = typeBits.slice(0, 6);
const keyBits = typeBits.slice(6, 11);
const hashBits = typeBits.slice(11, 16);
type = {
role: Number.parseInt(roleBits, 2),
pk: Number.parseInt(keyBits, 2),
hash: Number.parseInt(hashBits, 2),
address: isHex(toAddress(did)) ? types.EncodingType.BASE16 : types.EncodingType.BASE58
};
}
Object.keys(type).forEach((x) => {
if (Object.values(types[`${upperFirst(mapping[x] || x)}Type`]).includes(type[x]) === false) delete type[x];
});
return type;
} catch {
return type;
}
};
const toTypeInfoStr = (did) => {
const type = toTypeInfo(did);
return Object.keys(type).reduce((acc, x) => {
const enums = types[`${upperFirst(mapping[x] || x)}Type`];
acc[x] = Object.keys(enums).find((d) => enums[d] === type[x]);
return acc;
}, {});
};
//#endregion
export { DID_TYPE_ARCBLOCK, DID_TYPE_ETHEREUM, DID_TYPE_PASSKEY, DidType, fromTypeInfo, isEthereumDid, isEthereumType, toChecksumAddress, toTypeInfo, toTypeInfoStr };