PHP WebShell
Текущая директория: /opt/BitGoJS/node_modules/micro-eth-signer/src
Просмотр файла: index.ts
/*! micro-eth-signer - MIT License (c) 2021 Paul Miller (paulmillr.com) */
import { keccak_256 } from '@noble/hashes/sha3';
import { bytesToHex, hexToBytes as _hexToBytes } from '@noble/hashes/utils';
import { isBytes } from '@noble/curves/abstract/utils';
import { secp256k1 } from '@noble/curves/secp256k1';
import * as RLP from '@ethereumjs/rlp';
export const CHAIN_TYPES = { mainnet: 1, ropsten: 3, rinkeby: 4, goerli: 5, kovan: 42 };
export const TRANSACTION_TYPES = { legacy: 0, eip2930: 1, eip1559: 2 };
type Hex = string | Uint8Array;
type RSRec = { r: bigint; s: bigint; recovery?: number };
type SignOpts = { extraEntropy?: boolean };
// All secp methods are in this object. This makes secp library easily replaceable
const secp = {
getPublicKey65b: (priv: Hex) => secp256k1.getPublicKey(priv, false),
normalizePublicKeyTo65b: (pub: Hex) => secp256k1.ProjectivePoint.fromHex(pub).toRawBytes(false),
sign: (msg: Hex, priv: Hex, opts?: SignOpts) => {
return secp256k1.sign(msg, priv, {
extraEntropy: opts?.extraEntropy === false ? undefined : true,
});
},
signAsync: async (msg: Hex, priv: Hex, opts?: SignOpts): Promise<RSRec> => {
return secp256k1.sign(msg, priv, {
extraEntropy: opts?.extraEntropy === false ? undefined : true,
});
},
sigRecoverPub: (rsrec: RSRec, msg: Hex, checkHighS = true) => {
const sig = new secp256k1.Signature(rsrec.r, rsrec.s).addRecoveryBit(rsrec.recovery!);
if (checkHighS && sig.hasHighS()) throw new Error('Invalid signature: s is invalid');
return sig.recoverPublicKey(msg).toRawBytes();
},
};
export function add0x(hex: string) {
return /^0x/i.test(hex) ? hex : `0x${hex}`;
}
export function strip0x(hex: string) {
return hex.replace(/^0x/i, '');
}
export function hexToBytes(hex: string): Uint8Array {
return _hexToBytes(strip0x(hex));
}
export function numberTo0xHex(num: number | bigint): string {
const hex = num.toString(16);
const x2 = hex.length & 1 ? `0${hex}` : hex;
return add0x(x2);
}
function hexToNumber(hex: string): bigint {
if (typeof hex !== 'string') {
throw new TypeError('hexToNumber: expected string, got ' + typeof hex);
}
return hex ? BigInt(add0x(hex)) : 0n;
}
function isObject(item: unknown): item is object {
return item != null && typeof item === 'object';
}
function cloneDeep<T>(obj: T): T {
if (Array.isArray(obj)) {
return obj.map(cloneDeep) as unknown as T;
} else if (typeof obj === 'bigint') {
return BigInt(obj) as unknown as T;
} else if (typeof obj === 'object') {
// should be last, so it won't catch other types
let res: any = {};
for (let key in obj) res[key] = cloneDeep(obj[key]);
return res;
} else return obj;
}
type Chain = keyof typeof CHAIN_TYPES;
type Type = keyof typeof TRANSACTION_TYPES;
// The order is important.
const FIELDS = ['nonce', 'gasPrice', 'gasLimit', 'to', 'value', 'data', 'v', 'r', 's'] as const;
// prettier-ignore
const FIELDS2930 = [
'chainId', 'nonce', 'gasPrice', 'gasLimit',
'to', 'value', 'data', 'accessList', 'yParity', 'r', 's'
] as const;
// prettier-ignore
const FIELDS1559 = [
'chainId', 'nonce', 'maxPriorityFeePerGas', 'maxFeePerGas', 'gasLimit',
'to', 'value', 'data', 'accessList', 'yParity', 'r', 's'
] as const;
const TypeToFields = {
legacy: FIELDS,
eip2930: FIELDS2930,
eip1559: FIELDS1559,
};
export type Field =
| (typeof FIELDS)[number]
| (typeof FIELDS2930)[number]
| (typeof FIELDS1559)[number]
| 'address'
| 'storageKey';
type str = string;
export type AccessList = [str, str[]][];
// These types will should be serializable by rlp as is
export type RawTxLegacy = [str, str, str, str, str, str, str, str, str];
export type RawTx2930 = [str, str, str, str, str, str, AccessList, str, str, str];
export type RawTx1559 = [str, str, str, str, str, str, str, AccessList, str, str, str];
export type RawTx = RawTxLegacy | RawTx2930 | RawTx1559;
export type RawTxMap = {
chainId?: string;
nonce: string;
gasPrice?: string;
maxPriorityFeePerGas?: string;
maxFeePerGas?: string;
gasLimit: string;
to: string;
value: string;
data: string;
accessList?: AccessList;
yParity?: string;
v?: string;
r: string;
s: string;
};
// Normalizes field to format which can easily be serialized by rlp (strings & arrays)
// prettier-ignore
const FIELD_NUMBER = new Set([
'chainId', 'nonce', 'gasPrice', 'maxPriorityFeePerGas', 'maxFeePerGas',
'gasLimit', 'value', 'v', 'yParity', 'r', 's'
]);
const FIELD_DATA = new Set(['data', 'to', 'storageKey', 'address']);
function normalizeField(
field: Field,
value:
| number
| bigint
| string
| Uint8Array
| Record<string, string[]>
| AccessList
| { address: string; storageKeys: string[] }[]
): string | AccessList {
// can be number, bignumber, decimal number in string (123), hex number in string (0x123)
if (FIELD_NUMBER.has(field)) {
// bytes
if (isBytes(value)) value = add0x(bytesToHex(value));
if (field === 'yParity' && typeof value === 'boolean') value = value ? '0x1' : '0x0';
// '123' -> 0x7b (handles both hex and non-hex numbers)
if (typeof value === 'string') value = BigInt(value === '0x' ? '0x0' : value);
// 123 -> '0x7b' && 1 -> 0x01
if (typeof value === 'number' || typeof value === 'bigint') value = numberTo0xHex(value);
// 21000, default / minimum
if (field === 'gasLimit' && (!value || BigInt(value as string) === 0n)) value = '0x5208';
if (typeof value !== 'string') throw new TypeError(`Invalid type for field ${field}`);
// should be hex string starting with '0x' at this point.
if (field === 'gasPrice' && BigInt(value) === 0n)
throw new TypeError('The gasPrice must have non-zero value');
// '0x00' and '' serializes differently
return BigInt(value) === 0n ? '' : value;
}
// Can be string or Uint8Array
if (FIELD_DATA.has(field)) {
if (!value) value = '';
if (isBytes(value)) value = bytesToHex(value);
if (typeof value !== 'string') throw new TypeError(`Invalid type for field ${field}`);
value = add0x(value);
return value === '0x' ? '' : value;
}
if (field === 'accessList') {
if (!value) return [];
let res: Record<string, Set<string>> = {};
if (Array.isArray(value)) {
for (let access of value) {
if (Array.isArray(access)) {
// AccessList
if (access.length !== 2 || !Array.isArray(access[1]))
throw new TypeError(`Invalid type for field ${field}`);
const key = normalizeField('address', access[0]) as string;
if (!res[key]) res[key] = new Set();
for (let i of access[1]) res[key].add(normalizeField('storageKey', i) as string);
} else {
// {address: string, storageKeys: string[]}[]
if (!isObject(access) || !access.address || !Array.isArray(access.storageKeys))
throw new TypeError(`Invalid type for field ${field}`);
const key = normalizeField('address', access.address) as string;
if (!res[key]) res[key] = new Set();
for (let i of access.storageKeys) res[key].add(normalizeField('storageKey', i) as string);
}
}
} else {
// {[address]: string[]}
if (!isObject(value) || isBytes(value))
throw new TypeError(`Invalid type for field ${field}`);
for (let k in value) {
const key = normalizeField('address', k) as string;
// undefined/empty allowed
if (!value[k]) continue;
if (!Array.isArray(value[k])) throw new TypeError(`Invalid type for field ${field}`);
res[key] = new Set(value[k].map((i) => normalizeField('storageKey', i) as string));
}
}
return Object.keys(res).map((i) => [i, Array.from(res[i])]) as AccessList;
}
throw new TypeError(`Invalid type for field ${field}`);
}
function possibleTypes(input: RawTxMap): Set<Type> {
let types: Set<Type> = new Set(Object.keys(TRANSACTION_TYPES) as Type[]);
const keys = new Set(Object.keys(input));
if (keys.has('maxPriorityFeePerGas') || keys.has('maxFeePerGas')) {
types.delete('legacy');
types.delete('eip2930');
}
if (keys.has('accessList') || keys.has('yParity')) types.delete('legacy');
if (keys.has('gasPrice')) types.delete('eip1559');
return types;
}
const RawTxLength: Record<number, Type> = { 9: 'legacy', 11: 'eip2930', 12: 'eip1559' };
const RawTxLengthRev: Record<Type, number> = { legacy: 9, eip2930: 11, eip1559: 12 };
function rawToSerialized(input: RawTx | RawTxMap, chain?: Chain, type?: Type): string {
let chainId;
if (chain) chainId = CHAIN_TYPES[chain];
if (Array.isArray(input)) {
if (!type) type = RawTxLength[input.length];
if (!type || RawTxLengthRev[type] !== input.length)
throw new Error(`Invalid fields length for ${type}`);
} else {
const types = possibleTypes(input);
if (type && !types.has(type)) {
throw new Error(
`Invalid type=${type}. Possible types with current fields: ${Array.from(types)}`
);
}
if (!type) {
if (types.has('legacy')) type = 'legacy';
else if (!types.size) throw new Error('Impossible fields set');
else type = Array.from(types)[0];
}
if (input.chainId) {
if (chain) {
const fromChain = normalizeField('chainId', CHAIN_TYPES[chain]);
const fromInput = normalizeField('chainId', input.chainId);
if (fromChain !== fromInput) {
throw new Error(
`Both chain=${chain}(${fromChain}) and chainId=${input.chainId}(${fromInput}) specified at same time`
);
}
}
chainId = input.chainId;
} else input.chainId = chainId as any;
input = (TypeToFields[type] as unknown as Field[]).map((key) => (input as any)[key]) as RawTx;
}
if (input) {
const sign = input.slice(-3);
// remove signature if any of fields is empty
if (!sign[0] || !sign[1] || !sign[2]) {
input = input.slice(0, -3) as any;
// EIP-155
if (type === 'legacy' && chainId)
(input as any).push(normalizeField('chainId', chainId), '', '');
}
}
let normalized = (input as Field[]).map((value, i) =>
normalizeField(TypeToFields[type as Type][i], value)
);
if (chainId) chainId = normalizeField('chainId', chainId);
if (type !== 'legacy' && chainId && normalized[0] !== chainId)
throw new Error(`ChainId=${normalized[0]} incompatible with Chain=${chainId}`);
const tNum = TRANSACTION_TYPES[type];
return (tNum ? `0x0${tNum}` : '0x') + bytesToHex(RLP.encode(normalized));
}
export const Address = {
fromPrivateKey(key: string | Uint8Array): string {
if (typeof key === 'string') key = hexToBytes(key);
return Address.fromPublicKey(secp.getPublicKey65b(key));
},
fromPublicKey(key: string | Uint8Array): string {
const pub = secp.normalizePublicKeyTo65b(key);
const addr = bytesToHex(keccak_256(pub.subarray(1, 65))).slice(24);
return Address.checksum(addr);
},
// ETH addr checksum is calculated by hashing the string with keccak.
// NOTE: it hashes *string*, not a bytearray: keccak('beef') not keccak([0xbe, 0xef])
checksum(nonChecksummedAddress: string): string {
const addr = strip0x(nonChecksummedAddress.toLowerCase());
if (addr.length !== 40) throw new Error('Invalid address, must have 40 chars');
const hash = strip0x(bytesToHex(keccak_256(addr)));
let checksummed = '';
for (let i = 0; i < addr.length; i++) {
// If ith character is 9 to f then make it uppercase
const nth = Number.parseInt(hash[i], 16);
let char = addr[i];
if (nth > 7) char = char.toUpperCase();
checksummed += char;
}
return add0x(checksummed);
},
verifyChecksum(address: string): boolean {
const addr = strip0x(address);
if (addr.length !== 40) throw new Error('Invalid address, must have 40 chars');
if (addr === addr.toLowerCase() || addr === addr.toUpperCase()) return true;
const hash = bytesToHex(keccak_256(addr.toLowerCase()));
for (let i = 0; i < 40; i++) {
// the nth letter should be uppercase if the nth digit of casemap is 1
const char = addr[i]!;
const nth = Number.parseInt(hash[i]!, 16);
if (nth > 7 && char.toUpperCase() !== char) return false;
if (nth <= 7 && char.toLowerCase() !== char) return false;
}
return true;
},
};
export class Transaction {
static DEFAULT_HARDFORK = 'london';
static DEFAULT_CHAIN: Chain = 'mainnet';
static DEFAULT_TYPE: Type = 'eip1559';
readonly hex: string;
readonly raw: RawTxMap;
readonly isSigned: boolean;
readonly type: Type;
constructor(
data: string | Uint8Array | RawTx | RawTxMap,
chain?: Chain,
readonly hardfork = Transaction.DEFAULT_HARDFORK,
type?: Type
) {
let norm;
if (typeof data === 'string') {
norm = data;
} else if (data instanceof Uint8Array) {
norm = bytesToHex(data);
} else if (Array.isArray(data) || (typeof data === 'object' && data != null)) {
norm = rawToSerialized(data, chain, type);
} else {
throw new TypeError('Expected valid serialized tx');
}
if (norm.length <= 6) throw new Error('Invalid tx length');
this.hex = add0x(norm);
let txData;
const prevType = type;
if (this.hex.startsWith('0x01')) [txData, type] = [add0x(this.hex.slice(4)), 'eip2930'];
else if (this.hex.startsWith('0x02')) [txData, type] = [add0x(this.hex.slice(4)), 'eip1559'];
else [txData, type] = [this.hex, 'legacy'];
if (prevType && prevType !== type) throw new Error('Invalid transaction type');
this.type = type;
const ui8a = RLP.decode(txData) as Uint8Array[];
this.raw = ui8a.reduce((res: any, value: any, i: number) => {
const name = TypeToFields[type!][i];
if (!name) return res;
res[name] = normalizeField(name, value);
return res;
}, {} as RawTxMap);
if (!this.raw.chainId) {
// Unsigned transaction with EIP-155
if (type === 'legacy' && !this.raw.r && !this.raw.s) {
this.raw.chainId = this.raw.v;
this.raw.v = '';
}
}
if (!this.raw.chainId) {
this.raw.chainId = normalizeField(
'chainId',
CHAIN_TYPES[chain || Transaction.DEFAULT_CHAIN]
) as string;
}
this.isSigned = !!(this.raw.r && this.raw.r !== '0x');
}
private isNew() {
return this.type === 'eip1559';
}
get bytes(): Uint8Array {
return hexToBytes(this.hex);
}
equals(other: Transaction) {
return this.getMessageToSign() === other.getMessageToSign();
}
get chain(): Chain | undefined {
for (let k in CHAIN_TYPES)
if (CHAIN_TYPES[k as Chain] === Number(this.raw.chainId!)) return k as Chain;
return undefined;
}
get sender(): string {
const sender = this.recoverSenderPublicKey();
if (!sender) throw new Error('Invalid signed transaction');
return Address.fromPublicKey(sender);
}
get gasPrice(): bigint {
if (this.isNew()) throw new Error('Field only available for "legacy" transactions');
return BigInt(this.raw.gasPrice!);
}
// maxFeePerGas: Represents the maximum amount that a user is willing to pay for their tx (inclusive of baseFeePerGas and maxPriorityFeePerGas)
get maxFeePerGas() {
if (!this.isNew()) throw new Error('Field only available for "eip1559" transactions');
return BigInt(this.raw.maxFeePerGas!);
}
get maxPriorityFeePerGas() {
if (!this.isNew()) throw new Error('Field only available for "eip1559" transactions');
return BigInt(this.raw.maxPriorityFeePerGas!);
}
get gasLimit(): bigint {
return BigInt(this.raw.gasLimit!);
}
// Amount in wei
get amount(): bigint {
return BigInt(this.raw.value);
}
// Total fee in wei
get fee(): bigint {
const price = this.isNew() ? this.maxFeePerGas : this.gasPrice;
return price * this.gasLimit;
}
// Amount + fee in wei
get upfrontCost(): bigint {
return this.amount + this.fee;
}
// Checksummed address
get to(): string {
return Address.checksum(this.raw.to);
}
// Nonce is a counter that represents a number of outgoing transactions on the acct
get nonce(): number {
return Number.parseInt(this.raw.nonce, 16) || 0;
}
private supportsReplayProtection() {
const properBlock = !['chainstart', 'homestead', 'dao', 'tangerineWhistle'].includes(
this.hardfork
);
if (!this.isSigned) return true; // Unsigned, supports EIP155
const v = Number(hexToNumber(this.raw.v!));
const chainId = Number(this.raw.chainId!);
const meetsConditions = v === chainId * 2 + 35 || v === chainId * 2 + 36;
return properBlock && meetsConditions;
}
getMessageToSign(signed: boolean = false): string {
let values = (TypeToFields[this.type] as any).map((i: any) => (this.raw as any)[i]);
if (!signed) {
// TODO: merge with line #252 somehow? (same strip & EIP-155)
// Strip signature (last 3 values)
values = values.slice(0, -3);
// EIP-155
if (this.type === 'legacy' && this.supportsReplayProtection())
values.push(this.raw.chainId! as any, '', '');
}
let encoded = RLP.encode(values);
if (this.type !== 'legacy')
encoded = new Uint8Array([TRANSACTION_TYPES[this.type], ...Array.from(encoded)]);
return bytesToHex(keccak_256(encoded));
}
// Used in block explorers etc
get hash(): string {
if (!this.isSigned) throw new Error('Expected signed transaction');
return this.getMessageToSign(true);
}
sign(privateKey: string | Uint8Array, extraEntropy = false): Transaction {
if (this.isSigned) throw new Error('Expected unsigned transaction');
if (typeof privateKey === 'string') privateKey = strip0x(privateKey);
const sig = secp.sign(this.getMessageToSign(), privateKey, { extraEntropy });
const rec = sig.recovery!;
const chainId = Number(this.raw.chainId!);
const vv = this.type === 'legacy' ? (chainId ? rec + (chainId * 2 + 35) : rec + 27) : rec;
const [v, r, s] = [vv, sig.r, sig.s].map(numberTo0xHex);
const signedRaw: RawTxMap =
this.type === 'legacy'
? { ...this.raw, v, r, s }
: { ...cloneDeep(this.raw), yParity: v, r, s };
return new Transaction(signedRaw, this.chain, this.hardfork, this.type);
}
recoverSenderPublicKey(): Uint8Array | undefined {
if (!this.isSigned)
throw new Error('Expected signed transaction: cannot recover sender of unsigned tx');
const v = Number(hexToNumber(this.type === 'legacy' ? this.raw.v! : this.raw.yParity!));
const chainId = Number(this.raw.chainId!);
const recovery = this.type === 'legacy' ? (chainId ? v - (chainId * 2 + 35) : v - 27) : v;
const [r, s] = [this.raw.r, this.raw.s].map(hexToNumber);
const checkHighS = this.hardfork !== 'chainstart';
return secp.sigRecoverPub({ r, s, recovery }, this.getMessageToSign(), checkHighS);
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!