Initial code drop for v6.

This commit is contained in:
Richard Moore 2022-04-11 17:09:17 -04:00
commit 9ca3dc557d
330 changed files with 62744 additions and 0 deletions

27
.gitignore vendored Normal file
View File

@ -0,0 +1,27 @@
# Normal things to ignore; temporary files, libs, monorepo goop
node_modules/
packages/*/node_modules
.package_node_modules/
.DS_Store
.tmp/
**/*.swp
*~
# A random stash folder I use for code I'm not quite willing
# to part ways with yet
obsolete/
# Intermediate files tsc generates for references (sometimes)
**/src.ts/*.js
# TypeScript build caches
packages/*/tsconfig.tsbuildinfo
# Coverage and Testing output
output/**
# Issue tests
misc/testing/**
# Temp
#packages/*/lib/

21
LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
README.md Normal file
View File

@ -0,0 +1,13 @@
Ethers - v6 beta
================
This is VERY beta, but I want to start getting it into the hands
of people to try out.
This will be updated soon with some examples.
License
-------
MIT Licensed (including all dependencies)

79
bootstrap-hoist.js vendored Normal file
View File

@ -0,0 +1,79 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const fs_1 = __importDefault(require("fs"));
const path_1 = require("path");
const root = path_1.resolve(".");
function loadJson(path) {
return JSON.parse(fs_1.default.readFileSync(path).toString());
}
function atomicWrite(path, value) {
const tmp = path_1.resolve(root, ".atomic-tmp");
fs_1.default.writeFileSync(tmp, value);
fs_1.default.renameSync(tmp, path);
}
function saveJson(filename, data, sort) {
let replacer = undefined;
if (sort) {
replacer = (key, value) => {
if (Array.isArray(value)) {
// pass
}
else if (value && typeof (value) === "object") {
const keys = Object.keys(value);
keys.sort();
return keys.reduce((accum, key) => {
accum[key] = value[key];
return accum;
}, {});
}
return value;
};
}
atomicWrite(filename, JSON.stringify(data, replacer, 2) + "\n");
}
(function () {
const filename = path_1.resolve(root, "package.json");
const pkg = loadJson(filename);
const packageFolder = (pkg.reticulate || {}).pacakges || "packages";
{
// @TODO: Check within root
}
const pkgs = fs_1.default.readdirSync(packageFolder).reduce((accum, folder) => {
const pkg = loadJson(path_1.resolve(root, packageFolder, folder, "package.json"));
if (accum[pkg.name]) {
throw new Error(`duplicate package named ${pkg.name}`);
}
accum[pkg.name] = pkg.dependencies || {};
return accum;
}, {});
const result = {};
Object.keys(pkgs).forEach((name) => {
const versions = pkgs[name];
for (const dep in versions) {
// This package is managed by this monorepo
if (dep in pkgs) {
continue;
}
// The required dependency version
const ver = versions[dep];
// This already exists in the result...
const existing = result[dep];
if (existing) {
// ...but doesn't match
if (existing !== ver) {
throw new Error(`package dependency version mismatch: ${dep}`);
}
}
else {
result[dep] = ver;
}
}
});
console.log(`Hoisting ${Object.keys(result).length} dependencies from ${packageFolder}/*/package.json...\n`);
pkg.dependencies = result;
saveJson(filename, pkg, true);
})();
//# sourceMappingURL=bootstrap-hoist.js.map

1
output/.keep Normal file
View File

@ -0,0 +1 @@
# Keep this folder

1938
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

52
package.json Normal file
View File

@ -0,0 +1,52 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"dependencies": {
"@noble/hashes": "1.0.0",
"@noble/secp256k1": "1.5.5",
"aes-js": "4.0.0-beta.2",
"ws": "8.5.0"
},
"description": "Root monorepo pacakge for ethers core",
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.1.3",
"@rollup/plugin-replace": "^4.0.0",
"@types/mocha": "^9.1.0",
"@types/node": "^14.14.25",
"@types/ws": "^8.5.3",
"c8": "^7.11.0",
"mocha": "^9.2.1",
"reticulate": "^0.0.9",
"rollup": "^2.70.1",
"rollup-plugin-replace": "^2.2.0",
"rollup-plugin-terser": "^7.0.2",
"typescript": "4.6.3",
"uglify-js": "^3.15.3"
},
"license": "MIT",
"name": "ethers-monorepo",
"private": true,
"reticulate": {
"packages": "packages",
"root": "ethers",
"spelling": {
"dicts": [
"/usr/share/dict/words",
"./misc/dict"
]
}
},
"scripts": {
"__build-dist": "npx rollup -c && uglifyjs ./packages/ethers/dist/ethers.js -o ./packages/ethers/dist/ethers.min.js && uglifyjs ./packages/ethers/dist/ethers-wordlists.js -o ./packages/ethers/dist/ethers-wordlists.min.js && npm run _build-dist-stats",
"_build-dist-stats": "gzip -k9f -S '.gz' packages/ethers/dist/ethers-wordlists.min.js && gzip -k9f -S '.gz' packages/ethers/dist/ethers.min.js && du -hs packages/ethers/dist/*",
"auto-build": "npm run build -- -w",
"build": "tsc --build tsconfig.project.json",
"build-dist": "npx rollup -c && npm run _build-dist-stats",
"clean": "rm -rf packages/*/tsconfig.tsbuildinfo packages/*/lib package-lock.json .package_node_modules/ node_modules/",
"postinstall": "reticulate ratsnest",
"preinstall": "node ./bootstrap-hoist",
"serve-coverage": "python3 -m http.server -d output/lcov-report 8080",
"test": "mocha packages/*/lib/tests/test-*.js",
"test-coverage": "c8 -o output -r lcov -r text mocha packages/*/lib/tests/test-*.js | tee output/summary.txt"
},
"version": "0.0.0"
}

3
packages/abi/.npmignore Normal file
View File

