ethers.js/packages/bytes/src.ts/index.ts

484 lines
14 KiB
TypeScript
Raw Permalink Normal View History

2019-05-14 18:25:46 -04:00
"use strict";
2019-08-01 18:04:06 -04:00
import { Logger } from "@ethersproject/logger";
import { version } from "./_version";
const logger = new Logger(version);
2019-05-14 18:25:46 -04:00
///////////////////////////////
// Exported Types
export type Bytes = ArrayLike<number>;
export type BytesLike = Bytes | string;
export type DataOptions = {
allowMissingPrefix?: boolean;
hexPad?: "left" | "right" | null;
2019-05-14 18:25:46 -04:00
};
export interface Hexable {
toHexString(): string;
}
/*
export interface HexString {
length: number;
substring: (start: number, end?: number) => string;
[index: number]: string;
}
*/
export type SignatureLike = {
r: string;
s?: string;
_vs?: string,
recoveryParam?: number;
v?: number;
} | BytesLike;
export interface Signature {
r: string;
s: string;
_vs: string,
recoveryParam: number;
v: number;
yParityAndS: string
compact: string;
2019-05-14 18:25:46 -04:00
}
///////////////////////////////
function isHexable(value: any): value is Hexable {
return !!(value.toHexString);
}
function addSlice(array: Uint8Array): Uint8Array {
if (array.slice) { return array; }
array.slice = function() {
const args = Array.prototype.slice.call(arguments);
2019-05-14 18:25:46 -04:00
return addSlice(new Uint8Array(Array.prototype.slice.apply(array, args)));
}
return array;
}
export function isBytesLike(value: any): value is BytesLike {
return ((isHexString(value) && !(value.length % 2)) || isBytes(value));
}
function isInteger(value: number) {
return (typeof(value) === "number" && value == value && (value % 1) === 0);
}
2019-05-14 18:25:46 -04:00
export function isBytes(value: any): value is Bytes {
if (value == null) { return false; }
if (value.constructor === Uint8Array) { return true; }
if (typeof(value) === "string") { return false; }
if (!isInteger(value.length) || value.length < 0) { return false; }
2019-05-14 18:25:46 -04:00
for (let i = 0; i < value.length; i++) {
const v = value[i];
if (!isInteger(v) || v < 0 || v >= 256) { return false; }
2019-05-14 18:25:46 -04:00
}
return true;
}
export function arrayify(value: BytesLike | Hexable | number, options?: DataOptions): Uint8Array {
if (!options) { options = { }; }
if (typeof(value) === "number") {
2019-08-01 18:04:06 -04:00
logger.checkSafeUint53(value, "invalid arrayify value");
2019-05-14 18:25:46 -04:00
const result = [];
2019-05-14 18:25:46 -04:00
while (value) {
result.unshift(value & 0xff);
value = parseInt(String(value / 256));
2019-05-14 18:25:46 -04:00
}
if (result.length === 0) { result.push(0); }
return addSlice(new Uint8Array(result));
}
if (options.allowMissingPrefix && typeof(value) === "string" && value.substring(0, 2) !== "0x") {
value = "0x" + value;
}
if (isHexable(value)) { value = value.toHexString(); }
if (isHexString(value)) {
let hex = (<string>value).substring(2);
if (hex.length % 2) {
if (options.hexPad === "left") {
hex = "0" + hex;
} else if (options.hexPad === "right") {
hex += "0";
} else {
logger.throwArgumentError("hex data is odd-length", "value", value);
}
2019-05-14 18:25:46 -04:00
}
const result = [];
2019-05-14 18:25:46 -04:00
for (let i = 0; i < hex.length; i += 2) {
result.push(parseInt(hex.substring(i, i + 2), 16));
}
return addSlice(new Uint8Array(result));
}
if (isBytes(value)) {
return addSlice(new Uint8Array(value));
}
2019-08-01 18:04:06 -04:00
return logger.throwArgumentError("invalid arrayify value", "value", value);
2019-05-14 18:25:46 -04:00
}
export function concat(items: ReadonlyArray<BytesLike>): Uint8Array {
const objects = items.map(item => arrayify(item));
const length = objects.reduce((accum, item) => (accum + item.length), 0);
2019-05-14 18:25:46 -04:00
const result = new Uint8Array(length);
2019-05-14 18:25:46 -04:00
objects.reduce((offset, object) => {
result.set(object, offset);
return offset + object.length;
}, 0);
return addSlice(result);
}
export function stripZeros(value: BytesLike): Uint8Array {
let result: Uint8Array = arrayify(value);
if (result.length === 0) { return result; }
// Find the first non-zero entry
let start = 0;
while (start < result.length && result[start] === 0) { start++ }
// If we started with zeros, strip them
if (start) {
result = result.slice(start);
}
return result;
}
export function zeroPad(value: BytesLike, length: number): Uint8Array {
value = arrayify(value);
if (value.length > length) {
2019-08-01 18:04:06 -04:00
logger.throwArgumentError("value out of range", "value", arguments[0]);
2019-05-14 18:25:46 -04:00
}
const result = new Uint8Array(length);
2019-05-14 18:25:46 -04:00
result.set(value, length - value.length);
return addSlice(result);
}
export function isHexString(value: any, length?: number): boolean {
if (typeof(value) !== "string" || !value.match(/^0x[0-9A-Fa-f]*$/)) {
return false
}
if (length && value.length !== 2 + 2 * length) { return false; }
return true;
}
const HexCharacters: string = "0123456789abcdef";
2021-04-23 17:44:22 -04:00
export function hexlify(value: BytesLike | Hexable | number | bigint, options?: DataOptions): string {
2019-05-14 18:25:46 -04:00
if (!options) { options = { }; }
if (typeof(value) === "number") {
2019-08-01 18:04:06 -04:00
logger.checkSafeUint53(value, "invalid hexlify value");
2019-05-14 18:25:46 -04:00
let hex = "";
while (value) {
2021-04-23 17:44:22 -04:00
hex = HexCharacters[value & 0xf] + hex;
2019-05-14 18:25:46 -04:00
value = Math.floor(value / 16);
}
if (hex.length) {
if (hex.length % 2) { hex = "0" + hex; }
return "0x" + hex;
}
return "0x00";
}
2021-04-23 17:44:22 -04:00
if (typeof(value) === "bigint") {
value = value.toString(16);
if (value.length % 2) { return ("0x0" + value); }
return "0x" + value;
}
2019-05-14 18:25:46 -04:00
if (options.allowMissingPrefix && typeof(value) === "string" && value.substring(0, 2) !== "0x") {
value = "0x" + value;
}
if (isHexable(value)) { return value.toHexString(); }
if (isHexString(value)) {
if ((<string>value).length % 2) {
if (options.hexPad === "left") {
value = "0x0" + (<string>value).substring(2);
} else if (options.hexPad === "right") {
value += "0";
} else {
logger.throwArgumentError("hex data is odd-length", "value", value);
}
2019-05-14 18:25:46 -04:00
}
return (<string>value).toLowerCase();
}
if (isBytes(value)) {
let result = "0x";
for (let i = 0; i < value.length; i++) {
let v = value[i];
result += HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f];
}
return result;
}
2019-08-01 18:04:06 -04:00
return logger.throwArgumentError("invalid hexlify value", "value", value);
2019-05-14 18:25:46 -04:00
}
/*
function unoddify(value: BytesLike | Hexable | number): BytesLike | Hexable | number {
if (typeof(value) === "string" && value.length % 2 && value.substring(0, 2) === "0x") {
return "0x0" + value.substring(2);
}
return value;
}
*/
export function hexDataLength(data: BytesLike) {
if (typeof(data) !== "string") {
data = hexlify(data);
} else if (!isHexString(data) || (data.length % 2)) {
return null;
}
return (data.length - 2) / 2;
}
export function hexDataSlice(data: BytesLike, offset: number, endOffset?: number): string {
if (typeof(data) !== "string") {
data = hexlify(data);
} else if (!isHexString(data) || (data.length % 2)) {
2019-08-01 18:04:06 -04:00
logger.throwArgumentError("invalid hexData", "value", data );
2019-05-14 18:25:46 -04:00
}
offset = 2 + 2 * offset;
if (endOffset != null) {
return "0x" + data.substring(offset, 2 + 2 * endOffset);
}
return "0x" + data.substring(offset);
}
export function hexConcat(items: ReadonlyArray<BytesLike>): string {
2019-05-14 18:25:46 -04:00
let result = "0x";
items.forEach((item) => {
result += hexlify(item).substring(2);
});
return result;
}
2021-04-23 17:44:22 -04:00
export function hexValue(value: BytesLike | Hexable | number | bigint): string {
const trimmed = hexStripZeros(hexlify(value, { hexPad: "left" }));
2019-05-14 18:25:46 -04:00
if (trimmed === "0x") { return "0x0"; }
return trimmed;
}
export function hexStripZeros(value: BytesLike): string {
if (typeof(value) !== "string") { value = hexlify(value); }
if (!isHexString(value)) {
2019-08-01 18:04:06 -04:00
logger.throwArgumentError("invalid hex string", "value", value);
2019-05-14 18:25:46 -04:00
}
value = value.substring(2);
let offset = 0;
while (offset < value.length && value[offset] === "0") { offset++; }
return "0x" + value.substring(offset);
}
export function hexZeroPad(value: BytesLike, length: number): string {
if (typeof(value) !== "string") {
value = hexlify(value);
} else if (!isHexString(value)) {
2019-08-01 18:04:06 -04:00
logger.throwArgumentError("invalid hex string", "value", value);
2019-05-14 18:25:46 -04:00
}
if (value.length > 2 * length + 2) {
2019-08-01 18:04:06 -04:00
logger.throwArgumentError("value out of range", "value", arguments[1]);
2019-05-14 18:25:46 -04:00
}
while (value.length < 2 * length + 2) {
value = "0x0" + value.substring(2);
}
return value;
}
export function splitSignature(signature: SignatureLike): Signature {
const result = {
2019-05-14 18:25:46 -04:00
r: "0x",
s: "0x",
_vs: "0x",
recoveryParam: 0,
v: 0,
yParityAndS: "0x",
compact: "0x"
2019-05-14 18:25:46 -04:00
};
if (isBytesLike(signature)) {
let bytes: Uint8Array = arrayify(signature);
2019-05-14 18:25:46 -04:00
// Get the r, s and v
if (bytes.length === 64) {
// EIP-2098; pull the v from the top bit of s and clear it
result.v = 27 + (bytes[32] >> 7);
bytes[32] &= 0x7f;
result.r = hexlify(bytes.slice(0, 32));
result.s = hexlify(bytes.slice(32, 64));
} else if (bytes.length === 65) {
result.r = hexlify(bytes.slice(0, 32));
result.s = hexlify(bytes.slice(32, 64));
result.v = bytes[64];
} else {
logger.throwArgumentError("invalid signature string", "signature", signature);
}
2019-05-14 18:25:46 -04:00
// Allow a recid to be used as the v
if (result.v < 27) {
if (result.v === 0 || result.v === 1) {
result.v += 27;
} else {
logger.throwArgumentError("signature invalid v byte", "signature", signature);
}
}
// Compute recoveryParam from v
result.recoveryParam = 1 - (result.v % 2);
2019-05-14 18:25:46 -04:00
// Compute _vs from recoveryParam and s
if (result.recoveryParam) { bytes[32] |= 0x80; }
result._vs = hexlify(bytes.slice(32, 64))
} else {
result.r = signature.r;
result.s = signature.s;
result.v = signature.v;
result.recoveryParam = signature.recoveryParam;
result._vs = signature._vs;
// If the _vs is available, use it to populate missing s, v and recoveryParam
// and verify non-missing s, v and recoveryParam
if (result._vs != null) {
const vs = zeroPad(arrayify(result._vs), 32);
result._vs = hexlify(vs);
2019-05-14 18:25:46 -04:00
// Set or check the recid
const recoveryParam = ((vs[0] >= 128) ? 1: 0);
if (result.recoveryParam == null) {
result.recoveryParam = recoveryParam;
} else if (result.recoveryParam !== recoveryParam) {
logger.throwArgumentError("signature recoveryParam mismatch _vs", "signature", signature);
}
2019-05-14 18:25:46 -04:00
// Set or check the s
2019-05-14 18:25:46 -04:00
vs[0] &= 0x7f;
const s = hexlify(vs);
2019-05-14 18:25:46 -04:00
if (result.s == null) {
result.s = s;
} else if (result.s !== s) {
2019-08-01 18:04:06 -04:00
logger.throwArgumentError("signature v mismatch _vs", "signature", signature);
2019-05-14 18:25:46 -04:00
}
}
2019-05-14 18:25:46 -04:00
// Use recid and v to populate each other
if (result.recoveryParam == null) {
2019-05-14 18:25:46 -04:00
if (result.v == null) {
logger.throwArgumentError("signature missing v and recoveryParam", "signature", signature);
2021-03-26 16:16:56 -04:00
} else if (result.v === 0 || result.v === 1) {
result.recoveryParam = result.v;
} else {
result.recoveryParam = 1 - (result.v % 2);
2019-05-14 18:25:46 -04:00
}
} else {
if (result.v == null) {
result.v = 27 + result.recoveryParam;
} else {
const recId = (result.v === 0 || result.v === 1) ? result.v :(1 - (result.v % 2));
if (result.recoveryParam !== recId) {
logger.throwArgumentError("signature recoveryParam mismatch v", "signature", signature);
}
2019-05-14 18:25:46 -04:00
}
}
if (result.r == null || !isHexString(result.r)) {
logger.throwArgumentError("signature missing or invalid r", "signature", signature);
} else {
result.r = hexZeroPad(result.r, 32);
2019-05-14 18:25:46 -04:00
}
if (result.s == null || !isHexString(result.s)) {
logger.throwArgumentError("signature missing or invalid s", "signature", signature);
} else {
result.s = hexZeroPad(result.s, 32);
2019-05-14 18:25:46 -04:00
}
const vs = arrayify(result.s);
if (vs[0] >= 128) {
logger.throwArgumentError("signature s out of range", "signature", signature);
2019-05-14 18:25:46 -04:00
}
if (result.recoveryParam) { vs[0] |= 0x80; }
const _vs = hexlify(vs);
2019-05-14 18:25:46 -04:00
if (result._vs) {
if (!isHexString(result._vs)) {
logger.throwArgumentError("signature invalid _vs", "signature", signature);
2019-05-14 18:25:46 -04:00
}
result._vs = hexZeroPad(result._vs, 32);
}
// Set or check the _vs
if (result._vs == null) {
result._vs = _vs;
} else if (result._vs !== _vs) {
logger.throwArgumentError("signature _vs mismatch v and s", "signature", signature);
2019-05-14 18:25:46 -04:00
}
}
result.yParityAndS = result._vs;
result.compact = result.r + result.yParityAndS.substring(2);
2019-05-14 18:25:46 -04:00
return result;
}
export function joinSignature(signature: SignatureLike): string {
2019-05-14 18:25:46 -04:00
signature = splitSignature(signature);
return hexlify(concat([
signature.r,
signature.s,
(signature.recoveryParam ? "0x1c": "0x1b")
]));
}