micro-eth-signer
Advanced tools
Comparing version 0.1.7 to 0.2.0
@@ -9,9 +9,35 @@ /*! micro-eth-signer - MIT License (c) Paul Miller (paulmillr.com) */ | ||
}; | ||
export declare const TRANSACTION_TYPES: { | ||
legacy: number; | ||
eip2930: number; | ||
eip1559: number; | ||
}; | ||
export declare function add0x(hex: string): string; | ||
export declare function strip0x(hex: string): string; | ||
declare type Chain = keyof typeof CHAIN_TYPES; | ||
declare type Type = keyof typeof TRANSACTION_TYPES; | ||
declare const FIELDS: readonly ["nonce", "gasPrice", "gasLimit", "to", "value", "data", "v", "r", "s"]; | ||
export declare type Field = typeof FIELDS[number]; | ||
export declare type RawTx = [string, string, string, string, string, string, string, string, string]; | ||
export declare type RawTxMap = Record<Field, string>; | ||
declare const FIELDS2930: readonly ["chainId", "nonce", "gasPrice", "gasLimit", "to", "value", "data", "accessList", "yParity", "r", "s"]; | ||
declare const FIELDS1559: readonly ["chainId", "nonce", "maxPriorityFeePerGas", "maxFeePerGas", "gasLimit", "to", "value", "data", "accessList", "yParity", "r", "s"]; | ||
export declare type Field = typeof FIELDS[number] | typeof FIELDS2930[number] | typeof FIELDS1559[number] | 'address' | 'storageKey'; | ||
export declare type RawTxLegacy = [string, string, string, string, string, string, string, string, string]; | ||
export declare type RawTx2930 = [string, string, string, string, string, string, [string, string[]][], string, string, string]; | ||
export declare type RawTx1559 = [string, string, string, string, string, string, string, [string, string[]][], string, string, string]; | ||
export declare type RawTx = RawTxLegacy | RawTx2930 | RawTx1559; | ||
export declare type RawTxMap = { | ||
chainId?: string; | ||
nonce: string; | ||
gasPrice?: string; | ||
maxPriorityFeePerGas?: string; | ||
maxFeePerGas?: string; | ||
gasLimit: string; | ||
to: string; | ||
value: string; | ||
data: string; | ||
accessList?: [string, string[]][]; | ||
yParity?: string; | ||
v?: string; | ||
r: string; | ||
s: string; | ||
}; | ||
export declare const Address: { | ||
@@ -24,12 +50,14 @@ fromPrivateKey(key: string | Uint8Array): string; | ||
export declare class Transaction { | ||
readonly chain: Chain; | ||
readonly hardfork: string; | ||
static DEFAULT_HARDFORK: string; | ||
static DEFAULT_CHAIN: Chain; | ||
static DEFAULT_TYPE: Type; | ||
readonly hex: string; | ||
readonly raw: RawTxMap; | ||
readonly isSigned: boolean; | ||
constructor(data: string | Uint8Array | RawTx | RawTxMap, chain?: Chain, hardfork?: string); | ||
readonly type: Type; | ||
constructor(data: string | Uint8Array | RawTx | RawTxMap, chain?: Chain, hardfork?: string, type?: Type); | ||
get bytes(): Uint8Array; | ||
equals(other: Transaction): boolean; | ||
get chain(): Chain | undefined; | ||
get sender(): string; | ||
@@ -41,5 +69,4 @@ get amount(): bigint; | ||
get nonce(): number; | ||
private prepare; | ||
private supportsReplayProtection; | ||
getMessageToSign(): string; | ||
getMessageToSign(signed?: boolean): string; | ||
get hash(): string; | ||
@@ -46,0 +73,0 @@ sign(privateKey: string | Uint8Array): Promise<Transaction>; |
298
index.js
"use strict"; | ||
/*! micro-eth-signer - MIT License (c) Paul Miller (paulmillr.com) */ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.Transaction = exports.Address = exports.strip0x = exports.add0x = exports.CHAIN_TYPES = void 0; | ||
exports.Transaction = exports.Address = exports.strip0x = exports.add0x = exports.TRANSACTION_TYPES = exports.CHAIN_TYPES = void 0; | ||
const js_sha3_1 = require("js-sha3"); | ||
@@ -9,2 +9,3 @@ const rlp = require("micro-rlp"); | ||
exports.CHAIN_TYPES = { mainnet: 1, ropsten: 3, rinkeby: 4, goerli: 5, kovan: 42 }; | ||
exports.TRANSACTION_TYPES = { legacy: 0, eip2930: 1, eip1559: 2 }; | ||
function add0x(hex) { | ||
@@ -18,2 +19,18 @@ return /^0x/i.test(hex) ? hex : `0x${hex}`; | ||
exports.strip0x = strip0x; | ||
function cloneDeep(obj) { | ||
if (Array.isArray(obj)) { | ||
return obj.map((i) => cloneDeep(i)); | ||
} | ||
else if (typeof obj === 'bigint') { | ||
return BigInt(obj); | ||
} | ||
else if (typeof obj === 'object') { | ||
let res = {}; | ||
for (let key in obj) | ||
res[key] = cloneDeep(obj[key]); | ||
return res; | ||
} | ||
else | ||
return obj; | ||
} | ||
function bytesToHex(uint8a) { | ||
@@ -26,6 +43,5 @@ let hex = ''; | ||
} | ||
const padHex = (hex) => (hex.length & 1 ? `0${hex}` : hex); | ||
function hexToBytes(hex) { | ||
hex = strip0x(hex); | ||
if (hex.length & 1) | ||
hex = `0${hex}`; | ||
hex = padHex(strip0x(hex)); | ||
const array = new Uint8Array(hex.length / 2); | ||
@@ -38,5 +54,2 @@ for (let i = 0; i < array.length; i++) { | ||
} | ||
function hexToBytesUnpadded(num) { | ||
return num === '0x' || BigInt(num) === 0n ? new Uint8Array() : hexToBytes(num); | ||
} | ||
function numberToHex(num, padToBytes = 0) { | ||
@@ -54,34 +67,154 @@ const hex = num.toString(16); | ||
const FIELDS = ['nonce', 'gasPrice', 'gasLimit', 'to', 'value', 'data', 'v', 'r', 's']; | ||
function mapToArray(input) { | ||
return FIELDS.map((key) => input[key]); | ||
} | ||
const FIELDS2930 = ['chainId', 'nonce', 'gasPrice', 'gasLimit', 'to', 'value', 'data', 'accessList', | ||
'yParity', 'r', 's']; | ||
const FIELDS1559 = ['chainId', 'nonce', 'maxPriorityFeePerGas', 'maxFeePerGas', 'gasLimit', 'to', 'value', | ||
'data', 'accessList', 'yParity', 'r', 's']; | ||
const TypeToFields = { | ||
legacy: FIELDS, | ||
eip2930: FIELDS2930, | ||
eip1559: FIELDS1559, | ||
}; | ||
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, value) { | ||
if (field === 'gasLimit' && !value) { | ||
value = '0x5208'; | ||
if (FIELD_NUMBER.has(field)) { | ||
if (value instanceof Uint8Array) | ||
value = add0x(bytesToHex(value)); | ||
if (field === 'yParity' && typeof value === 'boolean') | ||
value = value ? '0x1' : '0x0'; | ||
if (typeof value === 'string') | ||
value = BigInt(value === '0x' ? '0x0' : value); | ||
if (typeof value === 'number' || typeof value === 'bigint') | ||
value = add0x(padHex(value.toString(16))); | ||
if (field === 'gasLimit' && (!value || BigInt(value) === 0n)) | ||
value = '0x5208'; | ||
if (typeof value !== 'string') | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
if (field === 'gasPrice' && BigInt(value) === 0n) | ||
throw new TypeError('The gasPrice must have non-zero value'); | ||
return BigInt(value) === 0n ? '' : value; | ||
} | ||
if (['nonce', 'gasPrice', 'gasLimit', 'value'].includes(field)) { | ||
if (typeof value === 'number' || typeof value === 'bigint') { | ||
value = value.toString(16); | ||
if (FIELD_DATA.has(field)) { | ||
if (!value) | ||
value = ''; | ||
if (value instanceof Uint8Array) | ||
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 = {}; | ||
if (Array.isArray(value)) { | ||
for (let access of value) { | ||
if (Array.isArray(access)) { | ||
if (access.length !== 2 || !Array.isArray(access[1])) | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
const key = normalizeField('address', access[0]); | ||
if (!res[key]) | ||
res[key] = new Set(); | ||
for (let i of access[1]) | ||
res[key].add(normalizeField('storageKey', i)); | ||
} | ||
else { | ||
if (typeof access !== 'object' || | ||
access === null || | ||
!access.address || | ||
!Array.isArray(access.storageKeys)) | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
const key = normalizeField('address', access.address); | ||
if (!res[key]) | ||
res[key] = new Set(); | ||
for (let i of access.storageKeys) | ||
res[key].add(normalizeField('storageKey', i)); | ||
} | ||
} | ||
} | ||
if (typeof value === 'string') { | ||
if (['0', '00', '0x', '0x00'].includes(value)) | ||
value = ''; | ||
} | ||
else { | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
if (typeof value !== 'object' || value === null || value instanceof Uint8Array) | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
for (let k in value) { | ||
const key = normalizeField('address', k); | ||
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))); | ||
} | ||
} | ||
return Object.keys(res).map((i) => [i, Array.from(res[i])]); | ||
} | ||
if (['gasPrice'].includes(field) && !value) { | ||
throw new TypeError('The field must have non-zero value'); | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
} | ||
function possibleTypes(input) { | ||
let types = new Set(Object.keys(exports.TRANSACTION_TYPES)); | ||
const keys = new Set(Object.keys(input)); | ||
if (keys.has('maxPriorityFeePerGas') || keys.has('maxFeePerGas')) { | ||
types.delete('legacy'); | ||
types.delete('eip2930'); | ||
} | ||
if (['v', 'r', 's', 'data'].includes(field) && !value) | ||
return ''; | ||
if (typeof value !== 'string') | ||
throw new TypeError(`Invalid type for field ${field}`); | ||
return value; | ||
if (keys.has('accessList') || keys.has('yParity')) | ||
types.delete('legacy'); | ||
if (keys.has('gasPrice')) | ||
types.delete('eip1559'); | ||
return types; | ||
} | ||
function rawToSerialized(input) { | ||
const initial = Array.isArray(input) ? input : mapToArray(input); | ||
const normalized = initial.map((value, i) => add0x(normalizeField(FIELDS[i], value))); | ||
return add0x(bytesToHex(rlp.encode(normalized))); | ||
const RawTxLength = { 9: 'legacy', 11: 'eip2930', 12: 'eip1559' }; | ||
const RawTxLengthRev = { legacy: 9, eip2930: 11, eip1559: 12 }; | ||
function rawToSerialized(input, chain, type) { | ||
let chainId; | ||
if (chain) | ||
chainId = exports.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', exports.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; | ||
input = TypeToFields[type].map((key) => input[key]); | ||
} | ||
if (input) { | ||
const sign = input.slice(-3); | ||
if (!sign[0] || !sign[1] || !sign[2]) { | ||
input = input.slice(0, -3); | ||
if (type === 'legacy' && chainId) | ||
input.push(normalizeField('chainId', chainId), '', ''); | ||
} | ||
} | ||
let normalized = input.map((value, i) => normalizeField(TypeToFields[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 = exports.TRANSACTION_TYPES[type]; | ||
return (tNum ? `0x0${tNum}` : '0x') + bytesToHex(rlp.encode(normalized)); | ||
} | ||
@@ -138,4 +271,3 @@ exports.Address = { | ||
class Transaction { | ||
constructor(data, chain = Transaction.DEFAULT_CHAIN, hardfork = Transaction.DEFAULT_HARDFORK) { | ||
this.chain = chain; | ||
constructor(data, chain, hardfork = Transaction.DEFAULT_HARDFORK, type) { | ||
this.hardfork = hardfork; | ||
@@ -150,3 +282,3 @@ let norm; | ||
else if (Array.isArray(data) || (typeof data === 'object' && data != null)) { | ||
norm = rawToSerialized(data); | ||
norm = rawToSerialized(data, chain, type); | ||
} | ||
@@ -158,10 +290,31 @@ else { | ||
throw new Error('Invalid tx length'); | ||
this.hex = norm; | ||
const ui8a = rlp.decode(add0x(norm)); | ||
const arr = ui8a.map(bytesToHex).map((i) => (i ? add0x(i) : i)); | ||
this.raw = arr.reduce((res, value, i) => { | ||
const name = FIELDS[i]; | ||
res[name] = value; | ||
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); | ||
this.raw = ui8a.reduce((res, value, i) => { | ||
const name = TypeToFields[type][i]; | ||
if (!name) | ||
return res; | ||
res[name] = normalizeField(name, value); | ||
return res; | ||
}, {}); | ||
if (!this.raw.chainId) { | ||
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', exports.CHAIN_TYPES[chain || Transaction.DEFAULT_CHAIN]); | ||
} | ||
this.isSigned = !!(this.raw.r && this.raw.r !== '0x'); | ||
@@ -175,2 +328,7 @@ } | ||
} | ||
get chain() { | ||
for (let k in exports.CHAIN_TYPES) | ||
if (exports.CHAIN_TYPES[k] === Number(this.raw.chainId)) | ||
return k; | ||
} | ||
get sender() { | ||
@@ -186,2 +344,4 @@ const sender = this.recoverSenderPublicKey(); | ||
get fee() { | ||
if (this.type === 'eip1559') | ||
return BigInt(this.raw.maxFeePerGas) * BigInt(this.raw.gasLimit); | ||
return BigInt(this.raw.gasPrice) * BigInt(this.raw.gasLimit); | ||
@@ -198,15 +358,2 @@ } | ||
} | ||
prepare() { | ||
return [ | ||
hexToBytesUnpadded(this.raw.nonce), | ||
hexToBytesUnpadded(this.raw.gasPrice), | ||
hexToBytesUnpadded(this.raw.gasLimit), | ||
hexToBytes(this.raw.to), | ||
hexToBytesUnpadded(this.raw.value), | ||
hexToBytesUnpadded(this.raw.data), | ||
hexToBytesUnpadded(this.raw.v), | ||
hexToBytesUnpadded(this.raw.r), | ||
hexToBytesUnpadded(this.raw.s), | ||
]; | ||
} | ||
supportsReplayProtection() { | ||
@@ -217,14 +364,17 @@ const properBlock = !['chainstart', 'homestead', 'dao', 'tangerineWhistle'].includes(this.hardfork); | ||
const v = Number(hexToNumber(this.raw.v)); | ||
const chainId = exports.CHAIN_TYPES[this.chain]; | ||
const chainId = Number(this.raw.chainId); | ||
const meetsConditions = v === chainId * 2 + 35 || v === chainId * 2 + 36; | ||
return properBlock && meetsConditions; | ||
} | ||
getMessageToSign() { | ||
const values = this.prepare().slice(0, 6); | ||
if (this.supportsReplayProtection()) { | ||
values.push(hexToBytes(numberToHex(exports.CHAIN_TYPES[this.chain]))); | ||
values.push(new Uint8Array()); | ||
values.push(new Uint8Array()); | ||
getMessageToSign(signed = false) { | ||
let values = TypeToFields[this.type].map((i) => this.raw[i]); | ||
if (!signed) { | ||
values = values.slice(0, -3); | ||
if (this.type === 'legacy' && this.supportsReplayProtection()) | ||
values.push(this.raw.chainId, '', ''); | ||
} | ||
return js_sha3_1.keccak256(rlp.encode(values)); | ||
let encoded = rlp.encode(values); | ||
if (this.type !== 'legacy') | ||
encoded = new Uint8Array([exports.TRANSACTION_TYPES[this.type], ...Array.from(encoded)]); | ||
return js_sha3_1.keccak256(encoded); | ||
} | ||
@@ -234,3 +384,3 @@ get hash() { | ||
throw new Error('Expected signed transaction'); | ||
return js_sha3_1.keccak256(rlp.encode(this.prepare())); | ||
return this.getMessageToSign(true); | ||
} | ||
@@ -247,13 +397,14 @@ async sign(privateKey) { | ||
const signature = secp256k1.Signature.fromHex(hex); | ||
const chainId = exports.CHAIN_TYPES[this.chain]; | ||
const vv = chainId ? recovery + (chainId * 2 + 35) : recovery + 27; | ||
const chainId = Number(this.raw.chainId); | ||
const vv = this.type === 'legacy' ? (chainId ? recovery + (chainId * 2 + 35) : recovery + 27) : recovery; | ||
const [v, r, s] = [vv, signature.r, signature.s].map((n) => add0x(numberToHex(n))); | ||
const signedRaw = Object.assign({}, this.raw, { v, r, s }); | ||
return new Transaction(signedRaw, this.chain, this.hardfork); | ||
const signedRaw = 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() { | ||
if (!this.isSigned) { | ||
if (!this.isSigned) | ||
throw new Error('Expected signed transaction: cannot recover sender of unsigned tx'); | ||
} | ||
const [vv, r, s] = [this.raw.v, this.raw.r, this.raw.s].map((n) => hexToNumber(n)); | ||
const [r, s] = [this.raw.r, this.raw.s].map((n) => hexToNumber(n)); | ||
if (this.hardfork !== 'chainstart' && s && s > secp256k1.CURVE.n / 2n) { | ||
@@ -263,5 +414,5 @@ throw new Error('Invalid signature: s is invalid'); | ||
const signature = new secp256k1.Signature(r, s).toHex(); | ||
const chainId = exports.CHAIN_TYPES[this.chain]; | ||
const v = Number(vv); | ||
const recovery = chainId ? v - (chainId * 2 + 35) : v - 27; | ||
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; | ||
return secp256k1.recoverPublicKey(this.getMessageToSign(), signature, recovery); | ||
@@ -271,3 +422,4 @@ } | ||
exports.Transaction = Transaction; | ||
Transaction.DEFAULT_HARDFORK = 'berlin'; | ||
Transaction.DEFAULT_HARDFORK = 'london'; | ||
Transaction.DEFAULT_CHAIN = 'mainnet'; | ||
Transaction.DEFAULT_TYPE = 'legacy'; |
{ | ||
"name": "micro-eth-signer", | ||
"version": "0.1.7", | ||
"description": "Create, sign and validate Ethereum transactions & addresses with minimum deps", | ||
"version": "0.2.0", | ||
"description": "Create, sign and validate Ethereum transactions & addresses with minimum deps. Supports London & Berlin txs", | ||
"main": "index.js", | ||
@@ -45,3 +45,3 @@ "files": [ | ||
"micro-rlp": "2.2.9", | ||
"noble-secp256k1": "^1.2.4" | ||
"noble-secp256k1": "^1.2.7" | ||
}, | ||
@@ -48,0 +48,0 @@ "devDependencies": { |
@@ -7,2 +7,4 @@ # micro-eth-signer | ||
Validated with over 3 MB of ethers.js test vectors! | ||
## Usage | ||
@@ -35,7 +37,7 @@ | ||
console.log('Need wei', tx.upfrontCost); // also, tx.fee, tx.amount, tx.sender, etc | ||
// Address manipulation | ||
const addr = Address.fromPrivateKey(privateKey); | ||
const pubKey = signedTx.recoverSenderPublicKey(); | ||
console.log('Verified', Address.verifyChecksum(addr)); | ||
console.log('Verified', Address.verifyChecksum(addr)); | ||
console.log('addr is correct', signedTx.sender, signedTx.sender == addr); | ||
@@ -42,0 +44,0 @@ console.log(signedTx); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
26876
482
122
Updatednoble-secp256k1@^1.2.7