@ -0,0 +1,3 @@
/src.ts/tests/**
/lib/tests/**
tsconfig.tsbuildinfo

21
packages/abi/LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

40
packages/abi/package.json Normal file
View File

@ -0,0 +1,40 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"dependencies": {
"@ethersproject/address": "^6.0.0-beta.1",
"@ethersproject/bytes": "^6.0.0-beta.1",
"@ethersproject/crypto": "^6.0.0-beta.1",
"@ethersproject/hash": "^6.0.0-beta.1",
"@ethersproject/logger": "^6.0.0-beta.1",
"@ethersproject/math": "^6.0.0-beta.1",
"@ethersproject/properties": "^6.0.0-beta.1",
"@ethersproject/strings": "^6.0.0-beta.1",
"@ethersproject/rlp": "^6.0.0-beta.1"
},
"description": "ABI coding for the EVM.",
"engines" : { "node" : ">=12.17.0" },
"ethereum": "donations.ethers.eth",
"keywords": [
"Ethereum",
"ethers"
],
"license": "MIT",
"main": "./lib/index.js",
"name": "@ethersproject/abi",
"publishConfig": {
"access": "public",
"tag": "beta"
},
"repository": {
"directory": "packages/abi",
"type": "git",
"url": "git://github.com/ethers-io/ethers.js.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"sideEffects": false,
"type": "module",
"types": "./lib/index.d.ts",
"version": "6.0.0-beta.1"
}

View File

@ -0,0 +1 @@
export const version = "@ethersproject/abi@6.0.0-beta.1";

View File

@ -0,0 +1,103 @@
// See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
//import { arrayify } from "@ethersproject/bytes";
import { defineProperties } from "@ethersproject/properties";
import { logger } from "./logger.js";
import { Coder, Reader, Result, Writer } from "./coders/abstract-coder.js";
import { AddressCoder } from "./coders/address.js";
import { ArrayCoder } from "./coders/array.js";
import { BooleanCoder } from "./coders/boolean.js";
import { BytesCoder } from "./coders/bytes.js";
import { FixedBytesCoder } from "./coders/fixed-bytes.js";
import { NullCoder } from "./coders/null.js";
import { NumberCoder } from "./coders/number.js";
import { StringCoder } from "./coders/string.js";
import { TupleCoder } from "./coders/tuple.js";
import { ParamType } from "./fragments.js";
import type { BytesLike } from "@ethersproject/bytes";
const paramTypeBytes = new RegExp(/^bytes([0-9]*)$/);
const paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/);
export type CoerceFunc = (type: string, value: any) => any;
export class AbiCoder {
readonly coerceFunc!: null | CoerceFunc;
constructor(coerceFunc?: CoerceFunc) {
defineProperties<AbiCoder>(this, { coerceFunc }, { }, { coerceFunc: null });
}
#getCoder(param: ParamType): Coder {
if (param.isArray()) {
return new ArrayCoder(this.#getCoder(param.arrayChildren), param.arrayLength, param.name);
}
if (param.isTuple()) {
return new TupleCoder(param.components.map((c) => this.#getCoder(c)), param.name);
}
switch (param.baseType) {
case "address":
return new AddressCoder(param.name);
case "bool":
return new BooleanCoder(param.name);
case "string":
return new StringCoder(param.name);
case "bytes":
return new BytesCoder(param.name);
case "":
return new NullCoder(param.name);
}
// u?int[0-9]*
let match = param.type.match(paramTypeNumber);
if (match) {
let size = parseInt(match[2] || "256");
if (size === 0 || size > 256 || (size % 8) !== 0) {
logger.throwArgumentError("invalid " + match[1] + " bit length", "param", param);
}
return new NumberCoder(size / 8, (match[1] === "int"), param.name);
}
// bytes[0-9]+
match = param.type.match(paramTypeBytes);
if (match) {
let size = parseInt(match[1]);
if (size === 0 || size > 32) {
logger.throwArgumentError("invalid bytes length", "param", param);
}
return new FixedBytesCoder(size, param.name);
}
return logger.throwArgumentError("invalid type", "type", param.type);
}
getDefaultValue(types: ReadonlyArray<string | ParamType>): Result {
const coders: Array<Coder> = types.map((type) => this.#getCoder(ParamType.from(type)));
const coder = new TupleCoder(coders, "_");
return coder.defaultValue();
}
encode(types: ReadonlyArray<string | ParamType>, values: ReadonlyArray<any>): string {
logger.assertArgumentCount(values.length, types.length, "types/values length mismatch");
const coders = types.map((type) => this.#getCoder(ParamType.from(type)));
const coder = (new TupleCoder(coders, "_"));
const writer = new Writer();
coder.encode(writer, values);
return writer.data;
}
decode(types: ReadonlyArray<string | ParamType>, data: BytesLike, loose?: boolean): Result {
const coders: Array<Coder> = types.map((type) => this.#getCoder(ParamType.from(type)));
const coder = new TupleCoder(coders, "_");
return coder.decode(new Reader(data, this.coerceFunc, loose));
}
}
export const defaultAbiCoder: AbiCoder = new AbiCoder();

View File

@ -0,0 +1,322 @@
import { toArray, toBigInt, toNumber } from "@ethersproject/math";
import { arrayify, concat, hexlify } from "@ethersproject/bytes";
import { defineProperties } from "@ethersproject/properties";
import { logger } from "../logger.js";
import type { BytesLike } from "@ethersproject/bytes";
import type { BigNumberish } from "@ethersproject/logger";
export const WordSize = 32;
const Padding = new Uint8Array(WordSize);
// Properties used to immediate pass through to the underlying object
// - `then` is used to detect if an object is a Promise for await
const passProperties = [ "then" ];
const _guard = { };
export class Result extends Array<any> {
#indices: Map<string, Array<number>>;
[ K: string | number ]: any
constructor(guard: any, items: Array<any>, keys?: Array<null | string>) {
logger.assertPrivate(guard, _guard, "Result");
super(...items);
// Name lookup table
this.#indices = new Map();
if (keys) {
keys.forEach((key, index) => {
if (key == null) { return; }
if (this.#indices.has(key)) {
(<Array<number>>(this.#indices.get(key))).push(index);
} else {
this.#indices.set(key, [ index ]);
}
});
}
Object.freeze(this);
return new Proxy(this, {
get: (target, prop, receiver) => {
if (typeof(prop) === "string") {
if (prop.match(/^[0-9]+$/)) {
const index = logger.getNumber(prop, "%index");
if (index < 0 || index >= this.length) {
throw new RangeError("out of result range");
}
const item = target[index];
if (item instanceof Error) {
this.#throwError(`index ${ index }`, item);
}
return item;
}
// Pass important checks (like `then` for Promise) through
if (prop in target || passProperties.indexOf(prop) >= 0) {
return Reflect.get(target, prop, receiver);
}
// Something that could be a result keyword value
if (!(prop in target)) {
return target.getValue(prop);
}
}
return Reflect.get(target, prop, receiver);
}
});
}
slice(start?: number | undefined, end?: number | undefined): Array<any> {
if (start == null) { start = 0; }
if (end == null) { end = this.length; }
const result = [ ];
for (let i = start; i < end; i++) {
let value: any;
try {
value = this[i];
} catch (error: any) {
value = error.error;
}
result.push(value);
}
return result;
}
#throwError(name: string, error: Error): never {
const wrapped = new Error(`deferred error during ABI decoding triggered accessing ${ name }`);
(<any>wrapped).error = error;
throw wrapped;
}
getValue(name: string): any {
const index = this.#indices.get(name);
if (index != null && index.length === 1) {
const item = this[index[0]];
if (item instanceof Error) {
this.#throwError(`property ${ JSON.stringify(name) }`, item);
}
return item;
}
throw new Error(`no named parameter: ${ JSON.stringify(name) }`);
}
static fromItems(items: Array<any>, keys?: Array<null | string>) {
return new Result(_guard, items, keys);
}
}
export function checkResultErrors(result: Result): Array<{ path: Array<string | number>, error: Error }> {
// Find the first error (if any)
const errors: Array<{ path: Array<string | number>, error: Error }> = [ ];
const checkErrors = function(path: Array<string | number>, object: any): void {
if (!Array.isArray(object)) { return; }
for (let key in object) {
const childPath = path.slice();
childPath.push(key);
try {
checkErrors(childPath, object[key]);
} catch (error: any) {
errors.push({ path: childPath, error: error });
}
}
}
checkErrors([ ], result);
return errors;
}
function getValue(value: BigNumberish): Uint8Array {
let bytes = toArray(value);
if (bytes.length > WordSize) {
logger.throwError("value out-of-bounds", "BUFFER_OVERRUN", {
buffer: bytes,
length: WordSize,
offset: bytes.length
});
}
if (bytes.length % WordSize) {
bytes = arrayify(concat([ Padding.slice(bytes.length % WordSize), bytes ]));
}
return bytes;
}
export type CoerceFunc = (type: string, value: any) => any;
export abstract class Coder {
// The coder name:
// - address, uint256, tuple, array, etc.
readonly name!: string;
// The fully expanded type, including composite types:
// - address, uint256, tuple(address,bytes), uint256[3][4][], etc.
readonly type!: string;
// The localName bound in the signature, in this example it is "baz":
// - tuple(address foo, uint bar) baz
readonly localName!: string;
// Whether this type is dynamic:
// - Dynamic: bytes, string, address[], tuple(boolean[]), etc.
// - Not Dynamic: address, uint256, boolean[3], tuple(address, uint8)
readonly dynamic!: boolean;
constructor(name: string, type: string, localName: string, dynamic: boolean) {
defineProperties<Coder>(this, { name, type, localName, dynamic }, {
name: "string", type: "string", localName: "string", dynamic: "boolean"
});
}
_throwError(message: string, value: any): never {
return logger.throwArgumentError(message, this.localName, value);
}
abstract encode(writer: Writer, value: any): number;
abstract decode(reader: Reader): any;
abstract defaultValue(): any;
}
export class Writer {
// An array of WordSize lengthed objects to concatenation
#data: Array<Uint8Array>;
#dataLength: number;
constructor() {
this.#data = [ ];
this.#dataLength = 0;
}
get data(): string {
return concat(this.#data);
}
get length(): number { return this.#dataLength; }
#writeData(data: Uint8Array): number {
this.#data.push(data);
this.#dataLength += data.length;
return data.length;
}
appendWriter(writer: Writer): number {
return this.#writeData(arrayify(writer.data));
}
// Arrayish item; pad on the right to *nearest* WordSize
writeBytes(value: BytesLike): number {
let bytes = arrayify(value);
const paddingOffset = bytes.length % WordSize;
if (paddingOffset) {
bytes = arrayify(concat([ bytes, Padding.slice(paddingOffset) ]))
}
return this.#writeData(bytes);
}
// Numeric item; pad on the left *to* WordSize
writeValue(value: BigNumberish): number {
return this.#writeData(getValue(value));
}
// Inserts a numeric place-holder, returning a callback that can
// be used to asjust the value later
writeUpdatableValue(): (value: BigNumberish) => void {
const offset = this.#data.length;
this.#data.push(Padding);
this.#dataLength += WordSize;
return (value: BigNumberish) => {
this.#data[offset] = getValue(value);
};
}
}
export class Reader {
// Allows incomplete unpadded data to be read; otherwise an error
// is raised if attempting to overrun the buffer. This is required
// to deal with an old Solidity bug, in which event data for
// external (not public thoguh) was tightly packed.
readonly allowLoose!: boolean;
readonly #data: Uint8Array;
#offset: number;
readonly #coerceFunc: CoerceFunc;
constructor(data: BytesLike, coerceFunc?: null | CoerceFunc, allowLoose?: boolean) {
defineProperties<Reader>(this, { allowLoose: !!allowLoose });
this.#data = arrayify(data);
this.#coerceFunc = coerceFunc || Reader.coerce;
this.#offset = 0;
}
get data(): string { return hexlify(this.#data); }
get dataLength(): number { return this.#data.length; }
get consumed(): number { return this.#offset; }
get bytes(): Uint8Array { return new Uint8Array(this.#data); }
// The default Coerce function
static coerce(type: string, value: any): any {
let match = type.match("^u?int([0-9]+)$");
if (match && parseInt(match[1]) <= 48) { value = value.toNumber(); }
return value;
}
coerce(type: string, value: any): any {
return this.#coerceFunc(type, value);
}
#peekBytes(offset: number, length: number, loose?: boolean): Uint8Array {
let alignedLength = Math.ceil(length / WordSize) * WordSize;
if (this.#offset + alignedLength > this.#data.length) {
if (this.allowLoose && loose && this.#offset + length <= this.#data.length) {
alignedLength = length;
} else {
logger.throwError("data out-of-bounds", "BUFFER_OVERRUN", {
buffer: arrayify(this.#data),
length: this.#data.length,
offset: this.#offset + alignedLength
});
}
}
return this.#data.slice(this.#offset, this.#offset + alignedLength)
}
// Create a sub-reader with the same underlying data, but offset
subReader(offset: number): Reader {
return new Reader(this.#data.slice(this.#offset + offset), this.#coerceFunc, this.allowLoose);
}
// Read bytes
readBytes(length: number, loose?: boolean): Uint8Array {
let bytes = this.#peekBytes(0, length, !!loose);
this.#offset += bytes.length;
// @TODO: Make sure the length..end bytes are all 0?
return bytes.slice(0, length);
}
// Read a numeric values
readValue(): bigint {
return toBigInt(this.readBytes(WordSize));
}
readIndex(): number {
return toNumber(this.readBytes(WordSize));
}
}

View File

@ -0,0 +1,33 @@
import { getAddress } from "@ethersproject/address";
import { toHex } from "@ethersproject/math";
import { Typed } from "../typed.js";
import { Coder } from "./abstract-coder.js";
import type { Reader, Writer } from "./abstract-coder.js";
export class AddressCoder extends Coder {
constructor(localName: string) {
super("address", "address", localName, false);
}
defaultValue(): string {
return "0x0000000000000000000000000000000000000000";
}
encode(writer: Writer, _value: string | Typed): number {
let value = Typed.dereference(_value, "string");
try {
value = getAddress(value);
} catch (error: any) {
return this._throwError(error.message, _value);
}
return writer.writeValue(value);
}
decode(reader: Reader): any {
return getAddress(toHex(reader.readValue(), 20));
}
}

View File

@ -0,0 +1,25 @@
import { Coder } from "./abstract-coder.js";
import type { Reader, Writer } from "./abstract-coder.js";
// Clones the functionality of an existing Coder, but without a localName
export class AnonymousCoder extends Coder {
private coder: Coder;
constructor(coder: Coder) {
super(coder.name, coder.type, "_", coder.dynamic);
this.coder = coder;
}
defaultValue(): any {
return this.coder.defaultValue();
}
encode(writer: Writer, value: any): number {
return this.coder.encode(writer, value);
}
decode(reader: Reader): any {
return this.coder.decode(reader);
}
}

View File

@ -0,0 +1,208 @@
import { defineProperties } from "@ethersproject/properties";
import { isError } from "@ethersproject/logger";
import { logger } from "../logger.js";
import { Typed } from "../typed.js";
import { Coder, Result, WordSize, Writer } from "./abstract-coder.js";
import { AnonymousCoder } from "./anonymous.js";
import type { Reader } from "./abstract-coder.js";
export function pack(writer: Writer, coders: ReadonlyArray<Coder>, values: Array<any> | { [ name: string ]: any }): number {
let arrayValues: Array<any> = [ ];
if (Array.isArray(values)) {
arrayValues = values;
} else if (values && typeof(values) === "object") {
let unique: { [ name: string ]: boolean } = { };
arrayValues = coders.map((coder) => {
const name = coder.localName;
if (!name) {
logger.throwError("cannot encode object for signature with missing names", "INVALID_ARGUMENT", {
argument: "values",
info: { coder },
value: values
});
}
if (unique[name]) {
logger.throwError("cannot encode object for signature with duplicate names", "INVALID_ARGUMENT", {
argument: "values",
info: { coder },
value: values
});
}
unique[name] = true;
return values[name];
});
} else {
logger.throwArgumentError("invalid tuple value", "tuple", values);
}
if (coders.length !== arrayValues.length) {
logger.throwArgumentError("types/value length mismatch", "tuple", values);
}
let staticWriter = new Writer();
let dynamicWriter = new Writer();
let updateFuncs: Array<(baseOffset: number) => void> = [];
coders.forEach((coder, index) => {
let value = arrayValues[index];
if (coder.dynamic) {
// Get current dynamic offset (for the future pointer)
let dynamicOffset = dynamicWriter.length;
// Encode the dynamic value into the dynamicWriter
coder.encode(dynamicWriter, value);
// Prepare to populate the correct offset once we are done
let updateFunc = staticWriter.writeUpdatableValue();
updateFuncs.push((baseOffset: number) => {
updateFunc(baseOffset + dynamicOffset);
});
} else {
coder.encode(staticWriter, value);
}
});
// Backfill all the dynamic offsets, now that we know the static length
updateFuncs.forEach((func) => { func(staticWriter.length); });
let length = writer.appendWriter(staticWriter);
length += writer.appendWriter(dynamicWriter);
return length;
}
export function unpack(reader: Reader, coders: ReadonlyArray<Coder>): Result {
let values: Array<any> = [];
let keys: Array<null | string> = [ ];
// A reader anchored to this base
let baseReader = reader.subReader(0);
coders.forEach((coder) => {
let value: any = null;
if (coder.dynamic) {
let offset = reader.readIndex();
let offsetReader = baseReader.subReader(offset);
try {
value = coder.decode(offsetReader);
} catch (error: any) {
// Cannot recover from this
if (isError(error, "BUFFER_OVERRUN")) {
throw error;
}
value = error;
value.baseType = coder.name;
value.name = coder.localName;
value.type = coder.type;
}
} else {
try {
value = coder.decode(reader);
} catch (error: any) {
// Cannot recover from this
if (isError(error, "BUFFER_OVERRUN")) {
throw error;
}
value = error;
value.baseType = coder.name;
value.name = coder.localName;
value.type = coder.type;
}
}
if (value == undefined) {
throw new Error("investigate");
}
values.push(value);
keys.push(coder.localName || null);
});
return Result.fromItems(values, keys);
}
export class ArrayCoder extends Coder {
readonly coder!: Coder;
readonly length!: number;
constructor(coder: Coder, length: number, localName: string) {
const type = (coder.type + "[" + (length >= 0 ? length: "") + "]");
const dynamic = (length === -1 || coder.dynamic);
super("array", type, localName, dynamic);
defineProperties<ArrayCoder>(this, { coder, length });
}
defaultValue(): Array<any> {
// Verifies the child coder is valid (even if the array is dynamic or 0-length)
const defaultChild = this.coder.defaultValue();
const result: Array<any> = [];
for (let i = 0; i < this.length; i++) {
result.push(defaultChild);
}
return result;
}
encode(writer: Writer, _value: Array<any> | Typed): number {
const value = Typed.dereference(_value, "array");
if (!Array.isArray(value)) {
this._throwError("expected array value", value);
}
let count = this.length;
if (count === -1) {
count = value.length;
writer.writeValue(value.length);
}
logger.assertArgumentCount(value.length, count, "coder array" + (this.localName? (" "+ this.localName): ""));
let coders = [];
for (let i = 0; i < value.length; i++) { coders.push(this.coder); }
return pack(writer, coders, value);
}
decode(reader: Reader): any {
let count = this.length;
if (count === -1) {
count = reader.readIndex();
// Check that there is *roughly* enough data to ensure
// stray random data is not being read as a length. Each
// slot requires at least 32 bytes for their value (or 32
// bytes as a link to the data). This could use a much
// tighter bound, but we are erroring on the side of safety.
if (count * WordSize > reader.dataLength) {
logger.throwError("insufficient data length", "BUFFER_OVERRUN", {
buffer: reader.bytes,
offset: count * WordSize,
length: reader.dataLength
});
}
}
let coders = [];
for (let i = 0; i < count; i++) { coders.push(new AnonymousCoder(this.coder)); }
return reader.coerce(this.name, unpack(reader, coders));
}
}

View File

@ -0,0 +1,25 @@
import { Typed } from "../typed.js";
import { Coder } from "./abstract-coder.js";
import type { Reader, Writer } from "./abstract-coder.js";
export class BooleanCoder extends Coder {
constructor(localName: string) {
super("bool", "bool", localName, false);
}
defaultValue(): boolean {
return false;
}
encode(writer: Writer, _value: boolean | Typed): number {
const value = Typed.dereference(_value, "bool");
return writer.writeValue(value ? 1: 0);
}
decode(reader: Reader): any {
return reader.coerce(this.type, !reader.readValue());
}
}

View File

@ -0,0 +1,37 @@
import { arrayify, hexlify } from "@ethersproject/bytes";
import { Coder } from "./abstract-coder.js";
import type { Reader, Writer } from "./abstract-coder.js";
export class DynamicBytesCoder extends Coder {
constructor(type: string, localName: string) {
super(type, type, localName, true);
}
defaultValue(): string {
return "0x";
}
encode(writer: Writer, value: any): number {
value = arrayify(value);
let length = writer.writeValue(value.length);
length += writer.writeBytes(value);
return length;
}
decode(reader: Reader): any {
return reader.readBytes(reader.readIndex(), true);
}
}
export class BytesCoder extends DynamicBytesCoder {
constructor(localName: string) {
super("bytes", localName);
}
decode(reader: Reader): any {
return reader.coerce(this.name, hexlify(super.decode(reader)));
}
}

View File

@ -0,0 +1,35 @@
import { arrayify, hexlify } from "@ethersproject/bytes";
import { defineProperties } from "@ethersproject/properties";
import { Typed } from "../typed.js";
import { Coder } from "./abstract-coder.js";
import type { BytesLike } from "@ethersproject/bytes";
import type { Reader, Writer } from "./abstract-coder.js";
export class FixedBytesCoder extends Coder {
readonly size!: number;
constructor(size: number, localName: string) {
let name = "bytes" + String(size);
super(name, name, localName, false);
defineProperties<FixedBytesCoder>(this, { size }, { size: "number" });
}
defaultValue(): string {
return ("0x0000000000000000000000000000000000000000000000000000000000000000").substring(0, 2 + this.size * 2);
}
encode(writer: Writer, _value: BytesLike | Typed): number {
let data = arrayify(Typed.dereference(_value, this.type));
if (data.length !== this.size) { this._throwError("incorrect data length", _value); }
return writer.writeBytes(data);
}
decode(reader: Reader): any {
return reader.coerce(this.name, hexlify(reader.readBytes(this.size)));
}
}

View File

@ -0,0 +1,25 @@
import { Coder } from "./abstract-coder.js";
import type { Reader, Writer } from "./abstract-coder.js";
const Empty = new Uint8Array([ ]);
export class NullCoder extends Coder {
constructor(localName: string) {
super("null", "", localName, false);
}
defaultValue(): null {
return null;
}
encode(writer: Writer, value: any): number {
if (value != null) { this._throwError("not null", value); }
return writer.writeBytes(Empty);
}
decode(reader: Reader): any {
reader.readBytes(0);
return reader.coerce(this.name, null);
}
}

View File

@ -0,0 +1,65 @@
import { fromTwos, mask, toTwos } from "@ethersproject/math";
import { defineProperties } from "@ethersproject/properties";
import { logger } from "../logger.js";
import { Typed } from "../typed.js";
import { Coder, WordSize } from "./abstract-coder.js";
import type { BigNumberish } from "@ethersproject/logger";
import type { Reader, Writer } from "./abstract-coder.js";
const BN_0 = BigInt(0);
const BN_1 = BigInt(1);
const BN_MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
export class NumberCoder extends Coder {
readonly size!: number;
readonly signed!: boolean;
constructor(size: number, signed: boolean, localName: string) {
const name = ((signed ? "int": "uint") + (size * 8));
super(name, name, localName, false);
defineProperties<NumberCoder>(this, { size, signed }, { size: "number", signed: "boolean" });
}
defaultValue(): number {
return 0;
}
encode(writer: Writer, _value: BigNumberish | Typed): number {
let value = logger.getBigInt(Typed.dereference(_value, this.type));
// Check bounds are safe for encoding
let maxUintValue = mask(BN_MAX_UINT256, WordSize * 8);
if (this.signed) {
let bounds = mask(maxUintValue, (this.size * 8) - 1);
if (value > bounds || value < -(bounds + BN_1)) {
this._throwError("value out-of-bounds", _value);
}
} else if (value < BN_0 || value > mask(maxUintValue, this.size * 8)) {
this._throwError("value out-of-bounds", _value);
}
value = mask(toTwos(value, this.size * 8), this.size * 8);
if (this.signed) {
value = toTwos(fromTwos(value, this.size * 8), 8 * WordSize);
}
return writer.writeValue(value);
}
decode(reader: Reader): any {
let value = mask(reader.readValue(), this.size * 8);
if (this.signed) {
value = fromTwos(value, this.size * 8);
}
return reader.coerce(this.name, value);
}
}

View File

@ -0,0 +1,26 @@
import { toUtf8Bytes, toUtf8String } from "@ethersproject/strings";
import { Typed } from "../typed.js";
import { DynamicBytesCoder } from "./bytes.js";
import type { Reader, Writer } from "./abstract-coder.js";
export class StringCoder extends DynamicBytesCoder {
constructor(localName: string) {
super("string", localName);
}
defaultValue(): string {
return "";
}
encode(writer: Writer, _value: string | Typed): number {
return super.encode(writer, toUtf8Bytes(Typed.dereference(_value, "string")));
}
decode(reader: Reader): any {
return toUtf8String(super.decode(reader));
}
}

View File

@ -0,0 +1,66 @@
import { defineProperties } from "@ethersproject/properties";
import { Typed } from "../typed.js";
import { Coder } from "./abstract-coder.js";
import { pack, unpack } from "./array.js";
import type { Reader, Writer } from "./abstract-coder.js";
export class TupleCoder extends Coder {
readonly coders!: ReadonlyArray<Coder>;
constructor(coders: Array<Coder>, localName: string) {
let dynamic = false;
const types: Array<string> = [];
coders.forEach((coder) => {
if (coder.dynamic) { dynamic = true; }
types.push(coder.type);
});
const type = ("tuple(" + types.join(",") + ")");
super("tuple", type, localName, dynamic);
defineProperties<TupleCoder>(this, { coders: Object.freeze(coders.slice()) });
}
defaultValue(): any {
const values: any = [ ];
this.coders.forEach((coder) => {
values.push(coder.defaultValue());
});
// We only output named properties for uniquely named coders
const uniqueNames = this.coders.reduce((accum, coder) => {
const name = coder.localName;
if (name) {
if (!accum[name]) { accum[name] = 0; }
accum[name]++;
}
return accum;
}, <{ [ name: string ]: number }>{ });
// Add named values
this.coders.forEach((coder: Coder, index: number) => {
let name = coder.localName;
if (!name || uniqueNames[name] !== 1) { return; }
if (name === "length") { name = "_length"; }
if (values[name] != null) { return; }
values[name] = values[index];
});
return Object.freeze(values);
}
encode(writer: Writer, _value: Array<any> | { [ name: string ]: any } | Typed): number {
const value = Typed.dereference(_value, "tuple");
return pack(writer, this.coders, value);
}
decode(reader: Reader): any {
return reader.coerce(this.name, unpack(reader, this.coders));
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
// @TODO: export from
import { ConstructorFragment, ErrorFragment, EventFragment, Fragment, FunctionFragment, JsonFragment, JsonFragmentType, ParamType } from "./fragments.js";
import { AbiCoder, CoerceFunc, defaultAbiCoder } from "./abi-coder.js";
import { checkResultErrors, Indexed, Interface, InterfaceAbi, LogDescription, Result, TransactionDescription } from "./interface.js";
export { Typed } from "./typed.js";
export {
ConstructorFragment,
ErrorFragment,
EventFragment,
Fragment,
FunctionFragment,
ParamType,
AbiCoder,
defaultAbiCoder,
Interface,
Indexed,
};
export type {
CoerceFunc,
JsonFragment,
JsonFragmentType,
InterfaceAbi,
Result,
checkResultErrors,
LogDescription,
TransactionDescription
};

View File

@ -0,0 +1,883 @@
import { arrayify, concat, dataSlice, hexlify, zeroPadLeft, isHexString } from "@ethersproject/bytes";
import { keccak256 } from "@ethersproject/crypto"
import { id } from "@ethersproject/hash"
import { defineProperties } from "@ethersproject/properties";
import { AbiCoder, defaultAbiCoder } from "./abi-coder.js";
import { checkResultErrors, Result } from "./coders/abstract-coder.js";
import { ConstructorFragment, ErrorFragment, EventFragment, FormatType, Fragment, FunctionFragment, ParamType } from "./fragments.js";
import { logger } from "./logger.js";
import { Typed } from "./typed.js";
import type { BytesLike } from "@ethersproject/bytes";
import type { BigNumberish } from "@ethersproject/logger";
import type { JsonFragment } from "./fragments.js";
export { checkResultErrors, Result };
export class LogDescription {
readonly fragment!: EventFragment;
readonly name!: string;
readonly signature!: string;
readonly topic!: string;
readonly args!: Result
constructor(fragment: EventFragment, topic: string, args: Result) {
const name = fragment.name, signature = fragment.format();
defineProperties<LogDescription>(this, {
fragment, name, signature, topic, args
});
}
}
export class TransactionDescription {
readonly fragment!: FunctionFragment;
readonly name!: string;
readonly args!: Result;
readonly signature!: string;
readonly selector!: string;
readonly value!: bigint;
constructor(fragment: FunctionFragment, selector: string, args: Result, value: bigint) {
const name = fragment.name, signature = fragment.format();
defineProperties<TransactionDescription>(this, {
fragment, name, args, signature, selector, value
});
}
}
export class ErrorDescription {
readonly fragment!: ErrorFragment;
readonly name!: string;
readonly args!: Result;
readonly signature!: string;
readonly selector!: string;
constructor(fragment: ErrorFragment, selector: string, args: Result) {
const name = fragment.name, signature = fragment.format();
defineProperties<ErrorDescription>(this, {
fragment, name, args, signature, selector
});
}
}
export class Indexed {
readonly hash!: null | string;
readonly _isIndexed!: boolean;
static isIndexed(value: any): value is Indexed {
return !!(value && value._isIndexed);
}
constructor(hash: null | string) {
defineProperties<Indexed>(this, { hash, _isIndexed: true })
}
}
type ErrorInfo = {
signature: string,
inputs: Array<string>,
name: string,
reason: (...args: Array<any>) => string;
};
// https://docs.soliditylang.org/en/v0.8.13/control-structures.html?highlight=panic#panic-via-assert-and-error-via-require
const PanicReasons: Record<string, string> = {
"0": "generic panic",
"1": "assert(false)",
"17": "arithmetic overflow",
"18": "division or modulo by zero",
"33": "enum overflow",
"34": "invalid encoded storage byte array accessed",
"49": "out-of-bounds array access; popping on an empty array",
"50": "out-of-bounds access of an array or bytesN",
"65": "out of memory",
"81": "uninitialized function",
}
const BuiltinErrors: Record<string, ErrorInfo> = {
"0x08c379a0": {
signature: "Error(string)",
name: "Error",
inputs: [ "string" ],
reason: (message: string) => {
return `reverted with reason string ${ JSON.stringify(message) }`;
}
},
"0x4e487b71": {
signature: "Panic(uint256)",
name: "Panic",
inputs: [ "uint256" ],
reason: (code: bigint) => {
let reason = "unknown panic code";
if (code >= 0 && code <= 0xff && PanicReasons[code.toString()]) {
reason = PanicReasons[code.toString()];
}
return `reverted with panic code 0x${ code.toString(16) } (${ reason })`;
}
}
}
/*
function wrapAccessError(property: string, error: Error): Error {
const wrap = new Error(`deferred error during ABI decoding triggered accessing ${ property }`);
(<any>wrap).error = error;
return wrap;
}
*/
/*
function checkNames(fragment: Fragment, type: "input" | "output", params: Array<ParamType>): void {
params.reduce((accum, param) => {
if (param.name) {
if (accum[param.name]) {
logger.throwArgumentError(`duplicate ${ type } parameter ${ JSON.stringify(param.name) } in ${ fragment.format("full") }`, "fragment", fragment);
}
accum[param.name] = true;
}
return accum;
}, <{ [ name: string ]: boolean }>{ });
}
*/
//export type AbiCoder = any;
//const defaultAbiCoder: AbiCoder = { };
export type InterfaceAbi = string | ReadonlyArray<Fragment | JsonFragment | string>;
export class Interface {
readonly fragments!: ReadonlyArray<Fragment>;
readonly deploy!: ConstructorFragment;
#errors: Map<string, ErrorFragment>;
#events: Map<string, EventFragment>;
#functions: Map<string, FunctionFragment>;
// #structs: Map<string, StructFragment>;
#abiCoder: AbiCoder;
constructor(fragments: InterfaceAbi) {
let abi: ReadonlyArray<Fragment | JsonFragment | string> = [ ];
if (typeof(fragments) === "string") {
abi = JSON.parse(fragments);
} else {
abi = fragments;
}
this.#functions = new Map();
this.#errors = new Map();
this.#events = new Map();
// this.#structs = new Map();
defineProperties<Interface>(this, {
fragments: Object.freeze(abi.map((f) => Fragment.from(f)).filter((f) => (f != null))),
});
this.#abiCoder = this.getAbiCoder();
// Add all fragments by their signature
this.fragments.forEach((fragment) => {
let bucket: Map<string, Fragment>;
switch (fragment.type) {
case "constructor":
if (this.deploy) {
logger.warn("duplicate definition - constructor");
return;
}
//checkNames(fragment, "input", fragment.inputs);
defineProperties<Interface>(this, { deploy: <ConstructorFragment>fragment });
return;
case "function":
//checkNames(fragment, "input", fragment.inputs);
//checkNames(fragment, "output", (<FunctionFragment>fragment).outputs);
bucket = this.#functions;
break;
case "event":
//checkNames(fragment, "input", fragment.inputs);
bucket = this.#events;
break;
case "error":
bucket = this.#errors;
break;
default:
return;
}
const signature = fragment.format();
if (bucket.has(signature)) {
logger.warn("duplicate definition - " + signature);
return;
}
bucket.set(signature, fragment);
});
// If we do not have a constructor add a default
if (!this.deploy) {
defineProperties<Interface>(this, {
deploy: ConstructorFragment.fromString("constructor()")
});
}
}
// @TODO: multi sig?
format(format?: FormatType): string | Array<string> {
if (!format) { format = FormatType.full; }
if (format === FormatType.sighash) {
logger.throwArgumentError("interface does not support formatting sighash", "format", format);
}
const abi = this.fragments.map((f) => f.format(format));
// We need to re-bundle the JSON fragments a bit
if (format === FormatType.json) {
return JSON.stringify(abi.map((j) => JSON.parse(j)));
}
return abi;
}
getAbiCoder(): AbiCoder {
return defaultAbiCoder;
}
//static getAddress(address: string): string {
// return getAddress(address);
//}
//static getSelector(fragment: ErrorFragment | FunctionFragment): string {
// return dataSlice(id(fragment.format()), 0, 4);
//}
//static getEventTopic(eventFragment: EventFragment): string {
// return id(eventFragment.format());
//}
// Find a function definition by any means necessary (unless it is ambiguous)
#getFunction(key: string, values: null | Array<any | Typed>, forceUnique: boolean): FunctionFragment {
// Selector
if (isHexString(key)) {
const selector = key.toLowerCase();
for (const fragment of this.#functions.values()) {
if (selector === this.getSelector(fragment)) { return fragment; }
}
logger.throwArgumentError("no matching function", "selector", key);
}
// It is a bare name, look up the function (will return null if ambiguous)
if (key.indexOf("(") === -1) {
const matching: Array<FunctionFragment> = [ ];
for (const [ name, fragment ] of this.#functions) {
if (name.split("("/* fix:) */)[0] === key) { matching.push(fragment); }
}
if (values) {
const lastValue = (values.length > 0) ? values[values.length - 1]: null;
let valueLength = values.length;
let allowOptions = true;
if (Typed.isTyped(lastValue) && lastValue.type === "overrides") {
allowOptions = false;
valueLength--;
}
// Remove all matches that don't have a compatible length. The args
// may contain an overrides, so the match may have n or n - 1 parameters
for (let i = matching.length - 1; i >= 0; i--) {
const inputs = matching[i].inputs.length;
if (inputs !== valueLength && (!allowOptions || inputs !== valueLength - 1)) {
matching.splice(i, 1);
}
}
// Remove all matches that don't match the Typed signature
for (let i = matching.length - 1; i >= 0; i--) {
const inputs = matching[i].inputs;
for (let j = 0; j < values.length; j++) {
// Not a typed value
if (!Typed.isTyped(values[j])) { continue; }
// We are past the inputs
if (j >= inputs.length) {
if (values[j].type === "overrides") { continue; }
matching.splice(i, 1);
break;
}
// Make sure the value type matches the input type
if (values[j].type !== inputs[j].baseType) {
matching.splice(i, 1);
break;
}
}
}
}
// We found a single matching signature with an overrides, but the
// last value is something that cannot possibly be an options
if (matching.length === 1 && values && values.length !== matching[0].inputs.length) {
const lastArg = values[values.length - 1];
if (lastArg == null || Array.isArray(lastArg) || typeof(lastArg) !== "object") {
matching.splice(0, 1);
}
}
if (matching.length === 0) {
logger.throwArgumentError("no matching function", "name", key);
} else if (matching.length > 1 && forceUnique) {
const matchStr = matching.map((m) => JSON.stringify(m.format())).join(", ");
logger.throwArgumentError(`multiple matching functions (i.e. ${ matchStr })`, "name", key);
}
return matching[0];
}
// Normalize the signature and lookup the function
const result = this.#functions.get(FunctionFragment.fromString(key).format());
if (result) { return result; }
return logger.throwArgumentError("no matching function", "signature", key);
}
getFunctionName(key: string): string {
return (this.#getFunction(key, null, false)).name;
}
getFunction(key: string, values?: Array<any | Typed>): FunctionFragment {
return this.#getFunction(key, values || null, true)
}
// Find an event definition by any means necessary (unless it is ambiguous)
#getEvent(key: string, values: null | Array<null | any | Typed>, forceUnique: boolean): EventFragment {
// EventTopic
if (isHexString(key)) {
const eventTopic = key.toLowerCase();
for (const fragment of this.#events.values()) {
if (eventTopic === this.getEventTopic(fragment)) { return fragment; }
}
logger.throwArgumentError("no matching event", "eventTopic", key);
}
// It is a bare name, look up the function (will return null if ambiguous)
if (key.indexOf("(") === -1) {
const matching = [ ];
for (const [ name, fragment ] of this.#events) {
if (name.split("("/* fix:) */)[0] === key) { matching.push(fragment); }
}
if (values) {
// Remove all matches that don't have a compatible length.
for (let i = matching.length - 1; i >= 0; i--) {
if (matching[i].inputs.length < values.length) {
matching.splice(i, 1);
}
}
// Remove all matches that don't match the Typed signature
for (let i = matching.length - 1; i >= 0; i--) {
const inputs = matching[i].inputs;
for (let j = 0; j < values.length; j++) {
// Not a typed value
if (!Typed.isTyped(values[j])) { continue; }
// Make sure the value type matches the input type
if (values[j].type !== inputs[j].baseType) {
matching.splice(i, 1);
break;
}
}
}
}
if (matching.length === 0) {
logger.throwArgumentError("no matching event", "name", key);
} else if (matching.length > 1 && forceUnique) {
// @TODO: refine by Typed
logger.throwArgumentError("multiple matching events", "name", key);
}
return matching[0];
}
// Normalize the signature and lookup the function
const result = this.#events.get(EventFragment.fromString(key).format());
if (result) { return result; }
return logger.throwArgumentError("no matching event", "signature", key);
}
getEventName(key: string): string {
return (this.#getEvent(key, null, false)).name;
}
getEvent(key: string, values?: Array<any | Typed>): EventFragment {
return this.#getEvent(key, values || null, true)
}
// Find a function definition by any means necessary (unless it is ambiguous)
getError(key: string, values?: Array<any | Typed>): ErrorFragment {
if (isHexString(key)) {
const selector = key.toLowerCase();
for (const fragment of this.#errors.values()) {
if (selector === this.getSelector(fragment)) { return fragment; }
}
logger.throwArgumentError("no matching error", "selector", key);
}
// It is a bare name, look up the function (will return null if ambiguous)
if (key.indexOf("(") === -1) {
const matching = [ ];
for (const [ name, fragment ] of this.#errors) {
if (name.split("("/* fix:) */)[0] === key) { matching.push(fragment); }
}
if (matching.length === 0) {
logger.throwArgumentError("no matching error", "name", key);
} else if (matching.length > 1) {
// @TODO: refine by Typed
logger.throwArgumentError("multiple matching errors", "name", key);
}
return matching[0];
}
// Normalize the signature and lookup the function
const result = this.#errors.get(ErrorFragment.fromString(key).format());
if (result) { return result; }
return logger.throwArgumentError("no matching error", "signature", key);
}
// Get the 4-byte selector used by Solidity to identify a function
getSelector(fragment: ErrorFragment | FunctionFragment): string {
/*
if (typeof(fragment) === "string") {
const matches: Array<Fragment> = [ ];
try { matches.push(this.getFunction(fragment)); } catch (error) { }
try { matches.push(this.getError(<string>fragment)); } catch (_) { }
if (matches.length === 0) {
logger.throwArgumentError("unknown fragment", "key", fragment);
} else if (matches.length > 1) {
logger.throwArgumentError("ambiguous fragment matches function and error", "key", fragment);
}
fragment = matches[0];
}
*/
return dataSlice(id(fragment.format()), 0, 4);
}
// Get the 32-byte topic hash used by Solidity to identify an event
getEventTopic(fragment: EventFragment): string {
//if (typeof(fragment) === "string") { fragment = this.getEvent(eventFragment); }
return id(fragment.format());
}
_decodeParams(params: ReadonlyArray<ParamType>, data: BytesLike): Result {
return this.#abiCoder.decode(params, data)
}
_encodeParams(params: ReadonlyArray<ParamType>, values: ReadonlyArray<any>): string {
return this.#abiCoder.encode(params, values)
}
encodeDeploy(values?: ReadonlyArray<any>): string {
return this._encodeParams(this.deploy.inputs, values || [ ]);
}
decodeErrorResult(fragment: ErrorFragment | string, data: BytesLike): Result {
if (typeof(fragment) === "string") { fragment = this.getError(fragment); }
if (dataSlice(data, 0, 4) !== this.getSelector(fragment)) {
logger.throwArgumentError(`data signature does not match error ${ fragment.name }.`, "data", data);
}
return this._decodeParams(fragment.inputs, dataSlice(data, 4));
}
encodeErrorResult(fragment: ErrorFragment | string, values?: ReadonlyArray<any>): string {
if (typeof(fragment) === "string") { fragment = this.getError(fragment); }
return concat([
this.getSelector(fragment),
this._encodeParams(fragment.inputs, values || [ ])
]);
}
// Decode the data for a function call (e.g. tx.data)
decodeFunctionData(fragment: FunctionFragment | string, data: BytesLike): Result {
if (typeof(fragment) === "string") { fragment = this.getFunction(fragment); }
if (dataSlice(data, 0, 4) !== this.getSelector(fragment)) {
logger.throwArgumentError(`data signature does not match function ${ fragment.name }.`, "data", data);
}
return this._decodeParams(fragment.inputs, dataSlice(data, 4));
}
// Encode the data for a function call (e.g. tx.data)
encodeFunctionData(fragment: FunctionFragment | string, values?: ReadonlyArray<any>): string {
if (typeof(fragment) === "string") { fragment = this.getFunction(fragment); }
return concat([
this.getSelector(fragment),
this._encodeParams(fragment.inputs, values || [ ])
]);
}
// Decode the result from a function call (e.g. from eth_call)
decodeFunctionResult(fragment: FunctionFragment | string, data: BytesLike): Result {
if (typeof(fragment) === "string") { fragment = this.getFunction(fragment); }
let message = "invalid length for result data";
const bytes = arrayify(data);
if ((bytes.length % 32) === 0) {
try {
return this.#abiCoder.decode(fragment.outputs, bytes);
} catch (error) {
message = "could not decode result data";
}
}
// Call returned data with no error, but the data is junk
return logger.throwError(message, "BAD_DATA", {
value: hexlify(bytes),
info: { method: fragment.name, signature: fragment.format() }
});
}
makeError(fragment: FunctionFragment | string, _data: BytesLike, tx?: { data: string }): Error {
if (typeof(fragment) === "string") { fragment = this.getFunction(fragment); }
const data = logger.getBytes(_data);
let args: undefined | Result = undefined;
if (tx) {
try {
args = this.#abiCoder.decode(fragment.inputs, tx.data || "0x");
} catch (error) { console.log(error); }
}
let errorArgs: undefined | Result = undefined;
let errorName: undefined | string = undefined;
let errorSignature: undefined | string = undefined;
let reason: string = "unknown reason";
if (data.length === 0) {
reason = "missing error reason";
} else if ((data.length % 32) === 4) {
const selector = hexlify(data.slice(0, 4));
const builtin = BuiltinErrors[selector];
if (builtin) {
try {
errorName = builtin.name;
errorSignature = builtin.signature;
errorArgs = this.#abiCoder.decode(builtin.inputs, data.slice(4));
reason = builtin.reason(...errorArgs);
} catch (error) {
console.log(error); // @TODO: remove
}
} else {
reason = "unknown custom error";
try {
const error = this.getError(selector);
errorName = error.name;
errorSignature = error.format();
reason = `custom error: ${ errorSignature }`;
try {
errorArgs = this.#abiCoder.decode(error.inputs, data.slice(4));
} catch (error) {
reason = `custom error: ${ errorSignature } (coult not decode error data)`
}
} catch (error) {
console.log(error); // @TODO: remove
}
}
}
return logger.makeError("call revert exception", "CALL_EXCEPTION", {
data: hexlify(data), transaction: null,
method: fragment.name, signature: fragment.format(), args,
errorArgs, errorName, errorSignature, reason
});
}
// Encode the result for a function call (e.g. for eth_call)
encodeFunctionResult(functionFragment: FunctionFragment | string, values?: ReadonlyArray<any>): string {
if (typeof(functionFragment) === "string") {
functionFragment = this.getFunction(functionFragment);
}
return hexlify(this.#abiCoder.encode(functionFragment.outputs, values || [ ]));
}
/*
spelunk(inputs: Array<ParamType>, values: ReadonlyArray<any>, processfunc: (type: string, value: any) => Promise<any>): Promise<Array<any>> {
const promises: Array<Promise<>> = [ ];
const process = function(type: ParamType, value: any): any {
if (type.baseType === "array") {
return descend(type.child
}
if (type. === "address") {
}
};
const descend = function (inputs: Array<ParamType>, values: ReadonlyArray<any>) {
if (inputs.length !== values.length) { throw new Error("length mismatch"); }
};
const result: Array<any> = [ ];
values.forEach((value, index) => {
if (value == null) {
topics.push(null);
} else if (param.baseType === "array" || param.baseType === "tuple") {
logger.throwArgumentError("filtering with tuples or arrays not supported", ("contract." + param.name), value);
} else if (Array.isArray(value)) {
topics.push(value.map((value) => encodeTopic(param, value)));
} else {
topics.push(encodeTopic(param, value));
}
});
}
*/
// Create the filter for the event with search criteria (e.g. for eth_filterLog)
encodeFilterTopics(eventFragment: EventFragment, values: ReadonlyArray<any>): Array<null | string | Array<string>> {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
}
if (values.length > eventFragment.inputs.length) {
logger.throwError("too many arguments for " + eventFragment.format(), "UNEXPECTED_ARGUMENT", {
count: values.length,
expectedCount: eventFragment.inputs.length
})
}
const topics: Array<null | string | Array<string>> = [];
if (!eventFragment.anonymous) { topics.push(this.getEventTopic(eventFragment)); }
const encodeTopic = (param: ParamType, value: any): string => {
if (param.type === "string") {
return id(value);
} else if (param.type === "bytes") {
return keccak256(hexlify(value));
}
// Check addresses are valid
if (param.type === "address") { this.#abiCoder.encode( [ "address" ], [ value ]); }
return zeroPadLeft(hexlify(value), 32);
//@TOOD should probably be return toHex(value, 32)
};
values.forEach((value, index) => {
const param = eventFragment.inputs[index];
if (!param.indexed) {
if (value != null) {
logger.throwArgumentError("cannot filter non-indexed parameters; must be null", ("contract." + param.name), value);
}
return;
}
if (value == null) {
topics.push(null);
} else if (param.baseType === "array" || param.baseType === "tuple") {
logger.throwArgumentError("filtering with tuples or arrays not supported", ("contract." + param.name), value);
} else if (Array.isArray(value)) {
topics.push(value.map((value) => encodeTopic(param, value)));
} else {
topics.push(encodeTopic(param, value));
}
});
// Trim off trailing nulls
while (topics.length && topics[topics.length - 1] === null) {
topics.pop();
}
return topics;
}
encodeEventLog(eventFragment: EventFragment, values: ReadonlyArray<any>): { data: string, topics: Array<string> } {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
}
const topics: Array<string> = [ ];
const dataTypes: Array<ParamType> = [ ];
const dataValues: Array<string> = [ ];
if (!eventFragment.anonymous) {
topics.push(this.getEventTopic(eventFragment));
}
if (values.length !== eventFragment.inputs.length) {
logger.throwArgumentError("event arguments/values mismatch", "values", values);
}
eventFragment.inputs.forEach((param, index) => {
const value = values[index];
if (param.indexed) {
if (param.type === "string") {
topics.push(id(value))
} else if (param.type === "bytes") {
topics.push(keccak256(value))
} else if (param.baseType === "tuple" || param.baseType === "array") {
// @TODO
throw new Error("not implemented");
} else {
topics.push(this.#abiCoder.encode([ param.type] , [ value ]));
}
} else {
dataTypes.push(param);
dataValues.push(value);
}
});
return {
data: this.#abiCoder.encode(dataTypes , dataValues),
topics: topics
};
}
// Decode a filter for the event and the search criteria
decodeEventLog(eventFragment: EventFragment | string, data: BytesLike, topics?: ReadonlyArray<string>): Result {
if (typeof(eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
}
if (topics != null && !eventFragment.anonymous) {
const eventTopic = this.getEventTopic(eventFragment);
if (!isHexString(topics[0], 32) || topics[0].toLowerCase() !== eventTopic) {
logger.throwArgumentError("fragment/topic mismatch", "topics[0]", topics[0]);
}
topics = topics.slice(1);
}
const indexed: Array<ParamType> = [];
const nonIndexed: Array<ParamType> = [];
const dynamic: Array<boolean> = [];
eventFragment.inputs.forEach((param, index) => {
if (param.indexed) {
if (param.type === "string" || param.type === "bytes" || param.baseType === "tuple" || param.baseType === "array") {
indexed.push(ParamType.fromObject({ type: "bytes32", name: param.name }));
dynamic.push(true);
} else {
indexed.push(param);
dynamic.push(false);
}
} else {
nonIndexed.push(param);
dynamic.push(false);
}
});
const resultIndexed = (topics != null) ? this.#abiCoder.decode(indexed, concat(topics)): null;
const resultNonIndexed = this.#abiCoder.decode(nonIndexed, data, true);
//const result: (Array<any> & { [ key: string ]: any }) = [ ];
const values: Array<any> = [ ];
const keys: Array<null | string> = [ ];
let nonIndexedIndex = 0, indexedIndex = 0;
eventFragment.inputs.forEach((param, index) => {
let value = null;
if (param.indexed) {
if (resultIndexed == null) {
value = new Indexed(null);
} else if (dynamic[index]) {
value = new Indexed(resultIndexed[indexedIndex++]);
} else {
try {
value = resultIndexed[indexedIndex++];
} catch (error) {
value = error;
}
}
} else {
try {
value = resultNonIndexed[nonIndexedIndex++];
} catch (error) {
value = error;
}
}
values.push(value);
keys.push(param.name || null);
});
return Result.fromItems(values, keys);
}
// Given a transaction, find the matching function fragment (if any) and
// determine all its properties and call parameters
parseTransaction(tx: { data: string, value?: BigNumberish }): null | TransactionDescription {
const data = logger.getBytes(tx.data, "tx.data");
const value = logger.getBigInt((tx.value != null) ? tx.value: 0, "tx.value");
const fragment = this.getFunction(hexlify(data.slice(0, 4)));
if (!fragment) { return null; }
const args = this.#abiCoder.decode(fragment.inputs, data.slice(4));
return new TransactionDescription(fragment, this.getSelector(fragment), args, value);
}
// @TODO
//parseCallResult(data: BytesLike): ??
// Given an event log, find the matching event fragment (if any) and
// determine all its properties and values
parseLog(log: { topics: Array<string>, data: string}): null | LogDescription {
const fragment = this.getEvent(log.topics[0]);
if (!fragment || fragment.anonymous) { return null; }
// @TODO: If anonymous, and the only method, and the input count matches, should we parse?
// Probably not, because just because it is the only event in the ABI does
// not mean we have the full ABI; maybe just a fragment?
return new LogDescription(fragment, this.getEventTopic(fragment), this.decodeEventLog(fragment, log.data, log.topics));
}
parseError(data: BytesLike): null | ErrorDescription {
const hexData = hexlify(data);
const fragment = this.getError(dataSlice(hexData, 0, 4));
if (!fragment) { return null; }
const args = this.#abiCoder.decode(fragment.inputs, dataSlice(hexData, 4));
return new ErrorDescription(fragment, this.getSelector(fragment), args);
}
static from(value: ReadonlyArray<Fragment | string | JsonFragment> | string | Interface) {
// Already an Interface, which is immutable
if (value instanceof Interface) { return value; }
// JSON
if (typeof(value) === "string") { return new Interface(JSON.parse(value)); }
// Maybe an interface from an older version, or from a symlinked copy
if (typeof((<any>value).format) === "function") {
return new Interface((<any>value).format(FormatType.json));
}
// Array of fragments
return new Interface(value);
}
}

View File

@ -0,0 +1,3 @@
import { Logger } from "@ethersproject/logger";
import { version } from "./_version.js";
export const logger = new Logger(version);

View File

@ -0,0 +1,254 @@
import { defineProperties } from "@ethersproject/properties";
import type { Addressable } from "@ethersproject/address";
import type { BigNumberish, BytesLike } from "@ethersproject/logger";
import type { Result } from "./coders/abstract-coder.js";
const _gaurd = { };
function n(value: BigNumberish, width: number): Typed {
let signed = false;
if (width < 0) {
signed = true;
width *= -1;
}
// @TODO: Check range is valid for value
return new Typed(_gaurd, `${ signed ? "": "u" }int${ width }`, value, { signed, width });
}
function b(value: BytesLike, size?: number): Typed {
// @TODO: Check range is valid for value
return new Typed(_gaurd, `bytes${ (size) ? size: "" }`, value, { size });
}
export interface TypedNumber extends Typed {
defaultValue(): number;
minValue(): number;
maxValue(): number;
}
export interface TypedBigInt extends Typed {
defaultValue(): bigint;
minValue(): bigint;
maxValue(): bigint;
}
export interface TypedData extends Typed {
defaultValue(): string;
}
export interface TypedString extends Typed {
defaultValue(): string;
}
const _typedSymbol = Symbol.for("_ethers_typed");
export class Typed {
readonly type!: string;
readonly value!: any;
readonly #options: any;
readonly _typedSymbol!: Symbol;
constructor(gaurd: any, type: string, value: any, options: any = null) {
if (gaurd !== _gaurd) { throw new Error("private constructor"); }
defineProperties<Typed>(this, { _typedSymbol, type, value });
this.#options = options;
// Check the value is valid
this.format();
}
format(): string {
if (this.type === "array") {
throw new Error("");
} else if (this.type === "dynamicArray") {
throw new Error("");
} else if (this.type === "tuple") {
return `tuple(${ this.value.map((v: Typed) => v.format()).join(",") })`
}
return this.type;
}
defaultValue(): string | number | bigint | Result {
return 0;
}
minValue(): string | number | bigint {
return 0;
}
maxValue(): string | number | bigint {
return 0;
}
isBigInt(): this is TypedBigInt {
return !!(this.type.match(/^u?int[0-9]+$/));
}
isData(): this is TypedData {
return (this.type.substring(0, 5) === "bytes");
}
isString(): this is TypedString {
return (this.type === "string");
}
get tupleName(): null | string {
if (this.type !== "tuple") { throw TypeError("not a tuple"); }
return this.#options;
}
// Returns the length of this type as an array
// - `null` indicates the length is unforced, it could be dynamic
// - `-1` indicates the length is dynamic
// - any other value indicates it is a static array and is its length
get arrayLength(): null | number {
if (this.type !== "array") { throw TypeError("not an array"); }
if (this.#options === true) { return -1; }
if (this.#options === false) { return (<Array<any>>(this.value)).length; }
return null;
}
static from(type: string, value: any): Typed {
return new Typed(_gaurd, type, value);
}
static uint8(v: BigNumberish): Typed { return n(v, 8); }
static uint16(v: BigNumberish): Typed { return n(v, 16); }
static uint24(v: BigNumberish): Typed { return n(v, 24); }
static uint32(v: BigNumberish): Typed { return n(v, 32); }
static uint40(v: BigNumberish): Typed { return n(v, 40); }
static uint48(v: BigNumberish): Typed { return n(v, 46); }
static uint56(v: BigNumberish): Typed { return n(v, 56); }
static uint64(v: BigNumberish): Typed { return n(v, 64); }
static uint72(v: BigNumberish): Typed { return n(v, 72); }
static uint80(v: BigNumberish): Typed { return n(v, 80); }
static uint88(v: BigNumberish): Typed { return n(v, 88); }
static uint96(v: BigNumberish): Typed { return n(v, 96); }
static uint104(v: BigNumberish): Typed { return n(v, 104); }
static uint112(v: BigNumberish): Typed { return n(v, 112); }
static uint120(v: BigNumberish): Typed { return n(v, 120); }
static uint128(v: BigNumberish): Typed { return n(v, 128); }
static uint136(v: BigNumberish): Typed { return n(v, 136); }
static uint144(v: BigNumberish): Typed { return n(v, 144); }
static uint152(v: BigNumberish): Typed { return n(v, 152); }
static uint160(v: BigNumberish): Typed { return n(v, 160); }
static uint168(v: BigNumberish): Typed { return n(v, 168); }
static uint176(v: BigNumberish): Typed { return n(v, 176); }
static uint184(v: BigNumberish): Typed { return n(v, 184); }
static uint192(v: BigNumberish): Typed { return n(v, 192); }
static uint200(v: BigNumberish): Typed { return n(v, 200); }
static uint208(v: BigNumberish): Typed { return n(v, 208); }
static uint216(v: BigNumberish): Typed { return n(v, 216); }
static uint224(v: BigNumberish): Typed { return n(v, 224); }
static uint232(v: BigNumberish): Typed { return n(v, 232); }
static uint240(v: BigNumberish): Typed { return n(v, 240); }
static uint248(v: BigNumberish): Typed { return n(v, 248); }
static uint256(v: BigNumberish): Typed { return n(v, 256); }
static uint(v: BigNumberish): Typed { return n(v, 256); }
static int8(v: BigNumberish): Typed { return n(v, -8); }
static int16(v: BigNumberish): Typed { return n(v, -16); }
static int24(v: BigNumberish): Typed { return n(v, -24); }
static int32(v: BigNumberish): Typed { return n(v, -32); }
static int40(v: BigNumberish): Typed { return n(v, -40); }
static int48(v: BigNumberish): Typed { return n(v, -46); }
static int56(v: BigNumberish): Typed { return n(v, -56); }
static int64(v: BigNumberish): Typed { return n(v, -64); }
static int72(v: BigNumberish): Typed { return n(v, -72); }
static int80(v: BigNumberish): Typed { return n(v, -80); }
static int88(v: BigNumberish): Typed { return n(v, -88); }
static int96(v: BigNumberish): Typed { return n(v, -96); }
static int104(v: BigNumberish): Typed { return n(v, -104); }
static int112(v: BigNumberish): Typed { return n(v, -112); }
static int120(v: BigNumberish): Typed { return n(v, -120); }
static int128(v: BigNumberish): Typed { return n(v, -128); }
static int136(v: BigNumberish): Typed { return n(v, -136); }
static int144(v: BigNumberish): Typed { return n(v, -144); }
static int152(v: BigNumberish): Typed { return n(v, -152); }
static int160(v: BigNumberish): Typed { return n(v, -160); }
static int168(v: BigNumberish): Typed { return n(v, -168); }
static int176(v: BigNumberish): Typed { return n(v, -176); }
static int184(v: BigNumberish): Typed { return n(v, -184); }
static int192(v: BigNumberish): Typed { return n(v, -192); }
static int200(v: BigNumberish): Typed { return n(v, -200); }
static int208(v: BigNumberish): Typed { return n(v, -208); }
static int216(v: BigNumberish): Typed { return n(v, -216); }
static int224(v: BigNumberish): Typed { return n(v, -224); }
static int232(v: BigNumberish): Typed { return n(v, -232); }
static int240(v: BigNumberish): Typed { return n(v, -240); }
static int248(v: BigNumberish): Typed { return n(v, -248); }
static int256(v: BigNumberish): Typed { return n(v, -256); }
static int(v: BigNumberish): Typed { return n(v, -256); }
static bytes(v: BytesLike): Typed { return b(v); }
static bytes1(v: BytesLike): Typed { return b(v, 1); }
static bytes2(v: BytesLike): Typed { return b(v, 2); }
static bytes3(v: BytesLike): Typed { return b(v, 3); }
static bytes4(v: BytesLike): Typed { return b(v, 4); }
static bytes5(v: BytesLike): Typed { return b(v, 5); }
static bytes6(v: BytesLike): Typed { return b(v, 6); }
static bytes7(v: BytesLike): Typed { return b(v, 7); }
static bytes8(v: BytesLike): Typed { return b(v, 8); }
static bytes9(v: BytesLike): Typed { return b(v, 9); }
static bytes10(v: BytesLike): Typed { return b(v, 10); }
static bytes11(v: BytesLike): Typed { return b(v, 11); }
static bytes12(v: BytesLike): Typed { return b(v, 12); }
static bytes13(v: BytesLike): Typed { return b(v, 13); }
static bytes14(v: BytesLike): Typed { return b(v, 14); }
static bytes15(v: BytesLike): Typed { return b(v, 15); }
static bytes16(v: BytesLike): Typed { return b(v, 16); }
static bytes17(v: BytesLike): Typed { return b(v, 17); }
static bytes18(v: BytesLike): Typed { return b(v, 18); }
static bytes19(v: BytesLike): Typed { return b(v, 19); }
static bytes20(v: BytesLike): Typed { return b(v, 20); }
static bytes21(v: BytesLike): Typed { return b(v, 21); }
static bytes22(v: BytesLike): Typed { return b(v, 22); }
static bytes23(v: BytesLike): Typed { return b(v, 23); }
static bytes24(v: BytesLike): Typed { return b(v, 24); }
static bytes25(v: BytesLike): Typed { return b(v, 25); }
static bytes26(v: BytesLike): Typed { return b(v, 26); }
static bytes27(v: BytesLike): Typed { return b(v, 27); }
static bytes28(v: BytesLike): Typed { return b(v, 28); }
static bytes29(v: BytesLike): Typed { return b(v, 29); }
static bytes30(v: BytesLike): Typed { return b(v, 30); }
static bytes31(v: BytesLike): Typed { return b(v, 31); }
static bytes32(v: BytesLike): Typed { return b(v, 32); }
static address(v: string | Addressable): Typed { return new Typed(_gaurd, "address", v); }
static bool(v: any): Typed { return new Typed(_gaurd, "bool", !!v); }
static string(v: string): Typed { return new Typed(_gaurd, "string", v); }
static array(v: Array<any | Typed>, dynamic?: null | boolean): Typed {
throw new Error("not implemented yet");
return new Typed(_gaurd, "array", v, dynamic);
}
static tuple(v: Array<any | Typed> | Record<string, any | Typed>, name?: string): Typed {
throw new Error("not implemented yet");
return new Typed(_gaurd, "tuple", v, name);
}
static overrides(v: Record<string, any>): Typed {
return new Typed(_gaurd, "overrides", Object.assign({ }, v));
}
static isTyped(value: any): value is Typed {
return (value && value._typedSymbol === _typedSymbol);
}
static dereference<T>(value: Typed | T, type: string): T {
if (Typed.isTyped(value)) {
if (value.type !== type) {
throw new Error(`invalid type: expecetd ${ type }, got ${ value.type }`);
}
return value.value;
}
return value;
}
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./lib/",
"rootDir": "./src.ts"
},
"exclude": [],
"extends": "../../tsconfig.common.json",
"include": [
"./src.ts/**/*.ts"
]
}

View File

@ -0,0 +1,3 @@
/src.ts/tests/**
/lib/tests/**
tsconfig.tsbuildinfo

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,35 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"dependencies": {
"@ethersproject/bytes": "^6.0.0-beta.1",
"@ethersproject/crypto": "^6.0.0-beta.1",
"@ethersproject/logger": "^6.0.0-beta.1",
"@ethersproject/rlp": "^6.0.0-beta.1"
},
"description": "Utilities for handling Ethereum Addresses for ethers.",
"engines" : { "node" : ">=12.17.0" },
"ethereum": "donations.ethers.eth",
"keywords": [
"Ethereum",
"ethers"
],
"license": "MIT",
"main": "./lib/index.js",
"name": "@ethersproject/address",
"publishConfig": {
"access": "public",
"tag": "beta"
},
"repository": {
"directory": "packages/address",
"type": "git",
"url": "git://github.com/ethers-io/ethers.js.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"sideEffects": false,
"type": "module",
"types": "./lib/index.d.ts",
"version": "6.0.0-beta.1"
}

View File

@ -0,0 +1 @@
export const version = "@ethersproject/address@6.0.0-beta.1";

View File

@ -0,0 +1,128 @@
import { arrayify } from "@ethersproject/bytes";
import { keccak256 } from "@ethersproject/crypto";
import { logger } from "./logger.js";
const BN_0 = BigInt(0);
const BN_36 = BigInt(36);
function getChecksumAddress(address: string): string {
// if (!isHexString(address, 20)) {
// logger.throwArgumentError("invalid address", "address", address);
// }
address = address.toLowerCase();
const chars = address.substring(2).split("");
const expanded = new Uint8Array(40);
for (let i = 0; i < 40; i++) {
expanded[i] = chars[i].charCodeAt(0);
}
const hashed = arrayify(keccak256(expanded));
for (let i = 0; i < 40; i += 2) {
if ((hashed[i >> 1] >> 4) >= 8) {
chars[i] = chars[i].toUpperCase();
}
if ((hashed[i >> 1] & 0x0f) >= 8) {
chars[i + 1] = chars[i + 1].toUpperCase();
}
}
return "0x" + chars.join("");
}
// See: https://en.wikipedia.org/wiki/International_Bank_Account_Number
// Create lookup table
const ibanLookup: { [character: string]: string } = { };
for (let i = 0; i < 10; i++) { ibanLookup[String(i)] = String(i); }
for (let i = 0; i < 26; i++) { ibanLookup[String.fromCharCode(65 + i)] = String(10 + i); }
// How many decimal digits can we process? (for 64-bit float, this is 15)
// i.e. Math.floor(Math.log10(Number.MAX_SAFE_INTEGER));
const safeDigits = 15;
function ibanChecksum(address: string): string {
address = address.toUpperCase();
address = address.substring(4) + address.substring(0, 2) + "00";
let expanded = address.split("").map((c) => { return ibanLookup[c]; }).join("");
// Javascript can handle integers safely up to 15 (decimal) digits
while (expanded.length >= safeDigits){
let block = expanded.substring(0, safeDigits);
expanded = parseInt(block, 10) % 97 + expanded.substring(block.length);
}
let checksum = String(98 - (parseInt(expanded, 10) % 97));
while (checksum.length < 2) { checksum = "0" + checksum; }
return checksum;
};
const Base36 = (function() {;
const result: Record<string, bigint> = { };
for (let i = 0; i < 36; i++) {
const key = "0123456789abcdefghijklmnopqrstuvwxyz"[i];
result[key] = BigInt(i);
}
return result;
})();
function fromBase36(value: string): bigint {
value = value.toLowerCase();
let result = BN_0;
for (let i = 0; i < value.length; i++) {
result = result * BN_36 + Base36[value[i]];
}
return result;
}
export function getAddress(address: string): string {
if (typeof(address) !== "string") {
logger.throwArgumentError("invalid address", "address", address);
}
if (address.match(/^(0x)?[0-9a-fA-F]{40}$/)) {
// Missing the 0x prefix
if (address.substring(0, 2) !== "0x") { address = "0x" + address; }
const result = getChecksumAddress(address);
// It is a checksummed address with a bad checksum
if (address.match(/([A-F].*[a-f])|([a-f].*[A-F])/) && result !== address) {
logger.throwArgumentError("bad address checksum", "address", address);
}
return result;
}
// Maybe ICAP? (we only support direct mode)
if (address.match(/^XE[0-9]{2}[0-9A-Za-z]{30,31}$/)) {
// It is an ICAP address with a bad checksum
if (address.substring(2, 4) !== ibanChecksum(address)) {
logger.throwArgumentError("bad icap checksum", "address", address);
}
let result = fromBase36(address.substring(4)).toString(16);
while (result.length < 40) { result = "0" + result; }
return getChecksumAddress("0x" + result);
}
return logger.throwArgumentError("invalid address", "address", address);
}
export function getIcapAddress(address: string): string {
//let base36 = _base16To36(getAddress(address).substring(2)).toUpperCase();
let base36 = BigInt(getAddress(address)).toString(36).toUpperCase();
while (base36.length < 30) { base36 = "0" + base36; }
return "XE" + ibanChecksum("XE00" + base36) + base36;
}

View File

@ -0,0 +1,50 @@
import { getAddress } from "./address.js";
import { logger } from "./logger.js";
import type { Addressable, NameResolver } from "./types.js";
export function isAddressable(value: any): value is Addressable {
return (value && typeof(value.getAddress) === "function");
}
export function isAddress(value: any): boolean {
try {
getAddress(value);
return true;
} catch (error) { }
return false;
}
// Resolves an Ethereum address, ENS name or Addressable object,
// throwing if the result is null; an explicit null returns null
//export async function resolveAddress(target?: null, resolver?: null | NameResolver): Promise<null>;
//export async function resolveAddress(target: string | Addressable, resolver?: null | NameResolver): Promise<string>;
//export async function resolveAddress(target: null | undefined | string | Addressable, resolver?: null | NameResolver) {
export async function resolveAddress(target: string | Addressable, resolver?: null | NameResolver) {
//if (target == null) { return null; }
if (typeof(target) === "string") {
if (target.match(/^0x[0-9a-f]{40}$/i)) { return getAddress(target); }
if (resolver == null) {
return logger.throwError("ENS resolution requires a provider", "UNSUPPORTED_OPERATION", {
operation: "resolveName",
});
}
const result = await resolver.resolveName(target);
if (result == null || result === "0x0000000000000000000000000000000000000000") {
return logger.throwError("unconfigured name", "UNCONFIGURED_NAME", { value: target });
}
return getAddress(result);
} else if (isAddressable(target)) {
const result = await target.getAddress();
if (result == null) {
logger.throwArgumentError("addressable resolved to null", "target", target);
}
return getAddress(result);
}
return logger.throwArgumentError("unsupported addressable value", "target", target);
}

View File

@ -0,0 +1,47 @@
import { concat, dataSlice } from "@ethersproject/bytes";
import { keccak256 } from "@ethersproject/crypto";
import { encodeRlp } from "@ethersproject/rlp";
import { getAddress } from "./address.js";
import type { BigNumberish, BytesLike } from "@ethersproject/logger";
import { logger } from "./logger.js";
// http://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed
export function getCreateAddress(tx: { from: string, nonce: BigNumberish }): string {
const from = getAddress(tx.from);
const nonce = logger.getBigInt(tx.nonce, "tx.nonce");
let nonceHex = nonce.toString(16);
if (nonceHex === "0") {
nonceHex = "0x";
} else if (nonceHex.length % 2) {
nonceHex = "0x0" + nonceHex;
} else {
nonceHex = "0x" + nonceHex;
}
return getAddress(dataSlice(keccak256(encodeRlp([ from, nonceHex ])), 12));
}
export function getCreate2Address(_from: string, _salt: BytesLike, _initCodeHash: BytesLike): string {
const from = getAddress(_from);
const salt = logger.getBytes(_salt, "salt");
const initCodeHash = logger.getBytes(_initCodeHash, "initCodeHash");
if (salt.length !== 32) {
logger.throwArgumentError("salt must be 32 bytes", "salt", _salt);
}
if (initCodeHash.length !== 32) {
logger.throwArgumentError("initCodeHash must be 32 bytes", "initCodeHash", _initCodeHash);
}
return getAddress(dataSlice(keccak256(concat([ "0xff", from, salt, initCodeHash ])), 12))
}

View File

@ -0,0 +1,6 @@
export { getAddress, getIcapAddress } from "./address.js";
export { getCreateAddress, getCreate2Address } from "./contract-address.js";
export { isAddressable, isAddress, resolveAddress } from "./checks.js";
export type { Addressable, NameResolver } from "./types.js"

View File

@ -0,0 +1,6 @@
import { Logger } from "@ethersproject/logger";
import { version } from "./_version.js";
export const logger = new Logger(version);

View File

@ -0,0 +1,132 @@
import assert from "assert";
import { loadTests } from "./utils.js";
import type {
TestCaseAccount,
TestCaseCreate,
TestCaseCreate2
} from "./types.js";
import {
getAddress, getIcapAddress,
getCreateAddress, getCreate2Address
} from "../index.js";
describe("computes checksum address", function() {
const tests = loadTests<TestCaseAccount>("accounts");
for (const test of tests) {
it(`computes the checksum address for ${ test.name }`, function() {
assert.equal(getAddress(test.address), test.address);
assert.equal(getAddress(test.icap), test.address);
assert.equal(getAddress(test.address.substring(2)), test.address);
assert.equal(getAddress(test.address.toLowerCase()), test.address);
assert.equal(getAddress("0x" + test.address.substring(2).toUpperCase()), test.address);
});
}
const invalidAddresses: Array<{ name: string, value: any }> = [
{ name: "null", value: null },
{ name: "number", value: 1234 },
{ name: "emtpy bytes", value: "0x" },
{ name: "too short", value: "0x8ba1f109551bd432803012645ac136ddd64dba" },
{ name: "too long", value: "0x8ba1f109551bd432803012645ac136ddd64dba7200" },
];
invalidAddresses.forEach(({ name, value }) => {
it(`fails on invalid address: ${ name }`, function() {
assert.throws(function() {
getAddress(value);
}, function(error) {
return (error.code === "INVALID_ARGUMENT" &&
error.message.match(/^invalid address/) &&
error.argument === "address" &&
error.value === value);
});
});
});
it("fails on invalid checksum", function() {
const value = "0x8ba1f109551bD432803012645Ac136ddd64DBa72"
assert.throws(function() {
getAddress(value);
}, function(error) {
return (error.code === "INVALID_ARGUMENT" &&
error.message.match(/^bad address checksum/) &&
error.argument === "address" &&
error.value === value);
});
});
it("fails on invalid IBAN checksum", function() {
const value = "XE65GB6LDNXYOFTX0NSV3FUWKOWIXAMJK37";
assert.throws(function() {
getAddress(value);
}, function(error) {
return (error.code === "INVALID_ARGUMENT" &&
error.message.match(/^bad icap checksum/) &&
error.argument === "address" &&
error.value === value);
});
});
});
describe("computes ICAP address", function() {
const tests = loadTests<TestCaseAccount>("accounts");
for (const test of tests) {
it(`computes the ICAP address for ${ test.name }`, function() {
assert.equal(getIcapAddress(test.address), test.icap);
assert.equal(getAddress(test.address.toLowerCase()), test.address);
assert.equal(getAddress("0x" + test.address.substring(2).toUpperCase()), test.address);
});
}
});
describe("computes create address", function() {
const tests = loadTests<TestCaseCreate>("create");
for (const { sender, creates } of tests) {
for (const { name, nonce, address } of creates) {
it(`computes the create address for ${ name }`, function() {
assert.equal(getCreateAddress({ from: sender, nonce }), address);
});
}
}
});
describe("computes create2 address", function() {
const tests = loadTests<TestCaseCreate2>("create2");
for (const { sender, creates } of tests) {
for (const { name, salt, initCodeHash, address } of creates) {
it(`computes the create2 address for ${ name }`, function() {
assert.equal(getCreate2Address(sender, salt, initCodeHash), address);
});
}
}
const sender = "0x8ba1f109551bD432803012645Ac136ddd64DBA72";
const salt = "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8";
const initCodeHash = "0x8452c9b9140222b08593a26daa782707297be9f7b3e8281d7b4974769f19afd0";
it("fails on invalid salt", function() {
const badSalt = "0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36dea";
assert.throws(function() {
getCreate2Address(sender, badSalt, initCodeHash);
}, function(error) {
return (error.code === "INVALID_ARGUMENT" &&
error.argument === "salt" &&
error.value === badSalt);
});
});
it("fails on invalid initCodeHash", function() {
const badInitCodeHash = "0x8452c9b9140222b08593a26daa782707297be9f7b3e8281d7b4974769f19af";
assert.throws(function() {
getCreate2Address(sender, salt, badInitCodeHash);
}, function(error) {
return (error.code === "INVALID_ARGUMENT" &&
error.argument === "initCodeHash" &&
error.value === badInitCodeHash);
});
});
});

View File

@ -0,0 +1,27 @@
export interface TestCaseAccount {
name: string;
privateKey: string;
address: string;
icap: string;
}
export type TestCaseCreate = {
sender: string;
creates: Array<{
name: string,
nonce: number,
address: string
}>;
};
export type TestCaseCreate2 = {
sender: string;
creates: Array<{
name: string,
salt: string;
initCode: string
initCodeHash: string
address: string;
}>;
};

View File

@ -0,0 +1,25 @@
import fs from "fs"
import path from "path";
import zlib from 'zlib';
// Find the package root (based on the nyc output/ folder)
const root = (function() {
let root = process.cwd();
while (true) {
if (fs.existsSync(path.join(root, "output"))) { return root; }
const parent = path.join(root, "..");
if (parent === root) { break; }
root = parent;
}
throw new Error("could not find root");
})();
// Load the tests
export function loadTests<T>(tag: string): Array<T> {
const filename = path.resolve(root, "testcases", tag + '.json.gz');
return JSON.parse(zlib.gunzipSync(fs.readFileSync(filename)).toString());
}

View File

@ -0,0 +1,8 @@
export interface Addressable {
getAddress(): Promise<string>;
}
export interface NameResolver {
resolveName(name: string): Promise<null | string>;
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./lib/",
"rootDir": "./src.ts"
},
"exclude": [],
"extends": "../../tsconfig.common.json",
"include": [
"./src.ts/**/*.ts"
]
}

View File

@ -0,0 +1,3 @@
/src.ts/tests/**
/lib/tests/**
tsconfig.tsbuildinfo

21
packages/bytes/LICENSE.md Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,32 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"dependencies": {
"@ethersproject/logger": "^6.0.0-beta.1"
},
"description": "Bytes utility functions for ethers.",
"engines" : { "node" : ">=12.17.0" },
"ethereum": "donations.ethers.eth",
"keywords": [
"Ethereum",
"ethers"
],
"license": "MIT",
"main": "./lib/index.js",
"name": "@ethersproject/bytes",
"publishConfig": {
"access": "public",
"tag": "beta"
},
"repository": {
"directory": "packages/bytes",
"type": "git",
"url": "git://github.com/ethers-io/ethers.js.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"sideEffects": false,
"type": "module",
"types": "./lib/index.d.ts",
"version": "6.0.0-beta.1"
}

View File

@ -0,0 +1 @@
export const version = "@ethersproject/bytes@6.0.0-beta.1";

View File

@ -0,0 +1,80 @@
import { isBytesLike } from "./check.js";
import { logger } from "./logger.js";
import type { BytesLike, Hexable } from "./types.js";
let BN_8: bigint | null = null;
let BN_255: bigint | null = null;
try {
BN_8 = BigInt("8");
BN_255 = BigInt("255");
} catch (error) {
console.log("Unsupported bigint", error);
}
function isHexable(value: any): value is Hexable {
return (value && typeof(value.toHexString) === "function");
}
export function arrayify(data: BytesLike | Hexable | number | bigint): Uint8Array {
if (data == null) {
logger.throwArgumentError("cannot arrayify nullish", "data", data);
}
if (typeof(data) === "number") {
logger.assertUint53(data);
const result = [];
while (data) {
result.unshift(data & 0xff);
data = parseInt(String(data / 256));
}
if (result.length === 0) { result.push(0); }
return new Uint8Array(result);
}
if (BN_8 && typeof(data) === "bigint") {
const result = [];
while (data) {
result.unshift(Number(data & <bigint>BN_255));
data >>= <bigint>BN_8;
}
if (result.length === 0) { result.push(0); }
return new Uint8Array(result);
}
if (isHexable(data)) {
return arrayify(data.toHexString());
}
if (typeof(data) === "string") {
let hex = data;
//if (options.allowMissingPrefix && hex.substring(0, 2) !== "0x") {
// hex = "0x" + hex;
//}
if (!hex.match(/^0x[0-9a-f]*$/i)) {
throw new RangeError(`invalid hex data string (${ JSON.stringify(data) })`);
}
const result = [];
for (let i = 2; i < hex.length; i += 2) {
result.push(parseInt(hex.substring(i, i + 2), 16));
}
return new Uint8Array(result);
}
if (data instanceof Uint8Array) { return new Uint8Array(data); }
throw new TypeError(`cannot arrayify ${ JSON.stringify(data) }`);
}
export function _noCopyArrayify(data: BytesLike | Hexable): Uint8Array {
if (!isBytesLike(data)) {
logger.throwArgumentError("invalid BytesLike value", "data", data);
} // @TODO: ArgumentE>
if (data instanceof Uint8Array) { return data; }
return arrayify(data);
}

View File

@ -0,0 +1,28 @@
declare global {
function atob(ascii: string): string;
function btoa(binary: string): string;
}
import type { BytesLike } from "@ethersproject/logger";
import { logger } from "./logger.js";
export function decodeBase64(textData: string): Uint8Array {
textData = atob(textData);
const data = [];
for (let i = 0; i < textData.length; i++) {
data.push(textData.charCodeAt(i));
}
return new Uint8Array(data);
}
export function encodeBase64(_data: BytesLike): string {
const data = logger.getBytes(_data, "data");
let textData = "";
for (let i = 0; i < data.length; i++) {
textData += String.fromCharCode(data[i]);
}
return btoa(textData);
}

View File

@ -0,0 +1,13 @@
import type { BytesLike } from "@ethersproject/logger";
import { logger } from "./logger.js";
export function decodeBase64(textData: string): Uint8Array {
return new Uint8Array(Buffer.from(textData, "base64"));
};
export function encodeBase64(data: BytesLike): string {
return Buffer.from(logger.getBytes(data, "data")).toString("base64");
}

View File

@ -0,0 +1,37 @@
import type { BytesLike } from "./types.js";
export function isHexString(value: any, length?: number | boolean): value is string {
if (typeof(value) !== "string" || !value.match(/^0x[0-9A-Fa-f]*$/)) {
return false
}
if (typeof(length) === "number" && value.length !== 2 + 2 * length) { return false; }
if (length === true && (value.length % 2) !== 0) { return false; }
return true;
}
/*
function _isByte(value: number): boolean {
return (typeof(value) === "number" && value >= 0 && value < 256 && Math.floor(value) === value);
}
export function isBytes(value: any): value is Bytes {
if (value == null) { return false; }
if (value instanceof Uint8Array) { return true; }
if (typeof(value) === "string") { return false; }
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i++) {
if (!_isByte(value[i])) { return false; }
}
return true;
}
return false;
}
*/
export function isBytesLike(value: any): value is BytesLike {
return (isHexString(value, true) || (value instanceof Uint8Array));
}

View File

@ -0,0 +1,30 @@
import { _noCopyArrayify } from "./array.js";
import { isHexString } from "./check.js";
import { hexlify } from "./hex.js";
import { logger } from "./logger.js";
import type { BytesLike } from "./types.js";
export function concat(datas: ReadonlyArray<BytesLike>): string {
return "0x" + datas.map((d) => hexlify(d).substring(2)).join("");
}
export function dataLength(data: BytesLike): number {
if (isHexString(data, true)) { return (data.length - 2) / 2; }
return _noCopyArrayify(data).length;
}
export function dataSlice(data: BytesLike, start?: number, end?: number): string {
const bytes = _noCopyArrayify(data);
if (end != null && end > bytes.length) { logger.throwError("cannot slice beyond data bounds", "BUFFER_OVERRUN", {
buffer: bytes, length: bytes.length, offset: end
}); }
return hexlify(bytes.slice((start == null) ? 0: start, (end == null) ? bytes.length: end));
}
export function stripZerosLeft(data: BytesLike): string {
let bytes = hexlify(data).substring(2);
while (bytes.substring(0, 2) == "00") { bytes = bytes.substring(2); }
return "0x" + bytes;
}

View File

@ -0,0 +1,24 @@
import { arrayify, _noCopyArrayify } from "./array.js";
import type { BytesLike, Hexable } from "./types.js";
const HexCharacters: string = "0123456789abcdef";
export function hexlify(data: BytesLike | Hexable): string {
const bytes = _noCopyArrayify(data);
let result = "0x";
for (let i = 0; i < bytes.length; i++) {
const v = bytes[i];
result += HexCharacters[(v & 0xf0) >> 4] + HexCharacters[v & 0x0f];
}
return result;
}
export function quantity(value: BytesLike | Hexable | number | bigint): string {
let result = hexlify(arrayify(value)).substring(2);
while (result.substring(0, 1) === "0") { result = result.substring(1); }
if (result === "") { result = "0"; }
return "0x" + result;
}

View File

@ -0,0 +1,9 @@
export { arrayify } from "./array.js";
export { decodeBase64, encodeBase64 } from "./base64.js"; /*-browser.js*/
export { isHexString, isBytesLike } from "./check.js";
export { concat, dataLength, dataSlice, stripZerosLeft } from "./data.js";
export { hexlify, quantity } from "./hex.js";
export { zeroPadLeft, zeroPadRight } from "./pad.js";
export type { BytesLike, Hexable } from "./types.js";

View File

@ -0,0 +1,3 @@
import { version } from "./_version.js";
import { Logger } from "@ethersproject/logger";
export const logger = new Logger(version);

View File

@ -0,0 +1,36 @@
import { _noCopyArrayify } from "./array.js";
import { hexlify } from "./hex.js";
import { logger } from "./logger.js";
import type { BytesLike } from "./types.js";
function zeroPad(data: BytesLike, length: number, left: boolean): string {
const bytes = _noCopyArrayify(data);
if (length < bytes.length) {
logger.throwError("padding exceeds data length", "BUFFER_OVERRUN", {
buffer: new Uint8Array(bytes),
length: length,
offset: length + 1
});
}
const result = new Uint8Array(length);
result.fill(0);
if (left) {
result.set(bytes, length - bytes.length);
} else {
result.set(bytes, 0);
}
return hexlify(result);
}
export function zeroPadLeft(data: BytesLike, length: number): string {
return zeroPad(data, length, true);
}
export function zeroPadRight(data: BytesLike, length: number): string {
return zeroPad(data, length, false);
}

View File

@ -0,0 +1,6 @@
export type BytesLike = Uint8Array | string;
export interface Hexable {
toHexString(): string;
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./lib/",
"rootDir": "./src.ts"
},
"exclude": [],
"extends": "../../tsconfig.common.json",
"include": [
"./src.ts/**/*.ts"
]
}

View File

@ -0,0 +1,3 @@
/src.ts/tests/**
/lib/tests/**
tsconfig.tsbuildinfo

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,39 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"dependencies": {
"@ethersproject/abi": "^6.0.0-beta.1",
"@ethersproject/address": "^6.0.0-beta.1",
"@ethersproject/bytes": "^6.0.0-beta.1",
"@ethersproject/logger": "^6.0.0-beta.1",
"@ethersproject/math": "^6.0.0-beta.1",
"@ethersproject/properties": "^6.0.0-beta.1",
"@ethersproject/providers": "^6.0.0-beta.1",
"@ethersproject/transactions": "^6.0.0-beta.1"
},
"description": "Contract meta-class for ethers.",
"engines" : { "node" : ">=12.17.0" },
"ethereum": "donations.ethers.eth",
"keywords": [
"Ethereum",
"ethers"
],
"license": "MIT",
"main": "./lib/index.js",
"name": "@ethersproject/contract",
"publishConfig": {
"access": "public",
"tag": "beta"
},
"repository": {
"directory": "packages/contract",
"type": "git",
"url": "git://github.com/ethers-io/ethers.js.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"sideEffects": false,
"type": "module",
"types": "./lib/index.d.ts",
"version": "6.0.0-beta.1"
}

View File

@ -0,0 +1 @@
export const version = "@ethersproject/contract@6.0.0-beta.1";

View File

@ -0,0 +1,724 @@
import { resolveAddress } from "@ethersproject/address";
import { Interface, Typed } from "@ethersproject/abi";
import { isHexString } from "@ethersproject/bytes";
import { isCallException } from "@ethersproject/logger";
import { defineProperties, resolveProperties } from "@ethersproject/properties";
import { copyRequest, Log, TransactionResponse } from "@ethersproject/providers";
import {
ContractEventPayload,
ContractTransactionResponse,
EventLog
} from "./wrappers.js";
import { logger } from "./logger.js";
import type { EventFragment, FunctionFragment, InterfaceAbi, ParamType, Result } from "@ethersproject/abi";
import type { Addressable } from "@ethersproject/address";
import type { EventEmitterable, Listener } from "@ethersproject/properties";
import type {
BlockTag, EventFilter, Filter, Provider, TransactionRequest, TopicFilter
} from "@ethersproject/providers";
import type {
ContractEventName,
ContractInterface,
ContractMethodArgs,
BaseContractMethod,
ContractMethod,
ContractEventArgs,
ContractEvent,
ContractTransaction,
DeferredTopicFilter,
ContractRunner
} from "./types.js";
interface ContractRunnerEstimater extends ContractRunner {
estimateGas: (tx: TransactionRequest) => Promise<bigint>;
}
interface ContractRunnerCaller extends ContractRunner {
call: (tx: TransactionRequest) => Promise<string>;
}
interface ContractRunnerSender extends ContractRunner {
sendTransaction: (tx: TransactionRequest) => Promise<TransactionResponse>;
}
interface ContractRunnerResolver extends ContractRunner {
resolveName: (name: string | Addressable) => Promise<null | string>;
}
interface ContractRunnerQuerier extends ContractRunner {
getLogs: (filter: Filter) => Promise<Array<Log>>;
}
interface ContractRunnerSubscriber extends ContractRunner {
on: (event: EventFilter, listener: Listener) => Promise<this>;
off: (event: EventFilter, listener: Listener) => Promise<this>;
}
function canEstimate(value: any): value is ContractRunnerEstimater {
return (value && typeof(value.estimateGas) === "function");
}
function canCall(value: any): value is ContractRunnerCaller {
return (value && typeof(value.call) === "function");
}
function canSend(value: any): value is ContractRunnerSender {
return (value && typeof(value.sendTransaction) === "function");
}
function canResolve(value: any): value is ContractRunnerResolver {
return (value && typeof(value.resolveName) === "function");
}
function canQuery(value: any): value is ContractRunnerQuerier {
return (value && typeof(value.getLogs) === "function");
}
function canSubscribe(value: any): value is ContractRunnerSubscriber {
return (value && typeof(value.on) === "function" && typeof(value.off) === "function");
}
function concisify(items: Array<string>): Array<string> {
items = Array.from((new Set(items)).values())
items.sort();
return items;
}
class PreparedTopicFilter implements DeferredTopicFilter {
#filter: Promise<TopicFilter>;
readonly fragment!: EventFragment;
constructor(contract: BaseContract, fragment: EventFragment, args: Array<any>) {
defineProperties<PreparedTopicFilter>(this, { fragment });
if (fragment.inputs.length < args.length) {
throw new Error("too many arguments");
}
// Recursively descend into args and resolve any addresses
const runner = getRunner(contract.runner, "resolveName");
const resolver = canResolve(runner) ? runner: null;
this.#filter = (async function() {
const resolvedArgs = await Promise.all(fragment.inputs.map((param, index) => {
return param.walkAsync(args[index], (type, value) => {
if (type === "address") { return resolveAddress(value, resolver); }
return value;
});
}));
return contract.interface.encodeFilterTopics(fragment, resolvedArgs);
})();
}
getTopicFilter(): Promise<TopicFilter> {
return this.#filter;
}
}
// A = Arguments passed in as a tuple
// R = The result type of the call (i.e. if only one return type,
// the qualified type, otherwise Result)
// D = The type the default call will return (i.e. R for view/pure,
// TransactionResponse otherwise)
//export interface ContractMethod<A extends Array<any> = Array<any>, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse> {
function _WrappedMethodBase(): new () => Function & BaseContractMethod {
return Function as any;
}
function getRunner<T extends ContractRunner>(value: any, feature: keyof ContractRunner): null | T {
if (value == null) { return null; }
if (typeof(value[feature]) === "function") { return value; }
if (value.provider && typeof(value.provider[feature]) === "function") {
return value.provider;
}
return null;
}
export async function copyOverrides(arg: any): Promise<Omit<ContractTransaction, "data" | "to">> {
// Create a shallow copy (we'll deep-ify anything needed during normalizing)
const overrides = copyRequest(Typed.dereference(arg, "overrides"));
// Some sanity checking; these are what these methods adds
//if ((<any>overrides).to) {
if (overrides.to) {
logger.throwArgumentError("cannot override to", "overrides.to", overrides.to);
} else if (overrides.data) {
logger.throwArgumentError("cannot override data", "overrides.data", overrides.data);
}
// Resolve any from
if (overrides.from) {
overrides.from = await resolveAddress(overrides.from);
}
return overrides;
}
export async function resolveArgs(_runner: null | ContractRunner, inputs: ReadonlyArray<ParamType>, args: Array<any>): Promise<Array<any>> {
// Recursively descend into args and resolve any addresses
const runner = getRunner(_runner, "resolveName");
const resolver = canResolve(runner) ? runner: null;
return await Promise.all(inputs.map((param, index) => {
return param.walkAsync(args[index], (type, value) => {
if (type === "address") { return resolveAddress(value, resolver); }
return value;
});
}));
}
class WrappedMethod<A extends Array<any> = Array<any>, R = any, D extends R | ContractTransactionResponse = ContractTransactionResponse>
extends _WrappedMethodBase() implements BaseContractMethod<A, R, D> {
readonly name!: string;
readonly _contract!: BaseContract;
readonly _key!: string;
constructor (contract: BaseContract, key: string) {
super();
defineProperties<WrappedMethod>(this, {
name: contract.interface.getFunctionName(key),
_contract: contract, _key: key
});
const proxy = new Proxy(this, {
// Perform the default operation for this fragment type
apply: async (target, thisArg, args: ContractMethodArgs<A>) => {
const fragment = target.getFragment(...args);
if (fragment.constant) { return await target.staticCall(...args); }
return await target.send(...args);
},
});
return proxy;
}
// Only works on non-ambiguous keys (refined fragment is always non-ambiguous)
get fragment(): FunctionFragment {
return this._contract.interface.getFunction(this._key);
}
getFragment(...args: ContractMethodArgs<A>): FunctionFragment {
return this._contract.interface.getFunction(this._key, args);
}
async populateTransaction(...args: ContractMethodArgs<A>): Promise<ContractTransaction> {
const fragment = this.getFragment(...args);
// If an overrides was passed in, copy it and normalize the values
let overrides: Omit<ContractTransaction, "data" | "to"> = { };
if (fragment.inputs.length + 1 === args.length) {
overrides = await copyOverrides(args.pop());
}
if (fragment.inputs.length !== args.length) {
throw new Error("internal error: fragment inputs doesn't match arguments; should not happen");
}
const resolvedArgs = await resolveArgs(this._contract.runner, fragment.inputs, args);
return Object.assign({ }, overrides, await resolveProperties({
to: this._contract.getAddress(),
data: this._contract.interface.encodeFunctionData(fragment, resolvedArgs)
}));
}
async staticCall(...args: ContractMethodArgs<A>): Promise<R> {
const result = await this.staticCallResult(...args);
if (result.length === 1) { return result[0]; }
return <R><unknown>result;
}
async send(...args: ContractMethodArgs<A>): Promise<ContractTransactionResponse> {
const runner = getRunner(this._contract.runner, "sendTransaction");
if (!canSend(runner)) {
return logger.throwError("contract runner does not support sending transactions", "UNSUPPORTED_OPERATION", {
operation: "sendTransaction"
});
}
const tx = await runner.sendTransaction(await this.populateTransaction(...args));
const provider = getRunner(this._contract.runner, "getLogs");
return new ContractTransactionResponse(this._contract.interface, <Provider>provider, tx);
}
async estimateGas(...args: ContractMethodArgs<A>): Promise<bigint> {
const runner = getRunner(this._contract.runner, "estimateGas");
if (!canEstimate(runner)) {
return logger.throwError("contract runner does not support gas estimation", "UNSUPPORTED_OPERATION", {
operation: "estimateGas"
});
}
return await runner.estimateGas(await this.populateTransaction(...args));
}
async staticCallResult(...args: ContractMethodArgs<A>): Promise<Result> {
const runner = getRunner(this._contract.runner, "call");
if (!canCall(runner)) {
return logger.throwError("contract runner does not support calling", "UNSUPPORTED_OPERATION", {
operation: "call"
});
}
const fragment = this.getFragment(...args);
const tx = await this.populateTransaction(...args);
let result = "0x";
try {
result = await runner.call(tx);
} catch (error: any) {
if (isCallException(error)) {
throw this._contract.interface.makeError(fragment, error.data, tx);
}
throw error;
}
return this._contract.interface.decodeFunctionResult(fragment, result);
}
}
function _WrappedEventBase(): new () => Function & ContractEvent {
return Function as any;
}
class WrappedEvent<A extends Array<any> = Array<any>> extends _WrappedEventBase() implements ContractEvent<A> {
readonly name!: string;
readonly _contract!: BaseContract;
readonly _key!: string;
constructor (contract: BaseContract, key: string) {
super();
defineProperties<WrappedEvent>(this, {
name: contract.interface.getEventName(key),
_contract: contract, _key: key
});
return new Proxy(this, {
// Perform the default operation for this fragment type
apply: async (target, thisArg, args: ContractEventArgs<A>) => {
return new PreparedTopicFilter(contract, target.getFragment(...args), args);
},
});
}
// Only works on non-ambiguous keys
get fragment(): EventFragment {
return this._contract.interface.getEvent(this._key);
}
getFragment(...args: ContractEventArgs<A>): EventFragment {
return this._contract.interface.getEvent(this._key, args);
}
};
type Sub = {
tag: string;
listeners: Array<{ listener: Listener, once: boolean }>,
start: () => void;
stop: () => void;
};
// The combination of TypeScrype, Private Fields and Proxies makes
// the world go boom; so we hide variables with some trickery keeping
// a symbol attached to each BaseContract which its sub-class (even
// via a Proxy) can reach and use to look up its internal values.
const internal = Symbol.for("_ethersInternal_contract");
type Internal = {
addrPromise: Promise<string>;
addr: null | string;
deployTx: null | ContractTransactionResponse;
subs: Map<string, Sub>;
};
const internalValues: WeakMap<BaseContract, Internal> = new WeakMap();
function setInternal(contract: BaseContract, values: Internal): void {
internalValues.set(contract[internal], values);
}
function getInternal(contract: BaseContract): Internal {
return internalValues.get(contract[internal]) as Internal;
}
function isDeferred(value: any): value is DeferredTopicFilter {
return (value && typeof(value) === "object" && ("getTopicFilter" in value) &&
(typeof(value.getTopicFilter) === "function") && value.fragment);
}
async function getSubTag(contract: BaseContract, event: ContractEventName): Promise<{ tag: string, fragment: EventFragment, topics: TopicFilter }> {
let fragment: EventFragment;
let topics: Array<null | string | Array<string>>;
if (Array.isArray(event)) {
// Topics; e.g. `[ "0x1234...89ab" ]`
fragment = contract.interface.getEvent(event[0] as string);
topics = event;
} else if (typeof(event) === "string") {
// Event name (name or signature); `"Transfer"`
fragment = contract.interface.getEvent(event);
topics = [ contract.interface.getEventTopic(fragment) ];
} else if (isDeferred(event)) {
// Deferred Topic Filter; e.g. `contract.filter.Transfer(from)`
fragment = event.fragment;
topics = await event.getTopicFilter();
} else if ("fragment" in event) {
// ContractEvent; e.g. `contract.filter.Transfer`
fragment = event.fragment;
topics = [ contract.interface.getEventTopic(fragment) ];
} else {
console.log(event);
throw new Error("TODO");
}
// Normalize topics and sort TopicSets
topics = topics.map((t) => {
if (t == null) { return null; }
if (Array.isArray(t)) {
return concisify(t.map((t) => t.toLowerCase()));
}
return t.toLowerCase();
});
const tag = topics.map((t) => {
if (t == null) { return "null"; }
if (Array.isArray(t)) { return t.join("|"); }
return t;
}).join("&");
return { fragment, tag, topics }
}
async function hasSub(contract: BaseContract, event: ContractEventName): Promise<null | Sub> {
const { subs } = getInternal(contract);
return subs.get((await getSubTag(contract, event)).tag) || null;
}
async function getSub(contract: BaseContract, event: ContractEventName): Promise<Sub> {
// Make sure our runner can actually subscribe to events
const runner = getRunner(contract.runner, "on");
const runnerOff = getRunner(contract.runner, "off");
if (!runner || runner !== runnerOff || !canSubscribe(runner)) {
return logger.throwError("contract runner does not support subscribing", "UNSUPPORTED_OPERATION", {
operation: "on"
});
}
const { fragment, tag, topics } = await getSubTag(contract, event);
const { addr, subs } = getInternal(contract);
let sub = subs.get(tag);
if (!sub) {
const address: string | Addressable = (addr ? addr: contract);
const filter = { address, topics };
const listener = (log: Log) => {
const payload = new ContractEventPayload(contract, null, event, fragment, log);
emit(contract, event, payload.args, payload);
};
let started = false;
const start = () => {
if (started) { return; }
(runner as ContractRunnerSubscriber).on(filter, listener);
started = true;
};
const stop = () => {
if (!started) { return; }
(runner as ContractRunnerSubscriber).off(filter, listener);
started = false;
};
sub = { tag, listeners: [ ], start, stop };
subs.set(tag, sub);
}
return sub;
}
async function _emit(contract: BaseContract, event: ContractEventName, args: Array<any>, payload: null | ContractEventPayload): Promise<boolean> {
await lastEmit;
const sub = await hasSub(contract, event);
if (!sub) { return false; }
const count = sub.listeners.length;
sub.listeners = sub.listeners.filter(({ listener, once }) => {
const passArgs = args.slice();
if (payload) {
passArgs.push(new ContractEventPayload(contract, (once ? null: listener),
event, payload.fragment, payload.log));
}
setTimeout(() => { listener.call(contract, ...passArgs); }, 0);
return !once;
});
return (count > 0);
}
// We use this to ensure one emit resolves before firing the next to
// ensure correct ordering (note this cannot throw and just adds the
// notice to the event queu using setTimeout).
let lastEmit: Promise<any> = Promise.resolve();
async function emit(contract: BaseContract, event: ContractEventName, args: Array<any>, payload: null | ContractEventPayload): Promise<boolean> {
try {
await lastEmit;
} catch (error) { }
const resultPromise = _emit(contract, event, args, payload);
lastEmit = resultPromise;
return await resultPromise;
}
const passProperties = [ "then" ];
export class BaseContract implements Addressable, EventEmitterable<ContractEventName> {
readonly target!: string | Addressable;
readonly interface!: Interface;
readonly runner!: null | ContractRunner;
readonly filters!: Record<string, ContractEvent>;
readonly [internal]: any;
constructor(target: string | Addressable, abi: Interface | InterfaceAbi, runner: null | ContractRunner = null, _deployTx?: null | TransactionResponse) {
const iface = Interface.from(abi);
defineProperties<BaseContract>(this, { target, runner, interface: iface });
Object.defineProperty(this, internal, { value: { } });
let addrPromise;
let addr = null;
let deployTx: null | ContractTransactionResponse = null;
if (_deployTx) {
const provider = getRunner(runner, "getLogs");
deployTx = new ContractTransactionResponse(this.interface, <Provider>provider, _deployTx);
}
let subs = new Map();
// Resolve the target as the address
if (typeof(target) === "string") {
if (isHexString(target)) {
addr = target;
addrPromise = Promise.resolve(target);
} else {
const resolver = getRunner(runner, "resolveName");
if (!canResolve(resolver)) {
throw logger.makeError("contract runner does not support name resolution", "UNSUPPORTED_OPERATION", {
operation: "resolveName"
});
}
addrPromise = resolver.resolveName(target).then((addr) => {
if (addr == null) { throw new Error("TODO"); }
getInternal(this).addr = addr;
return addr;
});
}
} else {
addrPromise = target.getAddress().then((addr) => {
if (addr == null) { throw new Error("TODO"); }
getInternal(this).addr = addr;
return addr;
});
}
// Set our private values
setInternal(this, { addrPromise, addr, deployTx, subs });
// Add the event filters
const filters = new Proxy({ }, {
get: (target, _prop, receiver) => {
// Pass important checks (like `then` for Promise) through
if (passProperties.indexOf(<string>_prop) >= 0) {
return Reflect.get(target, _prop, receiver);
}
const prop = String(_prop);
const result = this.getEvent(prop);
if (result) { return result; }
throw new Error(`unknown contract event: ${ prop }`);
}
});
defineProperties<BaseContract>(this, { filters });
// Return a Proxy that will respond to functions
return new Proxy(this, {
get: (target, _prop, receiver) => {
if (_prop in target || passProperties.indexOf(<string>_prop) >= 0) {
return Reflect.get(target, _prop, receiver);
}
const prop = String(_prop);
const result = target.getFunction(prop);
if (result) { return result; }
throw new Error(`unknown contract method: ${ prop }`);
}
});
}
async getAddress(): Promise<string> { return await getInternal(this).addrPromise; }
deploymentTransaction(): null | ContractTransactionResponse {
return getInternal(this).deployTx;
}
getFunction<T extends ContractMethod = ContractMethod>(key: string | FunctionFragment): T {
if (typeof(key) !== "string") { key = key.format(); }
return <T><unknown>(new WrappedMethod(this, key));
}
getEvent(key: string | EventFragment): ContractEvent {
if (typeof(key) !== "string") { key = key.format(); }
return <ContractEvent><unknown>(new WrappedEvent(this, key));
}
async queryFilter(event: ContractEventName, fromBlock: BlockTag = 0, toBlock: BlockTag = "latest"): Promise<Array<EventLog>> {
const { addr, addrPromise } = getInternal(this);
const address = (addr ? addr: (await addrPromise));
const { fragment, topics } = await getSubTag(this, event);
const filter = { address, topics, fromBlock, toBlock };
const runner = getRunner(this.runner, "getLogs");
if (!canQuery(runner)) {
return logger.throwError("contract runner does not support querying", "UNSUPPORTED_OPERATION", {
operation: "getLogs"
});
}
return (await runner.getLogs(filter)).map((log) => {
return new EventLog(log, this.interface, fragment);
});
}
async on(event: ContractEventName, listener: Listener): Promise<this> {
const sub = await getSub(this, event);
sub.listeners.push({ listener, once: false });
sub.start();
return this;
}
async once(event: ContractEventName, listener: Listener): Promise<this> {
const sub = await getSub(this, event);
sub.listeners.push({ listener, once: true });
sub.start();
return this;
}
async emit(event: ContractEventName, ...args: Array<any>): Promise<boolean> {
return await emit(this, event, args, null);
}
async listenerCount(event?: ContractEventName): Promise<number> {
if (event) {
const sub = await hasSub(this, event);
if (!sub) { return 0; }
return sub.listeners.length;
}
const { subs } = getInternal(this);
let total = 0;
for (const { listeners } of subs.values()) {
total += listeners.length;
}
return total;
}
async listeners(event?: ContractEventName): Promise<Array<Listener>> {
if (event) {
const sub = await hasSub(this, event);
if (!sub) { return [ ]; }
return sub.listeners.map(({ listener }) => listener);
}
const { subs } = getInternal(this);
let result: Array<Listener> = [ ];
for (const { listeners } of subs.values()) {
result = result.concat(listeners.map(({ listener }) => listener));
}
return result;
}
async off(event: ContractEventName, listener?: Listener): Promise<this> {
const sub = await hasSub(this, event);
if (!sub) { return this; }
if (listener) {
const index = sub.listeners.map(({ listener }) => listener).indexOf(listener);
if (index >= 0) { sub.listeners.splice(index, 1); }
}
if (listener == null || sub.listeners.length === 0) {
sub.stop();
getInternal(this).subs.delete(sub.tag);
}
return this;
}
async removeAllListeners(event?: ContractEventName): Promise<this> {
if (event) {
const sub = await hasSub(this, event);
if (!sub) { return this; }
sub.stop();
getInternal(this).subs.delete(sub.tag);
} else {
const { subs } = getInternal(this);
for (const { tag, stop } of subs.values()) {
stop();
subs.delete(tag);
}
}
return this;
}
// Alias for "on"
async addListener(event: ContractEventName, listener: Listener): Promise<this> {
return await this.on(event, listener);
}
// Alias for "off"
async removeListener(event: ContractEventName, listener: Listener): Promise<this> {
return await this.off(event, listener);
}
static buildClass<T = ContractInterface>(abi: InterfaceAbi): new (target: string, runner?: null | ContractRunner) => BaseContract & Omit<T, keyof BaseContract> {
class CustomContract extends BaseContract {
constructor(address: string, runner: null | ContractRunner = null) {
super(address, abi, runner);
}
}
return CustomContract as any;
};
static from<T = ContractInterface>(target: string, abi: InterfaceAbi, runner: null | ContractRunner = null): BaseContract & Omit<T, keyof BaseContract> {
const contract = new this(target, abi, runner);
return contract as any;
}
}
function _ContractBase(): new (target: string, abi: InterfaceAbi, runner?: null | ContractRunner) => BaseContract & Omit<ContractInterface, keyof BaseContract> {
return BaseContract as any;
}
export class Contract extends _ContractBase() { }

View File

@ -0,0 +1,98 @@
import { Interface } from "@ethersproject/abi";
import { getCreateAddress } from "@ethersproject/address";
import { concat, hexlify } from "@ethersproject/bytes";
import { defineProperties } from "@ethersproject/properties";
import { BaseContract, copyOverrides, resolveArgs } from "./contract.js";
import { logger } from "./logger.js";
import type { InterfaceAbi } from "@ethersproject/abi";
import type { BytesLike } from "@ethersproject/logger";
import type {
ContractInterface, ContractMethodArgs, ContractRunner, ContractDeployTransaction,
} from "./types.js";
import type { ContractTransactionResponse } from "./wrappers.js";
// A = Arguments to the constructor
// I = Interface of deployed contracts
export class ContractFactory<A extends Array<any> = Array<any>, I = BaseContract> {
readonly interface!: Interface;
readonly bytecode!: string;
readonly runner!: null | ContractRunner;
constructor(abi: Interface | InterfaceAbi, bytecode: BytesLike | { object: string }, runner?: ContractRunner) {
const iface = Interface.from(abi);
// Dereference Solidity bytecode objects and allow a missing `0x`-prefix
if (bytecode instanceof Uint8Array) {
bytecode = hexlify(logger.getBytes(bytecode));
} else {
if (typeof(bytecode) === "object") { bytecode = bytecode.object; }
if (bytecode.substring(0, 2) !== "0x") { bytecode = "0x" + bytecode; }
bytecode = hexlify(logger.getBytes(bytecode));
}
defineProperties<ContractFactory>(this, {
bytecode, interface: iface, runner: (runner || null)
});
}
async getDeployTransaction(...args: ContractMethodArgs<A>): Promise<ContractDeployTransaction> {
let overrides: Omit<ContractDeployTransaction, "data"> = { };
const fragment = this.interface.deploy;
if (fragment.inputs.length + 1 === args.length) {
overrides = await copyOverrides(args.pop());
}
if (fragment.inputs.length !== args.length) {
throw new Error("incorrect number of arguments to constructor");
}
const resolvedArgs = await resolveArgs(this.runner, fragment.inputs, args);
const data = concat([ this.bytecode, this.interface.encodeDeploy(resolvedArgs) ]);
return Object.assign({ }, overrides, { data });
}
async deploy(...args: ContractMethodArgs<A>): Promise<BaseContract & { deploymentTransaction(): ContractTransactionResponse } & Omit<I, keyof BaseContract>> {
const tx = await this.getDeployTransaction(...args);
if (!this.runner || typeof(this.runner.sendTransaction) !== "function") {
return logger.throwError("factory runner does not support sending transactions", "UNSUPPORTED_OPERATION", {
operation: "sendTransaction"
});
}
const sentTx = await this.runner.sendTransaction(tx);
const address = getCreateAddress(sentTx);
return new (<any>BaseContract)(address, this.interface, this.runner, sentTx);
}
connect(runner: ContractRunner): ContractFactory<A, I> {
return new ContractFactory(this.interface, this.bytecode, runner);
}
static fromSolidity<A extends Array<any> = Array<any>, I = ContractInterface>(output: any, runner?: ContractRunner): ContractFactory<A, I> {
if (output == null) {
logger.throwArgumentError("bad compiler output", "output", output);
}
if (typeof(output) === "string") { output = JSON.parse(output); }
const abi = output.abi;
let bytecode = "";
if (output.bytecode) {
bytecode = output.bytecode;
} else if (output.evm && output.evm.bytecode) {
bytecode = output.evm.bytecode;
}
return new this(abi, bytecode, runner);
}
}

View File

@ -0,0 +1,19 @@
export {
BaseContract, Contract
} from "./contract.js";
export {
ContractFactory
} from "./factory.js";
export {
ContractEventPayload, ContractTransactionReceipt, ContractTransactionResponse,
EventLog
} from "./wrappers.js";
export type {
ConstantContractMethod, ContractEvent, ContractEventArgs, ContractEventName,
ContractInterface, ContractMethod, ContractMethodArgs, ContractRunner,
ContractTransaction, DeferredTopicFilter, Overrides
} from "./types.js";

View File

@ -0,0 +1,3 @@
import { Logger } from "@ethersproject/logger";
import { version } from "./_version.js";
export const logger = new Logger(version);

View File

@ -0,0 +1,94 @@
import { Typed } from "@ethersproject/abi";
import * as providers from "@ethersproject/providers";
import { Contract } from "../index.js";
//import type { Addressable } from "@ethersproject/address";
//import type { BigNumberish } from "@ethersproject/logger";
/*
import type {
ConstantContractMethod, ContractMethod, ContractEvent
} from "../index.js";
*/
// @TODO
describe("Test Contract Calls", function() {
it("finds typed methods", async function() {
const contract = new Contract("0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", [
"function foo(string s) view returns (uint)",
"function foo(uint8) view returns (uint)",
"function foo(uint u, bool b) view returns (uint)",
]);
const value = Typed.string("42");
await contract.foo.populateTransaction(value, Typed.overrides({ value: 100 }))
contract["foo(string)"].fragment
});
});
/*
describe("Test Contract Interface", function() {
it("builds contract interfaces", async function() {
this.timeout(60000);
interface Erc20Interface {
// Constant Methods
balanceOf: ConstantContractMethod<[ address: string | Addressable ], bigint>;
decimals: ConstantContractMethod<[ ], bigint>;
name: ConstantContractMethod<[ ], string>;
symbol: ConstantContractMethod<[ ], string>;
// Mutation Methods
transferFrom: ContractMethod<[ address: string | Addressable,
address: string | Addressable, amount: BigNumberish ], boolean>;
// Events
filters: {
Transfer: ContractEvent<[ from: Addressable | string, to: BigNumberish ]>;
}
}
const erc20Abi = [
"function balanceOf(address owner) view returns (uint)",
"function decimals() view returns (uint)",
"function name() view returns (string)",
"function symbol() view returns (string)",
"function transferFrom(address from, address to, uint amount) returns (boolean)",
"event Transfer(address indexed from, address indexed to, uint amount)"
];
class Erc20Contract extends BaseContract.buildClass<Erc20Interface>(erc20Abi) { };
const provider = new providers.InfuraProvider();
// ENS
//const addr = "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72";
// DAI
const addr = "0x6B175474E89094C44Da98b954EedeAC495271d0F";
const contract = new Erc20Contract(addr, provider);
console.log("SYMBOL", await contract.symbol());
console.log("DECIMALS", await contract.decimals());
console.log(await contract.balanceOf("0x5555763613a12D8F3e73be831DFf8598089d3dCa"));
console.log(await contract.balanceOf("ricmoo.eth"));
await contract.on(contract.filters.Transfer, (from, to, value, event) => {
console.log("HELLO!", { from, to, value, event });
event.removeListener();
});
const logs = await contract.queryFilter("Transfer", -10);
console.log(logs, logs[0], logs[0].args.from);
});
});
*/
describe("Test Contract Calls", function() {
it("calls ERC-20 methods", async function() {
const provider = new providers.AnkrProvider();
const contract = new Contract("0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", [
"function balanceOf(address owner) view returns (uint)",
], provider);
console.log("balance:", await contract.balanceOf("0x5555763613a12D8F3e73be831DFf8598089d3dCa"));
});
});

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,25 @@
import fs from "fs"
import path from "path";
import zlib from 'zlib';
// Find the package root (based on the nyc output/ folder)
const root = (function() {
let root = process.cwd();
while (true) {
if (fs.existsSync(path.join(root, "output"))) { return root; }
const parent = path.join(root, "..");
if (parent === root) { break; }
root = parent;
}
throw new Error("could not find root");
})();
// Load the tests
export function loadTests<T>(tag: string): Array<T> {
const filename = path.resolve(root, "testcases", tag + '.json.gz');
return JSON.parse(zlib.gunzipSync(fs.readFileSync(filename)).toString());
}

View File

@ -0,0 +1,111 @@
import type {
EventFragment, FunctionFragment, Result, Typed
} from "@ethersproject/abi";
import type { Addressable } from "@ethersproject/address";
import type {
CallRequest, EventFilter, Filter, Log, PreparedRequest, Provider,
TopicFilter, TransactionRequest, TransactionResponse
} from "@ethersproject/providers";
import type { Listener } from "@ethersproject/properties";
import type { ContractTransactionResponse } from "./wrappers.js";
// The types of events a Contract can listen for
export type ContractEventName = string | ContractEvent | TopicFilter;
// The object that will be used to run Contracts. The Signer and Provider
// both adhere to this, but other types of objects may wish to as well.
export interface ContractRunner {
provider?: Provider;
// Required to estimate gas
estimateGas?: (tx: TransactionRequest) => Promise<bigint>;
// Required for pure, view or static calls to contracts
call?: (tx: TransactionRequest) => Promise<string>;
// Required to support ENS names
resolveName?: (name: string | Addressable) => Promise<null | string>;
// Required for mutating calls
sendTransaction?: (tx: TransactionRequest) => Promise<TransactionResponse>;
// Required for queryFilter
getLogs?: (filter: Filter) => Promise<Array<Log>>;
// Both are required for a contract to support events
on?: (event: EventFilter, listener: Listener) => Promise<this>;
off?: (event: EventFilter, listener: Listener) => Promise<this>;
}
export interface ContractInterface {
[ name: string ]: BaseContractMethod;
};
export interface DeferredTopicFilter {
getTopicFilter(): Promise<TopicFilter>;
fragment: EventFragment;
}
export interface ContractTransaction extends PreparedRequest {
// These are populated by contract methods and cannot bu null
to: string;
data: string;
}
// Deployment Transactions have no `to`
export interface ContractDeployTransaction extends Omit<ContractTransaction, "to"> { }
// Overrides; cannot override `to` or `data` as Contract populates these
export interface Overrides extends Omit<CallRequest, "to" | "data"> { };
// Arguments for methods; with an optional (n+1)th Override
export type PostfixOverrides<A extends Array<any>> = A | [ ...A, Overrides ];
export type ContractMethodArgs<A extends Array<any>> = PostfixOverrides<{ [ I in keyof A ]-?: A[I] | Typed }>;
// A = Arguments passed in as a tuple
// R = The result type of the call (i.e. if only one return type,
// the qualified type, otherwise Result)
// D = The type the default call will return (i.e. R for view/pure,
// TransactionResponse otherwise)
export interface BaseContractMethod<A extends Array<any> = Array<any>, R = any, D extends R | ContractTransactionResponse = R | ContractTransactionResponse> {
(...args: ContractMethodArgs<A>): Promise<D>;
name: string;
fragment: FunctionFragment;
getFragment(...args: ContractMethodArgs<A>): FunctionFragment;
populateTransaction(...args: ContractMethodArgs<A>): Promise<ContractTransaction>;
staticCall(...args: ContractMethodArgs<A>): Promise<R>;
send(...args: ContractMethodArgs<A>): Promise<ContractTransactionResponse>;
estimateGas(...args: ContractMethodArgs<A>): Promise<bigint>;
staticCallResult(...args: ContractMethodArgs<A>): Promise<Result>;
}
export interface ContractMethod<
A extends Array<any> = Array<any>,
R = any,
D extends R | ContractTransactionResponse = R | ContractTransactionResponse
> extends BaseContractMethod<A, R, D> { }
export interface ConstantContractMethod<
A extends Array<any>,
R = any
> extends ContractMethod<A, R, R> { }
// Arguments for events; with each element optional and/or nullable
export type ContractEventArgs<A extends Array<any>> = { [ I in keyof A ]?: A[I] | Typed | null };
export interface ContractEvent<A extends Array<any> = Array<any>> {
(...args: ContractEventArgs<A>): DeferredTopicFilter;
name: string;
fragment: EventFragment;
getFragment(...args: ContractEventArgs<A>): EventFragment;
};

View File

@ -0,0 +1,99 @@
import { defineProperties, EventPayload } from "@ethersproject/properties";
import {
Block, Log, TransactionReceipt, TransactionResponse
} from "@ethersproject/providers";
import type { EventFragment, Interface, Result } from "@ethersproject/abi";
import type { Listener } from "@ethersproject/properties";
import type {
Provider
} from "@ethersproject/providers";
import type { BaseContract } from "./contract.js";
import type { ContractEventName } from "./types.js";
export class EventLog extends Log {
readonly interface!: Interface;
readonly fragment!: EventFragment;
readonly args!: Result;
constructor(log: Log, iface: Interface, fragment: EventFragment) {
super(log, log.provider);
const args = iface.decodeEventLog(fragment, log.data, log.topics);
defineProperties<EventLog>(this, { args, fragment, interface: iface });
}
get eventName(): string { return this.fragment.name; }
get eventSignature(): string { return this.fragment.format(); }
}
export class ContractTransactionReceipt extends TransactionReceipt {
readonly #interface: Interface;
constructor(iface: Interface, provider: null | Provider, tx: TransactionReceipt) {
super(tx, provider);
this.#interface = iface;
}
get logs(): Array<EventLog | Log> {
return super.logs.map((log) => {
const fragment = log.topics.length ? this.#interface.getEvent(log.topics[0]): null;
if (fragment) {
return new EventLog(log, this.#interface, fragment)
} else {
return log;
}
});
}
}
export class ContractTransactionResponse extends TransactionResponse {
readonly #interface: Interface;
constructor(iface: Interface, provider: null | Provider, tx: TransactionResponse) {
super(tx, provider);
this.#interface = iface;
}
async wait(confirms?: number): Promise<null | ContractTransactionReceipt> {
const receipt = await super.wait();
if (receipt == null) { return null; }
return new ContractTransactionReceipt(this.#interface, this.provider, receipt);
}
}
export class ContractEventPayload extends EventPayload<ContractEventName> {
readonly fragment!: EventFragment;
readonly log!: EventLog;
readonly args!: Result;
constructor(contract: BaseContract, listener: null | Listener, filter: ContractEventName, fragment: EventFragment, _log: Log) {
super(contract, listener, filter);
const log = new EventLog(_log, contract.interface, fragment);
const args = contract.interface.decodeEventLog(fragment, log.data, log.topics);
defineProperties<ContractEventPayload>(this, { args, fragment, log });
}
get eventName(): string {
return this.fragment.name;
}
get eventSignature(): string {
return this.fragment.format();
}
async getBlock(): Promise<Block<string>> {
return await this.log.getBlock();
}
async getTransaction(): Promise<TransactionResponse> {
return await this.log.getTransaction();
}
async getTransactionReceipt(): Promise<TransactionReceipt> {
return await this.log.getTransactionReceipt();
}
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./lib/",
"rootDir": "./src.ts"
},
"exclude": [],
"extends": "../../tsconfig.common.json",
"include": [
"./src.ts/**/*.ts"
]
}

View File

@ -0,0 +1,3 @@
/src.ts/tests/**
/lib/tests/**
tsconfig.tsbuildinfo

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,34 @@
{
"author": "Richard Moore <me@ricmoo.com>",
"dependencies": {
"@ethersproject/bytes": "^6.0.0-beta.1",
"@ethersproject/logger": "^6.0.0-beta.1",
"@noble/hashes": "1.0.0"
},
"description": "Basic crypto algorithms used for Ethereum.",
"engines" : { "node" : ">=12.17.0" },
"ethereum": "donations.ethers.eth",
"keywords": [
"Ethereum",
"ethers"
],
"license": "MIT",
"main": "./lib/index.js",
"name": "@ethersproject/crypto",
"publishConfig": {
"access": "public",
"tag": "beta"
},
"repository": {
"directory": "packages/crypto",
"type": "git",
"url": "git://github.com/ethers-io/ethers.js.git"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"sideEffects": false,
"type": "module",
"types": "./lib/index.d.ts",
"version": "6.0.0-beta.1"
}

View File

@ -0,0 +1 @@
export const version = "@ethersproject/crypto@6.0.0-beta.1";

View File

@ -0,0 +1,70 @@
/* Browser Crypto Shims */
declare global {
interface Window { }
const window: Window;
const self: Window;
}
import { hmac } from "@noble/hashes/hmac";
import { pbkdf2 } from "@noble/hashes/pbkdf2";
import { sha256 } from "@noble/hashes/sha256";
import { sha512 } from "@noble/hashes/sha512";
import { logger } from "./logger.js";
function getGlobal(): any {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
const anyGlobal = getGlobal();
let crypto: any = anyGlobal.crypto || anyGlobal.msCrypto;
export interface CryptoHasher {
update(data: Uint8Array): CryptoHasher;
digest(): Uint8Array;
}
export function createHash(algo: string): CryptoHasher {
switch (algo) {
case "sha256": return sha256.create();
case "sha512": return sha512.create();
}
return logger.throwArgumentError("invalid hashing algorithm name", "algorithm", algo);
}
export function createHmac(_algo: string, key: Uint8Array): CryptoHasher {
const algo = ({ sha256, sha512 }[_algo]);
if (algo == null) {
return logger.throwArgumentError("invalid hmac algorithm", "algorithm", _algo);
}
return hmac.create(algo, key);
}
export function pbkdf2Sync(password: Uint8Array, salt: Uint8Array, iterations: number, keylen: number, _algo: "sha256" | "sha512"): Uint8Array {
const algo = ({ sha256, sha512 }[_algo]);
if (algo == null) {
return logger.throwArgumentError("invalid pbkdf2 algorithm", "algorithm", _algo);
}
return pbkdf2(algo, password, salt, { c: iterations, dkLen: keylen });
}
export function randomBytes(length: number): Uint8Array {
if (crypto == null) {
return logger.throwError("platform does not support secure random numbers", "UNSUPPORTED_OPERATION", {
operation: "randomBytes"
});
}
if (length <= 0 || length > 1024 || (length % 1) || length != length) {
logger.throwArgumentError("invalid length", "length", length);
}
const result = new Uint8Array(length);
crypto.getRandomValues(result);
return result;
}

View File

@ -0,0 +1,29 @@
import { createHmac } from "crypto"; /*-browser*/
import { hexlify } from "@ethersproject/bytes";
import { logger } from "./logger.js";
import type { BytesLike } from "@ethersproject/logger";
let locked = false;
const _computeHmac = function(algorithm: "sha256" | "sha512", key: Uint8Array, data: Uint8Array): BytesLike {
return "0x" + createHmac(algorithm, key).update(data).digest("hex");
}
let __computeHmac = _computeHmac;
export function computeHmac(algorithm: "sha256" | "sha512", _key: BytesLike, _data: BytesLike): string {
const key = logger.getBytes(_key, "key");
const data = logger.getBytes(_data, "data");
return hexlify(__computeHmac(algorithm, key, data));
}
computeHmac._ = _computeHmac;
computeHmac.lock = function() { locked = true; }
computeHmac.register = function(func: (algorithm: "sha256" | "sha512", key: Uint8Array, data: Uint8Array) => BytesLike) {
if (locked) { throw new Error("computeHmac is locked"); }
__computeHmac = func;
}
Object.freeze(computeHmac);

View File

@ -0,0 +1,37 @@
// We import all these so we can export lock()
import { computeHmac } from "./hmac.js";
import { keccak256 } from "./keccak.js";
import { ripemd160 } from "./ripemd160.js";
import { pbkdf2 } from "./pbkdf2.js";
import { randomBytes } from "./random.js";
import { scrypt, scryptSync } from "./scrypt.js";
import { sha256, sha512 } from "./sha2.js";
export {
computeHmac,
randomBytes,
keccak256,
ripemd160,
sha256, sha512,
pbkdf2,
scrypt, scryptSync
};
export function lock(): void {
computeHmac.lock();
keccak256.lock();
pbkdf2.lock();
randomBytes.lock();
ripemd160.lock();
scrypt.lock();
scryptSync.lock();
sha256.lock();
sha512.lock();
}
export type { ProgressCallback } from "./scrypt.js";

View File

@ -0,0 +1,26 @@
import { keccak_256 } from "@noble/hashes/sha3";
import { hexlify } from "@ethersproject/bytes";
import { logger } from "./logger.js";
import type { BytesLike } from "@ethersproject/logger";
let locked = false;
const _keccak256 = function(data: Uint8Array): Uint8Array {
return keccak_256(data);
}
let __keccak256: (data: Uint8Array) => BytesLike = _keccak256;
export function keccak256(_data: BytesLike): string {
const data = logger.getBytes(_data, "data");
return hexlify(__keccak256(data));
}
keccak256._ = _keccak256;
keccak256.lock = function(): void { locked = true; }
keccak256.register = function(func: (data: Uint8Array) => BytesLike) {
if (locked) { throw new TypeError("keccak256 is locked"); }
__keccak256 = func;
}
Object.freeze(keccak256);

View File

@ -0,0 +1,3 @@
import { Logger } from "@ethersproject/logger";
import { version } from "./_version.js";
export const logger = new Logger(version);

View File

@ -0,0 +1,28 @@
import { pbkdf2Sync } from "crypto"; /*-browser*/
import { hexlify } from "@ethersproject/bytes";
import { logger } from "./logger.js";
import type { BytesLike } from "@ethersproject/logger";
let locked = false;
const _pbkdf2 = function(password: Uint8Array, salt: Uint8Array, iterations: number, keylen: number, algo: "sha256" | "sha512"): BytesLike {
return pbkdf2Sync(password, salt, iterations, keylen, algo);
}
let __pbkdf2 = _pbkdf2;
export function pbkdf2(_password: BytesLike, _salt: BytesLike, iterations: number, keylen: number, algo: "sha256" | "sha512"): string {
const password = logger.getBytes(_password, "password");
const salt = logger.getBytes(_salt, "salt");
return hexlify(__pbkdf2(password, salt, iterations, keylen, algo));
}
pbkdf2._ = _pbkdf2;
pbkdf2.lock = function(): void { locked = true; }
pbkdf2.register = function(func: (password: Uint8Array, salt: Uint8Array, iterations: number, keylen: number, algo: "sha256" | "sha512") => BytesLike) {
if (locked) { throw new Error("pbkdf2 is locked"); }
__pbkdf2 = func;
}
Object.freeze(pbkdf2);

View File

@ -0,0 +1,22 @@
import { randomBytes as crypto_random } from "crypto"; /*-browser*/
let locked = false;
const _randomBytes = function(length: number): Uint8Array {
return new Uint8Array(crypto_random(length));
}
let __randomBytes = _randomBytes;
export function randomBytes(length: number): Uint8Array {
return __randomBytes(length);
}
randomBytes._ = _randomBytes;
randomBytes.lock = function(): void { locked = true; }
randomBytes.register = function(func: (length: number) => Uint8Array) {
if (locked) { throw new Error("random is locked"); }
__randomBytes = func;
}
Object.freeze(randomBytes);

View File

@ -0,0 +1,26 @@
import { ripemd160 as noble_ripemd160 } from "@noble/hashes/ripemd160";
import { hexlify } from "@ethersproject/bytes";
import { logger } from "./logger.js";
import type { BytesLike } from "@ethersproject/logger";
let locked = false;
const _ripemd160 = function(data: Uint8Array): Uint8Array {
return noble_ripemd160(data);
}
let __ripemd160: (data: Uint8Array) => BytesLike = _ripemd160;
export function ripemd160(_data: BytesLike): string {
const data = logger.getBytes(_data, "data");
return hexlify(__ripemd160(data));
}
ripemd160._ = _ripemd160;
ripemd160.lock = function(): void { locked = true; }
ripemd160.register = function(func: (data: Uint8Array) => BytesLike) {
if (locked) { throw new TypeError("ripemd160 is locked"); }
__ripemd160 = func;
}
Object.freeze(ripemd160);

View File

@ -0,0 +1,49 @@
import { scrypt as _nobleSync, scryptAsync as _nobleAsync } from "@noble/hashes/scrypt";
import { hexlify as H} from "@ethersproject/bytes";
import { logger } from "./logger.js";
import type { BytesLike } from "@ethersproject/logger";
export type ProgressCallback = (percent: number) => void;
let lockedSync = false, lockedAsync = false;
const _scryptAsync = async function(passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, onProgress?: ProgressCallback) {
return await _nobleAsync(passwd, salt, { N, r, p, dkLen, onProgress });
}
const _scryptSync = function(passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number) {
return _nobleSync(passwd, salt, { N, r, p, dkLen });
}
let __scryptAsync: (passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, onProgress?: ProgressCallback) => Promise<BytesLike> = _scryptAsync;
let __scryptSync: (passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number) => BytesLike = _scryptSync
export async function scrypt(_passwd: BytesLike, _salt: BytesLike, N: number, r: number, p: number, dkLen: number, progress?: ProgressCallback): Promise<string> {
const passwd = logger.getBytes(_passwd, "passwd");
const salt = logger.getBytes(_salt, "salt");
return H(await __scryptAsync(passwd, salt, N, r, p, dkLen, progress));
}
scrypt._ = _scryptAsync;
scrypt.lock = function(): void { lockedAsync = true; }
scrypt.register = function(func: (passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number, progress?: ProgressCallback) => Promise<BytesLike>) {
if (lockedAsync) { throw new Error("scrypt is locked"); }
__scryptAsync = func;
}
Object.freeze(scrypt);
export function scryptSync(_passwd: BytesLike, _salt: BytesLike, N: number, r: number, p: number, dkLen: number): string {
const passwd = logger.getBytes(_passwd, "passwd");
const salt = logger.getBytes(_salt, "salt");
return H(__scryptSync(passwd, salt, N, r, p, dkLen));
}
scryptSync._ = _scryptSync;
scryptSync.lock = function(): void { lockedSync = true; }
scryptSync.register = function(func: (passwd: Uint8Array, salt: Uint8Array, N: number, r: number, p: number, dkLen: number) => BytesLike) {
if (lockedSync) { throw new Error("scryptSync is locked"); }
__scryptSync = func;
}
Object.freeze(scryptSync);

View File

@ -0,0 +1,46 @@
import { createHash } from "crypto"; /*-browser*/
import { hexlify } from "@ethersproject/bytes";
import { logger } from "./logger.js";
import type { BytesLike } from "@ethersproject/logger";
const _sha256 = function(data: Uint8Array): Uint8Array {
return createHash("sha256").update(data).digest();
}
const _sha512 = function(data: Uint8Array): Uint8Array {
return createHash("sha512").update(data).digest();
}
let __sha256: (data: Uint8Array) => BytesLike = _sha256;
let __sha512: (data: Uint8Array) => BytesLike = _sha512;
let locked256 = false, locked512 = false;
export function sha256(_data: BytesLike): string {
const data = logger.getBytes(_data, "data");
return hexlify(__sha256(data));
}
sha256._ = _sha256;
sha256.lock = function(): void { locked256 = true; }
sha256.register = function(func: (data: Uint8Array) => BytesLike): void {
if (locked256) { throw new Error("sha256 is locked"); }
__sha256 = func;
}
Object.freeze(sha256);
export function sha512(_data: BytesLike): string {
const data = logger.getBytes(_data, "data");
return hexlify(__sha512(data));
}
sha512._ = _sha512;
sha512.lock = function(): void { locked512 = true; }
sha512.register = function(func: (data: Uint8Array) => BytesLike): void {
if (locked512) { throw new Error("sha512 is locked"); }
__sha512 = func;
}
Object.freeze(sha256);

View File

@ -0,0 +1,116 @@
import assert from "assert";
import {
lock,
computeHmac,
keccak256, ripemd160, sha256, sha512,
pbkdf2, scrypt, scryptSync
} from "../index.js";
interface Algorithm {
(...args: Array<any>): string | Promise<string>;
register: (func: any) => void;
lock: () => void;
_: (...args: Array<any>) => any;
}
interface TestCase {
name: string;
params: Array<any>;
algorithm: Algorithm;
hijackTag: string;
}
describe("test registration", function() {
let hijack = "";
function getHijack(algo: string) {
return function(...args: Array<any>) {
hijack = `hijacked ${ algo }: ${ JSON.stringify(args) }`;
return "0x42";
}
}
const tests: Array<TestCase> = [
{
name: "keccak256",
params: [ "0x" ],
hijackTag: 'hijacked keccak256: [{}]',
algorithm: keccak256
},
{
name: "sha256",
params: [ "0x" ],
hijackTag: 'hijacked sha256: [{}]',
algorithm: sha256
},
{
name: "sha512",
params: [ "0x" ],
hijackTag: 'hijacked sha512: [{}]',
algorithm: sha512
},
{
name: "ripemd160",
params: [ "0x" ],
hijackTag: 'hijacked ripemd160: [{}]',
algorithm: ripemd160
},
{
name: "pbkdf2",
params: [ "0x", "0x", 1024, 32, "sha256" ],
hijackTag: 'hijacked pbkdf2: [{},{},1024,32,"sha256"]',
algorithm: pbkdf2
},
{
name: "scryptSync",
params: [ "0x", "0x", 1024, 8, 1, 32 ],
hijackTag: 'hijacked scryptSync: [{},{},1024,8,1,32]',
algorithm: scryptSync
},
{
name: "scrypt",
params: [ "0x", "0x", 1024, 8, 1, 32 ],
hijackTag: 'hijacked scrypt: [{},{},1024,8,1,32,null]',
algorithm: scrypt
},
{
name: "computeHmac",
params: [ "sha256", "0x", "0x" ],
hijackTag: 'hijacked computeHmac: ["sha256",{},{}]',
algorithm: computeHmac
},
];
tests.forEach(({ name, params, hijackTag, algorithm }) => {
it(`swaps in hijacked callback: ${ name }`, async function() {
const initial = await algorithm(...params);
algorithm.register(getHijack(name));
assert.equal(await algorithm(...params), "0x42");
assert.equal(hijack, hijackTag);
algorithm.register(algorithm._);
assert.equal(await algorithm(...params), initial);
});
});
it("prevents swapping after locked", function() {
lock();
tests.forEach(({ name, params, hijackTag, algorithm }) => {
assert.throws(function() {
algorithm.register(getHijack("test"));
}, function(error) {
return (error.message === `${ name } is locked`);
});
});
});
});

View File

@ -0,0 +1,238 @@
import assert from "assert";
import { loadTests } from "./utils.js";
import type { TestCaseHash, TestCaseHmac, TestCasePbkdf } from "./types.js";
import {
computeHmac,
keccak256, ripemd160, sha256, sha512,
pbkdf2, scrypt, scryptSync
} from "../index.js";
describe("test hashing", function() {
const tests = loadTests<TestCaseHash>("hashes");
tests.forEach((test) => {
it(`computes sha2-256: ${ test.name }`, function() {
assert.equal(sha256(test.data), test.sha256);
});
});
tests.forEach((test) => {
it(`computes sha2-512: ${ test.name }`, function() {
assert.equal(sha512(test.data), test.sha512);
});
});
tests.forEach((test) => {
it(`computes ripemd160: ${ test.name }`, function() {
assert.equal(ripemd160(test.data), test.ripemd160);
});
});
tests.forEach((test) => {
it(`computes keccak256: ${ test.name }`, function() {
assert.equal(keccak256(test.data), test.keccak256);
});
});
});
describe("test password-based key derivation", function() {
const tests = loadTests<TestCasePbkdf>("pbkdf");
tests.forEach((test) => {
it(`computes pbkdf2: ${ test.name}`, function() {
const password = Buffer.from(test.password.substring(2), "hex");
const salt = Buffer.from(test.salt.substring(2), "hex");
const { iterations, algorithm, key } = test.pbkdf2;
const result = pbkdf2(password, salt, iterations, test.dkLen, algorithm);
assert.equal(result, key);
});
});
tests.forEach((test) => {
it(`computes scrypt (sync): ${ test.name}`, function() {
this.timeout(1000);
const password = Buffer.from(test.password.substring(2), "hex");
const salt = Buffer.from(test.salt.substring(2), "hex");
const { N, r, p, key } = test.scrypt;
const result = scryptSync(password, salt, N, r, p, test.dkLen);
assert.equal(result, key);
});
});
tests.forEach((test) => {
it(`computes scrypt (async): ${ test.name}`, async function() {
this.timeout(1000);
const password = Buffer.from(test.password.substring(2), "hex");
const salt = Buffer.from(test.salt.substring(2), "hex");
const { N, r, p, key } = test.scrypt;
let progressCount = 0, progressOk = true, lastProgress = -1;
const result = await scrypt(password, salt, N, r, p, test.dkLen, (progress) => {
if (progress < lastProgress) { progressOk = false; }
lastProgress = progress;
progressCount++;
});
assert.ok(progressOk, "progress was not monotonically increasing");
assert.ok(progressCount > 100, "progress callback was called at leat 100 times");
assert.equal(result, key);
});
});
});
describe("test hmac", function() {
const tests = loadTests<TestCaseHmac>("hmac");
tests.forEach((test) => {
it(`computes hmac: ${ test.name}`, async function() {
const { algorithm, key, data } = test;
assert.equal(computeHmac(algorithm, key, data), test.hmac);
});
});
});
/*
describe("test registration", function() {
let hijack = "";
function getHijack(algo: string) {
return function(...args: Array<any>) {
hijack = `hijacked ${ algo }: ${ JSON.stringify(args) }`;
return "0x42";
}
}
it("hijacks keccak256", function() {
const initial = keccak256("0x");
keccak256.register(getHijack("kecak256"));
assert.equal(keccak256("0x"), "0x42");
assert.equal(hijack, 'hijacked kecak256: [{}]');
keccak256.register(keccak256._);
assert.equal(keccak256("0x"), initial);
keccak256.lock();
assert.throws(function() {
keccak256.register(getHijack("test"));
}, function(error) {
return (error.message === "keccak256 is locked");
});
});
it("hijacks sha256", function() {
const initial = sha256("0x");
sha256.register(getHijack("sha256"));
assert.equal(sha256("0x"), "0x42");
assert.equal(hijack, 'hijacked sha256: [{}]');
sha256.register(sha256._);
assert.equal(sha256("0x"), initial);
sha256.lock();
assert.throws(function() {
sha256.register(getHijack("test"));
}, function(error) {
return (error.message === "sha256 is locked");
});
});
it("hijacks sha512", function() {
const initial = sha512("0x");
sha512.register(getHijack("sha512"));
assert.equal(sha512("0x"), "0x42");
assert.equal(hijack, 'hijacked sha512: [{}]');
sha512.register(sha512._);
assert.equal(sha512("0x"), initial);
sha512.lock();
assert.throws(function() {
sha512.register(getHijack("test"));
}, function(error) {
return (error.message === "sha512 is locked");
});
});
it("hijacks pbkdf2", function() {
const initial = pbkdf2("0x", "0x", 1024, 32, "sha256");
pbkdf2.register(getHijack("pbkdf2"));
assert.equal(pbkdf2("0x", "0x", 1024, 32, "sha256"), "0x42");
assert.equal(hijack, 'hijacked pbkdf2: [{},{},1024,32,"sha256"]');
pbkdf2.register(pbkdf2._);
assert.equal(pbkdf2("0x", "0x", 1024, 32, "sha256"), initial);
pbkdf2.lock();
assert.throws(function() {
pbkdf2.register(getHijack("test"));
}, function(error) {
return (error.message === "pbkdf2 is locked");
});
});
it("hijacks scryptSync", function() {
function getHijack(...args: Array<any>) {
hijack = `hijacked scryptSync: ${ JSON.stringify(args) }`;
return new Uint8Array([ 0x42 ]);
}
const initial = scryptSync("0x", "0x", 1024, 8, 1, 32);
scryptSync.register(getHijack);
assert.equal(scryptSync("0x", "0x", 1024, 8, 1, 32), "0x42");
assert.equal(hijack, 'hijacked scryptSync: [{},{},1024,8,1,32]');
scryptSync.register(scryptSync._);
assert.equal(scryptSync("0x", "0x", 1024, 8, 1, 32), initial);
scryptSync.lock();
assert.throws(function() {
scryptSync.register(getHijack);
}, function(error) {
return (error.message === "scryptSync is locked");
});
});
it("hijacks scrypt", async function() {
function getHijack(...args: Array<any>) {
hijack = `hijacked scrypt: ${ JSON.stringify(args) }`;
return Promise.resolve(new Uint8Array([ 0x42 ]));
}
const initial = await scrypt("0x", "0x", 1024, 8, 1, 32);
scrypt.register(getHijack);
assert.equal(await scrypt("0x", "0x", 1024, 8, 1, 32), "0x42");
assert.equal(hijack, 'hijacked scrypt: [{},{},1024,8,1,32,null]');
scrypt.register(scrypt._);
assert.equal(await scrypt("0x", "0x", 1024, 8, 1, 32), initial);
scrypt.lock();
assert.throws(function() {
scrypt.register(getHijack);
}, function(error) {
return (error.message === "scrypt is locked");
});
});
});
*/

View File

@ -0,0 +1,36 @@
export interface TestCaseHash {
name: string;
data: string;
sha256: string;
sha512: string;
ripemd160: string;
keccak256: string;
}
export interface TestCasePbkdf {
name: string;
password: string;
salt: string;
dkLen: number;
pbkdf2: {
iterations: number;
algorithm: "sha256" | "sha512";
key: string;
},
scrypt: {
N: number;
r: number;
p: number;
key: string;
}
}
export interface TestCaseHmac {
name: string;
data: string;
key: string;
algorithm: "sha256" | "sha512";
hmac: string;
}

View File

@ -0,0 +1,25 @@
import fs from "fs"
import path from "path";
import zlib from 'zlib';
// Find the package root (based on the nyc output/ folder)
const root = (function() {
let root = process.cwd();
while (true) {
if (fs.existsSync(path.join(root, "output"))) { return root; }
const parent = path.join(root, "..");
if (parent === root) { break; }
root = parent;
}
throw new Error("could not find root");
})();
// Load the tests
export function loadTests<T>(tag: string): Array<T> {
const filename = path.resolve(root, "testcases", tag + ".json.gz");
return JSON.parse(zlib.gunzipSync(fs.readFileSync(filename)).toString());
}

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"outDir": "./lib/",
"rootDir": "./src.ts"
},
"exclude": [],
"extends": "../../tsconfig.common.json",
"include": [
"./src.ts/**/*.ts"
]
}

View File

@ -0,0 +1,3 @@
/src.ts/tests/**
/lib/tests/**
tsconfig.tsbuildinfo

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Richard Moore
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
packages/ethers/dist/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
**.gz

File diff suppressed because one or more lines are too long

16146
packages/ethers/dist/ethers.js vendored Normal file

File diff suppressed because one or more lines are too long

1
packages/ethers/dist/ethers.js.map vendored Normal file

File diff suppressed because one or more lines are too long

6
packages/ethers/dist/ethers.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More