Initial code drop for v6-beta-exports.
This commit is contained in:
parent
8278dcb1ca
commit
f5336c19b1
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules/**
|
||||
output/**
|
||||
misc/**
|
26
.npmignore
Normal file
26
.npmignore
Normal file
@ -0,0 +1,26 @@
|
||||
|
||||
# Ignore TypeScript config and caches
|
||||
tsconfig.*.json
|
||||
tsconfig.tsbuildinfo
|
||||
rollup.config.js
|
||||
output/**
|
||||
|
||||
# Ignore admin scripts and files
|
||||
src.ts/_admin/**
|
||||
lib.commonjs/_admin/**
|
||||
lib.esm/_admin/**
|
||||
types/_admin/**
|
||||
reporter.cjs
|
||||
package-commonjs.json
|
||||
|
||||
# Ignore test cases
|
||||
src.ts/_tests/**
|
||||
lib.commonjs/_tests/**
|
||||
lib.esm/_tests/**
|
||||
types/_tests/**
|
||||
testcases/**
|
||||
|
||||
# Ignore random junk
|
||||
.DS_Store
|
||||
node_modules/**
|
||||
misc/**
|
3
package-commonjs.json
Normal file
3
package-commonjs.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "commonjs"
|
||||
}
|
131
package.json
Normal file
131
package.json
Normal file
@ -0,0 +1,131 @@
|
||||
{
|
||||
"author": "Richard Moore <me@ricmoo.com>",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.1.2",
|
||||
"@noble/secp256k1": "1.6.3",
|
||||
"aes-js": "4.0.0-beta.2",
|
||||
"tslib": "2.4.0",
|
||||
"ws": "8.5.0"
|
||||
},
|
||||
"description": "Ethereum library.",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-node-resolve": "13.3.0",
|
||||
"@types/mocha": "9.1.1",
|
||||
"c8": "7.12.0",
|
||||
"mocha": "10.0.0",
|
||||
"rollup": "2.78.1",
|
||||
"typescript": "4.7.4",
|
||||
"uglify-js": "3.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"browser": {
|
||||
"./lib.esm/crypto/crypto.js": "./lib.esm/crypto/crypto-browser.js",
|
||||
"./lib.esm/providers/provider-ipcsocket.js": "./lib.esm/providers/provider-ipcsocket-browser.js",
|
||||
"./lib.esm/providers/ws.js": "./lib.esm/providers/ws-browser.js",
|
||||
"./lib.esm/utils/base64.js": "./lib.esm/utils/base64-browser.js",
|
||||
"./lib.esm/utils/geturl.js": "./lib.esm/utils/geturl-browser.js",
|
||||
"./lib.esm/wordlists/wordlists.js": "./lib.esm/wordlists/wordlists-browser.js"
|
||||
},
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./lib.esm/index.js",
|
||||
"require": "./lib.commonjs/index.js",
|
||||
"types": "./types/index.d.ts"
|
||||
},
|
||||
"./abi": {
|
||||
"import": "./lib.esm/abi/index.js",
|
||||
"require": "./lib.commonjs/abi/index.js",
|
||||
"types": "./types/abi/index.d.ts"
|
||||
},
|
||||
"./address": {
|
||||
"import": "./lib.esm/address/index.js",
|
||||
"require": "./lib.commonjs/address/index.js",
|
||||
"types": "./types/address/index.d.ts"
|
||||
},
|
||||
"./constants": {
|
||||
"import": "./lib.esm/constants/index.js",
|
||||
"require": "./lib.commonjs/constants/index.js",
|
||||
"types": "./types/constants/index.d.ts"
|
||||
},
|
||||
"./contract": {
|
||||
"import": "./lib.esm/contract/index.js",
|
||||
"require": "./lib.commonjs/contract/index.js",
|
||||
"types": "./types/contract/index.d.ts"
|
||||
},
|
||||
"./crypto": {
|
||||
"import": "./lib.esm/crypto/index.js",
|
||||
"require": "./lib.commonjs/crypto/index.js",
|
||||
"types": "./types/crypto/index.d.ts"
|
||||
},
|
||||
"./hash": {
|
||||
"import": "./lib.esm/hash/index.js",
|
||||
"require": "./lib.commonjs/hash/index.js",
|
||||
"types": "./types/hash/index.d.ts"
|
||||
},
|
||||
"./providers": {
|
||||
"import": "./lib.esm/providers/index.js",
|
||||
"require": "./lib.commonjs/providers/index.js",
|
||||
"types": "./types/providers/index.d.ts"
|
||||
},
|
||||
"./transaction": {
|
||||
"import": "./lib.esm/transaction/index.js",
|
||||
"require": "./lib.commonjs/transaction/index.js",
|
||||
"types": "./types/transaction/index.d.ts"
|
||||
},
|
||||
"./utils": {
|
||||
"import": "./lib.esm/utils/index.js",
|
||||
"require": "./lib.commonjs/utils/index.js",
|
||||
"types": "./types/utils/index.d.ts"
|
||||
},
|
||||
"./wallet": {
|
||||
"import": "./lib.esm/wallet/index.js",
|
||||
"require": "./lib.commonjs/wallet/index.js",
|
||||
"types": "./types/wallet/index.d.ts"
|
||||
},
|
||||
"./wordlists": {
|
||||
"import": "./lib.esm/wordlists/index.js",
|
||||
"require": "./lib.commonjs/wordlists/index.js",
|
||||
"types": "./types/wordlistsindex.d.ts"
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
"ethereum",
|
||||
"ethers",
|
||||
"ethersjs"
|
||||
],
|
||||
"license": "MIT",
|
||||
"main": "./lib.commonjs/index.js",
|
||||
"module": "./lib.esm/index.js",
|
||||
"name": "ethers",
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"tag": "beta-exports"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/ethers-io/ethers.js.git"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist lib.esm lib.commonjs types",
|
||||
"build": "tsc --project tsconfig.esm.json",
|
||||
"build-commonjs": "tsc --project tsconfig.commonjs.json && cp ./package-commonjs.json ./lib.commonjs/package.json",
|
||||
"build-types": "tsc --project tsconfig.types.json",
|
||||
"build-docs": "echo 'foo'",
|
||||
"auto-build": "npm run build -- -w",
|
||||
"build-all": "npm run build && npm run build-commonjs && npm run build-types",
|
||||
"build-clean": "npm run clean && npm run build-all",
|
||||
"_dist-stats": "gzip -k9f -S '.gz' ./dist/ethers.min.js && gzip -k9f -S '.gz' ./dist/wordlists-extra.min.js && du -hs ./dist/*.gz && echo '' && du -hs ./dist/*.js",
|
||||
"_build-dist": "rollup -c && uglifyjs ./dist/ethers.js -o ./dist/ethers.min.js && uglifyjs ./dist/wordlists-extra.js -o ./dist/wordlists-extra.min.js && npm run _dist-stats",
|
||||
"build-dist": "npm run build && npm run _build-dist",
|
||||
"stats": "echo 'Dependencies' && npm ls --all --omit=dev",
|
||||
"test": "mocha --reporter ./reporter.cjs ./lib.esm/_tests/test-*.js",
|
||||
"test-commonjs": "mocha --reporter ./reporter.cjs ./lib.commonjs/_tests/test-*.js",
|
||||
"test-coverage": "c8 -o output -r lcov -r text mocha --no-color --reporter ./reporter.cjs ./lib.esm/_tests/test-*.js | tee output/summary.txt"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"type": "module",
|
||||
"types": "./types/index.d.ts",
|
||||
"version": "6.0.0-beta-exports.0"
|
||||
}
|
213
reporter.cjs
Normal file
213
reporter.cjs
Normal file
@ -0,0 +1,213 @@
|
||||
'use strict';
|
||||
|
||||
const Mocha = require('mocha');
|
||||
const {
|
||||
EVENT_RUN_BEGIN,
|
||||
EVENT_RUN_END,
|
||||
EVENT_TEST_BEGIN,
|
||||
EVENT_TEST_END,
|
||||
EVENT_TEST_FAIL,
|
||||
EVENT_TEST_PASS,
|
||||
EVENT_SUITE_BEGIN,
|
||||
EVENT_SUITE_END
|
||||
} = Mocha.Runner.constants;
|
||||
|
||||
|
||||
// See: https://stackoverflow.com/questions/9781218/how-to-change-node-jss-console-font-color
|
||||
let disableColor = false; //!(process.stdout.isTTY);
|
||||
process.argv.forEach((arg) => {
|
||||
if (arg === "--no-color") { disableColor = true; }
|
||||
});
|
||||
|
||||
const Colors = {
|
||||
"blue": "\x1b[0;34m",
|
||||
"blue+": "\x1b[0;1;34m",
|
||||
"cyan": "\x1b[0;36m",
|
||||
"cyan+": "\x1b[0;1;36m",
|
||||
"green": "\x1b[0;32m",
|
||||
"green+": "\x1b[0;1;32m",
|
||||
"magenta-": "\x1b[0;2;35m",
|
||||
"magenta": "\x1b[0;35m",
|
||||
"magenta+": "\x1b[0;1;35m",
|
||||
"red": "\x1b[0;31m",
|
||||
"red+": "\x1b[0;1;31m",
|
||||
"yellow": "\x1b[0;33m",
|
||||
"yellow+": "\x1b[0;1;33m",
|
||||
"dim": "\x1b[0;2;37m",
|
||||
"bold": "\x1b[0;1;37m",
|
||||
"normal": "\x1b[0m"
|
||||
};
|
||||
|
||||
function colorify(text) {
|
||||
return unescapeColor(text.replace(/(<([a-z+]+)>)/g, (all, _, color) => {
|
||||
if (disableColor) { return ""; }
|
||||
|
||||
const seq = Colors[color];
|
||||
if (seq == null) {
|
||||
console.log("UNKNOWN COLOR:", color);
|
||||
return "";
|
||||
}
|
||||
return seq;
|
||||
})) + Colors.normal;
|
||||
}
|
||||
|
||||
function escapeColor(text) {
|
||||
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function unescapeColor(text) {
|
||||
return text.replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&");
|
||||
}
|
||||
|
||||
function getString(value) {
|
||||
if (value instanceof Error) {
|
||||
return value.stack;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// To prevent environments from thinking we're dead due to lack of
|
||||
// output, we force output after 20s
|
||||
function getTime() { return (new Date()).getTime(); }
|
||||
const KEEP_ALIVE = 20 * 1000;
|
||||
|
||||
// this reporter outputs test results, indenting two spaces per suite
|
||||
class MyReporter {
|
||||
constructor(runner) {
|
||||
this._errors = [ ];
|
||||
this._indents = 1;
|
||||
this._lastLog = getTime();
|
||||
this._lastPass = "";
|
||||
this._lastPrefix = null;
|
||||
this._lastPrefixHeader = null;
|
||||
this._testLogs = [ ];
|
||||
this._suiteLogs = [ ];
|
||||
this._prefixCount = 0;
|
||||
const stats = runner.stats;
|
||||
|
||||
runner.once(EVENT_RUN_BEGIN, () => {
|
||||
|
||||
}).on(EVENT_SUITE_BEGIN, (suite) => {
|
||||
this._suiteLogs.push([ ]);
|
||||
suite._ethersLog = (text) => {
|
||||
this._suiteLogs[this._suiteLogs.length - 1].push(getString(text))
|
||||
};
|
||||
if (suite.title.trim()) {
|
||||
this.log(`<blue+>Suite: ${ escapeColor(suite.title) }`)
|
||||
}
|
||||
this.increaseIndent();
|
||||
|
||||
}).on(EVENT_SUITE_END, (suite) => {
|
||||
this.flush(true);
|
||||
this.decreaseIndent();
|
||||
const logs = this._suiteLogs.pop();
|
||||
if (logs.length) {
|
||||
logs.join("\n").split("\n").forEach((line) => {
|
||||
this.log(` <magenta+>>> <dim>${ escapeColor(line) }`);
|
||||
});
|
||||
}
|
||||
if (suite.title.trim()) { this.log(""); }
|
||||
|
||||
}).on(EVENT_TEST_BEGIN, (test) => {
|
||||
this._testLogs.push([ ]);
|
||||
test._ethersLog = (text) => {
|
||||
this._testLogs[this._testLogs.length - 1].push(getString(text))
|
||||
};
|
||||
|
||||
}).on(EVENT_TEST_END, (test) => {
|
||||
const logs = this._testLogs.pop();
|
||||
if (logs.length) {
|
||||
this.flush(false);
|
||||
logs.join("\n").split("\n").forEach((line) => {
|
||||
this.log(` <cyan+>>> <cyan->${ escapeColor(line) }`);
|
||||
});
|
||||
}
|
||||
|
||||
}).on(EVENT_TEST_PASS, (test) => {
|
||||
this.addPass(test.title);
|
||||
|
||||
}).on(EVENT_TEST_FAIL, (test, error) => {
|
||||
this.flush();
|
||||
this._errors.push({ test, error });
|
||||
this.log(
|
||||
` [ <red+>fail(${ this._errors.length }): <red>${ escapeColor(test.title) } - <normal>${ escapeColor(error.message) } ]`
|
||||
);
|
||||
|
||||
}).once(EVENT_RUN_END, () => {
|
||||
this.flush(true);
|
||||
this.indent = 0;
|
||||
|
||||
if (this._errors.length) {
|
||||
this._errors.forEach(({ test, error }, index) => {
|
||||
this.log("<cyan+>---------------------");
|
||||
this.log(`<red+>ERROR ${ index + 1 }: <red>${ escapeColor(test.title) }`);
|
||||
this.log(escapeColor(error.toString()));
|
||||
});
|
||||
this.log("<cyan+>=====================");
|
||||
}
|
||||
|
||||
const { duration, passes, failures } = stats;
|
||||
const total = passes + failures;
|
||||
this.log(`<bold>Done: <green+>${ passes }<green>/${ total } passed <red>(<red+>${ failures } <red>failed)`);
|
||||
});
|
||||
}
|
||||
|
||||
log(line) {
|
||||
this._lastLog = getTime();
|
||||
const indent = Array(this._indents).join(' ');
|
||||
console.log(`${ indent }${ colorify(line) }`);
|
||||
}
|
||||
|
||||
addPass(line) {
|
||||
const prefix = line.split(":")[0];
|
||||
if (prefix === this._lastPrefix) {
|
||||
this._prefixCount++;
|
||||
if (getTime() - this._lastLog > KEEP_ALIVE) {
|
||||
const didLog = this.flush(false);
|
||||
// Nothing was output, so show *something* so the
|
||||
// environment knows we're still alive and kicking
|
||||
if (!didLog) {
|
||||
this.log(" <yellow>[ keep-alive; forced output ]")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.flush(true);
|
||||
this._lastPrefixHeader = null;
|
||||
this._lastPrefix = prefix;
|
||||
this._prefixCount = 1;
|
||||
}
|
||||
this._lastLine = line;
|
||||
}
|
||||
|
||||
flush(reset) {
|
||||
let didLog = false;
|
||||
if (this._lastPrefix != null) {
|
||||
if (this._prefixCount === 1 && this._lastPrefixHeader == null) {
|
||||
this.log(escapeColor(this._lastLine));
|
||||
didLog = true;
|
||||
} else if (this._prefixCount > 0) {
|
||||
if (this._lastPrefixHeader !== this._lastPrefix) {
|
||||
this.log(`<cyan>${ escapeColor(this._lastPrefix) }:`);
|
||||
this._lastPrefixHeader = this._lastPrefix;
|
||||
}
|
||||
this.log(` - ${ this._prefixCount } tests passed (prefix coalesced)`);
|
||||
didLog = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (reset) {
|
||||
this._lastPrefixHeader = null;
|
||||
this._lastPrefix = null;
|
||||
}
|
||||
|
||||
this._prefixCount = 0;
|
||||
|
||||
return didLog;
|
||||
}
|
||||
|
||||
increaseIndent() { this._indents++; }
|
||||
|
||||
decreaseIndent() { this._indents--; }
|
||||
}
|
||||
|
||||
module.exports = MyReporter;
|
47
rollup.config.js
Normal file
47
rollup.config.js
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
||||
|
||||
function getConfig(opts) {
|
||||
if (opts == null) { opts = { }; }
|
||||
|
||||
const file = `./dist/ethers${ (opts.suffix || "") }.js`;
|
||||
const exportConditions = [ "default", "module", "import" ];
|
||||
const mainFields = [ "module", "main" ];
|
||||
if (opts.browser) { mainFields.unshift("browser"); }
|
||||
|
||||
return {
|
||||
input: "./lib.esm/index.js",
|
||||
output: {
|
||||
file,
|
||||
format: "esm",
|
||||
sourcemap: true
|
||||
},
|
||||
treeshake: false,
|
||||
plugins: [ nodeResolve({
|
||||
exportConditions,
|
||||
mainFields,
|
||||
modulesOnly: true,
|
||||
preferBuiltins: false
|
||||
}) ],
|
||||
// external: [ "crypto" ]
|
||||
};
|
||||
}
|
||||
|
||||
export default [
|
||||
getConfig({ browser: true }),
|
||||
{
|
||||
input: "./lib.esm/wordlists/wordlists-extra.js",
|
||||
output: {
|
||||
file: "./dist/wordlists-extra.js",
|
||||
format: "esm",
|
||||
sourcemap: true
|
||||
},
|
||||
treeshake: true,
|
||||
plugins: [ nodeResolve({
|
||||
exportConditions: [ "default", "module", "import" ],
|
||||
mainFields: [ "browser", "module", "main" ],
|
||||
modulesOnly: true,
|
||||
preferBuiltins: false
|
||||
}) ],
|
||||
}
|
||||
];
|
8
src.ts/_admin/update-version-const.ts
Normal file
8
src.ts/_admin/update-version-const.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { atomicWrite } from "./utils/fs.js";
|
||||
import { resolve } from "./utils/path.js";
|
||||
import { loadJson } from "./utils/json.js";
|
||||
|
||||
const version = loadJson(resolve("package.json")).version;
|
||||
|
||||
const content = `export const version = "${ version }";\n`;
|
||||
atomicWrite(resolve("src.ts/_version.ts"), content);
|
9
src.ts/_admin/utils/fs.ts
Normal file
9
src.ts/_admin/utils/fs.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import fs from "fs";
|
||||
|
||||
import { resolve } from "./path.js";
|
||||
|
||||
export function atomicWrite(path: string, value: string | Uint8Array): void {
|
||||
const tmp = resolve(".atomic-tmp");
|
||||
fs.writeFileSync(tmp, value);
|
||||
fs.renameSync(tmp, path);
|
||||
}
|
32
src.ts/_admin/utils/json.ts
Normal file
32
src.ts/_admin/utils/json.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import fs from "fs";
|
||||
|
||||
import { atomicWrite } from "./fs.js";
|
||||
|
||||
|
||||
export function loadJson(path: string): any {
|
||||
return JSON.parse(fs.readFileSync(path).toString());
|
||||
}
|
||||
|
||||
type Replacer = (key: string, value: any) => any;
|
||||
|
||||
export function saveJson(filename: string, data: any, sort?: boolean): any {
|
||||
|
||||
let replacer: (Replacer | undefined) = 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;
|
||||
}, <Record<string, any>>{});
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
atomicWrite(filename, JSON.stringify(data, replacer, 2) + "\n");
|
||||
}
|
14
src.ts/_admin/utils/path.ts
Normal file
14
src.ts/_admin/utils/path.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import { dirname, resolve as _resolve } from "path";
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
export const ROOT = _resolve(__dirname, "../../../");
|
||||
console.log(ROOT);
|
||||
|
||||
export function resolve(...args: Array<string>): string {
|
||||
args = args.slice();
|
||||
args.unshift(ROOT);
|
||||
return _resolve.apply(null, args);
|
||||
}
|
43
src.ts/_tests/test-abi.ts
Normal file
43
src.ts/_tests/test-abi.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import assert from "assert";
|
||||
import { loadTests } from "./utils.js";
|
||||
|
||||
import { TestCaseAbi, TestCaseAbiVerbose } from "./types.js";
|
||||
|
||||
import { defaultAbiCoder } from "../index.js";
|
||||
|
||||
function equal(actual: any, expected: TestCaseAbiVerbose): void {
|
||||
switch (expected.type) {
|
||||
case "address": case "boolean": case "hexstring": case "string":
|
||||
assert.equal(actual, expected.value);
|
||||
return;
|
||||
case "number":
|
||||
assert.equal(actual, BigInt(expected.value));
|
||||
return
|
||||
case "array": case "object":
|
||||
assert.ok(Array.isArray(actual), "!array");
|
||||
assert.equal(actual.length, expected.value.length, ".length mismatch");
|
||||
for (let i = 0; i < actual.length; i++) {
|
||||
equal(actual[i], expected.value[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
throw new Error(`unsupported: ${ expected }`);
|
||||
}
|
||||
|
||||
describe("Tests ABI Coder", function() {
|
||||
const tests = loadTests<TestCaseAbi>("abi");
|
||||
|
||||
for (const test of tests) {
|
||||
it(`tests ABI encoding: (${ test.name })`, function() {
|
||||
const encoded = defaultAbiCoder.encode([ test.type ], [ test.value ]);
|
||||
assert.equal(encoded, test.encoded, "encoded");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`tests ABI decoding: (${ test.name })`, function() {
|
||||
const decoded = defaultAbiCoder.decode([ test.type ], test.encoded)[0];
|
||||
equal(decoded, test.verbose);
|
||||
});
|
||||
}
|
||||
});
|
132
src.ts/_tests/test-address.ts
Normal file
132
src.ts/_tests/test-address.ts
Normal 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: ${ 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: any) {
|
||||
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: any) {
|
||||
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: any) {
|
||||
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: ${ 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: ${ 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: ${ 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: any) {
|
||||
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: any) {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.argument === "initCodeHash" &&
|
||||
error.value === badInitCodeHash);
|
||||
});
|
||||
});
|
||||
});
|
99
src.ts/_tests/test-contract.ts
Normal file
99
src.ts/_tests/test-contract.ts
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
import { Typed } from "../abi/index.js";
|
||||
import * as providers from "../providers/index.js";
|
||||
|
||||
import { Contract } from "../index.js";
|
||||
|
||||
import { log } from "./utils.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);
|
||||
log(this, `balance: ${ await contract.balanceOf("0x5555763613a12D8F3e73be831DFf8598089d3dCa") }`);
|
||||
});
|
||||
});
|
||||
*/
|
116
src.ts/_tests/test-crypto-algoswap.ts
Normal file
116
src.ts/_tests/test-crypto-algoswap.ts
Normal 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: any) {
|
||||
return (error.message === `${ name } is locked`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
238
src.ts/_tests/test-crypto.ts
Normal file
238
src.ts/_tests/test-crypto.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
*/
|
20
src.ts/_tests/test-hash-typeddata.ts
Normal file
20
src.ts/_tests/test-hash-typeddata.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import assert from "assert";
|
||||
import { loadTests } from "./utils.js";
|
||||
import type { TestCaseTypedData } from "./types.js";
|
||||
|
||||
import { TypedDataEncoder } from "../index.js";
|
||||
|
||||
|
||||
describe("Tests Typed Data (EIP-712)", function() {
|
||||
const tests = loadTests<TestCaseTypedData>("typed-data");
|
||||
for (const test of tests) {
|
||||
it(`tests encoding typed-data: ${ test.name }`, function() {
|
||||
const encoder = TypedDataEncoder.from(test.types);
|
||||
assert.equal(encoder.primaryType, test.primaryType, "primaryType");
|
||||
assert.equal(encoder.encode(test.data), test.encoded, "encoded");
|
||||
|
||||
assert.equal(TypedDataEncoder.getPrimaryType(test.types), test.primaryType, "primaryType");
|
||||
assert.equal(TypedDataEncoder.hash(test.domain, test.types, test.data), test.digest, "digest");
|
||||
});
|
||||
}
|
||||
});
|
110
src.ts/_tests/test-hash.ts
Normal file
110
src.ts/_tests/test-hash.ts
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
import assert from "assert";
|
||||
import { loadTests } from "./utils.js"
|
||||
import type { TestCaseNamehash } from "./types.js";
|
||||
|
||||
import { dnsEncode, isValidName, namehash } from "../index.js";
|
||||
|
||||
describe("Tests Namehash", function() {
|
||||
const tests = loadTests<TestCaseNamehash>("namehash");
|
||||
for (const test of tests) {
|
||||
it(`hashes ENS names: ${ JSON.stringify(test.ensName) }`, function() {
|
||||
const actual = namehash(test.ensName);
|
||||
|
||||
assert.equal(actual, test.namehash, "namehash");
|
||||
|
||||
// The empty string is not a valid ENS name
|
||||
if (test.ensName) {
|
||||
assert.ok(isValidName(test.ensName), "isValidName");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Tests Bad ENS Names", function() {
|
||||
const badTests: Array<{ ensName: any, prefix: string }> = [
|
||||
{ ensName: ".", prefix: "missing component" },
|
||||
{ ensName:"..", prefix: "missing component" },
|
||||
{ ensName:"ricmoo..eth", prefix: "missing component" },
|
||||
{ ensName:"ricmoo...eth", prefix: "missing component" },
|
||||
{ ensName:".foo", prefix: "missing component" },
|
||||
{ ensName:"foo.", prefix: "missing component" },
|
||||
{ ensName: 1234, prefix: "not a string" },
|
||||
{ ensName: true, prefix: "not a string" },
|
||||
];
|
||||
|
||||
// The empty string is not a valid name, but has a valid namehash
|
||||
// (the zero hash) as it is the base case for recursion
|
||||
it("empty ENS name", function() {
|
||||
assert.ok(!isValidName(""), "!isValidName");
|
||||
});
|
||||
|
||||
for (const { ensName, prefix } of badTests) {
|
||||
it(`fails on bad ENS name: ${ JSON.stringify(ensName) }`, function() {
|
||||
assert.ok(!isValidName(ensName), "!isValidName");
|
||||
assert.throws(() => {
|
||||
const result = namehash(ensName);
|
||||
console.log(result);
|
||||
}, (error) => {
|
||||
const errorPrefix = `invalid ENS name; ${ prefix }`;
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.argument === "name" && error.value === ensName &&
|
||||
error.message.substring(0, errorPrefix.length) === errorPrefix);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Tests DNS Encoding", function() {
|
||||
const tests: Array<{ ensName: string, dnsEncoded: string}> = [
|
||||
{ ensName: "", dnsEncoded: "0x00" },
|
||||
{ ensName: "ricmoo.eth", dnsEncoded: "0x067269636d6f6f0365746800" },
|
||||
];
|
||||
|
||||
for (const { ensName, dnsEncoded } of tests) {
|
||||
it(`computes the DNS Encoding: ${ JSON.stringify(ensName) }`, function() {
|
||||
assert.equal(dnsEncode(ensName), dnsEncoded, "dnsEncoded");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Tests DNS Names", function() {
|
||||
const badTests: Array<{ ensName: any, prefix: string}> = [
|
||||
{ ensName: ".", prefix: "invalid DNS name; missing component" },
|
||||
{ ensName: "foo..bar", prefix: "invalid DNS name; missing component" },
|
||||
{ ensName: ".foo", prefix: "invalid DNS name; missing component" },
|
||||
{ ensName: "foo.", prefix: "invalid DNS name; missing component" },
|
||||
{ ensName: 1234, prefix: "invalid DNS name; not a string" },
|
||||
{ ensName: true, prefix: "invalid DNS name; not a string" },
|
||||
];
|
||||
|
||||
for (const { ensName, prefix } of badTests) {
|
||||
it(`fails on bad DNS name: ${ JSON.stringify(ensName) }`, function() {
|
||||
assert.throws(() => {
|
||||
const result = dnsEncode(ensName);
|
||||
console.log(result);
|
||||
}, (error) => {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.argument === "name" && error.value === ensName &&
|
||||
error.message.substring(0, prefix.length) === prefix);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const ensName = "foobar012345678901234567890123456789012345678901234567890123456789";
|
||||
const prefix = "too long";
|
||||
it(`fails on bad DNS name: ${ JSON.stringify(ensName) }`, function() {
|
||||
assert.throws(() => {
|
||||
const result = dnsEncode(ensName);
|
||||
console.log(result);
|
||||
}, (error) => {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.argument === "value" && error.value === ensName &&
|
||||
error.message.substring(0, prefix.length) === prefix);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
*/
|
101
src.ts/_tests/test-rlp.ts
Normal file
101
src.ts/_tests/test-rlp.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import assert from "assert";
|
||||
|
||||
import { loadTests } from "./utils.js";
|
||||
|
||||
import { decodeRlp, encodeRlp } from "../index.js";
|
||||
|
||||
import type { TestCaseRlp } from "./types.js";
|
||||
|
||||
describe("Test RLP Coder", function() {
|
||||
|
||||
const tests = loadTests<TestCaseRlp>("rlp");
|
||||
|
||||
tests.forEach(({ name, encoded, decoded }) => {
|
||||
it(`encodes RLP: ${ name }`, function() {
|
||||
assert.equal(encodeRlp(decoded), encoded);
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach(({ name, encoded, decoded }) => {
|
||||
it(`decodes RLP: ${ name }`, function() {
|
||||
assert.deepStrictEqual(decodeRlp(encoded), decoded);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test bad RLP Data", function() {
|
||||
it("fails encoding data with invalid values", function() {
|
||||
assert.throws(() => {
|
||||
encodeRlp([ "0x1234", <string><unknown>1234 ]);
|
||||
}, (error: any) => {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.argument === "object" &&
|
||||
error.value === 1234)
|
||||
});
|
||||
});
|
||||
|
||||
it("fails decoding data with trailing junk", function() {
|
||||
assert.throws(() => {
|
||||
// Zeros_1
|
||||
decodeRlp("0x0042");
|
||||
}, (error: any) => {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^unexpected junk after rlp payload/) &&
|
||||
error.argument === "data" &&
|
||||
error.value === "0x0042")
|
||||
});
|
||||
});
|
||||
|
||||
it ("fails decoding short data", function() {
|
||||
assert.throws(() => {
|
||||
decodeRlp("0x");
|
||||
}, (error: any) => {
|
||||
return (error.code === "BUFFER_OVERRUN" &&
|
||||
error.message.match(/^data too short/) &&
|
||||
Buffer.from(error.buffer).toString("hex") === "" &&
|
||||
error.offset === 1 &&
|
||||
error.length === 0)
|
||||
});
|
||||
});
|
||||
|
||||
it ("fails decoding short data in child", function() {
|
||||
assert.throws(() => {
|
||||
decodeRlp("0xc8880102030405060708");
|
||||
}, (error: any) => {
|
||||
return (error.code === "BUFFER_OVERRUN" &&
|
||||
error.message.match(/^child data too short/) &&
|
||||
Buffer.from(error.buffer).toString("hex") === "c8880102030405060708" &&
|
||||
error.offset === 0 &&
|
||||
error.length === 8)
|
||||
});
|
||||
});
|
||||
|
||||
it ("fails decoding short segment data", function() {
|
||||
assert.throws(() => {
|
||||
// [["0x4243"], ["0x3145"]] = 0xc8 c3 82 4243 c3 82 3145
|
||||
// XXXX
|
||||
decodeRlp("0xc8c382c3823145");
|
||||
}, (error: any) => {
|
||||
return (error.code === "BUFFER_OVERRUN" &&
|
||||
error.message.match(/^data short segment too short/) &&
|
||||
Buffer.from(error.buffer).toString("hex") === "c8c382c3823145" &&
|
||||
error.offset === 9 &&
|
||||
error.length === 7)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
utils.RLP.encode([["0x4243"], ["0x3145"]])
|
||||
|
||||
0xc8 c3 82 4243 c3 82 3145
|
||||
|
||||
{
|
||||
"name": "arrayShort2",
|
||||
"decoded": [
|
||||
"0x48656c6c6f20576f726c64",
|
||||
"0x48656c6c6f20576f726c64"
|
||||
],
|
||||
"encoded": "0xd8 8b 48656c6c6f20576f726c64 8b 48656c6c6f20576f726c64"
|
||||
},
|
||||
*/
|
306
src.ts/_tests/test-transaction.ts
Normal file
306
src.ts/_tests/test-transaction.ts
Normal file
@ -0,0 +1,306 @@
|
||||
import assert from "assert";
|
||||
import { loadTests } from "./utils.js";
|
||||
import type { TestCaseTransaction, TestCaseTransactionTx } from "./types.js";
|
||||
|
||||
|
||||
import { Transaction } from "../index.js";
|
||||
|
||||
|
||||
const BN_0 = BigInt(0);
|
||||
|
||||
describe("Tests Unsigned Transaction Serializing", function() {
|
||||
const tests = loadTests<TestCaseTransaction>("transactions");
|
||||
|
||||
for (const test of tests) {
|
||||
it(`serialized unsigned legacy transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 0,
|
||||
accessList: undefined,
|
||||
maxFeePerGas: undefined,
|
||||
maxPriorityFeePerGas: undefined
|
||||
});
|
||||
|
||||
// Use the testcase sans the chainId for a legacy test
|
||||
if (txData.chainId != null && parseInt(txData.chainId) != 0) { txData.chainId = "0x00"; }
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.unsignedSerialized, test.unsignedLegacy, "unsignedLegacy");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
// Unsupported parameters for EIP-155; i.e. unspecified chain ID
|
||||
if (!test.unsignedEip155) { continue; }
|
||||
it(`serialized unsigned EIP-155 transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 0,
|
||||
accessList: undefined,
|
||||
maxFeePerGas: undefined,
|
||||
maxPriorityFeePerGas: undefined
|
||||
});
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.unsignedSerialized, test.unsignedEip155, "unsignedEip155");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`serialized unsigned Berlin transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 1,
|
||||
maxFeePerGas: undefined,
|
||||
maxPriorityFeePerGas: undefined
|
||||
});
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.unsignedSerialized, test.unsignedBerlin, "unsignedBerlin");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`serialized unsigned London transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, { type: 2 });
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.unsignedSerialized, test.unsignedLondon, "unsignedLondon");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Tests Signed Transaction Serializing", function() {
|
||||
const tests = loadTests<TestCaseTransaction>("transactions");
|
||||
|
||||
for (const test of tests) {
|
||||
it(`serialized signed legacy transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 0,
|
||||
accessList: undefined,
|
||||
maxFeePerGas: undefined,
|
||||
maxPriorityFeePerGas: undefined,
|
||||
signature: test.signatureLegacy
|
||||
});
|
||||
|
||||
// Use the testcase sans the chainId for a legacy test
|
||||
if (txData.chainId != null && parseInt(txData.chainId) != 0) { txData.chainId = "0x00"; }
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.serialized, test.signedLegacy, "signedLegacy");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
if (!test.unsignedEip155) { continue; }
|
||||
it(`serialized signed EIP-155 transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 0,
|
||||
accessList: undefined,
|
||||
maxFeePerGas: undefined,
|
||||
maxPriorityFeePerGas: undefined,
|
||||
signature: test.signatureEip155
|
||||
});
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.serialized, test.signedEip155, "signedEip155");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`serialized signed Berlin transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 1,
|
||||
maxFeePerGas: undefined,
|
||||
maxPriorityFeePerGas: undefined
|
||||
}, { signature: test.signatureBerlin });
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.serialized, test.signedBerlin, "signedBerlin");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`serialized signed London transaction: ${ test.name }`, function() {
|
||||
const txData = Object.assign({ }, test.transaction, {
|
||||
type: 2,
|
||||
signature: test.signatureLondon
|
||||
});
|
||||
|
||||
const tx = Transaction.from(txData);
|
||||
assert.equal(tx.serialized, test.signedLondon, "signedLondon");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function assertTxUint(actual: null | bigint, _expected: undefined | string, name: string): void {
|
||||
const expected = (_expected != null ? BigInt(_expected): null);
|
||||
assert.equal(actual, expected, name);
|
||||
}
|
||||
|
||||
function assertTxEqual(actual: Transaction, expected: TestCaseTransactionTx): void {
|
||||
assert.equal(actual.to, expected.to, "to");
|
||||
assert.equal(actual.nonce, expected.nonce, "nonce");
|
||||
|
||||
assertTxUint(actual.gasLimit, expected.gasLimit, "gasLimit");
|
||||
|
||||
assertTxUint(actual.gasPrice, expected.gasPrice, "gasPrice");
|
||||
assertTxUint(actual.maxFeePerGas, expected.maxFeePerGas, "maxFeePerGas");
|
||||
assertTxUint(actual.maxPriorityFeePerGas, expected.maxPriorityFeePerGas, "maxPriorityFeePerGas");
|
||||
|
||||
assert.equal(actual.data, expected.data, "data");
|
||||
assertTxUint(actual.value, expected.value, "value");
|
||||
|
||||
if (expected.accessList) {
|
||||
assert.equal(JSON.stringify(actual.accessList), JSON.stringify(expected.accessList), "accessList");
|
||||
} else {
|
||||
assert.equal(actual.accessList, null, "accessList:!null");
|
||||
}
|
||||
|
||||
assertTxUint(actual.chainId, expected.chainId, "chainId");
|
||||
}
|
||||
|
||||
function addDefault(tx: any, key: string, defaultValue: any): void {
|
||||
if (tx[key] == null) { tx[key] = defaultValue; }
|
||||
}
|
||||
|
||||
function addDefaults(tx: any): any {
|
||||
tx = Object.assign({ }, tx);
|
||||
addDefault(tx, "nonce", 0);
|
||||
addDefault(tx, "gasLimit", BN_0);
|
||||
addDefault(tx, "gasPrice", BN_0);
|
||||
addDefault(tx, "maxFeePerGas", BN_0);
|
||||
addDefault(tx, "maxPriorityFeePerGas", BN_0);
|
||||
addDefault(tx, "value", BN_0);
|
||||
addDefault(tx, "data", "0x");
|
||||
addDefault(tx, "accessList", [ ]);
|
||||
addDefault(tx, "chainId", BN_0);
|
||||
return tx;
|
||||
}
|
||||
|
||||
describe("Tests Unsigned Transaction Parsing", function() {
|
||||
const tests = loadTests<TestCaseTransaction>("transactions");
|
||||
|
||||
for (const test of tests) {
|
||||
it(`parses unsigned legacy transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.unsignedLegacy);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.maxFeePerGas = null;
|
||||
expected.maxPriorityFeePerGas = null;
|
||||
expected.accessList = null;
|
||||
expected.chainId = BN_0;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
if (!test.unsignedEip155) { continue; }
|
||||
it(`parses unsigned EIP-155 transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.unsignedEip155);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.maxFeePerGas = null;
|
||||
expected.maxPriorityFeePerGas = null;
|
||||
expected.accessList = null;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`parses unsigned Berlin transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.unsignedBerlin);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.maxFeePerGas = null;
|
||||
expected.maxPriorityFeePerGas = null;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`parses unsigned London transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.unsignedLondon);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.gasPrice = null;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Tests Signed Transaction Parsing", function() {
|
||||
const tests = loadTests<TestCaseTransaction>("transactions");
|
||||
|
||||
for (const test of tests) {
|
||||
it(`parses signed legacy transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.signedLegacy);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.maxFeePerGas = null;
|
||||
expected.maxPriorityFeePerGas = null;
|
||||
expected.accessList = null;
|
||||
expected.chainId = BN_0;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
|
||||
assert.ok(!!tx.signature, "signature:!null")
|
||||
assert.equal(tx.signature.r, test.signatureLegacy.r, "signature.r");
|
||||
assert.equal(tx.signature.s, test.signatureLegacy.s, "signature.s");
|
||||
assert.equal(BigInt(tx.signature.v), BigInt(test.signatureLegacy.v), "signature.v");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
if (!test.unsignedEip155) { continue; }
|
||||
it(`parses signed EIP-155 transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.signedEip155);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.maxFeePerGas = null;
|
||||
expected.maxPriorityFeePerGas = null;
|
||||
expected.accessList = null;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
|
||||
assert.ok(!!tx.signature, "signature:!null")
|
||||
assert.equal(tx.signature.r, test.signatureEip155.r, "signature.r");
|
||||
assert.equal(tx.signature.s, test.signatureEip155.s, "signature.s");
|
||||
assert.equal(tx.signature.networkV, BigInt(test.signatureEip155.v), "signature.v");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`parses signed Berlin transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.signedBerlin);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.maxFeePerGas = null;
|
||||
expected.maxPriorityFeePerGas = null;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
|
||||
assert.ok(!!tx.signature, "signature:!null")
|
||||
assert.equal(tx.signature.r, test.signatureBerlin.r, "signature.r");
|
||||
assert.equal(tx.signature.s, test.signatureBerlin.s, "signature.s");
|
||||
assert.equal(tx.signature.yParity, parseInt(test.signatureBerlin.v), "signature.v");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`parses signed London transaction: ${ test.name }`, function() {
|
||||
const tx = Transaction.from(test.signedLondon);
|
||||
|
||||
const expected = addDefaults(test.transaction);
|
||||
expected.gasPrice = null;
|
||||
|
||||
assertTxEqual(tx, expected);
|
||||
|
||||
assert.ok(!!tx.signature, "signature:!null")
|
||||
assert.equal(tx.signature.r, test.signatureLondon.r, "signature.r");
|
||||
assert.equal(tx.signature.s, test.signatureLondon.s, "signature.s");
|
||||
assert.equal(tx.signature.yParity, parseInt(test.signatureLondon.v), "signature.v");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
155
src.ts/_tests/test-wallet-hd.ts
Normal file
155
src.ts/_tests/test-wallet-hd.ts
Normal file
@ -0,0 +1,155 @@
|
||||
|
||||
import assert from "assert";
|
||||
|
||||
import { wordlists } from "../wordlists/wordlists.js";
|
||||
|
||||
import { loadTests } from "./utils.js";
|
||||
|
||||
import { HDNodeWallet, HDNodeVoidWallet, Mnemonic } from "../index.js";
|
||||
|
||||
import type { Wordlist } from "../wordlists/index.js";
|
||||
|
||||
import type { TestCaseMnemonic, TestCaseMnemonicNode } from "./types.js";
|
||||
|
||||
|
||||
declare global {
|
||||
class TextDecoder {
|
||||
decode(data: Uint8Array): string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
function fromHex(hex: string): string {
|
||||
const data = Buffer.from(hex.substring(2), "hex");
|
||||
return decoder.decode(data);
|
||||
}
|
||||
|
||||
type Test = {
|
||||
phrase: string;
|
||||
password: string;
|
||||
wordlist: Wordlist;
|
||||
mnemonic: Mnemonic;
|
||||
checkMnemonic: (a: Mnemonic) => void;
|
||||
test: TestCaseMnemonic;
|
||||
};
|
||||
|
||||
describe("Test HDWallets", function() {
|
||||
function checkWallet(wallet: HDNodeWallet | HDNodeVoidWallet, test: TestCaseMnemonicNode): void {
|
||||
assert.equal(wallet.chainCode, test.chainCode, "chainCode");
|
||||
assert.equal(wallet.depth, test.depth, "depth");
|
||||
assert.equal(wallet.index, test.index, "index");
|
||||
assert.equal(wallet.fingerprint, test.fingerprint, "fingerprint");
|
||||
assert.equal(wallet.parentFingerprint, test.parentFingerprint, "parentFingerprint");
|
||||
assert.equal(wallet.publicKey, test.publicKey, "publicKey");
|
||||
|
||||
if (wallet instanceof HDNodeWallet) {
|
||||
assert.equal(wallet.extendedKey, test.xpriv, "xpriv");
|
||||
assert.equal(wallet.privateKey, test.privateKey, "privateKey");
|
||||
assert.equal(wallet.neuter().extendedKey, test.xpub, "xpub");
|
||||
} else if (wallet instanceof HDNodeVoidWallet) {
|
||||
assert.equal(wallet.extendedKey, test.xpub, "xpub");
|
||||
}
|
||||
}
|
||||
|
||||
const tests = loadTests<TestCaseMnemonic>("mnemonics");
|
||||
|
||||
const checks: Array<Test> = [ ];
|
||||
tests.forEach((test) => {
|
||||
// The phrase and password are stored in the test as hex so they
|
||||
// are safe as ascii7 values for viewing, printing, etc.
|
||||
const phrase = fromHex(test.phrase);
|
||||
const password = fromHex(test.password);
|
||||
const wordlist = wordlists[test.locale];
|
||||
if (wordlist == null) {
|
||||
it(`tests ${ test.name }`, function() {
|
||||
this.skip();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist);
|
||||
|
||||
function checkMnemonic(actual: Mnemonic): void {
|
||||
assert.equal(actual.phrase, phrase, "phrase");
|
||||
assert.equal(actual.password, password, "password");
|
||||
assert.equal(actual.wordlist.locale, test.locale, "locale");
|
||||
assert.equal(actual.entropy, mnemonic.entropy, "entropy");
|
||||
assert.equal(actual.computeSeed(), mnemonic.computeSeed(), "seed");
|
||||
}
|
||||
|
||||
checks.push({
|
||||
phrase, password, wordlist, mnemonic, checkMnemonic, test
|
||||
});
|
||||
});
|
||||
|
||||
for (const { test, checkMnemonic, phrase, password, wordlist } of checks) {
|
||||
it(`computes the HD keys by mnemonic: ${ test.name }`, function() {
|
||||
for (const subtest of test.nodes) {
|
||||
const w = HDNodeWallet.fromPhrase(phrase, password, subtest.path, wordlist);
|
||||
assert.ok(w instanceof HDNodeWallet, "instanceof HDNodeWallet");
|
||||
assert.equal(w.path, subtest.path, "path")
|
||||
checkWallet(w, subtest);
|
||||
assert.ok(!!w.mnemonic, "has mnemonic");
|
||||
checkMnemonic(w.mnemonic as Mnemonic);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const { test } of checks) {
|
||||
it(`computes the HD keys by entropy: ${ test.name }`, function() {
|
||||
const seedRoot = HDNodeWallet.fromSeed(test.seed);
|
||||
for (const subtest of test.nodes) {
|
||||
const w = seedRoot.derivePath(subtest.path);
|
||||
assert.ok(w instanceof HDNodeWallet, "instanceof HDNodeWallet");
|
||||
assert.equal(w.path, subtest.path, "path")
|
||||
checkWallet(w, subtest);
|
||||
assert.equal(w.mnemonic, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const { test } of checks) {
|
||||
it(`computes the HD keys by enxtended private key: ${ test.name }`, function() {
|
||||
for (const subtest of test.nodes) {
|
||||
const w = HDNodeWallet.fromExtendedKey(subtest.xpriv);
|
||||
assert.ok(w instanceof HDNodeWallet, "instanceof HDNodeWallet");
|
||||
checkWallet(w, subtest);
|
||||
assert.equal(w.mnemonic, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const { test, phrase, password, wordlist } of checks) {
|
||||
it(`computes the neutered HD keys by paths: ${ test.name }`, function() {
|
||||
const root = HDNodeWallet.fromPhrase(phrase, password, "m", wordlist).neuter();
|
||||
for (const subtest of test.nodes) {
|
||||
if (subtest.path.indexOf("'") >= 0) {
|
||||
assert.throws(() => {
|
||||
const w = root.derivePath(subtest.path);
|
||||
console.log(w);
|
||||
}, (error: any) => {
|
||||
return (error.code === "UNSUPPORTED_OPERATION" &&
|
||||
error.message.match(/^cannot derive child of neutered node/) &&
|
||||
error.operation === "deriveChild");
|
||||
});
|
||||
} else {
|
||||
const w = root.derivePath(subtest.path);
|
||||
assert.ok(w instanceof HDNodeVoidWallet, "instanceof HDNodeVoidWallet");
|
||||
assert.equal(w.path, subtest.path, "path")
|
||||
checkWallet(w, subtest);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const { test } of checks) {
|
||||
it(`computes the neutered HD keys by enxtended public key: ${ test.name }`, function() {
|
||||
for (const subtest of test.nodes) {
|
||||
const w = HDNodeWallet.fromExtendedKey(subtest.xpub);
|
||||
assert.ok(w instanceof HDNodeVoidWallet, "instanceof HDNodeVoidWallet");
|
||||
checkWallet(w, subtest);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
65
src.ts/_tests/test-wallet-json.ts
Normal file
65
src.ts/_tests/test-wallet-json.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import assert from "assert";
|
||||
|
||||
import { loadTests } from "./utils.js";
|
||||
|
||||
import type { TestCaseWallet } from "./types.js";
|
||||
|
||||
import {
|
||||
decryptCrowdsaleJson,
|
||||
decryptKeystoreJson, decryptKeystoreJsonSync,
|
||||
Wallet
|
||||
} from "../index.js";
|
||||
|
||||
describe("Tests JSON Wallet Formats", function() {
|
||||
const tests = loadTests<TestCaseWallet>("wallets");
|
||||
tests.forEach((test) => {
|
||||
if (test.type !== "crowdsale") { return; }
|
||||
it(`tests decrypting Crowdsale JSON: ${ test.name }`, async function() {
|
||||
const password = Buffer.from(test.password.substring(2), "hex");
|
||||
const account = decryptCrowdsaleJson(test.content, password);
|
||||
assert.equal(account.address, test.address, "address");
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach((test) => {
|
||||
if (test.type !== "keystore") { return; }
|
||||
it(`tests decrypting Keystore JSON (sync): ${ test.name }`, function() {
|
||||
this.timeout(20000);
|
||||
const password = Buffer.from(test.password.substring(2), "hex");
|
||||
const account = decryptKeystoreJsonSync(test.content, password);
|
||||
//console.log(account);
|
||||
assert.equal(account.address, test.address, "address");
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach((test) => {
|
||||
if (test.type !== "keystore") { return; }
|
||||
it(`tests decrypting Keystore JSON (async): ${ test.name }`, async function() {
|
||||
this.timeout(20000);
|
||||
const password = Buffer.from(test.password.substring(2), "hex");
|
||||
const account = await decryptKeystoreJson(test.content, password);
|
||||
//console.log(account);
|
||||
assert.equal(account.address, test.address, "address");
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach((test) => {
|
||||
it(`tests decrypting JSON (sync): ${ test.name }`, function() {
|
||||
this.timeout(20000);
|
||||
const password = Buffer.from(test.password.substring(2), "hex");
|
||||
const wallet = Wallet.fromEncryptedJsonSync(test.content, password);
|
||||
//console.log(wallet);
|
||||
assert.equal(wallet.address, test.address, "address");
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach((test) => {
|
||||
it(`tests decrypting JSON (async): ${ test.name }`, async function() {
|
||||
this.timeout(20000);
|
||||
const password = Buffer.from(test.password.substring(2), "hex");
|
||||
const wallet = await Wallet.fromEncryptedJson(test.content, password);
|
||||
//console.log(wallet);
|
||||
assert.equal(wallet.address, test.address, "address");
|
||||
});
|
||||
});
|
||||
});
|
143
src.ts/_tests/test-wallet-mnemonic.ts
Normal file
143
src.ts/_tests/test-wallet-mnemonic.ts
Normal file
@ -0,0 +1,143 @@
|
||||
|
||||
import assert from "assert";
|
||||
|
||||
import { sha256 } from "../crypto/index.js";
|
||||
import { toUtf8Bytes } from "../utils/utf8.js";
|
||||
|
||||
import { wordlists } from "../wordlists/wordlists.js";
|
||||
|
||||
import { Mnemonic } from "../index.js";
|
||||
|
||||
import { loadTests } from "./utils.js";
|
||||
import type { TestCaseMnemonic } from "./types.js";
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
function fromHex(hex: string): string {
|
||||
const data = Buffer.from(hex.substring(2), "hex");
|
||||
return decoder.decode(data);
|
||||
}
|
||||
|
||||
function repeat(text: string, length: number): Array<string> {
|
||||
const result = [ ];
|
||||
while (result.length < length) { result.push(text); }
|
||||
return result;
|
||||
}
|
||||
|
||||
describe("Tests Mnemonics", function() {
|
||||
const tests = loadTests<TestCaseMnemonic>("mnemonics");
|
||||
|
||||
function runTest(phrase: string, mnemonic: Mnemonic, test: TestCaseMnemonic): void {
|
||||
assert.ok(Mnemonic.isValidMnemonic(phrase, mnemonic.wordlist), "isValidMnemonic");
|
||||
if (test.locale === "en") {
|
||||
assert.ok(Mnemonic.isValidMnemonic(phrase), "isValidMnemonic (default)");
|
||||
}
|
||||
|
||||
assert.equal(mnemonic.wordlist.locale, test.locale, "locale");
|
||||
|
||||
assert.equal(mnemonic.entropy, test.entropy, "entropy");
|
||||
assert.equal(mnemonic.computeSeed(), test.seed, "seed");
|
||||
assert.equal(sha256(toUtf8Bytes(phrase)), test.phraseHash, "phraseHash")
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
const wordlist = wordlists[test.locale];
|
||||
|
||||
it(`computes mnemonic from phrase: ${ test.name }`, function() {
|
||||
if (wordlist == null) {
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
|
||||
const phrase = fromHex(test.phrase);
|
||||
const password = fromHex(test.password);
|
||||
const mnemonic = Mnemonic.fromPhrase(phrase, password, wordlist);
|
||||
runTest(phrase, mnemonic, test);
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
const wordlist = wordlists[test.locale];
|
||||
|
||||
it(`computes mnemonic from entropy: ${ test.name }`, function() {
|
||||
if (wordlist == null) {
|
||||
this.skip();
|
||||
return;
|
||||
}
|
||||
const phrase = fromHex(test.phrase);
|
||||
const password = fromHex(test.password);
|
||||
const mnemonic = Mnemonic.fromEntropy(test.entropy, password, wordlist);
|
||||
runTest(phrase, mnemonic, test);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Tests Bad Mnemonics Fail", function() {
|
||||
|
||||
const badLengths = [
|
||||
repeat("abandon", 9), // 9 words; too short
|
||||
repeat("abandon", 16), // 16 words; not congruent to 0 mod 3
|
||||
repeat("abandon", 27), // 27 words; too long
|
||||
];
|
||||
|
||||
for (const _phrase of badLengths) {
|
||||
const phrase = _phrase.join(" ");
|
||||
it(`fails on invalid mnemonic length: ${ _phrase.length }`, function() {
|
||||
assert.ok(!Mnemonic.isValidMnemonic(phrase));
|
||||
assert.throws(function() {
|
||||
Mnemonic.fromPhrase(phrase);
|
||||
}, function(error: any) {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^invalid mnemonic length/) &&
|
||||
error.argument === "mnemonic" &&
|
||||
error.value === "[ REDACTED ]");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
it("fails on invalid mnemonic word", function() {
|
||||
const phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon wagmi";
|
||||
assert.ok(!Mnemonic.isValidMnemonic(phrase));
|
||||
assert.throws(function() {
|
||||
Mnemonic.fromPhrase(phrase);
|
||||
}, function(error: any) {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^invalid mnemonic word at index 11/) &&
|
||||
error.argument === "mnemonic" &&
|
||||
error.value === "[ REDACTED ]");
|
||||
});
|
||||
});
|
||||
|
||||
it("fails on invalid mnemonic checksum", function() {
|
||||
const phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon";
|
||||
assert.ok(!Mnemonic.isValidMnemonic(phrase));
|
||||
assert.throws(function() {
|
||||
Mnemonic.fromPhrase(phrase);
|
||||
}, function(error: any) {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^invalid mnemonic checksum/) &&
|
||||
error.argument === "mnemonic" &&
|
||||
error.value === "[ REDACTED ]");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
const badEntropyLengths = [
|
||||
repeat("42", 12), //12 bytes; too short
|
||||
repeat("42", 15), // 16 bytes; not congruent to 0 mod 4
|
||||
repeat("42", 36), // 36 bytes; too long
|
||||
];
|
||||
|
||||
for (const _entropy of badEntropyLengths) {
|
||||
const entropy = "0x" + _entropy.join("");
|
||||
it(`fails on invalid entropy length: ${ _entropy.length }`, function() {
|
||||
assert.throws(function() {
|
||||
Mnemonic.fromEntropy(entropy);
|
||||
}, function(error: any) {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^invalid entropy size/) &&
|
||||
error.argument === "entropy" &&
|
||||
error.value === "[ REDACTED ]");
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
83
src.ts/_tests/test-wallet.ts
Normal file
83
src.ts/_tests/test-wallet.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import assert from "assert";
|
||||
|
||||
import { loadTests } from "./utils.js";
|
||||
|
||||
import type {
|
||||
TestCaseAccount, TestCaseTypedData, TestCaseTransaction
|
||||
} from "./types.js";
|
||||
|
||||
|
||||
import { Wallet } from "../index.js";
|
||||
|
||||
|
||||
describe("Test Private Key Wallet", function() {
|
||||
const tests = loadTests<TestCaseAccount>("accounts");
|
||||
|
||||
tests.forEach(({ name, privateKey, address }) => {
|
||||
it(`creates wallet: ${ name }`, function() {
|
||||
const wallet = new Wallet(privateKey);
|
||||
assert.equal(wallet.privateKey, privateKey);
|
||||
assert.equal(wallet.address, address);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Test Transaction Signing", function() {
|
||||
const tests = loadTests<TestCaseTransaction>("transactions");
|
||||
for (const test of tests) {
|
||||
it(`tests signing a legacy transaction: ${ test.name }`, async function() {
|
||||
const wallet = new Wallet(test.privateKey);
|
||||
const txData = Object.assign({ }, test.transaction, { type: 0, accessList: undefined, maxFeePerGas: undefined, maxPriorityFeePerGas: undefined });
|
||||
|
||||
// Use the testcase sans the chainId for a legacy test
|
||||
if (txData.chainId != null && parseInt(txData.chainId) != 0) { txData.chainId = "0x00"; }
|
||||
|
||||
const signed = await wallet.signTransaction(txData);
|
||||
assert.equal(signed, test.signedLegacy, "signedLegacy");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
if (!test.signedEip155) { continue; }
|
||||
it(`tests signing an EIP-155 transaction: ${ test.name }`, async function() {
|
||||
const wallet = new Wallet(test.privateKey);
|
||||
const txData = Object.assign({ }, test.transaction, { type: 0, accessList: undefined, maxFeePerGas: undefined, maxPriorityFeePerGas: undefined });
|
||||
const signed = await wallet.signTransaction(txData);
|
||||
assert.equal(signed, test.signedEip155, "signedEip155");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`tests signing a Berlin transaction: ${ test.name }`, async function() {
|
||||
const wallet = new Wallet(test.privateKey);
|
||||
const txData = Object.assign({ }, test.transaction, { type: 1, maxFeePerGas: undefined, maxPriorityFeePerGas: undefined });
|
||||
const signed = await wallet.signTransaction(txData);
|
||||
assert.equal(signed, test.signedBerlin, "signedBerlin");
|
||||
});
|
||||
}
|
||||
|
||||
for (const test of tests) {
|
||||
it(`tests signing a London transaction: ${ test.name }`, async function() {
|
||||
const wallet = new Wallet(test.privateKey);
|
||||
const txData = Object.assign({ }, test.transaction, { type: 2 });
|
||||
const signed = await wallet.signTransaction(txData);
|
||||
assert.equal(signed, test.signedLondon, "signedLondon");
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Test Message Signing (EIP-191)", function() {
|
||||
});
|
||||
|
||||
describe("Test Typed-Data Signing (EIP-712)", function() {
|
||||
const tests = loadTests<TestCaseTypedData>("typed-data");
|
||||
for (const test of tests) {
|
||||
const { privateKey, signature } = test;
|
||||
if (privateKey == null || signature == null) { continue; }
|
||||
it(`tests signing typed-data: ${ test.name }`, async function() {
|
||||
const wallet = new Wallet(privateKey);
|
||||
const sig = await wallet.signTypedData(test.domain, test.types, test.data);
|
||||
assert.equal(sig, signature, "signature");
|
||||
});
|
||||
}
|
||||
});
|
79
src.ts/_tests/test-wordlists.ts
Normal file
79
src.ts/_tests/test-wordlists.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import assert from 'assert';
|
||||
|
||||
import { wordlists } from "../index.js";
|
||||
|
||||
import { loadTests } from "./utils.js";
|
||||
|
||||
import type { TestCaseWordlist } from "./types.js";
|
||||
|
||||
|
||||
describe('Check Wordlists', function() {
|
||||
const tests = loadTests<TestCaseWordlist>("wordlists");
|
||||
|
||||
tests.forEach((test) => {
|
||||
let wordlist = wordlists[test.locale];
|
||||
if (wordlist == null) { return; }
|
||||
|
||||
it(`matches wordlists: ${ test.locale }`, function() {
|
||||
const words = test.content.split('\n');
|
||||
|
||||
let check = "";
|
||||
for (let i = 0; i < 2048; i++) {
|
||||
let word = wordlist.getWord(i);
|
||||
check += (word + "\n");
|
||||
assert.equal(word, words[i]);
|
||||
assert.equal(wordlist.getWordIndex(word), i);
|
||||
}
|
||||
|
||||
assert.equal(check, test.content);
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach((test) => {
|
||||
let wordlist = wordlists[test.locale];
|
||||
if (wordlist == null) { return; }
|
||||
|
||||
it (`splitting and joining are equivalent: ${ test.locale }`, function() {
|
||||
const words: Array<string> = [ ];
|
||||
for (let i = 0; i < 12; i++) {
|
||||
words.push(wordlist.getWord(i));
|
||||
}
|
||||
|
||||
const phrase = wordlist.join(words);
|
||||
|
||||
const words2 = wordlist.split(phrase);
|
||||
const phrase2 = wordlist.join(words2);
|
||||
|
||||
assert.deepStrictEqual(words2, words, "split words");
|
||||
assert.deepStrictEqual(phrase2, phrase, "re-joined words");
|
||||
});
|
||||
});
|
||||
|
||||
tests.forEach((test) => {
|
||||
let wordlist = wordlists[test.locale];
|
||||
if (wordlist == null) { return; }
|
||||
|
||||
it(`handles out-of-range values: ${ test.locale }`, function() {
|
||||
assert.equal(wordlist.getWordIndex("foobar"), -1);
|
||||
|
||||
assert.throws(() => {
|
||||
wordlist.getWord(-1);
|
||||
}, (error: any) => {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^invalid word index/) &&
|
||||
error.argument === "index" &&
|
||||
error.value === -1);
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
wordlist.getWord(2048);
|
||||
}, (error: any) => {
|
||||
return (error.code === "INVALID_ARGUMENT" &&
|
||||
error.message.match(/^invalid word index/) &&
|
||||
error.argument === "index" &&
|
||||
error.value === 2048);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
245
src.ts/_tests/types.ts
Normal file
245
src.ts/_tests/types.ts
Normal file
@ -0,0 +1,245 @@
|
||||
|
||||
|
||||
export type TestCaseAbiVerbose = {
|
||||
type: "address" | "hexstring" | "number" | "string",
|
||||
value: string
|
||||
} | {
|
||||
type: "boolean",
|
||||
value: boolean
|
||||
} | {
|
||||
type: "array",
|
||||
value: Array<TestCaseAbiVerbose>
|
||||
} | {
|
||||
type: "object",
|
||||
value: Array<TestCaseAbiVerbose>
|
||||
}
|
||||
|
||||
export interface TestCaseAbi {
|
||||
name: string;
|
||||
type: string;
|
||||
value: any;
|
||||
verbose: TestCaseAbiVerbose;
|
||||
bytecode: string;
|
||||
encoded: string;
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// address
|
||||
|
||||
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;
|
||||
}>;
|
||||
};
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
// crypto
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// hash
|
||||
|
||||
export interface TestCaseHash {
|
||||
name: string;
|
||||
data: string;
|
||||
sha256: string;
|
||||
sha512: string;
|
||||
ripemd160: string;
|
||||
keccak256: string;
|
||||
}
|
||||
|
||||
export interface TestCaseNamehash {
|
||||
name: string;
|
||||
ensName: string;
|
||||
namehash: string;
|
||||
}
|
||||
|
||||
export interface TestCaseTypedDataDomain {
|
||||
name?: string;
|
||||
version?: string;
|
||||
chainId?: number;
|
||||
verifyingContract?: string;
|
||||
salt?: string;
|
||||
}
|
||||
|
||||
export interface TestCaseTypedDataType {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface TestCaseTypedData {
|
||||
name: string;
|
||||
|
||||
domain: TestCaseTypedDataDomain;
|
||||
primaryType: string;
|
||||
types: Record<string, Array<TestCaseTypedDataType>>
|
||||
data: any;
|
||||
|
||||
encoded: string;
|
||||
digest: string;
|
||||
|
||||
privateKey?: string;
|
||||
signature?: string;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
// rlp
|
||||
|
||||
export type NestedHexString = string | Array<string | NestedHexString>;
|
||||
|
||||
export interface TestCaseRlp {
|
||||
name: string;
|
||||
encoded: string;
|
||||
decoded: NestedHexString;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
// transaction
|
||||
|
||||
export interface TestCaseTransactionTx {
|
||||
to?: string;
|
||||
nonce?: number;
|
||||
gasLimit?: string;
|
||||
|
||||
gasPrice?: string;
|
||||
maxFeePerGas?: string;
|
||||
maxPriorityFeePerGas?: string;
|
||||
|
||||
data?: string;
|
||||
value?: string;
|
||||
|
||||
accessList?: Array<{ address: string, storageKeys: Array<string> }>;
|
||||
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
export interface TestCaseTransactionSig {
|
||||
r: string;
|
||||
s: string;
|
||||
v: string;
|
||||
}
|
||||
|
||||
export interface TestCaseTransaction {
|
||||
name: string;
|
||||
transaction: TestCaseTransactionTx;
|
||||
privateKey: string;
|
||||
|
||||
unsignedLegacy: string;
|
||||
signedLegacy: string;
|
||||
unsignedEip155: string;
|
||||
signedEip155: string;
|
||||
unsignedBerlin: string;
|
||||
signedBerlin: string;
|
||||
unsignedLondon: string;
|
||||
signedLondon: string;
|
||||
|
||||
signatureLegacy: TestCaseTransactionSig;
|
||||
signatureEip155: TestCaseTransactionSig;
|
||||
signatureBerlin: TestCaseTransactionSig;
|
||||
signatureLondon: TestCaseTransactionSig;
|
||||
}
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
// wallet
|
||||
|
||||
export interface TestCaseMnemonicNode {
|
||||
path: string,
|
||||
chainCode: string;
|
||||
depth: number;
|
||||
index: number;
|
||||
parentFingerprint: string;
|
||||
fingerprint: string;
|
||||
publicKey: string;
|
||||
privateKey: string;
|
||||
xpriv: string;
|
||||
xpub: string;
|
||||
}
|
||||
|
||||
export interface TestCaseMnemonic {
|
||||
name: string;
|
||||
phrase: string;
|
||||
phraseHash: string;
|
||||
password: string;
|
||||
locale: string;
|
||||
entropy: string;
|
||||
seed: string;
|
||||
nodes: Array<TestCaseMnemonicNode>;
|
||||
};
|
||||
|
||||
export interface TestCaseWallet {
|
||||
name: string;
|
||||
filename: string,
|
||||
type: string;
|
||||
address: string;
|
||||
password: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/////////////////////////////
|
||||
// wordlists
|
||||
|
||||
export interface TestCaseWordlist {
|
||||
name: string;
|
||||
filename: string,
|
||||
locale: string;
|
||||
content: string;
|
||||
}
|
32
src.ts/_tests/utils.ts
Normal file
32
src.ts/_tests/utils.ts
Normal file
@ -0,0 +1,32 @@
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
export function log(context: any, text: string): void {
|
||||
if (context && context.test && typeof(context.test._ethersLog) === "function") {
|
||||
context.test._ethersLog(text);
|
||||
} else {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
1
src.ts/_version.ts
Normal file
1
src.ts/_version.ts
Normal file
@ -0,0 +1 @@
|
||||
export const version = "6.0.0-beta-exports.0";
|
95
src.ts/abi/abi-coder.ts
Normal file
95
src.ts/abi/abi-coder.ts
Normal file
@ -0,0 +1,95 @@
|
||||
// See: https://github.com/ethereum/wiki/wiki/Ethereum-Contract-ABI
|
||||
|
||||
import { logger } from "../utils/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 "../utils/index.js";
|
||||
|
||||
|
||||
const paramTypeBytes = new RegExp(/^bytes([0-9]*)$/);
|
||||
const paramTypeNumber = new RegExp(/^(u?int)([0-9]*)$/);
|
||||
|
||||
export class AbiCoder {
|
||||
|
||||
#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, loose));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const defaultAbiCoder: AbiCoder = new AbiCoder();
|
37
src.ts/abi/bytes32.ts
Normal file
37
src.ts/abi/bytes32.ts
Normal file
@ -0,0 +1,37 @@
|
||||
|
||||
import { zeroPadBytes } from "../utils/data.js";
|
||||
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
import { toUtf8Bytes, toUtf8String } from "../utils/utf8.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
export function formatBytes32String(text: string): string {
|
||||
|
||||
// Get the bytes
|
||||
const bytes = toUtf8Bytes(text);
|
||||
|
||||
// Check we have room for null-termination
|
||||
if (bytes.length > 31) { throw new Error("bytes32 string must be less than 32 bytes"); }
|
||||
|
||||
// Zero-pad (implicitly null-terminates)
|
||||
return zeroPadBytes(bytes, 32);
|
||||
}
|
||||
|
||||
export function parseBytes32String(_bytes: BytesLike): string {
|
||||
const data = logger.getBytes(_bytes, "bytes");
|
||||
|
||||
// Must be 32 bytes with a null-termination
|
||||
if (data.length !== 32) { throw new Error("invalid bytes32 - not 32 bytes long"); }
|
||||
if (data[31] !== 0) { throw new Error("invalid bytes32 string - no null terminator"); }
|
||||
|
||||
// Find the null termination
|
||||
let length = 31;
|
||||
while (data[length - 1] === 0) { length--; }
|
||||
|
||||
// Determine the string value
|
||||
return toUtf8String(data.slice(0, length));
|
||||
}
|
||||
|
318
src.ts/abi/coders/abstract-coder.ts
Normal file
318
src.ts/abi/coders/abstract-coder.ts
Normal file
@ -0,0 +1,318 @@
|
||||
|
||||
import { toArray, toBigInt, toNumber } from "../../utils/maths.js";
|
||||
import { concat, hexlify } from "../../utils/data.js";
|
||||
import { defineProperties } from "../../utils/properties.js";
|
||||
|
||||
import { logger } from "../../utils/logger.js";
|
||||
|
||||
import type { BigNumberish, BytesLike } from "../../utils/index.js";
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
toJSON(): any {
|
||||
if (this.#indices.length === this.length) {
|
||||
const result: Record<string, any> = { };
|
||||
for (const key of this.#indices.keys()) {
|
||||
result[key] = ths.getValue(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
*/
|
||||
|
||||
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 = logger.getBytesCopy(concat([ Padding.slice(bytes.length % WordSize), bytes ]));
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
|
||||
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(logger.getBytesCopy(writer.data));
|
||||
}
|
||||
|
||||
// Arrayish item; pad on the right to *nearest* WordSize
|
||||
writeBytes(value: BytesLike): number {
|
||||
let bytes = logger.getBytesCopy(value);
|
||||
const paddingOffset = bytes.length % WordSize;
|
||||
if (paddingOffset) {
|
||||
bytes = logger.getBytesCopy(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;
|
||||
|
||||
constructor(data: BytesLike, allowLoose?: boolean) {
|
||||
defineProperties<Reader>(this, { allowLoose: !!allowLoose });
|
||||
|
||||
this.#data = logger.getBytesCopy(data);
|
||||
|
||||
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); }
|
||||
|
||||
#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: logger.getBytesCopy(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.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));
|
||||
}
|
||||
}
|
33
src.ts/abi/coders/address.ts
Normal file
33
src.ts/abi/coders/address.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import { getAddress } from "../../address/index.js";
|
||||
import { toHex } from "../../utils/maths.js";
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
25
src.ts/abi/coders/anonymous.ts
Normal file
25
src.ts/abi/coders/anonymous.ts
Normal 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);
|
||||
}
|
||||
}
|
208
src.ts/abi/coders/array.ts
Normal file
208
src.ts/abi/coders/array.ts
Normal file
@ -0,0 +1,208 @@
|
||||
import { defineProperties } from "../../utils/properties.js";
|
||||
import { isError } from "../../utils/errors.js";
|
||||
import { logger } from "../../utils/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 unpack(reader, coders);
|
||||
}
|
||||
}
|
||||
|
25
src.ts/abi/coders/boolean.ts
Normal file
25
src.ts/abi/coders/boolean.ts
Normal 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.readValue();
|
||||
}
|
||||
}
|
38
src.ts/abi/coders/bytes.ts
Normal file
38
src.ts/abi/coders/bytes.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { hexlify } from "../../utils/data.js";
|
||||
|
||||
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 = logger.getBytesCopy(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 hexlify(super.decode(reader));
|
||||
}
|
||||
}
|
36
src.ts/abi/coders/fixed-bytes.ts
Normal file
36
src.ts/abi/coders/fixed-bytes.ts
Normal file
@ -0,0 +1,36 @@
|
||||
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { hexlify } from "../../utils/data.js";
|
||||
import { defineProperties } from "../../utils/properties.js";
|
||||
|
||||
import { Typed } from "../typed.js";
|
||||
import { Coder } from "./abstract-coder.js";
|
||||
|
||||
import type { BytesLike } from "../../utils/index.js";
|
||||
|
||||
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 = logger.getBytesCopy(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 hexlify(reader.readBytes(this.size));
|
||||
}
|
||||
}
|
25
src.ts/abi/coders/null.ts
Normal file
25
src.ts/abi/coders/null.ts
Normal 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 null;
|
||||
}
|
||||
}
|
65
src.ts/abi/coders/number.ts
Normal file
65
src.ts/abi/coders/number.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { fromTwos, mask, toTwos } from "../../utils/maths.js";
|
||||
import { defineProperties } from "../../utils/properties.js";
|
||||
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { Typed } from "../typed.js";
|
||||
import { Coder, WordSize } from "./abstract-coder.js";
|
||||
|
||||
import type { BigNumberish } from "../../utils/index.js";
|
||||
|
||||
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 value;
|
||||
}
|
||||
}
|
||||
|
26
src.ts/abi/coders/string.ts
Normal file
26
src.ts/abi/coders/string.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { toUtf8Bytes, toUtf8String } from "../../utils/utf8.js";
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
66
src.ts/abi/coders/tuple.ts
Normal file
66
src.ts/abi/coders/tuple.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { defineProperties } from "../../utils/properties.js";
|
||||
|
||||
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 unpack(reader, this.coders);
|
||||
}
|
||||
}
|
||||
|
1091
src.ts/abi/fragments.ts
Normal file
1091
src.ts/abi/fragments.ts
Normal file
File diff suppressed because it is too large
Load Diff
38
src.ts/abi/index.ts
Normal file
38
src.ts/abi/index.ts
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
export {
|
||||
AbiCoder,
|
||||
defaultAbiCoder
|
||||
} from "./abi-coder.js";
|
||||
|
||||
export { formatBytes32String, parseBytes32String } from "./bytes32.js";
|
||||
|
||||
export {
|
||||
ConstructorFragment,
|
||||
ErrorFragment,
|
||||
EventFragment,
|
||||
Fragment,
|
||||
FunctionFragment,
|
||||
ParamType
|
||||
} from "./fragments.js";
|
||||
|
||||
export {
|
||||
checkResultErrors,
|
||||
Indexed,
|
||||
Interface,
|
||||
LogDescription,
|
||||
Result,
|
||||
TransactionDescription
|
||||
} from "./interface.js";
|
||||
|
||||
export { Typed } from "./typed.js";
|
||||
|
||||
export type {
|
||||
JsonFragment,
|
||||
JsonFragmentType,
|
||||
} from "./fragments.js";
|
||||
|
||||
|
||||
export type {
|
||||
InterfaceAbi,
|
||||
} from "./interface.js";
|
||||
|
905
src.ts/abi/interface.ts
Normal file
905
src.ts/abi/interface.ts
Normal file
@ -0,0 +1,905 @@
|
||||
import { concat, dataSlice, hexlify, zeroPadValue, isHexString } from "../utils/data.js";
|
||||
import { keccak256 } from "../crypto/index.js"
|
||||
import { id } from "../hash/index.js"
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { toHex } from "../utils/maths.js";
|
||||
|
||||
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 { Typed } from "./typed.js";
|
||||
|
||||
import type { BigNumberish, BytesLike } from "../utils/index.js";
|
||||
|
||||
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();
|
||||
|
||||
if (BuiltinErrors[selector]) {
|
||||
return ErrorFragment.fromString(BuiltinErrors[selector].signature);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (key === "Error") { return ErrorFragment.fromString("error Error(string)"); }
|
||||
if (key === "Panic") { return ErrorFragment.fromString("error Panic(uint256)"); }
|
||||
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
|
||||
key = ErrorFragment.fromString(key).format()
|
||||
if (key === "Error(string)") { return ErrorFragment.fromString("error Error(string)"); }
|
||||
if (key === "Panic(uint256)") { return ErrorFragment.fromString("error Panic(uint256)"); }
|
||||
|
||||
const result = this.#errors.get(key);
|
||||
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 = logger.getBytesCopy(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)); }
|
||||
|
||||
// @TODO: Use the coders for this; to properly support tuples, etc.
|
||||
const encodeTopic = (param: ParamType, value: any): string => {
|
||||
if (param.type === "string") {
|
||||
return id(value);
|
||||
} else if (param.type === "bytes") {
|
||||
return keccak256(hexlify(value));
|
||||
}
|
||||
|
||||
if (param.type === "bool" && typeof(value) === "boolean") {
|
||||
value = (value ? "0x01": "0x00");
|
||||
}
|
||||
|
||||
if (param.type.match(/^u?int/)) {
|
||||
value = toHex(value);
|
||||
}
|
||||
|
||||
// Check addresses are valid
|
||||
if (param.type === "address") { this.#abiCoder.encode( [ "address" ], [ value ]); }
|
||||
return zeroPadValue(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);
|
||||
}
|
||||
}
|
254
src.ts/abi/typed.ts
Normal file
254
src.ts/abi/typed.ts
Normal file
@ -0,0 +1,254 @@
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
|
||||
import type { Addressable } from "../address/index.js";
|
||||
import type { BigNumberish, BytesLike } from "../utils/index.js";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
125
src.ts/address/address.ts
Normal file
125
src.ts/address/address.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { keccak256 } from "../crypto/keccak.js";
|
||||
import { logger } from "../utils/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 = logger.getBytes(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;
|
||||
}
|
54
src.ts/address/checks.ts
Normal file
54
src.ts/address/checks.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
import { getAddress } from "./address.js";
|
||||
|
||||
import type { Addressable, AddressLike, NameResolver } from "./index.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;
|
||||
}
|
||||
|
||||
async function checkAddress(target: any, promise: Promise<null | string>): Promise<string> {
|
||||
const result = await promise;
|
||||
if (result == null || result === "0x0000000000000000000000000000000000000000") {
|
||||
if (typeof(target) === "string") {
|
||||
return logger.throwError("unconfigured name", "UNCONFIGURED_NAME", { value: target });
|
||||
}
|
||||
return logger.throwArgumentError("invalid AddressLike value; did not resolve to a value address", "target", target);
|
||||
}
|
||||
return getAddress(result);
|
||||
}
|
||||
|
||||
// Resolves an Ethereum address, ENS name or Addressable object,
|
||||
// throwing if the result is null.
|
||||
export function resolveAddress(target: AddressLike, resolver?: null | NameResolver): string | Promise<string> {
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
|
||||
return checkAddress(target, resolver.resolveName(target));
|
||||
|
||||
} else if (isAddressable(target)) {
|
||||
return checkAddress(target, target.getAddress());
|
||||
|
||||
} else if (typeof(target.then) === "function") {
|
||||
return checkAddress(target, target);
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("unsupported addressable value", "target", target);
|
||||
}
|
40
src.ts/address/contract-address.ts
Normal file
40
src.ts/address/contract-address.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { keccak256 } from "../crypto/keccak.js";
|
||||
import { concat, dataSlice, encodeRlp, logger } from "../utils/index.js";
|
||||
|
||||
import { getAddress } from "./address.js";
|
||||
|
||||
import type { BigNumberish, BytesLike } from "../utils/index.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))
|
||||
}
|
16
src.ts/address/index.ts
Normal file
16
src.ts/address/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export interface Addressable {
|
||||
getAddress(): Promise<string>;
|
||||
}
|
||||
|
||||
export type AddressLike = string | Promise<string> | Addressable;
|
||||
|
||||
export interface NameResolver {
|
||||
resolveName(name: string): Promise<null | string>;
|
||||
}
|
||||
|
||||
export { getAddress, getIcapAddress } from "./address.js";
|
||||
|
||||
export { getCreateAddress, getCreate2Address } from "./contract-address.js";
|
||||
|
||||
|
||||
export { isAddressable, isAddress, resolveAddress } from "./checks.js";
|
6
src.ts/constants/addresses.ts
Normal file
6
src.ts/constants/addresses.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
/**
|
||||
* A constant for the zero address.
|
||||
*/
|
||||
export const ZeroAddress = "0x0000000000000000000000000000000000000000";
|
||||
|
5
src.ts/constants/hashes.ts
Normal file
5
src.ts/constants/hashes.ts
Normal file
@ -0,0 +1,5 @@
|
||||
/**
|
||||
* A constant for the zero hash.
|
||||
*/
|
||||
export const ZeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000";
|
||||
|
14
src.ts/constants/index.ts
Normal file
14
src.ts/constants/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export { ZeroAddress } from "./addresses.js";
|
||||
export { ZeroHash } from "./hashes.js";
|
||||
export {
|
||||
NegativeOne,
|
||||
Zero,
|
||||
One,
|
||||
Two,
|
||||
N,
|
||||
WeiPerEther,
|
||||
MaxUint256,
|
||||
MinInt256,
|
||||
MaxInt256
|
||||
} from "./numbers.js";
|
||||
export { EtherSymbol, MessagePrefix } from "./strings.js";
|
57
src.ts/constants/numbers.ts
Normal file
57
src.ts/constants/numbers.ts
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
/**
|
||||
* A constant for the BigInt of -1.
|
||||
*/
|
||||
const NegativeOne = BigInt(-1);
|
||||
|
||||
/**
|
||||
* A constant for the BigInt of 0.
|
||||
*/
|
||||
const Zero = BigInt(0);
|
||||
|
||||
/**
|
||||
* A constant for the BigInt of 1.
|
||||
*/
|
||||
const One = BigInt(1);
|
||||
|
||||
/**
|
||||
* A constant for the BigInt of 2.
|
||||
*/
|
||||
const Two = BigInt(2);
|
||||
|
||||
/**
|
||||
* A constant for the order N for the secp256k1 curve.
|
||||
*/
|
||||
const N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
|
||||
|
||||
/**
|
||||
* A constant for the number of wei in a single ether.
|
||||
*/
|
||||
const WeiPerEther = BigInt("1000000000000000000");
|
||||
|
||||
/**
|
||||
* A constant for the maximum value for a ``uint256``.
|
||||
*/
|
||||
const MaxUint256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
||||
|
||||
/**
|
||||
* A constant for the minimum value for an ``int256``.
|
||||
*/
|
||||
const MinInt256 = BigInt("0x8000000000000000000000000000000000000000000000000000000000000000") * NegativeOne;
|
||||
|
||||
/**
|
||||
* A constant for the maximum value for an ``int256``.
|
||||
*/
|
||||
const MaxInt256 = BigInt("0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
||||
|
||||
export {
|
||||
NegativeOne,
|
||||
Zero,
|
||||
One,
|
||||
Two,
|
||||
N,
|
||||
WeiPerEther,
|
||||
MaxUint256,
|
||||
MinInt256,
|
||||
MaxInt256,
|
||||
};
|
9
src.ts/constants/strings.ts
Normal file
9
src.ts/constants/strings.ts
Normal file
@ -0,0 +1,9 @@
|
||||
// NFKC (composed) // (decomposed)
|
||||
|
||||
/**
|
||||
* A constant for the ether symbol (normalized using NFKC).
|
||||
*/
|
||||
export const EtherSymbol = "\u039e"; // "\uD835\uDF63";
|
||||
|
||||
|
||||
export const MessagePrefix = "\x19Ethereum Signed Message:\n";
|
763
src.ts/contract/contract.ts
Normal file
763
src.ts/contract/contract.ts
Normal file
@ -0,0 +1,763 @@
|
||||
import { resolveAddress } from "../address/index.js";
|
||||
import { Interface, Typed } from "../abi/index.js";
|
||||
import {
|
||||
defineProperties, isCallException, isHexString, resolveProperties, logger
|
||||
} from "../utils/index.js";
|
||||
import { copyRequest, Log, TransactionResponse } from "../providers/index.js";
|
||||
|
||||
import {
|
||||
ContractEventPayload,
|
||||
ContractTransactionResponse,
|
||||
EventLog
|
||||
} from "./wrappers.js";
|
||||
|
||||
import type { EventFragment, FunctionFragment, InterfaceAbi, ParamType, Result } from "../abi/index.js";
|
||||
import type { Addressable } from "../address/index.js";
|
||||
import type { EventEmitterable, Listener } from "../utils/index.js";
|
||||
import type {
|
||||
BlockTag, ContractRunner, Provider, TransactionRequest, TopicFilter
|
||||
} from "../providers/index.js";
|
||||
|
||||
import type {
|
||||
ContractEventName,
|
||||
ContractInterface,
|
||||
ContractMethodArgs,
|
||||
BaseContractMethod,
|
||||
ContractMethod,
|
||||
ContractEventArgs,
|
||||
ContractEvent,
|
||||
ContractTransaction,
|
||||
DeferredTopicFilter
|
||||
} from "./types.js";
|
||||
|
||||
interface ContractRunnerCaller extends ContractRunner {
|
||||
call: (tx: TransactionRequest) => Promise<string>;
|
||||
}
|
||||
|
||||
interface ContractRunnerEstimater extends ContractRunner {
|
||||
estimateGas: (tx: TransactionRequest) => Promise<bigint>;
|
||||
}
|
||||
|
||||
interface ContractRunnerSender extends ContractRunner {
|
||||
sendTransaction: (tx: TransactionRequest) => Promise<TransactionResponse>;
|
||||
}
|
||||
|
||||
interface ContractRunnerResolver extends ContractRunner {
|
||||
resolveName: (name: string | Addressable) => Promise<null | string>;
|
||||
}
|
||||
|
||||
function canCall(value: any): value is ContractRunnerCaller {
|
||||
return (value && typeof(value.call) === "function");
|
||||
}
|
||||
|
||||
function canEstimate(value: any): value is ContractRunnerEstimater {
|
||||
return (value && typeof(value.estimateGas) === "function");
|
||||
}
|
||||
|
||||
function canResolve(value: any): value is ContractRunnerResolver {
|
||||
return (value && typeof(value.resolveName) === "function");
|
||||
}
|
||||
|
||||
function canSend(value: any): value is ContractRunnerSender {
|
||||
return (value && typeof(value.sendTransaction) === "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;
|
||||
}
|
||||
|
||||
function getProvider(value: null | ContractRunner): null | Provider {
|
||||
if (value == null) { return null; }
|
||||
return value.provider || 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 = ""; // Investigate!
|
||||
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 = this._contract.runner;
|
||||
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 = getProvider(this._contract.runner);
|
||||
return new ContractTransactionResponse(this._contract.interface, 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 = ""; // @TODO: investigate
|
||||
|
||||
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 provider = getProvider(contract.runner);
|
||||
if (!provider) {
|
||||
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; }
|
||||
provider.on(filter, listener);
|
||||
started = true;
|
||||
};
|
||||
const stop = () => {
|
||||
if (!started) { return; }
|
||||
provider.off(filter, listener);
|
||||
started = false;
|
||||
};
|
||||
sub = { tag, listeners: [ ], start, stop };
|
||||
subs.set(tag, sub);
|
||||
}
|
||||
return sub;
|
||||
}
|
||||
|
||||
// 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> {
|
||||
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));
|
||||
}
|
||||
try {
|
||||
listener.call(contract, ...passArgs);
|
||||
} catch (error) { }
|
||||
return !once;
|
||||
});
|
||||
return (count > 0);
|
||||
}
|
||||
|
||||
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 = getProvider(runner);
|
||||
deployTx = new ContractTransactionResponse(this.interface, 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; }
|
||||
|
||||
async getDeployedCode(): Promise<null | string> {
|
||||
const provider = getProvider(this.runner);
|
||||
if (!provider) {
|
||||
return logger.throwError("runner does not support .provider", "UNSUPPORTED_OPERATION", {
|
||||
operation: "getDeployedCode"
|
||||
});
|
||||
}
|
||||
|
||||
const code = await provider.getCode(await this.getAddress());
|
||||
if (code === "0x") { return null; }
|
||||
return code;
|
||||
}
|
||||
|
||||
async waitForDeployment(): Promise<this> {
|
||||
// We have the deployement transaction; just use that (throws if deployement fails)
|
||||
const deployTx = this.deploymentTransaction();
|
||||
if (deployTx) {
|
||||
await deployTx.wait();
|
||||
return this;
|
||||
}
|
||||
|
||||
// Check for code
|
||||
const code = await this.getDeployedCode();
|
||||
if (code != null) { return this; }
|
||||
|
||||
// Make sure we can subscribe to a provider event
|
||||
const provider = getProvider(this.runner);
|
||||
if (provider == null) {
|
||||
return logger.throwError("contract runner does not support .provider", "UNSUPPORTED_OPERATION", {
|
||||
operation: "waitForDeployment"
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const checkCode = async () => {
|
||||
try {
|
||||
const code = await this.getDeployedCode();
|
||||
if (code != null) { return resolve(this); }
|
||||
provider.once("block", checkCode);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
};
|
||||
checkCode();
|
||||
});
|
||||
}
|
||||
|
||||
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 queryTransaction(hash: string): Promise<Array<EventLog>> {
|
||||
// Is this useful?
|
||||
throw new Error("@TODO");
|
||||
}
|
||||
|
||||
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 provider = getProvider(this.runner);
|
||||
if (!provider) {
|
||||
return logger.throwError("contract runner does not have a provider", "UNSUPPORTED_OPERATION", {
|
||||
operation: "queryFilter"
|
||||
});
|
||||
}
|
||||
|
||||
return (await provider.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() { }
|
97
src.ts/contract/factory.ts
Normal file
97
src.ts/contract/factory.ts
Normal file
@ -0,0 +1,97 @@
|
||||
|
||||
import { Interface } from "../abi/index.js";
|
||||
import { getCreateAddress } from "../address/index.js";
|
||||
import { concat, defineProperties, hexlify, logger } from "../utils/index.js";
|
||||
|
||||
import { BaseContract, copyOverrides, resolveArgs } from "./contract.js";
|
||||
|
||||
import type { InterfaceAbi } from "../abi/index.js";
|
||||
import type { ContractRunner } from "../providers/index.js";
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
import type {
|
||||
ContractInterface, ContractMethodArgs, 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?: null | 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: null | 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);
|
||||
}
|
||||
}
|
19
src.ts/contract/index.ts
Normal file
19
src.ts/contract/index.ts
Normal 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, ContractTransaction,
|
||||
DeferredTopicFilter, Overrides
|
||||
} from "./types.js";
|
83
src.ts/contract/types.ts
Normal file
83
src.ts/contract/types.ts
Normal file
@ -0,0 +1,83 @@
|
||||
import type {
|
||||
EventFragment, FunctionFragment, Result, Typed
|
||||
} from "../abi/index.js";
|
||||
import type {
|
||||
CallRequest, PreparedRequest, TopicFilter
|
||||
} from "../providers/index.js";
|
||||
|
||||
import type { ContractTransactionResponse } from "./wrappers.js";
|
||||
|
||||
|
||||
// The types of events a Contract can listen for
|
||||
export type ContractEventName = string | ContractEvent | TopicFilter;
|
||||
|
||||
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;
|
||||
};
|
99
src.ts/contract/wrappers.ts
Normal file
99
src.ts/contract/wrappers.ts
Normal file
@ -0,0 +1,99 @@
|
||||
import {
|
||||
Block, Log, TransactionReceipt, TransactionResponse
|
||||
} from "../providers/index.js";
|
||||
import { defineProperties, EventPayload } from "../utils/index.js";
|
||||
|
||||
import type { EventFragment, Interface, Result } from "../abi/index.js";
|
||||
import type { Listener } from "../utils/index.js";
|
||||
import type {
|
||||
Provider
|
||||
} from "../providers/index.js";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
73
src.ts/crypto/crypto-browser.ts
Normal file
73
src.ts/crypto/crypto-browser.ts
Normal file
@ -0,0 +1,73 @@
|
||||
/* Browser Crypto Shims */
|
||||
|
||||
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 "../utils/logger.js";
|
||||
|
||||
|
||||
declare global {
|
||||
interface Window { }
|
||||
|
||||
const window: Window;
|
||||
const self: Window;
|
||||
}
|
||||
|
||||
|
||||
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();
|
||||
const 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 (!Number.isInteger(length) || length <= 0 || length > 1024) {
|
||||
logger.throwArgumentError("invalid length", "length", length);
|
||||
}
|
||||
|
||||
const result = new Uint8Array(length);
|
||||
crypto.getRandomValues(result);
|
||||
return result;
|
||||
}
|
4
src.ts/crypto/crypto.ts
Normal file
4
src.ts/crypto/crypto.ts
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
export {
|
||||
createHash, createHmac, pbkdf2Sync, randomBytes
|
||||
} from "crypto";
|
26
src.ts/crypto/hmac.ts
Normal file
26
src.ts/crypto/hmac.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { createHmac } from "./crypto.js";
|
||||
import { hexlify, logger } from "../utils/index.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
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);
|
45
src.ts/crypto/index.ts
Normal file
45
src.ts/crypto/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
// 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 { SigningKey } from "./signing-key.js";
|
||||
export { Signature } from "./signature.js";
|
||||
|
||||
function lock(): void {
|
||||
computeHmac.lock();
|
||||
keccak256.lock();
|
||||
pbkdf2.lock();
|
||||
randomBytes.lock();
|
||||
ripemd160.lock();
|
||||
scrypt.lock();
|
||||
scryptSync.lock();
|
||||
sha256.lock();
|
||||
sha512.lock();
|
||||
}
|
||||
|
||||
export { lock };
|
||||
|
||||
/////////////////////////////
|
||||
// Types
|
||||
|
||||
export type { ProgressCallback } from "./scrypt.js";
|
||||
|
||||
export type { SignatureLike } from "./signature.js";
|
26
src.ts/crypto/keccak.ts
Normal file
26
src.ts/crypto/keccak.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { keccak_256 } from "@noble/hashes/sha3";
|
||||
|
||||
import { hexlify, logger } from "../utils/index.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
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);
|
27
src.ts/crypto/pbkdf2.ts
Normal file
27
src.ts/crypto/pbkdf2.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { pbkdf2Sync } from "./crypto.js";
|
||||
|
||||
import { hexlify, logger } from "../utils/index.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
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);
|
21
src.ts/crypto/random.ts
Normal file
21
src.ts/crypto/random.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { randomBytes as crypto_random } from "./crypto.js";
|
||||
|
||||
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);
|
26
src.ts/crypto/ripemd160.ts
Normal file
26
src.ts/crypto/ripemd160.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import { ripemd160 as noble_ripemd160 } from "@noble/hashes/ripemd160";
|
||||
|
||||
import { hexlify, logger } from "../utils/index.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
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);
|
48
src.ts/crypto/scrypt.ts
Normal file
48
src.ts/crypto/scrypt.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { scrypt as _nobleSync, scryptAsync as _nobleAsync } from "@noble/hashes/scrypt";
|
||||
|
||||
import { hexlify as H, logger } from "../utils/index.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
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);
|
44
src.ts/crypto/sha2.ts
Normal file
44
src.ts/crypto/sha2.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { createHash } from "./crypto.js";
|
||||
|
||||
import { hexlify, logger } from "../utils/index.js";
|
||||
|
||||
import type { BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
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);
|
261
src.ts/crypto/signature.ts
Normal file
261
src.ts/crypto/signature.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import { ZeroHash } from "../constants/index.js";
|
||||
import {
|
||||
concat, dataLength, getStore, hexlify, isHexString, logger, setStore
|
||||
} from "../utils/index.js";
|
||||
|
||||
import type { BigNumberish, BytesLike, Freezable, Frozen } from "../utils/index.js";
|
||||
|
||||
|
||||
// Constants
|
||||
const BN_0 = BigInt(0);
|
||||
const BN_1 = BigInt(1);
|
||||
const BN_2 = BigInt(2);
|
||||
const BN_27 = BigInt(27);
|
||||
const BN_28 = BigInt(28);
|
||||
const BN_35 = BigInt(35);
|
||||
|
||||
|
||||
const _guard = { };
|
||||
|
||||
|
||||
export type SignatureLike = Signature | string | {
|
||||
r: string;
|
||||
s: string;
|
||||
v: BigNumberish;
|
||||
yParity?: 0 | 1;
|
||||
yParityAndS?: string;
|
||||
} | {
|
||||
r: string;
|
||||
yParityAndS: string;
|
||||
yParity?: 0 | 1;
|
||||
s?: string;
|
||||
v?: number;
|
||||
} | {
|
||||
r: string;
|
||||
s: string;
|
||||
yParity: 0 | 1;
|
||||
v?: BigNumberish;
|
||||
yParityAndS?: string;
|
||||
};
|
||||
|
||||
|
||||
export class Signature implements Freezable<Signature> {
|
||||
#props: { r: string, s: string, v: 27 | 28, networkV: null | bigint };
|
||||
|
||||
get r(): string { return getStore(this.#props, "r"); }
|
||||
set r(value: BytesLike) {
|
||||
if (dataLength(value) !== 32) {
|
||||
logger.throwArgumentError("invalid r", "value", value);
|
||||
}
|
||||
setStore(this.#props, "r", hexlify(value));
|
||||
}
|
||||
|
||||
get s(): string { return getStore(this.#props, "s"); }
|
||||
set s(value: BytesLike) {
|
||||
if (dataLength(value) !== 32) {
|
||||
logger.throwArgumentError("invalid r", "value", value);
|
||||
} else if (logger.getBytes(value)[0] & 0x80) {
|
||||
logger.throwArgumentError("non-canonical s", "value", value);
|
||||
}
|
||||
setStore(this.#props, "s", hexlify(value));
|
||||
}
|
||||
|
||||
get v(): 27 | 28 { return getStore(this.#props, "v"); }
|
||||
set v(value: BigNumberish) {
|
||||
const v = logger.getNumber(value, "value");
|
||||
if (v !== 27 && v !== 28) { throw new Error("@TODO"); }
|
||||
setStore(this.#props, "v", v);
|
||||
}
|
||||
|
||||
get networkV(): null | bigint { return getStore(this.#props, "networkV"); }
|
||||
get legacyChainId(): null | bigint {
|
||||
const v = this.networkV;
|
||||
if (v == null) { return null; }
|
||||
return Signature.getChainId(v);
|
||||
}
|
||||
|
||||
get yParity(): 0 | 1 {
|
||||
if (this.v === 27) { return 0; }
|
||||
return 1;
|
||||
/*
|
||||
// When v is 0 or 1 it is the recid directly
|
||||
if (this.v.isZero()) { return 0; }
|
||||
if (this.v.eq(1)) { return 1; }
|
||||
|
||||
// Otherwise, odd (e.g. 27) is 0 and even (e.g. 28) is 1
|
||||
return this.v.and(1).isZero() ? 1: 0;
|
||||
*/
|
||||
}
|
||||
|
||||
get yParityAndS(): string {
|
||||
// The EIP-2098 compact representation
|
||||
const yParityAndS = logger.getBytes(this.s);
|
||||
if (this.yParity) { yParityAndS[0] |= 0x80; }
|
||||
return hexlify(yParityAndS);
|
||||
}
|
||||
|
||||
get compactSerialized(): string {
|
||||
return concat([ this.r, this.yParityAndS ]);
|
||||
}
|
||||
|
||||
get serialized(): string {
|
||||
return concat([ this.r, this.s, (this.yParity ? "0x1c": "0x1b") ]);
|
||||
}
|
||||
|
||||
constructor(guard: any, r: string, s: string, v: 27 | 28) {
|
||||
logger.assertPrivate(guard, _guard, "Signature");
|
||||
this.#props = { r, s, v, networkV: null };
|
||||
}
|
||||
|
||||
[Symbol.for('nodejs.util.inspect.custom')]() {
|
||||
return `Signature { r: "${ this.r }", s: "${ this.s }", yParity: ${ this.yParity }, networkV: ${ this.networkV } }`;
|
||||
}
|
||||
|
||||
clone(): Signature {
|
||||
const clone = new Signature(_guard, this.r, this.s, this.v);
|
||||
if (this.networkV) { setStore(clone.#props, "networkV", this.networkV); }
|
||||
return clone;
|
||||
}
|
||||
|
||||
freeze(): Frozen<Signature> {
|
||||
Object.freeze(this.#props);
|
||||
return this;
|
||||
}
|
||||
|
||||
isFrozen(): boolean {
|
||||
return Object.isFrozen(this.#props);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
const networkV = this.networkV;
|
||||
return {
|
||||
_type: "signature",
|
||||
networkV: ((networkV != null) ? networkV.toString(): null),
|
||||
r: this.r, s: this.s, v: this.v,
|
||||
};
|
||||
}
|
||||
|
||||
static create(): Signature {
|
||||
return new Signature(_guard, ZeroHash, ZeroHash, 27);
|
||||
}
|
||||
|
||||
// Get the chain ID from an EIP-155 v
|
||||
static getChainId(v: BigNumberish): bigint {
|
||||
const bv = logger.getBigInt(v, "v");
|
||||
|
||||
// The v is not an EIP-155 v, so it is the unspecified chain ID
|
||||
if ((bv == BN_27) || (bv == BN_28)) { return BN_0; }
|
||||
|
||||
// Bad value for an EIP-155 v
|
||||
if (bv < BN_35) { logger.throwArgumentError("invalid EIP-155 v", "v", v); }
|
||||
|
||||
return (bv - BN_35) / BN_2;
|
||||
}
|
||||
|
||||
// Get the EIP-155 v transformed for a given chainId
|
||||
static getChainIdV(chainId: BigNumberish, v: 27 | 28): bigint {
|
||||
return (logger.getBigInt(chainId) * BN_2) + BigInt(35 + v - 27);
|
||||
}
|
||||
|
||||
// Convert an EIP-155 v into a normalized v
|
||||
static getNormalizedV(v: BigNumberish): 27 | 28 {
|
||||
const bv = logger.getBigInt(v);
|
||||
|
||||
if (bv == BN_0) { return 27; }
|
||||
if (bv == BN_1) { return 28; }
|
||||
|
||||
// Otherwise, EIP-155 v means odd is 27 and even is 28
|
||||
return (bv & BN_1) ? 27: 28;
|
||||
}
|
||||
|
||||
static from(sig: SignatureLike): Signature {
|
||||
const throwError = (message: string) => {
|
||||
return logger.throwArgumentError(message, "signature", sig);
|
||||
};
|
||||
|
||||
if (typeof(sig) === "string") {
|
||||
const bytes = logger.getBytes(sig, "signature");
|
||||
if (bytes.length === 64) {
|
||||
const r = hexlify(bytes.slice(0, 32));
|
||||
const s = bytes.slice(32, 64);
|
||||
const v = (s[0] & 0x80) ? 28: 27;
|
||||
s[0] &= 0x7f;
|
||||
return new Signature(_guard, r, hexlify(s), v);
|
||||
}
|
||||
|
||||
if (dataLength(sig) !== 65) {
|
||||
const r = hexlify(sig.slice(0, 32));
|
||||
const s = bytes.slice(32, 64);
|
||||
if (s[0] & 0x80) { throwError("non-canonical s"); }
|
||||
const v = Signature.getNormalizedV(bytes[64]);
|
||||
return new Signature(_guard, r, hexlify(s), v);
|
||||
}
|
||||
|
||||
return throwError("invlaid raw signature length");
|
||||
}
|
||||
|
||||
if (sig instanceof Signature) { return sig.clone(); }
|
||||
|
||||
// Get r
|
||||
const r = sig.r;
|
||||
if (r == null) { throwError("missing r"); }
|
||||
if (!isHexString(r, 32)) { throwError("invalid r"); }
|
||||
|
||||
// Get s; by any means necessary (we check consistency below)
|
||||
const s = (function(s?: string, yParityAndS?: string) {
|
||||
if (s != null) {
|
||||
if (!isHexString(s, 32)) { throwError("invalid s"); }
|
||||
return s;
|
||||
}
|
||||
|
||||
if (yParityAndS != null) {
|
||||
if (!isHexString(yParityAndS, 32)) { throwError("invalid yParityAndS"); }
|
||||
const bytes = logger.getBytes(yParityAndS);
|
||||
bytes[0] &= 0x7f;
|
||||
return hexlify(bytes);
|
||||
}
|
||||
|
||||
return throwError("missing s");
|
||||
})(sig.s, sig.yParityAndS);
|
||||
if (logger.getBytes(s)[0] & 0x80) { throwError("non-canonical s"); }
|
||||
|
||||
// Get v; by any means necessary (we check consistency below)
|
||||
const { networkV, v } = (function(_v?: BigNumberish, yParityAndS?: string, yParity?: number): { networkV?: bigint, v: 27 | 28 } {
|
||||
if (_v != null) {
|
||||
const v = logger.getBigInt(_v);
|
||||
return {
|
||||
networkV: ((v >= BN_35) ? v: undefined),
|
||||
v: Signature.getNormalizedV(v)
|
||||
};
|
||||
}
|
||||
|
||||
if (yParityAndS != null) {
|
||||
if (!isHexString(yParityAndS, 32)) { throwError("invalid yParityAndS"); }
|
||||
return { v: ((logger.getBytes(yParityAndS)[0] & 0x80) ? 28: 27) };
|
||||
}
|
||||
|
||||
if (yParity != null) {
|
||||
switch (yParity) {
|
||||
case 0: return { v: 27 };
|
||||
case 1: return { v: 28 };
|
||||
}
|
||||
return throwError("invalid yParity");
|
||||
}
|
||||
|
||||
return throwError("missing v");
|
||||
})(sig.v, sig.yParityAndS, sig.yParity);
|
||||
|
||||
const result = new Signature(_guard, r, s, v);
|
||||
if (networkV) { setStore(result.#props, "networkV", networkV); }
|
||||
|
||||
// If multiple of v, yParity, yParityAndS we given, check they match
|
||||
if ("yParity" in sig && sig.yParity !== result.yParity) {
|
||||
throwError("yParity mismatch");
|
||||
} else if ("yParityAndS" in sig && sig.yParityAndS !== result.yParityAndS) {
|
||||
throwError("yParityAndS mismatch");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
130
src.ts/crypto/signing-key.ts
Normal file
130
src.ts/crypto/signing-key.ts
Normal file
@ -0,0 +1,130 @@
|
||||
|
||||
import * as secp256k1 from "@noble/secp256k1";
|
||||
|
||||
import { computeHmac } from "../crypto/index.js";
|
||||
import { concat, hexlify, toHex, logger } from "../utils/index.js";
|
||||
|
||||
import { Signature } from "./signature.js";
|
||||
|
||||
import type { BytesLike, Frozen } from "../utils/index.js";
|
||||
|
||||
import type { SignatureLike } from "./index.js";
|
||||
|
||||
|
||||
//const N = BigInt("0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141");
|
||||
|
||||
// Make noble-secp256k1 sync
|
||||
secp256k1.utils.hmacSha256Sync = function(key: Uint8Array, ...messages: Array<Uint8Array>): Uint8Array {
|
||||
return logger.getBytes(computeHmac("sha256", key, concat(messages)));
|
||||
}
|
||||
|
||||
export class SigningKey {
|
||||
#privateKey: string;
|
||||
|
||||
constructor(privateKey: BytesLike) {
|
||||
/* @TODO
|
||||
logger.assertArgument(() => {
|
||||
if (dataLength(privateKey) !== 32) { throw new Error("bad length"); }
|
||||
return toBigInt(privateKey) < N;
|
||||
}, "invalid private key", "privateKey", "[REDACTED]");
|
||||
*/
|
||||
this.#privateKey = hexlify(privateKey);
|
||||
}
|
||||
|
||||
get privateKey(): string { return this.#privateKey; }
|
||||
get publicKey(): string { return SigningKey.computePublicKey(this.#privateKey); }
|
||||
get compressedPublicKey(): string { return SigningKey.computePublicKey(this.#privateKey, true); }
|
||||
|
||||
sign(digest: BytesLike): Frozen<Signature> {
|
||||
/* @TODO
|
||||
logger.assertArgument(() => (dataLength(digest) === 32), "invalid digest length", "digest", digest);
|
||||
*/
|
||||
|
||||
const [ sigDer, recid ] = secp256k1.signSync(logger.getBytesCopy(digest), logger.getBytesCopy(this.#privateKey), {
|
||||
recovered: true,
|
||||
canonical: true
|
||||
});
|
||||
|
||||
const sig = secp256k1.Signature.fromHex(sigDer);
|
||||
|
||||
return Signature.from({
|
||||
r: toHex("0x" + sig.r.toString(16), 32),
|
||||
s: toHex("0x" + sig.s.toString(16), 32),
|
||||
v: (recid ? 0x1c: 0x1b)
|
||||
}).freeze();
|
||||
}
|
||||
|
||||
computeShardSecret(other: BytesLike): string {
|
||||
const pubKey = SigningKey.computePublicKey(other);
|
||||
return hexlify(secp256k1.getSharedSecret(logger.getBytesCopy(this.#privateKey), pubKey));
|
||||
}
|
||||
|
||||
static computePublicKey(key: BytesLike, compressed?: boolean): string {
|
||||
let bytes = logger.getBytes(key, "key");
|
||||
|
||||
if (bytes.length === 32) {
|
||||
const pubKey = secp256k1.getPublicKey(bytes, !!compressed);
|
||||
return hexlify(pubKey);
|
||||
}
|
||||
|
||||
if (bytes.length === 64) {
|
||||
const pub = new Uint8Array(65);
|
||||
pub[0] = 0x04;
|
||||
pub.set(bytes, 1);
|
||||
bytes = pub;
|
||||
}
|
||||
|
||||
const point = secp256k1.Point.fromHex(bytes);
|
||||
return hexlify(point.toRawBytes(compressed));
|
||||
}
|
||||
|
||||
static recoverPublicKey(digest: BytesLike, signature: SignatureLike): string {
|
||||
const sig = Signature.from(signature);
|
||||
const der = secp256k1.Signature.fromCompact(logger.getBytesCopy(concat([ sig.r, sig.s ]))).toDERRawBytes();
|
||||
|
||||
const pubKey = secp256k1.recoverPublicKey(logger.getBytesCopy(digest), der, sig.yParity);
|
||||
if (pubKey != null) { return hexlify(pubKey); }
|
||||
|
||||
return logger.throwArgumentError("invalid signautre for digest", "signature", signature);
|
||||
}
|
||||
|
||||
static _addPoints(p0: BytesLike, p1: BytesLike, compressed?: boolean): string {
|
||||
const pub0 = secp256k1.Point.fromHex(SigningKey.computePublicKey(p0).substring(2));
|
||||
const pub1 = secp256k1.Point.fromHex(SigningKey.computePublicKey(p1).substring(2));
|
||||
return "0x" + pub0.add(pub1).toHex(!!compressed)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
const key = new SigningKey("0x1234567890123456789012345678901234567890123456789012345678901234");
|
||||
console.log(key);
|
||||
console.log(key.sign("0x1234567890123456789012345678901234567890123456789012345678901234"));
|
||||
{
|
||||
const privKey = "0x1234567812345678123456781234567812345678123456781234567812345678";
|
||||
const signingKey = new SigningKey(privKey);
|
||||
console.log("0", signingKey, signingKey.publicKey, signingKey.publicKeyCompressed);
|
||||
|
||||
let pubKey = SigningKey.computePublicKey(privKey);
|
||||
let pubKeyComp = SigningKey.computePublicKey(privKey, true);
|
||||
let pubKeyRaw = "0x" + SigningKey.computePublicKey(privKey).substring(4);
|
||||
console.log("A", pubKey, pubKeyComp);
|
||||
|
||||
let a = SigningKey.computePublicKey(pubKey);
|
||||
let b = SigningKey.computePublicKey(pubKey, true);
|
||||
console.log("B", a, b);
|
||||
|
||||
a = SigningKey.computePublicKey(pubKeyComp);
|
||||
b = SigningKey.computePublicKey(pubKeyComp, true);
|
||||
console.log("C", a, b);
|
||||
|
||||
a = SigningKey.computePublicKey(pubKeyRaw);
|
||||
b = SigningKey.computePublicKey(pubKeyRaw, true);
|
||||
console.log("D", a, b);
|
||||
|
||||
const digest = "0x1122334411223344112233441122334411223344112233441122334411223344";
|
||||
const sig = signingKey.sign(digest);
|
||||
console.log("SS", sig, sig.r, sig.s, sig.yParity);
|
||||
|
||||
console.log("R", SigningKey.recoverPublicKey(digest, sig));
|
||||
}
|
||||
*/
|
147
src.ts/ethers.ts
Normal file
147
src.ts/ethers.ts
Normal file
@ -0,0 +1,147 @@
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
//
|
||||
|
||||
export { version } from "./_version.js";
|
||||
|
||||
export {
|
||||
formatBytes32String, parseBytes32String,
|
||||
|
||||
AbiCoder, defaultAbiCoder,
|
||||
ConstructorFragment, ErrorFragment, EventFragment, Fragment, FunctionFragment, ParamType,
|
||||
|
||||
checkResultErrors, Indexed, Interface, LogDescription, Result, TransactionDescription,
|
||||
Typed,
|
||||
} from "./abi/index.js";
|
||||
|
||||
export {
|
||||
getAddress, getIcapAddress,
|
||||
getCreateAddress, getCreate2Address
|
||||
} from "./address/index.js";
|
||||
|
||||
export {
|
||||
ZeroAddress,
|
||||
NegativeOne, Zero, One, Two, WeiPerEther, MaxUint256, MinInt256, MaxInt256,
|
||||
ZeroHash,
|
||||
EtherSymbol, MessagePrefix
|
||||
} from "./constants/index.js";
|
||||
|
||||
export {
|
||||
BaseContract, Contract,
|
||||
ContractFactory,
|
||||
ContractEventPayload, ContractTransactionReceipt, ContractTransactionResponse, EventLog,
|
||||
} from "./contract/index.js";
|
||||
|
||||
export {
|
||||
computeHmac,
|
||||
randomBytes,
|
||||
keccak256,
|
||||
ripemd160,
|
||||
sha256, sha512,
|
||||
pbkdf2,
|
||||
scrypt, scryptSync,
|
||||
lock,
|
||||
Signature, SigningKey
|
||||
} from "./crypto/index.js";
|
||||
|
||||
export {
|
||||
id,
|
||||
//isValidName, namehash, dnsEncode
|
||||
hashMessage,
|
||||
solidityPacked, solidityPackedKeccak256, solidityPackedSha256,
|
||||
TypedDataEncoder
|
||||
} from "./hash/index.js";
|
||||
|
||||
|
||||
export {
|
||||
accessListify,
|
||||
computeAddress, recoverAddress,
|
||||
Transaction
|
||||
} from "./transaction/index.js";
|
||||
|
||||
export {
|
||||
decodeBase58, encodeBase58,
|
||||
isCallException, isError,
|
||||
FetchRequest, FetchResponse,
|
||||
FixedFormat, FixedNumber, formatFixed, parseFixed,
|
||||
assertArgument, Logger, logger,
|
||||
fromTwos, toTwos, mask, toArray, toBigInt, toHex, toNumber,
|
||||
formatEther, parseEther, formatUnits, parseUnits,
|
||||
_toEscapedUtf8String, toUtf8Bytes, toUtf8CodePoints, toUtf8String,
|
||||
Utf8ErrorFuncs,
|
||||
decodeRlp, encodeRlp
|
||||
} from "./utils/index.js";
|
||||
|
||||
export {
|
||||
defaultPath,
|
||||
getAccountPath,
|
||||
HDNodeWallet, HDNodeVoidWallet, HDNodeWalletManager,
|
||||
isCrowdsaleJson, decryptCrowdsaleJson,
|
||||
isKeystoreJson, decryptKeystoreJsonSync, decryptKeystoreJson,
|
||||
encryptKeystoreJson,
|
||||
Mnemonic,
|
||||
Wallet
|
||||
} from "./wallet/index.js";
|
||||
|
||||
export {
|
||||
Wordlist, langEn, LangEn, wordlists, WordlistOwl, WordlistOwlA
|
||||
} from "./wordlists/index.js";
|
||||
|
||||
|
||||
|
||||
/////////////////////////////
|
||||
// Types
|
||||
|
||||
export type {
|
||||
JsonFragment, JsonFragmentType,
|
||||
InterfaceAbi,
|
||||
} from "./abi/index.js";
|
||||
|
||||
export type { Addressable } from "./address/index.js";
|
||||
|
||||
export type {
|
||||
ConstantContractMethod, ContractEvent, ContractEventArgs, ContractEventName,
|
||||
ContractInterface, ContractMethod, ContractMethodArgs, ContractTransaction,
|
||||
DeferredTopicFilter, Overrides
|
||||
} from "./contract/index.js";
|
||||
|
||||
export type { ProgressCallback, SignatureLike } from "./crypto/index.js";
|
||||
|
||||
export type { TypedDataDomain, TypedDataField } from "./hash/index.js";
|
||||
|
||||
export {
|
||||
FallbackProvider,
|
||||
JsonRpcApiProvider, JsonRpcProvider, JsonRpcSigner,
|
||||
|
||||
AlchemyProvider, AnkrProvider, CloudflareProvider, EtherscanProvider, InfuraProvider,
|
||||
//PocketProvider } from "./provider-pocket.js";
|
||||
|
||||
IpcSocketProvider, SocketProvider, WebSocketProvider,
|
||||
|
||||
Network
|
||||
} from "./providers/index.js";
|
||||
|
||||
export type {
|
||||
AccessList, AccessListish, AccessListSet,
|
||||
SignedTransaction, TransactionLike
|
||||
} from "./transaction/index.js";
|
||||
|
||||
export type {
|
||||
BytesLike,
|
||||
BigNumberish, Numeric,
|
||||
ErrorCode,
|
||||
Utf8ErrorFunc, UnicodeNormalizationForm, Utf8ErrorReason,
|
||||
RlpStructuredData,
|
||||
|
||||
GetUrlResponse,
|
||||
FetchRequestWithBody, FetchResponseWithBody,
|
||||
FetchPreflightFunc, FetchProcessFunc, FetchRetryFunc,
|
||||
FetchGatewayFunc, FetchGetUrlFunc
|
||||
} from "./utils/index.js";
|
||||
|
||||
export type {
|
||||
KeystoreAccountParams, KeystoreAccount,
|
||||
EncryptOptions
|
||||
} from "./wallet/index.js";
|
||||
|
6
src.ts/hash/id.ts
Normal file
6
src.ts/hash/id.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { keccak256 } from "../crypto/keccak.js";
|
||||
import { toUtf8Bytes } from "../utils/index.js";
|
||||
|
||||
export function id(value: string): string {
|
||||
return keccak256(toUtf8Bytes(value));
|
||||
}
|
10
src.ts/hash/index.ts
Normal file
10
src.ts/hash/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export { id } from "./id.js"
|
||||
export { isValidName, namehash, dnsEncode } from "./namehash.js";
|
||||
export { hashMessage } from "./message.js";
|
||||
export {
|
||||
solidityPacked, solidityPackedKeccak256, solidityPackedSha256
|
||||
} from "./solidity.js";
|
||||
export { TypedDataEncoder } from "./typed-data.js";
|
||||
|
||||
export type { TypedDataDomain, TypedDataField } from "./typed-data.js";
|
13
src.ts/hash/message.ts
Normal file
13
src.ts/hash/message.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { keccak256 } from "../crypto/keccak.js";
|
||||
import { MessagePrefix } from "../constants/index.js";
|
||||
import { concat, toUtf8Bytes } from "../utils/index.js";
|
||||
|
||||
|
||||
export function hashMessage(message: Uint8Array | string): string {
|
||||
if (typeof(message) === "string") { message = toUtf8Bytes(message); }
|
||||
return keccak256(concat([
|
||||
toUtf8Bytes(MessagePrefix),
|
||||
toUtf8Bytes(String(message.length)),
|
||||
message
|
||||
]));
|
||||
}
|
84
src.ts/hash/namehash.ts
Normal file
84
src.ts/hash/namehash.ts
Normal file
@ -0,0 +1,84 @@
|
||||
|
||||
import { keccak256 } from "../crypto/keccak.js";
|
||||
import { concat, hexlify, logger, toUtf8Bytes, toUtf8String } from "../utils/index.js";
|
||||
|
||||
|
||||
//import { ens_normalize } from "./ens-normalize/lib";
|
||||
// @TOOD:
|
||||
function ens_normalize(name: string): string {
|
||||
return name;
|
||||
}
|
||||
|
||||
const Zeros = new Uint8Array(32);
|
||||
Zeros.fill(0);
|
||||
|
||||
function checkComponent(comp: Uint8Array): Uint8Array {
|
||||
if (comp.length === 0) { throw new Error("invalid ENS name; empty component"); }
|
||||
return comp;
|
||||
}
|
||||
|
||||
function ensNameSplit(name: string): Array<Uint8Array> {
|
||||
const bytes = toUtf8Bytes(ens_normalize(name));
|
||||
const comps: Array<Uint8Array> = [ ];
|
||||
|
||||
if (name.length === 0) { return comps; }
|
||||
|
||||
let last = 0;
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
const d = bytes[i];
|
||||
|
||||
// A separator (i.e. "."); copy this component
|
||||
if (d === 0x2e) {
|
||||
comps.push(checkComponent(bytes.slice(last, i)));
|
||||
last = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// There was a stray separator at the end of the name
|
||||
if (last >= bytes.length) { throw new Error("invalid ENS name; empty component"); }
|
||||
|
||||
comps.push(checkComponent(bytes.slice(last)));
|
||||
return comps;
|
||||
}
|
||||
|
||||
export function ensNormalize(name: string): string {
|
||||
return ensNameSplit(name).map((comp) => toUtf8String(comp)).join(".");
|
||||
}
|
||||
|
||||
export function isValidName(name: string): boolean {
|
||||
try {
|
||||
return (ensNameSplit(name).length !== 0);
|
||||
} catch (error) { }
|
||||
return false;
|
||||
}
|
||||
|
||||
export function namehash(name: string): string {
|
||||
/* istanbul ignore if */
|
||||
if (typeof(name) !== "string") {
|
||||
logger.throwArgumentError("invalid ENS name; not a string", "name", name);
|
||||
}
|
||||
|
||||
let result: string | Uint8Array = Zeros;
|
||||
|
||||
const comps = ensNameSplit(name);
|
||||
while (comps.length) {
|
||||
result = keccak256(concat([ result, keccak256(<Uint8Array>(comps.pop()))] ));
|
||||
}
|
||||
|
||||
return hexlify(result);
|
||||
}
|
||||
|
||||
export function dnsEncode(name: string): string {
|
||||
return hexlify(concat(ensNameSplit(name).map((comp) => {
|
||||
// DNS does not allow components over 63 bytes in length
|
||||
if (comp.length > 63) {
|
||||
throw new Error("invalid DNS encoded entry; length exceeds 63 bytes");
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(comp.length + 1);
|
||||
bytes.set(comp, 1);
|
||||
bytes[0] = bytes.length - 1;
|
||||
return bytes;
|
||||
|
||||
}))) + "00";
|
||||
}
|
115
src.ts/hash/solidity.ts
Normal file
115
src.ts/hash/solidity.ts
Normal file
@ -0,0 +1,115 @@
|
||||
import {
|
||||
concat, dataLength, hexlify, logger, toArray, toTwos, toUtf8Bytes, zeroPadBytes, zeroPadValue
|
||||
} from "../utils/index.js";
|
||||
import { keccak256 as _keccak256 } from "../crypto/keccak.js";
|
||||
import { sha256 as _sha256 } from "../crypto/sha2.js";
|
||||
|
||||
|
||||
const regexBytes = new RegExp("^bytes([0-9]+)$");
|
||||
const regexNumber = new RegExp("^(u?int)([0-9]*)$");
|
||||
const regexArray = new RegExp("^(.*)\\[([0-9]*)\\]$");
|
||||
|
||||
|
||||
function _pack(type: string, value: any, isArray?: boolean): Uint8Array {
|
||||
switch(type) {
|
||||
case "address":
|
||||
if (isArray) { return logger.getBytes(zeroPadValue(value, 32)); }
|
||||
return logger.getBytes(value);
|
||||
case "string":
|
||||
return toUtf8Bytes(value);
|
||||
case "bytes":
|
||||
return logger.getBytes(value);
|
||||
case "bool":
|
||||
value = (!!value ? "0x01": "0x00");
|
||||
if (isArray) { return logger.getBytes(zeroPadValue(value, 32)); }
|
||||
return logger.getBytes(value);
|
||||
}
|
||||
|
||||
let match = type.match(regexNumber);
|
||||
if (match) {
|
||||
let size = parseInt(match[2] || "256")
|
||||
|
||||
if ((match[2] && String(size) !== match[2]) || (size % 8 !== 0) || size === 0 || size > 256) {
|
||||
return logger.throwArgumentError("invalid number type", "type", type)
|
||||
}
|
||||
|
||||
if (isArray) { size = 256; }
|
||||
|
||||
value = toTwos(value, size);
|
||||
|
||||
return logger.getBytes(zeroPadValue(toArray(value), size / 8));
|
||||
}
|
||||
|
||||
match = type.match(regexBytes);
|
||||
if (match) {
|
||||
const size = parseInt(match[1]);
|
||||
|
||||
if (String(size) !== match[1] || size === 0 || size > 32) {
|
||||
return logger.throwArgumentError("invalid bytes type", "type", type)
|
||||
}
|
||||
if (dataLength(value) !== size) {
|
||||
return logger.throwArgumentError(`invalid value for ${ type }`, "value", value)
|
||||
}
|
||||
if (isArray) { return logger.getBytes(zeroPadBytes(value, 32)); }
|
||||
return value;
|
||||
}
|
||||
|
||||
match = type.match(regexArray);
|
||||
if (match && Array.isArray(value)) {
|
||||
const baseType = match[1];
|
||||
const count = parseInt(match[2] || String(value.length));
|
||||
if (count != value.length) {
|
||||
logger.throwArgumentError(`invalid array length for ${ type }`, "value", value)
|
||||
}
|
||||
const result: Array<Uint8Array> = [];
|
||||
value.forEach(function(value) {
|
||||
result.push(_pack(baseType, value, true));
|
||||
});
|
||||
return logger.getBytes(concat(result));
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("invalid type", "type", type)
|
||||
}
|
||||
|
||||
// @TODO: Array Enum
|
||||
|
||||
export function solidityPacked(types: ReadonlyArray<string>, values: ReadonlyArray<any>) {
|
||||
if (types.length != values.length) {
|
||||
logger.throwArgumentError("wrong number of values; expected ${ types.length }", "values", values)
|
||||
}
|
||||
const tight: Array<Uint8Array> = [];
|
||||
types.forEach(function(type, index) {
|
||||
tight.push(_pack(type, values[index]));
|
||||
});
|
||||
return hexlify(concat(tight));
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the non-standard packed (tightly packed) keccak256 hash of
|
||||
* the values given the types.
|
||||
*
|
||||
* @param {Array<string>} types - The Solidity types to interpret each value as [default: bar]
|
||||
* @param {Array<any>} values - The values to pack
|
||||
*
|
||||
* @returns: {HexString} the hexstring of the hash
|
||||
* @example:
|
||||
* solidityPackedKeccak256([ "address", "uint" ], [ "0x1234", 45 ]);
|
||||
* //_result:
|
||||
*
|
||||
* @see https://docs.soliditylang.org/en/v0.8.14/abi-spec.html#non-standard-packed-mode
|
||||
*/
|
||||
export function solidityPackedKeccak256(types: ReadonlyArray<string>, values: ReadonlyArray<any>) {
|
||||
return _keccak256(solidityPacked(types, values));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Function, for fun
|
||||
*
|
||||
* @param foo - something fun
|
||||
*/
|
||||
export function test(foo: number | string): void {
|
||||
}
|
||||
|
||||
export function solidityPackedSha256(types: ReadonlyArray<string>, values: ReadonlyArray<any>) {
|
||||
return _sha256(solidityPacked(types, values));
|
||||
}
|
520
src.ts/hash/typed-data.ts
Normal file
520
src.ts/hash/typed-data.ts
Normal file
@ -0,0 +1,520 @@
|
||||
//import { TypedDataDomain, TypedDataField } from "@ethersproject/providerabstract-signer";
|
||||
import { getAddress } from "../address/index.js";
|
||||
import { keccak256 } from "../crypto/index.js";
|
||||
import {
|
||||
concat, defineProperties, hexlify, isHexString, mask, toHex, toTwos, zeroPadValue
|
||||
} from "../utils/index.js";
|
||||
|
||||
import { id } from "./id.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
import type { BigNumberish, BytesLike } from "../utils/index.js";
|
||||
|
||||
|
||||
const padding = new Uint8Array(32);
|
||||
padding.fill(0);
|
||||
|
||||
const BN__1 = BigInt(-1);
|
||||
const BN_0 = BigInt(0);
|
||||
const BN_1 = BigInt(1);
|
||||
const BN_MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
||||
|
||||
export interface TypedDataDomain {
|
||||
name?: string;
|
||||
version?: string;
|
||||
chainId?: BigNumberish;
|
||||
verifyingContract?: string;
|
||||
salt?: BytesLike;
|
||||
};
|
||||
|
||||
export interface TypedDataField {
|
||||
name: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
function hexPadRight(value: BytesLike) {
|
||||
const bytes = logger.getBytes(value);
|
||||
const padOffset = bytes.length % 32
|
||||
if (padOffset) {
|
||||
return concat([ bytes, padding.slice(padOffset) ]);
|
||||
}
|
||||
return hexlify(bytes);
|
||||
}
|
||||
|
||||
const hexTrue = toHex(BN_1, 32);
|
||||
const hexFalse = toHex(BN_0, 32);
|
||||
|
||||
const domainFieldTypes: Record<string, string> = {
|
||||
name: "string",
|
||||
version: "string",
|
||||
chainId: "uint256",
|
||||
verifyingContract: "address",
|
||||
salt: "bytes32"
|
||||
};
|
||||
|
||||
const domainFieldNames: Array<string> = [
|
||||
"name", "version", "chainId", "verifyingContract", "salt"
|
||||
];
|
||||
|
||||
function checkString(key: string): (value: any) => string {
|
||||
return function (value: any){
|
||||
if (typeof(value) !== "string") {
|
||||
logger.throwArgumentError(`invalid domain value for ${ JSON.stringify(key) }`, `domain.${ key }`, value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
const domainChecks: Record<string, (value: any) => any> = {
|
||||
name: checkString("name"),
|
||||
version: checkString("version"),
|
||||
chainId: function(value: any) {
|
||||
return logger.getBigInt(value, "domain.chainId");
|
||||
},
|
||||
verifyingContract: function(value: any) {
|
||||
try {
|
||||
return getAddress(value).toLowerCase();
|
||||
} catch (error) { }
|
||||
return logger.throwArgumentError(`invalid domain value "verifyingContract"`, "domain.verifyingContract", value);
|
||||
},
|
||||
salt: function(value: any) {
|
||||
const bytes = logger.getBytes(value, "domain.salt");
|
||||
if (bytes.length !== 32) {
|
||||
logger.throwArgumentError(`invalid domain value "salt"`, "domain.salt", value);
|
||||
}
|
||||
return hexlify(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseEncoder(type: string): null | ((value: any) => string) {
|
||||
// intXX and uintXX
|
||||
{
|
||||
const match = type.match(/^(u?)int(\d*)$/);
|
||||
if (match) {
|
||||
const signed = (match[1] === "");
|
||||
|
||||
const width = parseInt(match[2] || "256");
|
||||
if (width % 8 !== 0 || width > 256 || (match[2] && match[2] !== String(width))) {
|
||||
logger.throwArgumentError("invalid numeric width", "type", type);
|
||||
}
|
||||
|
||||
const boundsUpper = mask(BN_MAX_UINT256, signed ? (width - 1): width);
|
||||
const boundsLower = signed ? ((boundsUpper + BN_1) * BN__1): BN_0;
|
||||
|
||||
return function(_value: BigNumberish) {
|
||||
const value = logger.getBigInt(_value, "value");
|
||||
|
||||
if (value < boundsLower || value > boundsUpper) {
|
||||
logger.throwArgumentError(`value out-of-bounds for ${ type }`, "value", value);
|
||||
}
|
||||
|
||||
return toHex(toTwos(value, 256), 32);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// bytesXX
|
||||
{
|
||||
const match = type.match(/^bytes(\d+)$/);
|
||||
if (match) {
|
||||
const width = parseInt(match[1]);
|
||||
if (width === 0 || width > 32 || match[1] !== String(width)) {
|
||||
logger.throwArgumentError("invalid bytes width", "type", type);
|
||||
}
|
||||
|
||||
return function(value: BytesLike) {
|
||||
const bytes = logger.getBytes(value);
|
||||
if (bytes.length !== width) {
|
||||
logger.throwArgumentError(`invalid length for ${ type }`, "value", value);
|
||||
}
|
||||
return hexPadRight(value);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "address": return function(value: string) {
|
||||
return zeroPadValue(getAddress(value), 32);
|
||||
};
|
||||
case "bool": return function(value: boolean) {
|
||||
return ((!value) ? hexFalse: hexTrue);
|
||||
};
|
||||
case "bytes": return function(value: BytesLike) {
|
||||
return keccak256(value);
|
||||
};
|
||||
case "string": return function(value: string) {
|
||||
return id(value);
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function encodeType(name: string, fields: Array<TypedDataField>): string {
|
||||
return `${ name }(${ fields.map(({ name, type }) => (type + " " + name)).join(",") })`;
|
||||
}
|
||||
|
||||
export class TypedDataEncoder {
|
||||
readonly primaryType!: string;
|
||||
|
||||
readonly #types: string;
|
||||
get types(): Record<string, Array<TypedDataField>> {
|
||||
return JSON.parse(this.#types);
|
||||
}
|
||||
|
||||
readonly #fullTypes: Map<string, string>
|
||||
|
||||
readonly #encoderCache: Map<string, (value: any) => string>;
|
||||
|
||||
constructor(types: Record<string, Array<TypedDataField>>) {
|
||||
this.#types = JSON.stringify(types);
|
||||
this.#fullTypes = new Map();
|
||||
this.#encoderCache = new Map();
|
||||
|
||||
// Link struct types to their direct child structs
|
||||
const links: Map<string, Set<string>> = new Map();
|
||||
|
||||
// Link structs to structs which contain them as a child
|
||||
const parents: Map<string, Array<string>> = new Map();
|
||||
|
||||
// Link all subtypes within a given struct
|
||||
const subtypes: Map<string, Set<string>> = new Map();
|
||||
|
||||
Object.keys(types).forEach((type) => {
|
||||
links.set(type, new Set());
|
||||
parents.set(type, [ ]);
|
||||
subtypes.set(type, new Set());
|
||||
});
|
||||
|
||||
for (const name in types) {
|
||||
const uniqueNames: Set<string> = new Set();
|
||||
|
||||
for (const field of types[name]) {
|
||||
|
||||
// Check each field has a unique name
|
||||
if (uniqueNames.has(field.name)) {
|
||||
logger.throwArgumentError(`duplicate variable name ${ JSON.stringify(field.name) } in ${ JSON.stringify(name) }`, "types", types);
|
||||
}
|
||||
uniqueNames.add(field.name);
|
||||
|
||||
// Get the base type (drop any array specifiers)
|
||||
const baseType = (<any>(field.type.match(/^([^\x5b]*)(\x5b|$)/)))[1] || null;
|
||||
if (baseType === name) {
|
||||
logger.throwArgumentError(`circular type reference to ${ JSON.stringify(baseType) }`, "types", types);
|
||||
}
|
||||
|
||||
// Is this a base encoding type?
|
||||
const encoder = getBaseEncoder(baseType);
|
||||
if (encoder) { continue; }
|
||||
|
||||
if (!parents.has(baseType)) {
|
||||
logger.throwArgumentError(`unknown type ${ JSON.stringify(baseType) }`, "types", types);
|
||||
}
|
||||
|
||||
// Add linkage
|
||||
(parents.get(baseType) as Array<string>).push(name);
|
||||
(links.get(name) as Set<string>).add(baseType);
|
||||
}
|
||||
}
|
||||
|
||||
// Deduce the primary type
|
||||
const primaryTypes = Array.from(parents.keys()).filter((n) => ((parents.get(n) as Array<string>).length === 0));
|
||||
|
||||
if (primaryTypes.length === 0) {
|
||||
logger.throwArgumentError("missing primary type", "types", types);
|
||||
} else if (primaryTypes.length > 1) {
|
||||
logger.throwArgumentError(`ambiguous primary types or unused types: ${ primaryTypes.map((t) => (JSON.stringify(t))).join(", ") }`, "types", types);
|
||||
}
|
||||
|
||||
defineProperties<TypedDataEncoder>(this, { primaryType: primaryTypes[0] });
|
||||
|
||||
// Check for circular type references
|
||||
function checkCircular(type: string, found: Set<string>) {
|
||||
if (found.has(type)) {
|
||||
logger.throwArgumentError(`circular type reference to ${ JSON.stringify(type) }`, "types", types);
|
||||
}
|
||||
|
||||
found.add(type);
|
||||
|
||||
for (const child of (links.get(type) as Set<string>)) {
|
||||
if (!parents.has(child)) { continue; }
|
||||
|
||||
// Recursively check children
|
||||
checkCircular(child, found);
|
||||
|
||||
// Mark all ancestors as having this decendant
|
||||
for (const subtype of found) {
|
||||
(subtypes.get(subtype) as Set<string>).add(child);
|
||||
}
|
||||
}
|
||||
|
||||
found.delete(type);
|
||||
}
|
||||
checkCircular(this.primaryType, new Set());
|
||||
|
||||
// Compute each fully describe type
|
||||
for (const [ name, set ] of subtypes) {
|
||||
const st = Array.from(set);
|
||||
st.sort();
|
||||
this.#fullTypes.set(name, encodeType(name, types[name]) + st.map((t) => encodeType(t, types[t])).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
getEncoder(type: string): (value: any) => string {
|
||||
let encoder = this.#encoderCache.get(type);
|
||||
if (!encoder) {
|
||||
encoder = this.#getEncoder(type);
|
||||
this.#encoderCache.set(type, encoder);
|
||||
}
|
||||
return encoder;
|
||||
}
|
||||
|
||||
#getEncoder(type: string): (value: any) => string {
|
||||
|
||||
// Basic encoder type (address, bool, uint256, etc)
|
||||
{
|
||||
const encoder = getBaseEncoder(type);
|
||||
if (encoder) { return encoder; }
|
||||
}
|
||||
|
||||
// Array
|
||||
const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/);
|
||||
if (match) {
|
||||
const subtype = match[1];
|
||||
const subEncoder = this.getEncoder(subtype);
|
||||
const length = parseInt(match[3]);
|
||||
return (value: Array<any>) => {
|
||||
if (length >= 0 && value.length !== length) {
|
||||
logger.throwArgumentError("array length mismatch; expected length ${ arrayLength }", "value", value);
|
||||
}
|
||||
|
||||
let result = value.map(subEncoder);
|
||||
if (this.#fullTypes.has(subtype)) {
|
||||
result = result.map(keccak256);
|
||||
}
|
||||
|
||||
return keccak256(concat(result));
|
||||
};
|
||||
}
|
||||
|
||||
// Struct
|
||||
const fields = this.types[type];
|
||||
if (fields) {
|
||||
const encodedType = id(this.#fullTypes.get(type) as string);
|
||||
return (value: Record<string, any>) => {
|
||||
const values = fields.map(({ name, type }) => {
|
||||
const result = this.getEncoder(type)(value[name]);
|
||||
if (this.#fullTypes.has(type)) { return keccak256(result); }
|
||||
return result;
|
||||
});
|
||||
values.unshift(encodedType);
|
||||
return concat(values);
|
||||
}
|
||||
}
|
||||
|
||||
return logger.throwArgumentError(`unknown type: ${ type }`, "type", type);
|
||||
}
|
||||
|
||||
encodeType(name: string): string {
|
||||
const result = this.#fullTypes.get(name);
|
||||
if (!result) {
|
||||
return logger.throwArgumentError(`unknown type: ${ JSON.stringify(name) }`, "name", name);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
encodeData(type: string, value: any): string {
|
||||
return this.getEncoder(type)(value);
|
||||
}
|
||||
|
||||
hashStruct(name: string, value: Record<string, any>): string {
|
||||
return keccak256(this.encodeData(name, value));
|
||||
}
|
||||
|
||||
encode(value: Record<string, any>): string {
|
||||
return this.encodeData(this.primaryType, value);
|
||||
}
|
||||
|
||||
hash(value: Record<string, any>): string {
|
||||
return this.hashStruct(this.primaryType, value);
|
||||
}
|
||||
|
||||
_visit(type: string, value: any, callback: (type: string, data: any) => any): any {
|
||||
// Basic encoder type (address, bool, uint256, etc)
|
||||
{
|
||||
const encoder = getBaseEncoder(type);
|
||||
if (encoder) { return callback(type, value); }
|
||||
}
|
||||
|
||||
// Array
|
||||
const match = type.match(/^(.*)(\x5b(\d*)\x5d)$/);
|
||||
if (match) {
|
||||
const subtype = match[1];
|
||||
const length = parseInt(match[3]);
|
||||
if (length >= 0 && value.length !== length) {
|
||||
logger.throwArgumentError("array length mismatch; expected length ${ arrayLength }", "value", value);
|
||||
}
|
||||
return value.map((v: any) => this._visit(subtype, v, callback));
|
||||
}
|
||||
|
||||
// Struct
|
||||
const fields = this.types[type];
|
||||
if (fields) {
|
||||
return fields.reduce((accum, { name, type }) => {
|
||||
accum[name] = this._visit(type, value[name], callback);
|
||||
return accum;
|
||||
}, <Record<string, any>>{});
|
||||
}
|
||||
|
||||
return logger.throwArgumentError(`unknown type: ${ type }`, "type", type);
|
||||
}
|
||||
|
||||
visit(value: Record<string, any>, callback: (type: string, data: any) => any): any {
|
||||
return this._visit(this.primaryType, value, callback);
|
||||
}
|
||||
|
||||
static from(types: Record<string, Array<TypedDataField>>): TypedDataEncoder {
|
||||
return new TypedDataEncoder(types);
|
||||
}
|
||||
|
||||
static getPrimaryType(types: Record<string, Array<TypedDataField>>): string {
|
||||
return TypedDataEncoder.from(types).primaryType;
|
||||
}
|
||||
|
||||
static hashStruct(name: string, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): string {
|
||||
return TypedDataEncoder.from(types).hashStruct(name, value);
|
||||
}
|
||||
|
||||
static hashDomain(domain: TypedDataDomain): string {
|
||||
const domainFields: Array<TypedDataField> = [ ];
|
||||
for (const name in domain) {
|
||||
const type = domainFieldTypes[name];
|
||||
if (!type) {
|
||||
logger.throwArgumentError(`invalid typed-data domain key: ${ JSON.stringify(name) }`, "domain", domain);
|
||||
}
|
||||
domainFields.push({ name, type });
|
||||
}
|
||||
|
||||
domainFields.sort((a, b) => {
|
||||
return domainFieldNames.indexOf(a.name) - domainFieldNames.indexOf(b.name);
|
||||
});
|
||||
|
||||
return TypedDataEncoder.hashStruct("EIP712Domain", { EIP712Domain: domainFields }, domain);
|
||||
}
|
||||
|
||||
static encode(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): string {
|
||||
return concat([
|
||||
"0x1901",
|
||||
TypedDataEncoder.hashDomain(domain),
|
||||
TypedDataEncoder.from(types).hash(value)
|
||||
]);
|
||||
}
|
||||
|
||||
static hash(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): string {
|
||||
return keccak256(TypedDataEncoder.encode(domain, types, value));
|
||||
}
|
||||
|
||||
// Replaces all address types with ENS names with their looked up address
|
||||
static async resolveNames(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>, resolveName: (name: string) => Promise<string>): Promise<{ domain: TypedDataDomain, value: any }> {
|
||||
// Make a copy to isolate it from the object passed in
|
||||
domain = Object.assign({ }, domain);
|
||||
|
||||
// Look up all ENS names
|
||||
const ensCache: Record<string, string> = { };
|
||||
|
||||
// Do we need to look up the domain's verifyingContract?
|
||||
if (domain.verifyingContract && !isHexString(domain.verifyingContract, 20)) {
|
||||
ensCache[domain.verifyingContract] = "0x";
|
||||
}
|
||||
|
||||
// We are going to use the encoder to visit all the base values
|
||||
const encoder = TypedDataEncoder.from(types);
|
||||
|
||||
// Get a list of all the addresses
|
||||
encoder.visit(value, (type: string, value: any) => {
|
||||
if (type === "address" && !isHexString(value, 20)) {
|
||||
ensCache[value] = "0x";
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
||||
// Lookup each name
|
||||
for (const name in ensCache) {
|
||||
ensCache[name] = await resolveName(name);
|
||||
}
|
||||
|
||||
// Replace the domain verifyingContract if needed
|
||||
if (domain.verifyingContract && ensCache[domain.verifyingContract]) {
|
||||
domain.verifyingContract = ensCache[domain.verifyingContract];
|
||||
}
|
||||
|
||||
// Replace all ENS names with their address
|
||||
value = encoder.visit(value, (type: string, value: any) => {
|
||||
if (type === "address" && ensCache[value]) { return ensCache[value]; }
|
||||
return value;
|
||||
});
|
||||
|
||||
return { domain, value };
|
||||
}
|
||||
|
||||
static getPayload(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): any {
|
||||
// Validate the domain fields
|
||||
TypedDataEncoder.hashDomain(domain);
|
||||
|
||||
// Derive the EIP712Domain Struct reference type
|
||||
const domainValues: Record<string, any> = { };
|
||||
const domainTypes: Array<{ name: string, type:string }> = [ ];
|
||||
|
||||
domainFieldNames.forEach((name) => {
|
||||
const value = (<any>domain)[name];
|
||||
if (value == null) { return; }
|
||||
domainValues[name] = domainChecks[name](value);
|
||||
domainTypes.push({ name, type: domainFieldTypes[name] });
|
||||
});
|
||||
|
||||
const encoder = TypedDataEncoder.from(types);
|
||||
|
||||
const typesWithDomain = Object.assign({ }, types);
|
||||
if (typesWithDomain.EIP712Domain) {
|
||||
logger.throwArgumentError("types must not contain EIP712Domain type", "types.EIP712Domain", types);
|
||||
} else {
|
||||
typesWithDomain.EIP712Domain = domainTypes;
|
||||
}
|
||||
|
||||
// Validate the data structures and types
|
||||
encoder.encode(value);
|
||||
|
||||
return {
|
||||
types: typesWithDomain,
|
||||
domain: domainValues,
|
||||
primaryType: encoder.primaryType,
|
||||
message: encoder.visit(value, (type: string, value: any) => {
|
||||
|
||||
// bytes
|
||||
if (type.match(/^bytes(\d*)/)) {
|
||||
return hexlify(logger.getBytes(value));
|
||||
}
|
||||
|
||||
// uint or int
|
||||
if (type.match(/^u?int/)) {
|
||||
return logger.getBigInt(value).toString();
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case "address":
|
||||
return value.toLowerCase();
|
||||
case "bool":
|
||||
return !!value;
|
||||
case "string":
|
||||
if (typeof(value) !== "string") {
|
||||
logger.throwArgumentError(`invalid string`, "value", value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("unsupported type", "type", type);
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
|
6
src.ts/index.ts
Normal file
6
src.ts/index.ts
Normal file
@ -0,0 +1,6 @@
|
||||
|
||||
import * as ethers from "./ethers.js";
|
||||
|
||||
export { ethers };
|
||||
|
||||
export * from "./ethers.js";
|
1314
src.ts/providers/abstract-provider.ts
Normal file
1314
src.ts/providers/abstract-provider.ts
Normal file
File diff suppressed because it is too large
Load Diff
199
src.ts/providers/abstract-signer.ts
Normal file
199
src.ts/providers/abstract-signer.ts
Normal file
@ -0,0 +1,199 @@
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Transaction } from "../transaction/index.js";
|
||||
import { defineProperties, resolveProperties } from "../utils/index.js";
|
||||
|
||||
import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
|
||||
import type { TransactionLike } from "../transaction/index.js";
|
||||
|
||||
import type {
|
||||
BlockTag, CallRequest, Provider, TransactionRequest, TransactionResponse
|
||||
} from "./provider.js";
|
||||
import type { Signer } from "./signer.js";
|
||||
|
||||
|
||||
export abstract class AbstractSigner<P extends null | Provider = null | Provider> implements Signer {
|
||||
readonly provider!: P;
|
||||
|
||||
constructor(provider?: P) {
|
||||
defineProperties<AbstractSigner>(this, { provider: (provider || null) });
|
||||
}
|
||||
|
||||
abstract getAddress(): Promise<string>;
|
||||
abstract connect(provider: null | Provider): Signer;
|
||||
|
||||
#checkProvider(operation: string): Provider {
|
||||
if (this.provider) { return this.provider; }
|
||||
return logger.throwError("missing provider", "UNSUPPORTED_OPERATION", { operation });
|
||||
}
|
||||
|
||||
async getNonce(blockTag?: BlockTag): Promise<number> {
|
||||
return this.#checkProvider("getTransactionCount").getTransactionCount(await this.getAddress(), blockTag);
|
||||
}
|
||||
|
||||
async #populate(op: string, tx: CallRequest | TransactionRequest): Promise<TransactionLike<string>> {
|
||||
const provider = this.#checkProvider(op);
|
||||
|
||||
//let pop: Deferrable<TransactionRequest> = Object.assign({ }, tx);
|
||||
let pop: any = Object.assign({ }, tx);
|
||||
|
||||
if (pop.to != null) {
|
||||
pop.to = provider.resolveName(pop.to).then((to) => {
|
||||
if (to == null) {
|
||||
return logger.throwArgumentError("transaction to ENS name not configured", "tx.to", pop.to);
|
||||
}
|
||||
return to;
|
||||
});
|
||||
}
|
||||
|
||||
if (pop.from != null) {
|
||||
const from = pop.from;
|
||||
pop.from = Promise.all([
|
||||
this.getAddress(),
|
||||
this.resolveName(from)
|
||||
]).then(([ address, from ]) => {
|
||||
if (!from || address.toLowerCase() !== from.toLowerCase()) {
|
||||
return logger.throwArgumentError("transaction from mismatch", "tx.from", from);
|
||||
}
|
||||
return address;
|
||||
});
|
||||
}
|
||||
|
||||
return pop;
|
||||
}
|
||||
|
||||
async populateCall(tx: CallRequest): Promise<TransactionLike<string>> {
|
||||
const pop = await this.#populate("populateCall", tx);
|
||||
|
||||
return pop;
|
||||
}
|
||||
|
||||
async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||
const pop = await this.#populate("populateTransaction", tx);
|
||||
|
||||
if (pop.nonce == null) {
|
||||
pop.nonce = await this.getNonce("pending");
|
||||
}
|
||||
|
||||
if (pop.gasLimit == null) {
|
||||
pop.gasLimit = await this.estimateGas(pop);
|
||||
}
|
||||
|
||||
//@TODO: Copy type logic from AbstractSigner in v5
|
||||
|
||||
return await resolveProperties(pop);
|
||||
}
|
||||
|
||||
async estimateGas(tx: CallRequest): Promise<bigint> {
|
||||
return this.#checkProvider("estimateGas").estimateGas(await this.populateCall(tx));
|
||||
}
|
||||
|
||||
async call(tx: CallRequest): Promise<string> {
|
||||
return this.#checkProvider("call").call(await this.populateCall(tx));
|
||||
}
|
||||
|
||||
async resolveName(name: string): Promise<null | string> {
|
||||
const provider = this.#checkProvider("resolveName");
|
||||
return await provider.resolveName(name);
|
||||
}
|
||||
|
||||
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
||||
const provider = this.#checkProvider("sendTransaction");
|
||||
|
||||
const txObj = Transaction.from(await this.populateTransaction(tx));
|
||||
return await provider.broadcastTransaction(await this.signTransaction(txObj));
|
||||
}
|
||||
|
||||
abstract signTransaction(tx: TransactionRequest): Promise<string>;
|
||||
abstract signMessage(message: string | Uint8Array): Promise<string>;
|
||||
abstract signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string>;
|
||||
}
|
||||
|
||||
export class VoidSigner extends AbstractSigner {
|
||||
readonly address!: string;
|
||||
|
||||
constructor(address: string, provider?: null | Provider) {
|
||||
super(provider);
|
||||
defineProperties<VoidSigner>(this, { address });
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> { return this.address; }
|
||||
|
||||
connect(provider: null | Provider): VoidSigner {
|
||||
return new VoidSigner(this.address, provider);
|
||||
}
|
||||
|
||||
#throwUnsupported(suffix: string, operation: string): never {
|
||||
return logger.throwError(`VoidSigner cannot sign ${ suffix }`, "UNSUPPORTED_OPERATION", {
|
||||
operation
|
||||
});
|
||||
}
|
||||
|
||||
async signTransaction(tx: TransactionRequest): Promise<string> {
|
||||
this.#throwUnsupported("transactions", "signTransaction");
|
||||
}
|
||||
|
||||
async signMessage(message: string | Uint8Array): Promise<string> {
|
||||
this.#throwUnsupported("messages", "signMessage");
|
||||
}
|
||||
|
||||
async signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> {
|
||||
this.#throwUnsupported("typed-data", "signTypedData");
|
||||
}
|
||||
}
|
||||
|
||||
export class WrappedSigner extends AbstractSigner {
|
||||
#signer: Signer;
|
||||
|
||||
constructor(signer: Signer) {
|
||||
super(signer.provider);
|
||||
this.#signer = signer;
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> {
|
||||
return await this.#signer.getAddress();
|
||||
}
|
||||
|
||||
connect(provider: null | Provider): WrappedSigner {
|
||||
return new WrappedSigner(this.#signer.connect(provider));
|
||||
}
|
||||
|
||||
async getNonce(blockTag?: BlockTag): Promise<number> {
|
||||
return await this.#signer.getNonce(blockTag);
|
||||
}
|
||||
|
||||
async populateCall(tx: CallRequest): Promise<TransactionLike<string>> {
|
||||
return await this.#signer.populateCall(tx);
|
||||
}
|
||||
|
||||
async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||
return await this.#signer.populateTransaction(tx);
|
||||
}
|
||||
|
||||
async estimateGas(tx: CallRequest): Promise<bigint> {
|
||||
return await this.#signer.estimateGas(tx);
|
||||
}
|
||||
|
||||
async call(tx: CallRequest): Promise<string> {
|
||||
return await this.#signer.call(tx);
|
||||
}
|
||||
|
||||
async resolveName(name: string): Promise<null | string> {
|
||||
return this.#signer.resolveName(name);
|
||||
}
|
||||
|
||||
async signTransaction(tx: TransactionRequest): Promise<string> {
|
||||
return await this.#signer.signTransaction(tx);
|
||||
}
|
||||
|
||||
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
||||
return await this.#signer.sendTransaction(tx);
|
||||
}
|
||||
|
||||
async signMessage(message: string | Uint8Array): Promise<string> {
|
||||
return await this.#signer.signMessage(message);
|
||||
}
|
||||
|
||||
async signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, value: Record<string, any>): Promise<string> {
|
||||
return await this.#signer.signTypedData(domain, types, value);
|
||||
}
|
||||
}
|
102
src.ts/providers/common-networks.ts
Normal file
102
src.ts/providers/common-networks.ts
Normal file
@ -0,0 +1,102 @@
|
||||
|
||||
/**
|
||||
* Exports the same Network as "./network.js" except with common
|
||||
* networks injected registered.
|
||||
*/
|
||||
|
||||
import { EnsPlugin, GasCostPlugin } from "./plugins-network.js";
|
||||
import { EtherscanPlugin } from "./provider-etherscan.js";
|
||||
|
||||
import { Network } from "./network.js";
|
||||
|
||||
type Options = {
|
||||
ensNetwork?: number;
|
||||
priorityFee?: number
|
||||
altNames?: Array<string>;
|
||||
etherscan?: { url: string, apiKey: string };
|
||||
};
|
||||
|
||||
// See: https://chainlist.org
|
||||
export function injectCommonNetworks(): void {
|
||||
|
||||
/// Register popular Ethereum networks
|
||||
function registerEth(name: string, chainId: number, options: Options): void {
|
||||
const func = function() {
|
||||
const network = new Network(name, chainId);
|
||||
|
||||
// We use 0 to disable ENS
|
||||
if (options.ensNetwork != null) {
|
||||
network.attachPlugin(new EnsPlugin(null, options.ensNetwork));
|
||||
}
|
||||
|
||||
if (options.priorityFee) {
|
||||
// network.attachPlugin(new MaxPriorityFeePlugin(options.priorityFee));
|
||||
}
|
||||
|
||||
if (options.etherscan) {
|
||||
const { url, apiKey } = options.etherscan;
|
||||
network.attachPlugin(new EtherscanPlugin(url, apiKey));
|
||||
}
|
||||
|
||||
network.attachPlugin(new GasCostPlugin());
|
||||
|
||||
return network;
|
||||
};
|
||||
|
||||
// Register the network by name and chain ID
|
||||
Network.register(name, func);
|
||||
Network.register(chainId, func);
|
||||
|
||||
if (options.altNames) {
|
||||
options.altNames.forEach((name) => {
|
||||
Network.register(name, func);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEth("homestead", 1, { ensNetwork: 1, altNames: [ "mainnet" ] });
|
||||
registerEth("ropsten", 3, { ensNetwork: 3 });
|
||||
registerEth("rinkeby", 4, { ensNetwork: 4 });
|
||||
registerEth("goerli", 5, { ensNetwork: 5 });
|
||||
registerEth("kovan", 42, { ensNetwork: 42 });
|
||||
|
||||
registerEth("classic", 61, { });
|
||||
registerEth("classicKotti", 6, { });
|
||||
|
||||
registerEth("xdai", 100, { ensNetwork: 1 });
|
||||
|
||||
// Polygon has a 35 gwei maxPriorityFee requirement
|
||||
registerEth("matic", 137, {
|
||||
ensNetwork: 1,
|
||||
// priorityFee: 35000000000,
|
||||
etherscan: {
|
||||
apiKey: "W6T8DJW654GNTQ34EFEYYP3EZD9DD27CT7",
|
||||
url: "https:/\/api.polygonscan.com/"
|
||||
}
|
||||
});
|
||||
registerEth("maticMumbai", 80001, {
|
||||
// priorityFee: 35000000000,
|
||||
etherscan: {
|
||||
apiKey: "W6T8DJW654GNTQ34EFEYYP3EZD9DD27CT7",
|
||||
url: "https:/\/api-testnet.polygonscan.com/"
|
||||
}
|
||||
});
|
||||
|
||||
registerEth("bnb", 56, {
|
||||
ensNetwork: 1,
|
||||
etherscan: {
|
||||
apiKey: "EVTS3CU31AATZV72YQ55TPGXGMVIFUQ9M9",
|
||||
url: "http:/\/api.bscscan.com"
|
||||
}
|
||||
});
|
||||
registerEth("bnbt", 97, {
|
||||
etherscan: {
|
||||
apiKey: "EVTS3CU31AATZV72YQ55TPGXGMVIFUQ9M9",
|
||||
url: "http:/\/api-testnet.bscscan.com"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
injectCommonNetworks();
|
||||
|
||||
export { Network };
|
24
src.ts/providers/community.ts
Normal file
24
src.ts/providers/community.ts
Normal file
@ -0,0 +1,24 @@
|
||||
|
||||
export interface CommunityResourcable {
|
||||
isCommunityResource(): boolean;
|
||||
}
|
||||
|
||||
// Show the throttle message only once
|
||||
const shown: Set<string> = new Set();
|
||||
export function showThrottleMessage(service: string) {
|
||||
if (shown.has(service)) { return; }
|
||||
shown.add(service);
|
||||
|
||||
console.log("========= NOTICE =========")
|
||||
console.log(`Request-Rate Exceeded for ${ service } (this message will not be repeated)`);
|
||||
console.log("");
|
||||
console.log("The default API keys for each service are provided as a highly-throttled,");
|
||||
console.log("community resource for low-traffic projects and early prototyping.");
|
||||
console.log("");
|
||||
console.log("While your application will continue to function, we highly recommended");
|
||||
console.log("signing up for your own API keys to improve performance, increase your");
|
||||
console.log("request rate/limit and enable other perks, such as metrics and advanced APIs.");
|
||||
console.log("");
|
||||
console.log("For more details: https:/\/docs.ethers.io/api-keys/");
|
||||
console.log("==========================");
|
||||
}
|
21
src.ts/providers/contracts.ts
Normal file
21
src.ts/providers/contracts.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type {
|
||||
CallRequest, Provider, TransactionRequest, TransactionResponse
|
||||
} from "./provider.js";
|
||||
|
||||
// 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: null | Provider;
|
||||
|
||||
// Required to estimate gas; usually a Signer or Provider
|
||||
estimateGas?: (tx: TransactionRequest) => Promise<bigint>;
|
||||
|
||||
// Required for pure, view or static calls to contracts; usually a Signer or Provider
|
||||
call?: (tx: CallRequest) => Promise<string>;
|
||||
|
||||
// Required to support ENS names; usually a Signer or Provider
|
||||
resolveName?: (name: string) => Promise<null | string>;
|
||||
|
||||
// Required for mutating calls; usually a Signer
|
||||
sendTransaction?: (tx: TransactionRequest) => Promise<TransactionResponse>;
|
||||
}
|
89
src.ts/providers/default-provider.ts
Normal file
89
src.ts/providers/default-provider.ts
Normal file
@ -0,0 +1,89 @@
|
||||
|
||||
import { AnkrProvider } from "./provider-ankr.js";
|
||||
import { AlchemyProvider } from "./provider-alchemy.js";
|
||||
import { CloudflareProvider } from "./provider-cloudflare.js";
|
||||
import { EtherscanProvider } from "./provider-etherscan.js";
|
||||
import { InfuraProvider } from "./provider-infura.js";
|
||||
//import { PocketProvider } from "./provider-pocket.js";
|
||||
|
||||
import { FallbackProvider } from "./provider-fallback.js";
|
||||
import { JsonRpcProvider } from "./provider-jsonrpc.js";
|
||||
import { WebSocketProvider } from "./provider-websocket.js";
|
||||
|
||||
import type { AbstractProvider } from "./abstract-provider.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
import { WebSocketLike } from "./provider-websocket.js";
|
||||
|
||||
function isWebSocketLike(value: any): value is WebSocketLike {
|
||||
return (value && typeof(value.send) === "function" &&
|
||||
typeof(value.close) === "function");
|
||||
}
|
||||
|
||||
export function getDefaultProvider(network: string | Networkish | WebSocketLike, options?: any): AbstractProvider {
|
||||
if (options == null) { options = { }; }
|
||||
|
||||
if (typeof(network) === "string" && network.match(/^https?:/)) {
|
||||
return new JsonRpcProvider(network);
|
||||
}
|
||||
|
||||
if (typeof(network) === "string" && network.match(/^wss?:/) || isWebSocketLike(network)) {
|
||||
return new WebSocketProvider(network);
|
||||
}
|
||||
|
||||
const providers: Array<AbstractProvider> = [ ];
|
||||
|
||||
if (options.alchemy !== "-") {
|
||||
try {
|
||||
providers.push(new AlchemyProvider(network, options.alchemy));
|
||||
} catch (error) { console.log(error); }
|
||||
}
|
||||
|
||||
if (options.ankr !== "-") {
|
||||
try {
|
||||
providers.push(new AnkrProvider(network, options.ankr));
|
||||
} catch (error) { console.log(error); }
|
||||
}
|
||||
|
||||
if (options.cloudflare !== "-") {
|
||||
try {
|
||||
providers.push(new CloudflareProvider(network));
|
||||
} catch (error) { console.log(error); }
|
||||
}
|
||||
|
||||
if (options.etherscan !== "-") {
|
||||
try {
|
||||
providers.push(new EtherscanProvider(network, options.etherscan));
|
||||
} catch (error) { console.log(error); }
|
||||
}
|
||||
|
||||
if (options.infura !== "-") {
|
||||
try {
|
||||
let projectId = options.infura;
|
||||
let projectSecret: undefined | string = undefined;
|
||||
if (typeof(projectId) === "object") {
|
||||
projectSecret = projectId.projectSecret;
|
||||
projectId = projectId.projectId;
|
||||
}
|
||||
providers.push(new InfuraProvider(network, projectId, projectSecret));
|
||||
} catch (error) { console.log(error); }
|
||||
}
|
||||
/*
|
||||
if (options.pocket !== "-") {
|
||||
try {
|
||||
let appId = options.pocket;
|
||||
let secretKey: undefined | string = undefined;
|
||||
let loadBalancer: undefined | boolean = undefined;
|
||||
if (typeof(appId) === "object") {
|
||||
loadBalancer = !!appId.loadBalancer;
|
||||
secretKey = appId.secretKey;
|
||||
appId = appId.appId;
|
||||
}
|
||||
providers.push(new PocketProvider(network, appId, secretKey, loadBalancer));
|
||||
} catch (error) { console.log(error); }
|
||||
}
|
||||
*/
|
||||
if (providers.length === 0) { throw new Error("TODO"); }
|
||||
if (providers.length === 1) { return providers[0]; }
|
||||
|
||||
return new FallbackProvider(providers);
|
||||
}
|
535
src.ts/providers/ens-resolver.ts
Normal file
535
src.ts/providers/ens-resolver.ts
Normal file
@ -0,0 +1,535 @@
|
||||
import { ZeroHash } from "../constants/hashes.js";
|
||||
import { dnsEncode, namehash } from "../hash/index.js";
|
||||
import {
|
||||
defineProperties, encodeBase58, toArray, toNumber, toUtf8Bytes, toUtf8String
|
||||
} from "../utils/index.js";
|
||||
import { concat, dataSlice, hexlify, zeroPadValue } from "../utils/data.js";
|
||||
import { FetchRequest } from "../utils/fetch.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
import type { BigNumberish, BytesLike, EthersError } from "../utils/index.js";
|
||||
|
||||
import type { AbstractProvider, ProviderPlugin } from "./abstract-provider.js";
|
||||
import type { EnsPlugin } from "./plugins-network.js";
|
||||
import type { CallRequest, Provider } from "./provider.js";
|
||||
|
||||
const BN_1 = BigInt(1);
|
||||
|
||||
const Empty = new Uint8Array([ ]);
|
||||
|
||||
function parseBytes(result: string, start: number): null | string {
|
||||
if (result === "0x") { return null; }
|
||||
|
||||
const offset = toNumber(dataSlice(result, start, start + 32));
|
||||
const length = toNumber(dataSlice(result, offset, offset + 32));
|
||||
|
||||
return dataSlice(result, offset + 32, offset + 32 + length);
|
||||
}
|
||||
|
||||
function parseString(result: string, start: number): null | string {
|
||||
try {
|
||||
const bytes = parseBytes(result, start);
|
||||
if (bytes != null) { return toUtf8String(bytes); }
|
||||
} catch(error) { }
|
||||
return null;
|
||||
}
|
||||
|
||||
function numPad(value: BigNumberish): Uint8Array {
|
||||
const result = toArray(value);
|
||||
if (result.length > 32) { throw new Error("internal; should not happen"); }
|
||||
|
||||
const padded = new Uint8Array(32);
|
||||
padded.set(result, 32 - result.length);
|
||||
return padded;
|
||||
}
|
||||
|
||||
function bytesPad(value: Uint8Array): Uint8Array {
|
||||
if ((value.length % 32) === 0) { return value; }
|
||||
|
||||
const result = new Uint8Array(Math.ceil(value.length / 32) * 32);
|
||||
result.set(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ABI Encodes a series of (bytes, bytes, ...)
|
||||
function encodeBytes(datas: Array<BytesLike>) {
|
||||
const result: Array<Uint8Array> = [ ];
|
||||
|
||||
let byteCount = 0;
|
||||
|
||||
// Add place-holders for pointers as we add items
|
||||
for (let i = 0; i < datas.length; i++) {
|
||||
result.push(Empty);
|
||||
byteCount += 32;
|
||||
}
|
||||
|
||||
for (let i = 0; i < datas.length; i++) {
|
||||
const data = logger.getBytes(datas[i]);
|
||||
|
||||
// Update the bytes offset
|
||||
result[i] = numPad(byteCount);
|
||||
|
||||
// The length and padded value of data
|
||||
result.push(numPad(data.length));
|
||||
result.push(bytesPad(data));
|
||||
byteCount += 32 + Math.ceil(data.length / 32) * 32;
|
||||
}
|
||||
|
||||
return concat(result);
|
||||
}
|
||||
|
||||
// @TODO: This should use the fetch-data:ipfs gateway
|
||||
// Trim off the ipfs:// prefix and return the default gateway URL
|
||||
function getIpfsLink(link: string): string {
|
||||
if (link.match(/^ipfs:\/\/ipfs\//i)) {
|
||||
link = link.substring(12);
|
||||
} else if (link.match(/^ipfs:\/\//i)) {
|
||||
link = link.substring(7);
|
||||
} else {
|
||||
logger.throwArgumentError("unsupported IPFS format", "link", link);
|
||||
}
|
||||
|
||||
return `https:/\/gateway.ipfs.io/ipfs/${ link }`;
|
||||
}
|
||||
|
||||
export type AvatarLinkageType = "name" | "avatar" | "!avatar" | "url" | "data" | "ipfs" |
|
||||
"erc721" | "erc1155" | "!erc721-caip" | "!erc1155-caip" |
|
||||
"!owner" | "owner" | "!balance" | "balance" |
|
||||
"metadata-url-base" | "metadata-url-expanded" | "metadata-url" | "!metadata-url" |
|
||||
"!metadata" | "metadata" |
|
||||
"!imageUrl" | "imageUrl-ipfs" | "imageUrl" | "!imageUrl-ipfs";
|
||||
|
||||
export interface AvatarLinkage {
|
||||
type: AvatarLinkageType;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export interface AvatarResult {
|
||||
linkage: Array<AvatarLinkage>;
|
||||
url: null | string;
|
||||
};
|
||||
|
||||
export abstract class MulticoinProviderPlugin implements ProviderPlugin {
|
||||
readonly name!: string;
|
||||
|
||||
constructor(name: string) {
|
||||
defineProperties<MulticoinProviderPlugin>(this, { name });
|
||||
}
|
||||
|
||||
validate(proivder: Provider): ProviderPlugin {
|
||||
return this;
|
||||
}
|
||||
|
||||
supportsCoinType(coinType: number): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
async encodeAddress(coinType: number, address: string): Promise<string> {
|
||||
throw new Error("unsupported coin");
|
||||
}
|
||||
|
||||
async decodeAddress(coinType: number, data: BytesLike): Promise<string> {
|
||||
throw new Error("unsupported coin");
|
||||
}
|
||||
}
|
||||
|
||||
const BasicMulticoinPluginId = "org.ethers.provider-prugins.basicmulticoin";
|
||||
|
||||
export class BasicMulticoinProviderPlugin extends MulticoinProviderPlugin {
|
||||
constructor() {
|
||||
super(BasicMulticoinPluginId);
|
||||
}
|
||||
}
|
||||
|
||||
const matcherIpfs = new RegExp("^(ipfs):/\/(.*)$", "i");
|
||||
const matchers = [
|
||||
new RegExp("^(https):/\/(.*)$", "i"),
|
||||
new RegExp("^(data):(.*)$", "i"),
|
||||
matcherIpfs,
|
||||
new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"),
|
||||
];
|
||||
|
||||
export class EnsResolver {
|
||||
provider!: AbstractProvider;
|
||||
address!: string;
|
||||
|
||||
name!: string;
|
||||
|
||||
// For EIP-2544 names, the ancestor that provided the resolver
|
||||
#supports2544: null | Promise<boolean>;
|
||||
|
||||
constructor(provider: AbstractProvider, address: string, name: string) {
|
||||
defineProperties<EnsResolver>(this, { provider, address, name });
|
||||
this.#supports2544 = null;
|
||||
}
|
||||
|
||||
async supportsWildcard(): Promise<boolean> {
|
||||
if (!this.#supports2544) {
|
||||
// supportsInterface(bytes4 = selector("resolve(bytes,bytes)"))
|
||||
this.#supports2544 = this.provider.call({
|
||||
to: this.address,
|
||||
data: "0x01ffc9a79061b92300000000000000000000000000000000000000000000000000000000"
|
||||
}).then((result) => {
|
||||
return (logger.getBigInt(result) === BN_1);
|
||||
}).catch((error) => {
|
||||
if (error.code === "CALL_EXCEPTION") { return false; }
|
||||
// Rethrow the error: link is down, etc. Let future attempts retry.
|
||||
this.#supports2544 = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return await this.#supports2544;
|
||||
}
|
||||
|
||||
async _fetch(selector: string, parameters: BytesLike = "0x"): Promise<null | string> {
|
||||
|
||||
// e.g. keccak256("addr(bytes32,uint256)")
|
||||
const addrData = concat([ selector, namehash(this.name), parameters ]);
|
||||
const tx: CallRequest = {
|
||||
to: this.address,
|
||||
enableCcipRead: true,
|
||||
data: addrData
|
||||
};
|
||||
|
||||
// Wildcard support; use EIP-2544 to resolve the request
|
||||
let wrapped = false;
|
||||
if (await this.supportsWildcard()) {
|
||||
wrapped = true;
|
||||
|
||||
// selector("resolve(bytes,bytes)")
|
||||
tx.data = concat([ "0x9061b923", encodeBytes([ dnsEncode(this.name), addrData ]) ]);
|
||||
}
|
||||
|
||||
try {
|
||||
let data = await this.provider.call(tx);
|
||||
if ((logger.getBytes(data).length % 32) === 4) {
|
||||
return logger.throwError("resolver threw error", "CALL_EXCEPTION", {
|
||||
transaction: tx, data
|
||||
});
|
||||
}
|
||||
if (wrapped) { return parseBytes(data, 0); }
|
||||
return data;
|
||||
} catch (error: any) {
|
||||
if ((error as EthersError).code !== "CALL_EXCEPTION") { throw error; }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getAddress(coinType: number = 60): Promise<null | string> {
|
||||
if (coinType === 60) {
|
||||
try {
|
||||
// keccak256("addr(bytes32)")
|
||||
const result = await this._fetch("0x3b3b57de");
|
||||
|
||||
// No address
|
||||
if (result === "0x" || result === ZeroHash) { return null; }
|
||||
|
||||
const network = await this.provider.getNetwork();
|
||||
return network.formatter.callAddress(result);
|
||||
} catch (error: any) {
|
||||
if ((error as EthersError).code === "CALL_EXCEPTION") { return null; }
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let coinPlugin: null | MulticoinProviderPlugin = null;
|
||||
for (const plugin of this.provider.plugins) {
|
||||
if (!(plugin instanceof MulticoinProviderPlugin)) { continue; }
|
||||
if (plugin.supportsCoinType(coinType)) {
|
||||
coinPlugin = plugin;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (coinPlugin == null) { return null; }
|
||||
|
||||
// keccak256("addr(bytes32,uint256")
|
||||
const data = parseBytes((await this._fetch("0xf1cb7e06", numPad(coinType))) || "0x", 0);
|
||||
|
||||
// No address
|
||||
if (data == null || data === "0x") { return null; }
|
||||
|
||||
// Compute the address
|
||||
const address = await coinPlugin.encodeAddress(coinType, data);
|
||||
|
||||
if (address != null) { return address; }
|
||||
|
||||
return logger.throwError(`invalid coin data`, "UNSUPPORTED_OPERATION", {
|
||||
operation: `getAddress(${ coinType })`,
|
||||
info: { coinType, data }
|
||||
});
|
||||
}
|
||||
|
||||
async getText(key: string): Promise<null | string> {
|
||||
// The key encoded as parameter to fetchBytes
|
||||
let keyBytes = toUtf8Bytes(key);
|
||||
|
||||
// The nodehash consumes the first slot, so the string pointer targets
|
||||
// offset 64, with the length at offset 64 and data starting at offset 96
|
||||
const calldata = logger.getBytes(concat([ numPad(64), numPad(keyBytes.length), keyBytes ]));
|
||||
|
||||
const hexBytes = parseBytes((await this._fetch("0x59d1d43c", bytesPad(calldata))) || "0x", 0);
|
||||
if (hexBytes == null || hexBytes === "0x") { return null; }
|
||||
|
||||
return toUtf8String(hexBytes);
|
||||
}
|
||||
|
||||
async getContentHash(): Promise<null | string> {
|
||||
// keccak256("contenthash()")
|
||||
const hexBytes = parseBytes((await this._fetch("0xbc1c58d1")) || "0x", 0);
|
||||
|
||||
// No contenthash
|
||||
if (hexBytes == null || hexBytes === "0x") { return null; }
|
||||
|
||||
// IPFS (CID: 1, Type: 70=DAG-PB, 72=libp2p-key)
|
||||
const ipfs = hexBytes.match(/^0x(e3010170|e5010172)(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
|
||||
if (ipfs) {
|
||||
const scheme = (ipfs[1] === "e3010170") ? "ipfs": "ipns";
|
||||
const length = parseInt(ipfs[4], 16);
|
||||
if (ipfs[5].length === length * 2) {
|
||||
return `${ scheme }:/\/${ encodeBase58("0x" + ipfs[2])}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Swarm (CID: 1, Type: swarm-manifest; hash/length hard-coded to keccak256/32)
|
||||
const swarm = hexBytes.match(/^0xe40101fa011b20([0-9a-f]*)$/)
|
||||
if (swarm && swarm[1].length === 64) {
|
||||
return `bzz:/\/${ swarm[1] }`;
|
||||
}
|
||||
|
||||
return logger.throwError(`invalid or unsupported content hash data`, "UNSUPPORTED_OPERATION", {
|
||||
operation: "getContentHash()",
|
||||
info: { data: hexBytes }
|
||||
});
|
||||
}
|
||||
|
||||
async getAvatar(): Promise<null | string> {
|
||||
return (await this._getAvatar()).url;
|
||||
}
|
||||
|
||||
async _getAvatar(): Promise<AvatarResult> {
|
||||
const linkage: Array<AvatarLinkage> = [ { type: "name", value: this.name } ];
|
||||
try {
|
||||
// test data for ricmoo.eth
|
||||
//const avatar = "eip155:1/erc721:0x265385c7f4132228A0d54EB1A9e7460b91c0cC68/29233";
|
||||
const avatar = await this.getText("avatar");
|
||||
if (avatar == null) {
|
||||
linkage.push({ type: "!avatar", value: "" });
|
||||
throw new Error("!avatar");
|
||||
}
|
||||
linkage.push({ type: "avatar", value: avatar });
|
||||
|
||||
for (let i = 0; i < matchers.length; i++) {
|
||||
const match = avatar.match(matchers[i]);
|
||||
if (match == null) { continue; }
|
||||
|
||||
const scheme = match[1].toLowerCase();
|
||||
|
||||
switch (scheme) {
|
||||
case "https":
|
||||
case "data":
|
||||
linkage.push({ type: "url", value: avatar });
|
||||
return { linkage, url: avatar };
|
||||
case "ipfs": {
|
||||
const url = getIpfsLink(avatar);
|
||||
linkage.push({ type: "ipfs", value: avatar });
|
||||
linkage.push({ type: "url", value: url });
|
||||
return { linkage, url };
|
||||
}
|
||||
|
||||
case "erc721":
|
||||
case "erc1155": {
|
||||
// Depending on the ERC type, use tokenURI(uint256) or url(uint256)
|
||||
const selector = (scheme === "erc721") ? "0xc87b56dd": "0x0e89341c";
|
||||
linkage.push({ type: scheme, value: avatar });
|
||||
|
||||
// The owner of this name
|
||||
const owner = await this.getAddress();
|
||||
if (owner == null) {
|
||||
linkage.push({ type: "!owner", value: "" });
|
||||
throw new Error("!owner");
|
||||
}
|
||||
|
||||
const comps = (match[2] || "").split("/");
|
||||
if (comps.length !== 2) {
|
||||
linkage.push({ type: <any>`!${ scheme }caip`, value: (match[2] || "") });
|
||||
throw new Error("!caip");
|
||||
}
|
||||
|
||||
const formatter = (await this.provider.getNetwork()).formatter;
|
||||
|
||||
const addr = formatter.address(comps[0]);
|
||||
const tokenId = numPad(comps[1]);
|
||||
|
||||
// Check that this account owns the token
|
||||
if (scheme === "erc721") {
|
||||
// ownerOf(uint256 tokenId)
|
||||
const tokenOwner = formatter.callAddress(await this.provider.call({
|
||||
to: addr, data: concat([ "0x6352211e", tokenId ])
|
||||
}));
|
||||
if (owner !== tokenOwner) {
|
||||
linkage.push({ type: "!owner", value: tokenOwner });
|
||||
throw new Error("!owner");
|
||||
}
|
||||
linkage.push({ type: "owner", value: tokenOwner });
|
||||
|
||||
} else if (scheme === "erc1155") {
|
||||
// balanceOf(address owner, uint256 tokenId)
|
||||
const balance = logger.getBigInt(await this.provider.call({
|
||||
to: addr, data: concat([ "0x00fdd58e", zeroPadValue(owner, 32), tokenId ])
|
||||
}));
|
||||
if (!balance) {
|
||||
linkage.push({ type: "!balance", value: "0" });
|
||||
throw new Error("!balance");
|
||||
}
|
||||
linkage.push({ type: "balance", value: balance.toString() });
|
||||
}
|
||||
|
||||
// Call the token contract for the metadata URL
|
||||
const tx = {
|
||||
to: comps[0],
|
||||
data: concat([ selector, tokenId ])
|
||||
};
|
||||
|
||||
let metadataUrl = parseString(await this.provider.call(tx), 0);
|
||||
if (metadataUrl == null) {
|
||||
linkage.push({ type: "!metadata-url", value: "" });
|
||||
throw new Error("!metadata-url");
|
||||
}
|
||||
|
||||
linkage.push({ type: "metadata-url-base", value: metadataUrl });
|
||||
|
||||
// ERC-1155 allows a generic {id} in the URL
|
||||
if (scheme === "erc1155") {
|
||||
metadataUrl = metadataUrl.replace("{id}", hexlify(tokenId).substring(2));
|
||||
linkage.push({ type: "metadata-url-expanded", value: metadataUrl });
|
||||
}
|
||||
|
||||
// Transform IPFS metadata links
|
||||
if (metadataUrl.match(/^ipfs:/i)) {
|
||||
metadataUrl = getIpfsLink(metadataUrl);
|
||||
}
|
||||
linkage.push({ type: "metadata-url", value: metadataUrl });
|
||||
|
||||
// Get the token metadata
|
||||
let metadata: any = { };
|
||||
const response = await (new FetchRequest(metadataUrl)).send();
|
||||
response.assertOk();
|
||||
|
||||
try {
|
||||
metadata = response.bodyJson;
|
||||
} catch (error) {
|
||||
try {
|
||||
linkage.push({ type: "!metadata", value: response.bodyText });
|
||||
} catch (error) {
|
||||
const bytes = response.body;
|
||||
if (bytes) {
|
||||
linkage.push({ type: "!metadata", value: hexlify(bytes) });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!metadata) {
|
||||
linkage.push({ type: "!metadata", value: "" });
|
||||
throw new Error("!metadata");
|
||||
}
|
||||
|
||||
linkage.push({ type: "metadata", value: JSON.stringify(metadata) });
|
||||
|
||||
// Pull the image URL out
|
||||
let imageUrl = metadata.image;
|
||||
if (typeof(imageUrl) !== "string") {
|
||||
linkage.push({ type: "!imageUrl", value: "" });
|
||||
throw new Error("!imageUrl");
|
||||
}
|
||||
|
||||
if (imageUrl.match(/^(https:\/\/|data:)/i)) {
|
||||
// Allow
|
||||
} else {
|
||||
// Transform IPFS link to gateway
|
||||
const ipfs = imageUrl.match(matcherIpfs);
|
||||
if (ipfs == null) {
|
||||
linkage.push({ type: "!imageUrl-ipfs", value: imageUrl });
|
||||
throw new Error("!imageUrl-ipfs");
|
||||
}
|
||||
|
||||
linkage.push({ type: "imageUrl-ipfs", value: imageUrl });
|
||||
imageUrl = getIpfsLink(imageUrl);
|
||||
}
|
||||
|
||||
linkage.push({ type: "url", value: imageUrl });
|
||||
|
||||
return { linkage, url: imageUrl };
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) { console.log("EE", error); }
|
||||
|
||||
return { linkage, url: null };
|
||||
}
|
||||
|
||||
static async #getResolver(provider: Provider, name: string): Promise<null | string> {
|
||||
const network = await provider.getNetwork();
|
||||
|
||||
const ensPlugin = network.getPlugin<EnsPlugin>("org.ethers.network-plugins.ens");
|
||||
|
||||
// No ENS...
|
||||
if (!ensPlugin) {
|
||||
return logger.throwError("network does not support ENS", "UNSUPPORTED_OPERATION", {
|
||||
operation: "getResolver", info: { network: network.name }
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// keccak256("resolver(bytes32)")
|
||||
const addrData = await provider.call({
|
||||
to: ensPlugin.address,
|
||||
data: concat([ "0x0178b8bf", namehash(name) ]),
|
||||
enableCcipRead: true
|
||||
});
|
||||
|
||||
const addr = network.formatter.callAddress(addrData);
|
||||
if (addr === dataSlice(ZeroHash, 0, 20)) { return null; }
|
||||
return addr;
|
||||
|
||||
} catch (error) {
|
||||
// ENS registry cannot throw errors on resolver(bytes32),
|
||||
// so probably a link error
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
static async fromName(provider: AbstractProvider, name: string): Promise<null | EnsResolver> {
|
||||
|
||||
let currentName = name;
|
||||
while (true) {
|
||||
if (currentName === "" || currentName === ".") { return null; }
|
||||
|
||||
// Optimization since the eth node cannot change and does
|
||||
// not have a wildcar resolver
|
||||
if (name !== "eth" && currentName === "eth") { return null; }
|
||||
|
||||
// Check the current node for a resolver
|
||||
const addr = await EnsResolver.#getResolver(provider, currentName);
|
||||
|
||||
// Found a resolver!
|
||||
if (addr != null) {
|
||||
const resolver = new EnsResolver(provider, addr, name);
|
||||
|
||||
// Legacy resolver found, using EIP-2544 so it isn't safe to use
|
||||
if (currentName !== name && !(await resolver.supportsWildcard())) { return null; }
|
||||
|
||||
return resolver;
|
||||
}
|
||||
|
||||
// Get the parent node
|
||||
currentName = currentName.split(".").slice(1).join(".");
|
||||
}
|
||||
}
|
||||
}
|
442
src.ts/providers/formatter.ts
Normal file
442
src.ts/providers/formatter.ts
Normal file
@ -0,0 +1,442 @@
|
||||
// Belongs to Networks; requires abstract-provider
|
||||
// provider requires abstract-provider and network
|
||||
|
||||
/**
|
||||
* Formatter
|
||||
*
|
||||
* This is responsibile for converting much of the various
|
||||
* loose network values into a concrete ethers-ready value.
|
||||
*
|
||||
* For example, converting addresses to checksum addresses,
|
||||
* validating a hash is 32 bytes, and so on.
|
||||
*
|
||||
* By sub-classing this class and providing it in a custom
|
||||
* Network object this allows exotic (non-Ethereum) networks
|
||||
* to be fairly simple to adapt to ethers.
|
||||
*/
|
||||
|
||||
import { getAddress, getCreateAddress } from "../address/index.js";
|
||||
import { dataLength, dataSlice, isHexString } from "../utils/data.js";
|
||||
import { toQuantity } from "../utils/maths.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Signature } from "../crypto/signature.js";
|
||||
import { accessListify } from "../transaction/index.js";
|
||||
|
||||
import { Block, Log, TransactionReceipt, TransactionResponse } from "./provider.js";
|
||||
|
||||
import type { AccessList } from "../transaction/index.js";
|
||||
|
||||
import type { PerformActionTransaction } from "./abstract-provider.js";
|
||||
import type { Filter, Provider } from "./provider.js";
|
||||
|
||||
|
||||
const BN_MAX_UINT256 = BigInt("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff");
|
||||
|
||||
export type FormatFunc = (value: any) => any;
|
||||
|
||||
//export type AccessListSet = { address: string, storageKeys: Array<string> };
|
||||
//export type AccessList = Array<AccessListSet>;
|
||||
|
||||
//export type AccessListish = AccessList |
|
||||
// Array<[ string, Array<string> ]> |
|
||||
// Record<string, Array<string>>;
|
||||
|
||||
function stringify(value: any): string {
|
||||
if (typeof(value) !== "string") { throw new Error("invalid string"); }
|
||||
return value;
|
||||
}
|
||||
|
||||
export class Formatter {
|
||||
#format: {
|
||||
address: FormatFunc,
|
||||
bigNumber: FormatFunc,
|
||||
blockTag: FormatFunc,
|
||||
data: FormatFunc,
|
||||
filter: FormatFunc,
|
||||
hash: FormatFunc,
|
||||
number: FormatFunc,
|
||||
topics: FormatFunc,
|
||||
transactionRequest: FormatFunc,
|
||||
transactionResponse: FormatFunc,
|
||||
uint256: FormatFunc,
|
||||
};
|
||||
|
||||
#baseBlock: FormatFunc;
|
||||
|
||||
constructor() {
|
||||
const address = this.address.bind(this);
|
||||
const bigNumber = this.bigNumber.bind(this);
|
||||
const blockTag = this.blockTag.bind(this);
|
||||
const data = this.data.bind(this);
|
||||
const hash = this.hash.bind(this);
|
||||
const number = this.number.bind(this);
|
||||
const uint256 = this.uint256.bind(this);
|
||||
|
||||
const topics = this.arrayOf(hash);
|
||||
|
||||
this.#format = {
|
||||
address,
|
||||
bigNumber,
|
||||
blockTag,
|
||||
data,
|
||||
hash,
|
||||
number,
|
||||
uint256,
|
||||
|
||||
topics,
|
||||
|
||||
filter: this.object({
|
||||
fromBlock: this.allowNull(blockTag, undefined),
|
||||
toBlock: this.allowNull(blockTag, undefined),
|
||||
blockHash: this.allowNull(hash, undefined),
|
||||
address: this.allowNull(address, undefined),
|
||||
topics: this.allowNull(topics, undefined)
|
||||
}),
|
||||
|
||||
transactionRequest: this.object({
|
||||
from: this.allowNull(address),
|
||||
type: this.allowNull(number),
|
||||
to: this.allowNull(address),
|
||||
nonce: this.allowNull(number),
|
||||
gasLimit: this.allowNull(uint256),
|
||||
gasPrice: this.allowNull(uint256),
|
||||
maxFeePerGas: this.allowNull(uint256),
|
||||
maxPriorityFeePerGas: this.allowNull(uint256),
|
||||
data: this.allowNull(data),
|
||||
value: this.allowNull(uint256),
|
||||
}),
|
||||
|
||||
transactionResponse: this.object({
|
||||
hash: hash,
|
||||
index: number,
|
||||
|
||||
type: this.allowNull(number, 0),
|
||||
|
||||
// These can be null for pending blocks
|
||||
blockHash: this.allowNull(hash),
|
||||
blockNumber: this.allowNull(number),
|
||||
|
||||
// For Legacy transactions, this comes from the v
|
||||
chainId: this.allowNull(number),
|
||||
|
||||
from: address,
|
||||
to: this.address,
|
||||
|
||||
gasLimit: bigNumber,
|
||||
|
||||
gasPrice: this.allowNull(bigNumber),
|
||||
maxFeePerGas: this.allowNull(bigNumber),
|
||||
maxPriorityFeePerGas: this.allowNull(bigNumber),
|
||||
|
||||
value: bigNumber,
|
||||
data: data,
|
||||
nonce: number,
|
||||
r: hash,
|
||||
s: hash,
|
||||
v: number,
|
||||
accessList: this.allowNull(this.accessList)
|
||||
}, {
|
||||
index: [ "transactionIndex" ]
|
||||
}),
|
||||
};
|
||||
|
||||
this.#baseBlock = this.object({
|
||||
number: number,
|
||||
hash: this.allowNull(hash, null),
|
||||
timestamp: number,
|
||||
|
||||
parentHash: hash,
|
||||
|
||||
nonce: this.allowNull(stringify, "0x0000000000000000"),
|
||||
difficulty: bigNumber,
|
||||
|
||||
gasLimit: bigNumber,
|
||||
gasUsed: bigNumber,
|
||||
miner: this.allowNull(address, "0x0000000000000000000000000000000000000000"),
|
||||
extraData: stringify,
|
||||
|
||||
baseFeePerGas: this.allowNull(bigNumber),
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// An address
|
||||
address(value: any): string {
|
||||
return getAddress(value);
|
||||
}
|
||||
|
||||
// An address from a call result; may be zero-padded
|
||||
callAddress(value: any): string {
|
||||
if (dataLength(value) !== 32 || dataSlice(value, 0, 12) !== "0x000000000000000000000000") {
|
||||
logger.throwArgumentError("invalid call address", "value", value);
|
||||
}
|
||||
return this.address(dataSlice(value, 12));
|
||||
}
|
||||
|
||||
// An address from a transaction (e.g. { from: string, nonce: number })
|
||||
contractAddress(value: any): string {
|
||||
return getCreateAddress({
|
||||
from: this.address(value.from),
|
||||
nonce: logger.getNumber(value.nonce, "value.nonce")
|
||||
});
|
||||
}
|
||||
|
||||
// Block Tag
|
||||
blockTag(value?: any): string {
|
||||
if (value == null) { return "latest"; }
|
||||
|
||||
switch (value) {
|
||||
case "earliest":
|
||||
return "0x0";
|
||||
case "latest": case "pending": case "safe": case "finalized":
|
||||
return value;
|
||||
}
|
||||
|
||||
if (typeof(value) === "number" || (isHexString(value) && dataLength(value) < 32)) {
|
||||
return toQuantity(value);
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("invalid blockTag", "value", value);
|
||||
}
|
||||
|
||||
// Block objects
|
||||
block(value: any, provider?: Provider): Block<string> {
|
||||
const params = this.#baseBlock(value);
|
||||
params.transactions = value.transactions.map((t: any) => this.hash(t));
|
||||
return new Block(params, provider);
|
||||
}
|
||||
blockWithTransactions(value: any, provider?: Provider): Block<TransactionResponse> {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// Transactions
|
||||
transactionRequest(value: any, provider?: Provider): PerformActionTransaction {
|
||||
return this.#format.transactionRequest(value);
|
||||
}
|
||||
|
||||
transactionResponse(value: any, provider?: Provider): TransactionResponse {
|
||||
value = Object.assign({ }, value);
|
||||
|
||||
// @TODO: Use the remap feature
|
||||
if (value.data == null && value.input != null) { value.data = value.input; }
|
||||
if (value.gasLimit == null && value.gas) { value.gasLimit = value.gas; }
|
||||
|
||||
value = this.#format.transactionResponse(value);
|
||||
|
||||
const sig = Signature.from({ r: value.r, s: value.s, v: value.v });
|
||||
value.signature = sig;
|
||||
if (value.chainId == null) { value.chainId = sig.legacyChainId; }
|
||||
|
||||
return new TransactionResponse(value, provider);
|
||||
}
|
||||
|
||||
// Receipts
|
||||
log(value: any, provider?: Provider): Log {
|
||||
const log = this.object({
|
||||
address: this.address,
|
||||
blockHash: this.hash,
|
||||
blockNumber: this.number,
|
||||
data: this.data,
|
||||
index: this.number,
|
||||
removed: this.boolean,
|
||||
topics: this.topics,
|
||||
transactionHash: this.hash,
|
||||
transactionIndex: this.number,
|
||||
}, {
|
||||
index: [ "logIndex" ]
|
||||
})(value);
|
||||
return new Log(log, provider);
|
||||
}
|
||||
|
||||
receipt(value: any, provider?: Provider): TransactionReceipt {
|
||||
const receipt = this.object({
|
||||
blockHash: this.hash,
|
||||
blockNumber: this.number,
|
||||
contractAddress: this.allowNull(this.address),
|
||||
cumulativeGasUsed: this.bigNumber,
|
||||
from: this.address,
|
||||
gasUsed: this.bigNumber,
|
||||
logs: this.arrayOf((v: any) => (this.log(v, provider))),
|
||||
logsBloom: this.data,
|
||||
root: this.allowNull(this.data),
|
||||
status: this.allowNull(this.number),
|
||||
to: this.address,
|
||||
gasPrice: this.allowNull(this.bigNumber),
|
||||
hash: this.hash,
|
||||
index: this.number,
|
||||
type: this.allowNull(this.number, 0),
|
||||
}, {
|
||||
hash: [ "transactionHash" ],
|
||||
gasPrice: [ "effectiveGasPrice" ],
|
||||
index: [ "transactionIndex" ]
|
||||
})(value);
|
||||
|
||||
// RSK incorrectly implemented EIP-658, so we munge things a bit here for it
|
||||
if (receipt.root != null) {
|
||||
if (receipt.root.length <= 4) {
|
||||
// Could be 0x00, 0x0, 0x01 or 0x1
|
||||
const value = parseInt(receipt.root);
|
||||
if (value === 0 || value === 1) {
|
||||
// Make sure if both are specified, they match
|
||||
if (receipt.status != null && receipt.status !== value) {
|
||||
return logger.throwError("alt-root-status/status mismatch", "BAD_DATA", {
|
||||
value: { root: receipt.root, status: receipt.status }
|
||||
});
|
||||
}
|
||||
receipt.status = value;
|
||||
delete receipt.root;
|
||||
} else {
|
||||
return logger.throwError("invalid alt-root-status", "BAD_DATA", {
|
||||
value: receipt.root
|
||||
});
|
||||
}
|
||||
} else if (!isHexString(receipt.root, 32)) {
|
||||
// Must be a valid bytes32
|
||||
return logger.throwError("invalid receipt root hash", "BAD_DATA", {
|
||||
value: receipt.root
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//receipt.byzantium = (receipt.root == null);
|
||||
|
||||
return new TransactionReceipt(receipt, provider);
|
||||
}
|
||||
|
||||
// Fitlers
|
||||
topics(value: any): Array<string> {
|
||||
return this.#format.topics(value);
|
||||
}
|
||||
|
||||
filter(value: any): Filter {
|
||||
return this.#format.filter(value);
|
||||
}
|
||||
|
||||
filterLog(value: any): any {
|
||||
console.log("ME", value);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Converts a serialized transaction to a TransactionResponse
|
||||
transaction(value: any): TransactionResponse {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
// Useful utility formatters functions, which if need be use the
|
||||
// methods within the formatter to ensure internal compatibility
|
||||
|
||||
// Access List; converts an AccessListish to an AccessList
|
||||
accessList(value: any): AccessList {
|
||||
return accessListify(value);
|
||||
}
|
||||
|
||||
// Converts falsish values to a specific value, otherwise use the formatter. Calls preserve `this`.
|
||||
allowFalsish(format: FormatFunc, ifFalse: any): FormatFunc {
|
||||
return ((value: any) => {
|
||||
if (!value) { return ifFalse; }
|
||||
return format.call(this, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Allows null, optionally replacing it with a default value. Calls preserve `this`.
|
||||
allowNull(format: FormatFunc, ifNull?: any): FormatFunc {
|
||||
return ((value: any) => {
|
||||
if (value == null) { return ifNull; }
|
||||
return format.call(this, value);
|
||||
});
|
||||
}
|
||||
|
||||
// Requires an Array satisfying the formatter. Calls preserves `this`.
|
||||
arrayOf(format: FormatFunc): FormatFunc {
|
||||
return ((array: any) => {
|
||||
if (!Array.isArray(array)) { throw new Error("not an array"); }
|
||||
return array.map((i) => format.call(this, i));
|
||||
});
|
||||
}
|
||||
|
||||
// Requires a value which is a value BigNumber
|
||||
bigNumber(value: any): bigint {
|
||||
return logger.getBigInt(value, "value");
|
||||
}
|
||||
|
||||
uint256(value: any): bigint {
|
||||
const result = this.bigNumber(value);
|
||||
if (result < 0 || result > BN_MAX_UINT256) {
|
||||
logger.throwArgumentError("invalid uint256", "value", value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Requires a value which is a value boolean or string equivalent
|
||||
boolean(value: any): boolean {
|
||||
switch (value) {
|
||||
case true: case "true":
|
||||
return true;
|
||||
case false: case "false":
|
||||
return false;
|
||||
}
|
||||
return logger.throwArgumentError(`invalid boolean; ${ JSON.stringify(value) }`, "value", value);
|
||||
}
|
||||
|
||||
// Requires a value which is a valid hexstring. If dataOrLength is true,
|
||||
// the length must be even (i.e. a datahexstring) or if it is a number,
|
||||
// specifies teh number of bytes value must represent
|
||||
_hexstring(dataOrLength?: boolean | number): FormatFunc {
|
||||
if (dataOrLength == null) { dataOrLength = false; }
|
||||
return (function(value: any) {
|
||||
if (isHexString(value, dataOrLength)) {
|
||||
return value.toLowerCase();
|
||||
}
|
||||
throw new Error("bad hexstring");
|
||||
});
|
||||
}
|
||||
|
||||
data(value: string): string {
|
||||
if (dataLength(value) == null) {
|
||||
logger.throwArgumentError("", "value", value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Requires a network-native hash
|
||||
hash(value: any): string {
|
||||
if (dataLength(value) !== 32) {
|
||||
logger.throwArgumentError("", "value", value);
|
||||
}
|
||||
return this.#format.data(value);
|
||||
}
|
||||
|
||||
// Requires a valid number, within the IEEE 754 safe range
|
||||
number(value: any): number {
|
||||
return logger.getNumber(value);
|
||||
}
|
||||
|
||||
// Requires an object which matches a fleet of other formatters
|
||||
// Any FormatFunc may return `undefined` to have the value omitted
|
||||
// from the result object. Calls preserve `this`.
|
||||
object(format: Record<string, FormatFunc>, altNames?: Record<string, Array<string>>): FormatFunc {
|
||||
return ((value: any) => {
|
||||
const result: any = { };
|
||||
for (const key in format) {
|
||||
let srcKey = key;
|
||||
if (altNames && key in altNames && !(srcKey in value)) {
|
||||
for (const altKey of altNames[key]) {
|
||||
if (altKey in value) {
|
||||
srcKey = altKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const nv = format[key].call(this, value[srcKey]);
|
||||
if (nv !== undefined) { result[key] = nv; }
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error) ? error.message: "not-an-error";
|
||||
logger.throwError(`invalid value for value.${ key } (${ message })`, "BAD_DATA", { value })
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
92
src.ts/providers/index.ts
Normal file
92
src.ts/providers/index.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/*
|
||||
export {
|
||||
AbstractProvider, UnmanagedSubscriber
|
||||
} from "./abstract-provider.js";
|
||||
*/
|
||||
|
||||
export {
|
||||
AbstractSigner,
|
||||
VoidSigner,
|
||||
WrappedSigner
|
||||
} from "./abstract-signer.js";
|
||||
/*
|
||||
export {
|
||||
showThrottleMessage
|
||||
} from "./community.js";
|
||||
|
||||
export { getDefaultProvider } from "./default-provider.js";
|
||||
|
||||
export { EnsResolver } from "./ens-resolver.js";
|
||||
*/
|
||||
|
||||
export { Formatter } from "./formatter.js";
|
||||
|
||||
export { Network } from "./common-networks.js";
|
||||
|
||||
export {
|
||||
NetworkPlugin,
|
||||
GasCostPlugin,
|
||||
EnsPlugin,
|
||||
//LayerOneConnectionPlugin,
|
||||
//MaxPriorityFeePlugin,
|
||||
//PriceOraclePlugin,
|
||||
} from "./plugins-network.js";
|
||||
|
||||
export {
|
||||
Block,
|
||||
FeeData,
|
||||
Log,
|
||||
TransactionReceipt,
|
||||
TransactionResponse,
|
||||
|
||||
dummyProvider,
|
||||
|
||||
copyRequest,
|
||||
//resolveTransactionRequest,
|
||||
} from "./provider.js";
|
||||
|
||||
export { FallbackProvider } from "./provider-fallback.js";
|
||||
export { JsonRpcApiProvider, JsonRpcProvider, JsonRpcSigner } from "./provider-jsonrpc.js"
|
||||
|
||||
export { AlchemyProvider } from "./provider-alchemy.js";
|
||||
export { AnkrProvider } from "./provider-ankr.js";
|
||||
export { CloudflareProvider } from "./provider-cloudflare.js";
|
||||
export { EtherscanProvider } from "./provider-etherscan.js";
|
||||
export { InfuraProvider } from "./provider-infura.js";
|
||||
//export { PocketProvider } from "./provider-pocket.js";
|
||||
|
||||
import { IpcSocketProvider } from "./provider-ipcsocket.js"; /*-browser*/
|
||||
export { IpcSocketProvider };
|
||||
export { SocketProvider } from "./provider-socket.js";
|
||||
export { WebSocketProvider } from "./provider-websocket.js";
|
||||
|
||||
|
||||
/*
|
||||
export type {
|
||||
ProviderPlugin, Subscriber, Subscription
|
||||
} from "./abstract-provider.js"
|
||||
*/
|
||||
export type { ContractRunner } from "./contracts.js";
|
||||
/*
|
||||
export type {
|
||||
CommunityResourcable
|
||||
} from "./community.js";
|
||||
|
||||
export type {
|
||||
AvatarLinkageType, AvatarLinkage, AvatarResult
|
||||
} from "./ens-resolver.js";
|
||||
*/
|
||||
export type { FormatFunc } from "./formatter.js";
|
||||
|
||||
export type { Networkish } from "./network.js";
|
||||
|
||||
export type { GasCostParameters } from "./plugins-network.js";
|
||||
|
||||
export type {
|
||||
BlockTag,
|
||||
CallRequest, TransactionRequest, PreparedRequest,
|
||||
EventFilter, Filter, FilterByBlockHash, OrphanFilter, ProviderEvent, TopicFilter,
|
||||
Provider,
|
||||
} from "./provider.js";
|
||||
|
||||
export type { Signer } from "./signer.js";
|
236
src.ts/providers/network.ts
Normal file
236
src.ts/providers/network.ts
Normal file
@ -0,0 +1,236 @@
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { getStore, setStore } from "../utils/storage.js";
|
||||
|
||||
import { Formatter } from "./formatter.js";
|
||||
import { EnsPlugin, GasCostPlugin } from "./plugins-network.js";
|
||||
|
||||
import type { BigNumberish } from "../utils/index.js";
|
||||
import type { Freezable, Frozen } from "../utils/index.js";
|
||||
import type { TransactionLike } from "../transaction/index.js";
|
||||
|
||||
import type { NetworkPlugin } from "./plugins-network.js";
|
||||
|
||||
/**
|
||||
* A Networkish can be used to allude to a Network, by specifing:
|
||||
* - a [[Network]] object
|
||||
* - a well-known (or registered) network name
|
||||
* - a well-known (or registered) chain ID
|
||||
* - an object with sufficient details to describe a network
|
||||
*/
|
||||
export type Networkish = Network | number | bigint | string | {
|
||||
name?: string,
|
||||
chainId?: number,
|
||||
//layerOneConnection?: Provider,
|
||||
ensAddress?: string,
|
||||
ensNetwork?: number
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/* * * *
|
||||
// Networks which operation against an L2 can use this plugin to
|
||||
// specify how to access L1, for the purpose of resolving ENS,
|
||||
// for example.
|
||||
export class LayerOneConnectionPlugin extends NetworkPlugin {
|
||||
readonly provider!: Provider;
|
||||
// @TODO: Rename to ChainAccess and allow for connecting to any chain
|
||||
constructor(provider: Provider) {
|
||||
super("org.ethers.plugins.layer-one-connection");
|
||||
defineProperties<LayerOneConnectionPlugin>(this, { provider });
|
||||
}
|
||||
|
||||
clone(): LayerOneConnectionPlugin {
|
||||
return new LayerOneConnectionPlugin(this.provider);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/* * * *
|
||||
export class PriceOraclePlugin extends NetworkPlugin {
|
||||
readonly address!: string;
|
||||
|
||||
constructor(address: string) {
|
||||
super("org.ethers.plugins.price-oracle");
|
||||
defineProperties<PriceOraclePlugin>(this, { address });
|
||||
}
|
||||
|
||||
clone(): PriceOraclePlugin {
|
||||
return new PriceOraclePlugin(this.address);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Networks or clients with a higher need for security (such as clients
|
||||
// that may automatically make CCIP requests without user interaction)
|
||||
// can use this plugin to anonymize requests or intercept CCIP requests
|
||||
// to notify and/or receive authorization from the user
|
||||
/* * * *
|
||||
export type FetchDataFunc = (req: Frozen<FetchRequest>) => Promise<FetchRequest>;
|
||||
export class CcipPreflightPlugin extends NetworkPlugin {
|
||||
readonly fetchData!: FetchDataFunc;
|
||||
|
||||
constructor(fetchData: FetchDataFunc) {
|
||||
super("org.ethers.plugins.ccip-preflight");
|
||||
defineProperties<CcipPreflightPlugin>(this, { fetchData });
|
||||
}
|
||||
|
||||
clone(): CcipPreflightPlugin {
|
||||
return new CcipPreflightPlugin(this.fetchData);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const Networks: Map<string | bigint, () => Network> = new Map();
|
||||
|
||||
const defaultFormatter = new Formatter();
|
||||
|
||||
export class Network implements Freezable<Network> {
|
||||
#props: {
|
||||
name: string,
|
||||
chainId: bigint,
|
||||
|
||||
formatter: Formatter,
|
||||
|
||||
plugins: Map<string, NetworkPlugin>
|
||||
};
|
||||
|
||||
constructor(name: string, _chainId: BigNumberish, formatter?: Formatter) {
|
||||
const chainId = logger.getBigInt(_chainId);
|
||||
if (formatter == null) { formatter = defaultFormatter; }
|
||||
const plugins = new Map();
|
||||
this.#props = { name, chainId, formatter, plugins };
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
return { name: this.name, chainId: this.chainId };
|
||||
}
|
||||
|
||||
get name(): string { return getStore(this.#props, "name"); }
|
||||
set name(value: string) { setStore(this.#props, "name", value); }
|
||||
|
||||
get chainId(): bigint { return getStore(this.#props, "chainId"); }
|
||||
set chainId(value: BigNumberish) { setStore(this.#props, "chainId", logger.getBigInt(value, "chainId")); }
|
||||
|
||||
get formatter(): Formatter { return getStore(this.#props, "formatter"); }
|
||||
set formatter(value: Formatter) { setStore(this.#props, "formatter", value); }
|
||||
|
||||
get plugins(): Array<NetworkPlugin> {
|
||||
return Array.from(this.#props.plugins.values());
|
||||
}
|
||||
|
||||
attachPlugin(plugin: NetworkPlugin): this {
|
||||
if (this.isFrozen()) { throw new Error("frozen"); }
|
||||
if (this.#props.plugins.get(plugin.name)) {
|
||||
throw new Error(`cannot replace existing plugin: ${ plugin.name } `);
|
||||
}
|
||||
this.#props.plugins.set(plugin.name, plugin.validate(this));
|
||||
return this;
|
||||
}
|
||||
|
||||
getPlugin<T extends NetworkPlugin = NetworkPlugin>(name: string): null | T {
|
||||
return <T>(this.#props.plugins.get(name)) || null;
|
||||
}
|
||||
|
||||
// Gets a list of Plugins which match basename, ignoring any fragment
|
||||
getPlugins<T extends NetworkPlugin = NetworkPlugin>(basename: string): Array<T> {
|
||||
return <Array<T>>(this.plugins.filter((p) => (p.name.split("#")[0] === basename)));
|
||||
}
|
||||
|
||||
clone(): Network {
|
||||
const clone = new Network(this.name, this.chainId, this.formatter);
|
||||
this.plugins.forEach((plugin) => {
|
||||
clone.attachPlugin(plugin.clone());
|
||||
});
|
||||
return clone;
|
||||
}
|
||||
|
||||
freeze(): Frozen<Network> {
|
||||
Object.freeze(this.#props);
|
||||
return this;
|
||||
}
|
||||
|
||||
isFrozen(): boolean {
|
||||
return Object.isFrozen(this.#props);
|
||||
}
|
||||
|
||||
computeIntrinsicGas(tx: TransactionLike): number {
|
||||
const costs = this.getPlugin<GasCostPlugin>("org.ethers.gas-cost") || (new GasCostPlugin());
|
||||
|
||||
let gas = costs.txBase;
|
||||
if (tx.to == null) { gas += costs.txCreate; }
|
||||
if (tx.data) {
|
||||
for (let i = 2; i < tx.data.length; i += 2) {
|
||||
if (tx.data.substring(i, i + 2) === "00") {
|
||||
gas += costs.txDataZero;
|
||||
} else {
|
||||
gas += costs.txDataNonzero;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tx.accessList) {
|
||||
const accessList = this.formatter.accessList(tx.accessList);
|
||||
for (const addr in accessList) {
|
||||
gas += costs.txAccessListAddress + costs.txAccessListStorageKey * accessList[addr].storageKeys.length;
|
||||
}
|
||||
}
|
||||
|
||||
return gas;
|
||||
}
|
||||
|
||||
static from(network?: Networkish): Network {
|
||||
// Default network
|
||||
if (network == null) { return Network.from("homestead"); }
|
||||
|
||||
// Canonical name or chain ID
|
||||
if (typeof(network) === "number") { network = BigInt(network); }
|
||||
if (typeof(network) === "string" || typeof(network) === "bigint") {
|
||||
const networkFunc = Networks.get(network);
|
||||
if (networkFunc) { return networkFunc(); }
|
||||
if (typeof(network) === "bigint") {
|
||||
return new Network("unknown", network);
|
||||
}
|
||||
|
||||
logger.throwArgumentError("unknown network", "network", network);
|
||||
}
|
||||
|
||||
// Clonable with network-like abilities
|
||||
if (typeof((<Network>network).clone) === "function") {
|
||||
const clone = (<Network>network).clone();
|
||||
//if (typeof(network.name) !== "string" || typeof(network.chainId) !== "number") {
|
||||
//}
|
||||
return clone;
|
||||
}
|
||||
|
||||
// Networkish
|
||||
if (typeof(network) === "object") {
|
||||
if (typeof(network.name) !== "string" || typeof(network.chainId) !== "number") {
|
||||
logger.throwArgumentError("invalid network object name or chainId", "network", network);
|
||||
}
|
||||
|
||||
const custom = new Network(<string>(network.name), <number>(network.chainId));
|
||||
|
||||
if ((<any>network).ensAddress || (<any>network).ensNetwork != null) {
|
||||
custom.attachPlugin(new EnsPlugin((<any>network).ensAddress, (<any>network).ensNetwork));
|
||||
}
|
||||
|
||||
//if ((<any>network).layerOneConnection) {
|
||||
// custom.attachPlugin(new LayerOneConnectionPlugin((<any>network).layerOneConnection));
|
||||
//}
|
||||
|
||||
return custom;
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("invalid network", "network", network);
|
||||
}
|
||||
|
||||
static register(nameOrChainId: string | number | bigint, networkFunc: () => Network): void {
|
||||
if (typeof(nameOrChainId) === "number") { nameOrChainId = BigInt(nameOrChainId); }
|
||||
const existing = Networks.get(nameOrChainId);
|
||||
if (existing) {
|
||||
logger.throwArgumentError(`conflicting network for ${ JSON.stringify(existing.name) }`, "nameOrChainId", nameOrChainId);
|
||||
}
|
||||
Networks.set(nameOrChainId, networkFunc);
|
||||
}
|
||||
}
|
8
src.ts/providers/pagination.ts
Normal file
8
src.ts/providers/pagination.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface PaginationResult<R> extends Array<R> {
|
||||
next(): Promise<PaginationResult<R>>;
|
||||
|
||||
// The total number of results available or null if unknown
|
||||
totalResults: null | number;
|
||||
|
||||
done: boolean;
|
||||
}
|
143
src.ts/providers/plugins-network.ts
Normal file
143
src.ts/providers/plugins-network.ts
Normal file
@ -0,0 +1,143 @@
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
//import { BigNumberish } from "../math/index.js";
|
||||
|
||||
import type { Network } from "./network.js";
|
||||
import type { FeeData, Provider } from "./provider.js";
|
||||
|
||||
|
||||
const EnsAddress = "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e";
|
||||
|
||||
export class NetworkPlugin {
|
||||
readonly name!: string;
|
||||
|
||||
constructor(name: string) {
|
||||
defineProperties<NetworkPlugin>(this, { name });
|
||||
}
|
||||
|
||||
clone(): NetworkPlugin {
|
||||
return new NetworkPlugin(this.name);
|
||||
}
|
||||
|
||||
validate(network: Network): NetworkPlugin {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// Networks can use this plugin to override calculations for the
|
||||
// intrinsic gas cost of a transaction for networks that differ
|
||||
// from the latest hardfork on Ethereum mainnet.
|
||||
export type GasCostParameters = {
|
||||
txBase?: number;
|
||||
txCreate?: number;
|
||||
txDataZero?: number;
|
||||
txDataNonzero?: number;
|
||||
txAccessListStorageKey?: number;
|
||||
txAccessListAddress?: number;
|
||||
};
|
||||
|
||||
export class GasCostPlugin extends NetworkPlugin {
|
||||
readonly effectiveBlock!: number;
|
||||
|
||||
readonly txBase!: number;
|
||||
readonly txCreate!: number;
|
||||
readonly txDataZero!: number;
|
||||
readonly txDataNonzero!: number;
|
||||
readonly txAccessListStorageKey!: number;
|
||||
readonly txAccessListAddress!: number;
|
||||
|
||||
constructor(effectiveBlock: number = 0, costs?: GasCostParameters) {
|
||||
super(`org.ethers.network-plugins.gas-cost#${ (effectiveBlock || 0) }`);
|
||||
|
||||
const props: Record<string, number> = { effectiveBlock };
|
||||
function set(name: keyof GasCostParameters, nullish: number): void {
|
||||
let value = (costs || { })[name];
|
||||
if (value == null) { value = nullish; }
|
||||
if (typeof(value) !== "number") {
|
||||
logger.throwArgumentError(`invalud value for ${ name }`, "costs", costs);
|
||||
}
|
||||
props[name] = value;
|
||||
}
|
||||
|
||||
set("txBase", 21000);
|
||||
set("txCreate", 32000);
|
||||
set("txDataZero", 4);
|
||||
set("txDataNonzero", 16);
|
||||
set("txAccessListStorageKey", 1900);
|
||||
set("txAccessListAddress", 2400);
|
||||
|
||||
defineProperties<GasCostPlugin>(this, props);
|
||||
}
|
||||
|
||||
clone(): GasCostPlugin {
|
||||
return new GasCostPlugin(this.effectiveBlock, this);
|
||||
}
|
||||
}
|
||||
|
||||
// Networks shoudl use this plugin to specify the contract address
|
||||
// and network necessary to resolve ENS names.
|
||||
export class EnsPlugin extends NetworkPlugin {
|
||||
|
||||
// The ENS contract address
|
||||
readonly address!: string;
|
||||
|
||||
// The network ID that the ENS contract lives on
|
||||
readonly targetNetwork!: number;
|
||||
|
||||
constructor(address?: null | string, targetNetwork?: null | number) {
|
||||
super("org.ethers.network-plugins.ens");
|
||||
defineProperties<EnsPlugin>(this, {
|
||||
address: (address || EnsAddress),
|
||||
targetNetwork: ((targetNetwork == null) ? 1: targetNetwork)
|
||||
});
|
||||
}
|
||||
|
||||
clone(): EnsPlugin {
|
||||
return new EnsPlugin(this.address, this.targetNetwork);
|
||||
}
|
||||
|
||||
validate(network: Network): this {
|
||||
network.formatter.address(this.address);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
/*
|
||||
export class MaxPriorityFeePlugin extends NetworkPlugin {
|
||||
readonly priorityFee!: bigint;
|
||||
|
||||
constructor(priorityFee: BigNumberish) {
|
||||
super("org.ethers.plugins.max-priority-fee");
|
||||
defineProperties<MaxPriorityFeePlugin>(this, {
|
||||
priorityFee: logger.getBigInt(priorityFee)
|
||||
});
|
||||
}
|
||||
|
||||
async getPriorityFee(provider: Provider): Promise<bigint> {
|
||||
return this.priorityFee;
|
||||
}
|
||||
|
||||
clone(): MaxPriorityFeePlugin {
|
||||
return new MaxPriorityFeePlugin(this.priorityFee);
|
||||
}
|
||||
}
|
||||
*/
|
||||
export class FeeDataNetworkPlugin extends NetworkPlugin {
|
||||
readonly #feeDataFunc: (provider: Provider) => Promise<FeeData>;
|
||||
|
||||
get feeDataFunc(): (provider: Provider) => Promise<FeeData> { return this.#feeDataFunc; }
|
||||
|
||||
constructor(feeDataFunc: (provider: Provider) => Promise<FeeData>) {
|
||||
super("org.ethers.network-plugins.fee-data");
|
||||
this.#feeDataFunc = feeDataFunc;
|
||||
}
|
||||
|
||||
async getFeeData(provider: Provider): Promise<FeeData> {
|
||||
return await this.#feeDataFunc(provider);
|
||||
}
|
||||
|
||||
clone(): FeeDataNetworkPlugin {
|
||||
return new FeeDataNetworkPlugin(this.#feeDataFunc);
|
||||
}
|
||||
}
|
114
src.ts/providers/provider-alchemy.ts
Normal file
114
src.ts/providers/provider-alchemy.ts
Normal file
@ -0,0 +1,114 @@
|
||||
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { FetchRequest } from "../utils/fetch.js";
|
||||
|
||||
import { showThrottleMessage } from "./community.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Network } from "./network.js";
|
||||
import { JsonRpcProvider } from "./provider-jsonrpc.js";
|
||||
|
||||
import type { AbstractProvider, PerformActionRequest } from "./abstract-provider.js";
|
||||
import type { CommunityResourcable } from "./community.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
const defaultApiKey = "_gg7wSSi0KMBsdKnGVfHDueq6xMB9EkC"
|
||||
|
||||
function getHost(name: string): string {
|
||||
switch(name) {
|
||||
case "homestead":
|
||||
return "eth-mainnet.alchemyapi.io";
|
||||
case "ropsten":
|
||||
return "eth-ropsten.alchemyapi.io";
|
||||
case "rinkeby":
|
||||
return "eth-rinkeby.alchemyapi.io";
|
||||
case "goerli":
|
||||
return "eth-goerli.alchemyapi.io";
|
||||
case "kovan":
|
||||
return "eth-kovan.alchemyapi.io";
|
||||
case "matic":
|
||||
return "polygon-mainnet.g.alchemy.com";
|
||||
case "maticmum":
|
||||
return "polygon-mumbai.g.alchemy.com";
|
||||
case "arbitrum":
|
||||
return "arb-mainnet.g.alchemy.com";
|
||||
case "arbitrum-rinkeby":
|
||||
return "arb-rinkeby.g.alchemy.com";
|
||||
case "optimism":
|
||||
return "opt-mainnet.g.alchemy.com";
|
||||
case "optimism-kovan":
|
||||
return "opt-kovan.g.alchemy.com";
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("unsupported network", "network", name);
|
||||
}
|
||||
|
||||
export class AlchemyProvider extends JsonRpcProvider implements CommunityResourcable {
|
||||
readonly apiKey!: string;
|
||||
|
||||
constructor(_network: Networkish = "homestead", apiKey?: null | string) {
|
||||
const network = Network.from(_network);
|
||||
if (apiKey == null) { apiKey = defaultApiKey; }
|
||||
|
||||
const request = AlchemyProvider.getRequest(network, apiKey);
|
||||
super(request, network, { staticNetwork: network });
|
||||
|
||||
defineProperties<AlchemyProvider>(this, { apiKey });
|
||||
}
|
||||
|
||||
_getProvider(chainId: number): AbstractProvider {
|
||||
try {
|
||||
return new AlchemyProvider(chainId, this.apiKey);
|
||||
} catch (error) { }
|
||||
return super._getProvider(chainId);
|
||||
}
|
||||
|
||||
async _perform(req: PerformActionRequest): Promise<any> {
|
||||
|
||||
// https://docs.alchemy.com/reference/trace-transaction
|
||||
if (req.method === "getTransactionResult") {
|
||||
const trace = await this.send("trace_transaction", [ req.hash ]);
|
||||
if (trace == null) { return null; }
|
||||
|
||||
let data: undefined | string;
|
||||
let error = false;
|
||||
try {
|
||||
data = trace[0].result.output;
|
||||
error = (trace[0].error === "Reverted");
|
||||
} catch (error) { }
|
||||
|
||||
if (data) {
|
||||
if (error) {
|
||||
logger.throwError("an error occurred during transaction executions", "CALL_EXCEPTION", {
|
||||
data
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
return logger.throwError("could not parse trace result", "BAD_DATA", { value: trace });
|
||||
}
|
||||
|
||||
return await super._perform(req);
|
||||
}
|
||||
|
||||
isCommunityResource(): boolean {
|
||||
return (this.apiKey === defaultApiKey);
|
||||
}
|
||||
|
||||
static getRequest(network: Network, apiKey?: string): FetchRequest {
|
||||
if (apiKey == null) { apiKey = defaultApiKey; }
|
||||
|
||||
const request = new FetchRequest(`https:/\/${ getHost(network.name) }/v2/${ apiKey }`);
|
||||
request.allowGzip = true;
|
||||
|
||||
if (apiKey === defaultApiKey) {
|
||||
request.retryFunc = async (request, response, attempt) => {
|
||||
showThrottleMessage("alchemy");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
}
|
77
src.ts/providers/provider-ankr.ts
Normal file
77
src.ts/providers/provider-ankr.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { FetchRequest } from "../utils/fetch.js";
|
||||
|
||||
import { AbstractProvider } from "./abstract-provider.js";
|
||||
import { showThrottleMessage } from "./community.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Network } from "./network.js";
|
||||
import { JsonRpcProvider } from "./provider-jsonrpc.js";
|
||||
|
||||
import type { CommunityResourcable } from "./community.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
const defaultApiKey = "9f7d929b018cdffb338517efa06f58359e86ff1ffd350bc889738523659e7972";
|
||||
|
||||
function getHost(name: string): string {
|
||||
switch (name) {
|
||||
case "homestead":
|
||||
return "rpc.ankr.com/eth";
|
||||
case "ropsten":
|
||||
return "rpc.ankr.com/eth_ropsten";
|
||||
case "rinkeby":
|
||||
return "rpc.ankr.com/eth_rinkeby";
|
||||
case "goerli":
|
||||
return "rpc.ankr.com/eth_goerli";
|
||||
case "matic":
|
||||
return "rpc.ankr.com/polygon";
|
||||
case "arbitrum":
|
||||
return "rpc.ankr.com/arbitrum";
|
||||
}
|
||||
return logger.throwArgumentError("unsupported network", "network", name);
|
||||
}
|
||||
|
||||
|
||||
export class AnkrProvider extends JsonRpcProvider implements CommunityResourcable {
|
||||
readonly apiKey!: string;
|
||||
|
||||
constructor(_network: Networkish = "homestead", apiKey?: null | string) {
|
||||
const network = Network.from(_network);
|
||||
if (apiKey == null) { apiKey = defaultApiKey; }
|
||||
|
||||
// Ankr does not support filterId, so we force polling
|
||||
const options = { polling: true, staticNetwork: network };
|
||||
|
||||
const request = AnkrProvider.getRequest(network, apiKey);
|
||||
super(request, network, options);
|
||||
|
||||
defineProperties<AnkrProvider>(this, { apiKey });
|
||||
}
|
||||
|
||||
_getProvider(chainId: number): AbstractProvider {
|
||||
try {
|
||||
return new AnkrProvider(chainId, this.apiKey);
|
||||
} catch (error) { }
|
||||
return super._getProvider(chainId);
|
||||
}
|
||||
|
||||
static getRequest(network: Network, apiKey?: null | string): FetchRequest {
|
||||
if (apiKey == null) { apiKey = defaultApiKey; }
|
||||
|
||||
const request = new FetchRequest(`https:/\/${ getHost(network.name) }/${ apiKey }`);
|
||||
request.allowGzip = true;
|
||||
|
||||
if (apiKey === defaultApiKey) {
|
||||
request.retryFunc = async (request, response, attempt) => {
|
||||
showThrottleMessage("AnkrProvider");
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
isCommunityResource(): boolean {
|
||||
return (this.apiKey === defaultApiKey);
|
||||
}
|
||||
}
|
17
src.ts/providers/provider-cloudflare.ts
Normal file
17
src.ts/providers/provider-cloudflare.ts
Normal file
@ -0,0 +1,17 @@
|
||||
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Network } from "./network.js";
|
||||
import { JsonRpcProvider } from "./provider-jsonrpc.js";
|
||||
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
export class CloudflareProvider extends JsonRpcProvider {
|
||||
constructor(_network: Networkish = "homestead") {
|
||||
const network = Network.from(_network);
|
||||
if (network.name !== "homestead") {
|
||||
return logger.throwArgumentError("unsupported network", "network", _network);
|
||||
}
|
||||
super("https:/\/cloudflare-eth.com/", network, { staticNetwork: network });
|
||||
}
|
||||
}
|
508
src.ts/providers/provider-etherscan.ts
Normal file
508
src.ts/providers/provider-etherscan.ts
Normal file
@ -0,0 +1,508 @@
|
||||
import { hexlify, isHexString } from "../utils/data.js";
|
||||
import { toQuantity } from "../utils/maths.js";
|
||||
import { isError } from "../utils/errors.js";
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { toUtf8String } from "../utils/utf8.js";
|
||||
import { FetchRequest } from "../utils/fetch.js";
|
||||
|
||||
if (false) { console.log(isHexString, isError); } // @TODO
|
||||
|
||||
import { AbstractProvider } from "./abstract-provider.js";
|
||||
import { Network } from "./network.js";
|
||||
import { NetworkPlugin } from "./plugins-network.js";
|
||||
|
||||
|
||||
import { PerformActionRequest } from "./abstract-provider.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
//import type { } from "./pagination";
|
||||
import type { TransactionRequest } from "./provider.js";
|
||||
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
|
||||
const defaultApiKey = "9D13ZE7XSBTJ94N9BNJ2MA33VMAY2YPIRB";
|
||||
|
||||
|
||||
|
||||
const EtherscanPluginId = "org.ethers.plugins.etherscan";
|
||||
export class EtherscanPlugin extends NetworkPlugin {
|
||||
readonly baseUrl!: string;
|
||||
readonly communityApiKey!: string;
|
||||
|
||||
constructor(baseUrl: string, communityApiKey: string) {
|
||||
super(EtherscanPluginId);
|
||||
//if (communityApiKey == null) { communityApiKey = null; }
|
||||
defineProperties<EtherscanPlugin>(this, { baseUrl, communityApiKey });
|
||||
}
|
||||
|
||||
clone(): EtherscanPlugin {
|
||||
return new EtherscanPlugin(this.baseUrl, this.communityApiKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class EtherscanProvider extends AbstractProvider {
|
||||
readonly network!: Network;
|
||||
readonly apiKey!: string;
|
||||
|
||||
constructor(_network?: Networkish, apiKey?: string) {
|
||||
super();
|
||||
|
||||
const network = Network.from(_network);
|
||||
if (apiKey == null) {
|
||||
const plugin = network.getPlugin<EtherscanPlugin>(EtherscanPluginId);
|
||||
if (plugin) {
|
||||
apiKey = plugin.communityApiKey;
|
||||
} else {
|
||||
apiKey = defaultApiKey;
|
||||
}
|
||||
}
|
||||
|
||||
defineProperties<EtherscanProvider>(this, { apiKey, network });
|
||||
|
||||
// Test that the network is supported by Etherscan
|
||||
this.getBaseUrl();
|
||||
}
|
||||
|
||||
getBaseUrl(): string {
|
||||
const plugin = this.network.getPlugin<EtherscanPlugin>(EtherscanPluginId);
|
||||
if (plugin) { return plugin.baseUrl; }
|
||||
|
||||
switch(this.network.name) {
|
||||
case "homestead":
|
||||
return "https:/\/api.etherscan.io";
|
||||
case "ropsten":
|
||||
return "https:/\/api-ropsten.etherscan.io";
|
||||
case "rinkeby":
|
||||
return "https:/\/api-rinkeby.etherscan.io";
|
||||
case "kovan":
|
||||
return "https:/\/api-kovan.etherscan.io";
|
||||
case "goerli":
|
||||
return "https:/\/api-goerli.etherscan.io";
|
||||
default:
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("unsupported network", "network", this.network);
|
||||
}
|
||||
|
||||
getUrl(module: string, params: Record<string, string>): string {
|
||||
const query = Object.keys(params).reduce((accum, key) => {
|
||||
const value = params[key];
|
||||
if (value != null) {
|
||||
accum += `&${ key }=${ value }`
|
||||
}
|
||||
return accum
|
||||
}, "");
|
||||
const apiKey = ((this.apiKey) ? `&apikey=${ this.apiKey }`: "");
|
||||
return `${ this.getBaseUrl() }/api?module=${ module }${ query }${ apiKey }`;
|
||||
}
|
||||
|
||||
getPostUrl(): string {
|
||||
return `${ this.getBaseUrl() }/api`;
|
||||
}
|
||||
|
||||
getPostData(module: string, params: Record<string, any>): Record<string, any> {
|
||||
params.module = module;
|
||||
params.apikey = this.apiKey;
|
||||
return params;
|
||||
}
|
||||
|
||||
async detectNetwork(): Promise<Network> {
|
||||
return this.network;
|
||||
}
|
||||
|
||||
async fetch(module: string, params: Record<string, any>, post?: boolean): Promise<any> {
|
||||
const url = (post ? this.getPostUrl(): this.getUrl(module, params));
|
||||
const payload = (post ? this.getPostData(module, params): null);
|
||||
|
||||
/*
|
||||
this.emit("debug", {
|
||||
action: "request",
|
||||
request: url,
|
||||
provider: this
|
||||
});
|
||||
*/
|
||||
const request = new FetchRequest(url);
|
||||
request.processFunc = async (request, response) => {
|
||||
const result = response.hasBody() ? JSON.parse(toUtf8String(response.body)): { };
|
||||
const throttle = ((typeof(result.result) === "string") ? result.result: "").toLowerCase().indexOf("rate limit") >= 0;
|
||||
if (module === "proxy") {
|
||||
// This JSON response indicates we are being throttled
|
||||
if (result && result.status == 0 && result.message == "NOTOK" && throttle) {
|
||||
response.throwThrottleError(result.result);
|
||||
}
|
||||
} else {
|
||||
if (throttle) {
|
||||
response.throwThrottleError(result.result);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
};
|
||||
// @TODO:
|
||||
//throttleSlotInterval: 1000,
|
||||
|
||||
if (payload) {
|
||||
request.setHeader("content-type", "application/x-www-form-urlencoded; charset=UTF-8");
|
||||
request.body = Object.keys(payload).map((k) => `${ k }=${ payload[k] }`).join("&");
|
||||
}
|
||||
|
||||
const response = await request.send();
|
||||
response.assertOk();
|
||||
|
||||
if (!response.hasBody()) {
|
||||
throw new Error();
|
||||
}
|
||||
|
||||
/*
|
||||
this.emit("debug", {
|
||||
action: "response",
|
||||
request: url,
|
||||
response: deepCopy(result),
|
||||
provider: this
|
||||
});
|
||||
*/
|
||||
|
||||
const result = JSON.parse(toUtf8String(response.body));
|
||||
|
||||
if (module === "proxy") {
|
||||
if (result.jsonrpc != "2.0") {
|
||||
// @TODO: not any
|
||||
const error: any = new Error("invalid response");
|
||||
error.result = JSON.stringify(result);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
// @TODO: not any
|
||||
const error: any = new Error(result.error.message || "unknown error");
|
||||
if (result.error.code) { error.code = result.error.code; }
|
||||
if (result.error.data) { error.data = result.error.data; }
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result.result;
|
||||
|
||||
} else {
|
||||
// getLogs, getHistory have weird success responses
|
||||
if (result.status == 0 && (result.message === "No records found" || result.message === "No transactions found")) {
|
||||
return result.result;
|
||||
}
|
||||
|
||||
if (result.status != 1 || result.message != "OK") {
|
||||
const error: any = new Error("invalid response");
|
||||
error.result = JSON.stringify(result);
|
||||
// if ((result.result || "").toLowerCase().indexOf("rate limit") >= 0) {
|
||||
// error.throttleRetry = true;
|
||||
// }
|
||||
throw error;
|
||||
}
|
||||
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
|
||||
// The transaction has already been sanitized by the calls in Provider
|
||||
_getTransactionPostData(transaction: TransactionRequest): Record<string, string> {
|
||||
const result: Record<string, string> = { };
|
||||
for (let key in transaction) {
|
||||
if ((<any>transaction)[key] == null) { continue; }
|
||||
let value = (<any>transaction)[key];
|
||||
if (key === "type" && value === 0) { continue; }
|
||||
|
||||
// Quantity-types require no leading zero, unless 0
|
||||
if ((<any>{ type: true, gasLimit: true, gasPrice: true, maxFeePerGs: true, maxPriorityFeePerGas: true, nonce: true, value: true })[key]) {
|
||||
value = toQuantity(hexlify(value));
|
||||
} else if (key === "accessList") {
|
||||
value = "[" + this.network.formatter.accessList(value).map((set) => {
|
||||
return `{address:"${ set.address }",storageKeys:["${ set.storageKeys.join('","') }"]}`;
|
||||
}).join(",") + "]";
|
||||
} else {
|
||||
value = hexlify(value);
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
_checkError(req: PerformActionRequest, error: Error, transaction: any): never {
|
||||
/*
|
||||
let body = "";
|
||||
if (isError(error, Logger.Errors.SERVER_ERROR) && error.response && error.response.hasBody()) {
|
||||
body = toUtf8String(error.response.body);
|
||||
}
|
||||
console.log(body);
|
||||
|
||||
// Undo the "convenience" some nodes are attempting to prevent backwards
|
||||
// incompatibility; maybe for v6 consider forwarding reverts as errors
|
||||
if (method === "call" && body) {
|
||||
|
||||
// Etherscan keeps changing their string
|
||||
if (body.match(/reverted/i) || body.match(/VM execution error/i)) {
|
||||
|
||||
// Etherscan prefixes the data like "Reverted 0x1234"
|
||||
let data = e.data;
|
||||
if (data) { data = "0x" + data.replace(/^.*0x/i, ""); }
|
||||
if (!isHexString(data)) { data = "0x"; }
|
||||
|
||||
logger.throwError("call exception", Logger.Errors.CALL_EXCEPTION, {
|
||||
error, data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Get the message from any nested error structure
|
||||
let message = error.message;
|
||||
if (isError(error, Logger.Errors.SERVER_ERROR)) {
|
||||
if (error.error && typeof(error.error.message) === "string") {
|
||||
message = error.error.message;
|
||||
} else if (typeof(error.body) === "string") {
|
||||
message = error.body;
|
||||
} else if (typeof(error.responseText) === "string") {
|
||||
message = error.responseText;
|
||||
}
|
||||
}
|
||||
message = (message || "").toLowerCase();
|
||||
|
||||
// "Insufficient funds. The account you tried to send transaction from
|
||||
// does not have enough funds. Required 21464000000000 and got: 0"
|
||||
if (message.match(/insufficient funds/)) {
|
||||
logger.throwError("insufficient funds for intrinsic transaction cost", Logger.Errors.INSUFFICIENT_FUNDS, {
|
||||
error, transaction, info: { method }
|
||||
});
|
||||
}
|
||||
|
||||
// "Transaction with the same hash was already imported."
|
||||
if (message.match(/same hash was already imported|transaction nonce is too low|nonce too low/)) {
|
||||
logger.throwError("nonce has already been used", Logger.Errors.NONCE_EXPIRED, {
|
||||
error, transaction, info: { method }
|
||||
});
|
||||
}
|
||||
|
||||
// "Transaction gas price is too low. There is another transaction with
|
||||
// same nonce in the queue. Try increasing the gas price or incrementing the nonce."
|
||||
if (message.match(/another transaction with same nonce/)) {
|
||||
logger.throwError("replacement fee too low", Logger.Errors.REPLACEMENT_UNDERPRICED, {
|
||||
error, transaction, info: { method }
|
||||
});
|
||||
}
|
||||
|
||||
if (message.match(/execution failed due to an exception|execution reverted/)) {
|
||||
logger.throwError("cannot estimate gas; transaction may fail or may require manual gas limit", Logger.Errors.UNPREDICTABLE_GAS_LIMIT, {
|
||||
error, transaction, info: { method }
|
||||
});
|
||||
}
|
||||
*/
|
||||
throw error;
|
||||
}
|
||||
|
||||
async _detectNetwork(): Promise<Network> {
|
||||
return this.network;
|
||||
}
|
||||
|
||||
async _perform(req: PerformActionRequest): Promise<any> {
|
||||
switch (req.method) {
|
||||
case "chainId":
|
||||
return this.network.chainId;
|
||||
|
||||
case "getBlockNumber":
|
||||
return this.fetch("proxy", { action: "eth_blockNumber" });
|
||||
|
||||
case "getGasPrice":
|
||||
return this.fetch("proxy", { action: "eth_gasPrice" });
|
||||
|
||||
case "getBalance":
|
||||
// Returns base-10 result
|
||||
return this.fetch("account", {
|
||||
action: "balance",
|
||||
address: req.address,
|
||||
tag: req.blockTag
|
||||
});
|
||||
|
||||
case "getTransactionCount":
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_getTransactionCount",
|
||||
address: req.address,
|
||||
tag: req.blockTag
|
||||
});
|
||||
|
||||
case "getCode":
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_getCode",
|
||||
address: req.address,
|
||||
tag: req.blockTag
|
||||
});
|
||||
|
||||
case "getStorageAt":
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_getStorageAt",
|
||||
address: req.address,
|
||||
position: req.position,
|
||||
tag: req.blockTag
|
||||
});
|
||||
|
||||
case "broadcastTransaction":
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_sendRawTransaction",
|
||||
hex: req.signedTransaction
|
||||
}, true).catch((error) => {
|
||||
return this._checkError(req, <Error>error, req.signedTransaction);
|
||||
});
|
||||
|
||||
case "getBlock":
|
||||
if ("blockTag" in req) {
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_getBlockByNumber",
|
||||
tag: req.blockTag,
|
||||
boolean: (req.includeTransactions ? "true": "false")
|
||||
});
|
||||
}
|
||||
|
||||
return logger.throwError("getBlock by blockHash not supported by Etherscan", "UNSUPPORTED_OPERATION", {
|
||||
operation: "getBlock(blockHash)"
|
||||
});
|
||||
|
||||
case "getTransaction":
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_getTransactionByHash",
|
||||
txhash: req.hash
|
||||
});
|
||||
|
||||
case "getTransactionReceipt":
|
||||
return this.fetch("proxy", {
|
||||
action: "eth_getTransactionReceipt",
|
||||
txhash: req.hash
|
||||
});
|
||||
|
||||
case "call": {
|
||||
if (req.blockTag !== "latest") {
|
||||
throw new Error("EtherscanProvider does not support blockTag for call");
|
||||
}
|
||||
|
||||
const postData = this._getTransactionPostData(req.transaction);
|
||||
postData.module = "proxy";
|
||||
postData.action = "eth_call";
|
||||
|
||||
try {
|
||||
return await this.fetch("proxy", postData, true);
|
||||
} catch (error) {
|
||||
return this._checkError(req, <Error>error, req.transaction);
|
||||
}
|
||||
}
|
||||
|
||||
case "estimateGas": {
|
||||
const postData = this._getTransactionPostData(req.transaction);
|
||||
postData.module = "proxy";
|
||||
postData.action = "eth_estimateGas";
|
||||
|
||||
try {
|
||||
return await this.fetch("proxy", postData, true);
|
||||
} catch (error) {
|
||||
return this._checkError(req, <Error>error, req.transaction);
|
||||
}
|
||||
}
|
||||
/*
|
||||
case "getLogs": {
|
||||
// Needs to complain if more than one address is passed in
|
||||
const args: Record<string, any> = { action: "getLogs" }
|
||||
|
||||
if (params.filter.fromBlock) {
|
||||
args.fromBlock = checkLogTag(params.filter.fromBlock);
|
||||
}
|
||||
|
||||
if (params.filter.toBlock) {
|
||||
args.toBlock = checkLogTag(params.filter.toBlock);
|
||||
}
|
||||
|
||||
if (params.filter.address) {
|
||||
args.address = params.filter.address;
|
||||
}
|
||||
|
||||
// @TODO: We can handle slightly more complicated logs using the logs API
|
||||
if (params.filter.topics && params.filter.topics.length > 0) {
|
||||
if (params.filter.topics.length > 1) {
|
||||
logger.throwError("unsupported topic count", Logger.Errors.UNSUPPORTED_OPERATION, { topics: params.filter.topics });
|
||||
}
|
||||
if (params.filter.topics.length === 1) {
|
||||
const topic0 = params.filter.topics[0];
|
||||
if (typeof(topic0) !== "string" || topic0.length !== 66) {
|
||||
logger.throwError("unsupported topic format", Logger.Errors.UNSUPPORTED_OPERATION, { topic0: topic0 });
|
||||
}
|
||||
args.topic0 = topic0;
|
||||
}
|
||||
}
|
||||
|
||||
const logs: Array<any> = await this.fetch("logs", args);
|
||||
|
||||
// Cache txHash => blockHash
|
||||
let blocks: { [tag: string]: string } = {};
|
||||
|
||||
// Add any missing blockHash to the logs
|
||||
for (let i = 0; i < logs.length; i++) {
|
||||
const log = logs[i];
|
||||
if (log.blockHash != null) { continue; }
|
||||
if (blocks[log.blockNumber] == null) {
|
||||
const block = await this.getBlock(log.blockNumber);
|
||||
if (block) {
|
||||
blocks[log.blockNumber] = block.hash;
|
||||
}
|
||||
}
|
||||
|
||||
log.blockHash = blocks[log.blockNumber];
|
||||
}
|
||||
|
||||
return logs;
|
||||
}
|
||||
*/
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return super._perform(req);
|
||||
}
|
||||
|
||||
async getNetwork(): Promise<Network> {
|
||||
return this.network;
|
||||
}
|
||||
|
||||
async getEtherPrice(): Promise<number> {
|
||||
if (this.network.name !== "homestead") { return 0.0; }
|
||||
return parseFloat((await this.fetch("stats", { action: "ethprice" })).ethusd);
|
||||
}
|
||||
|
||||
isCommunityResource(): boolean {
|
||||
const plugin = this.network.getPlugin<EtherscanPlugin>(EtherscanPluginId);
|
||||
if (plugin) { return (plugin.communityApiKey === this.apiKey); }
|
||||
|
||||
return (defaultApiKey === this.apiKey);
|
||||
}
|
||||
}
|
||||
/*
|
||||
(async function() {
|
||||
const provider = new EtherscanProvider();
|
||||
console.log(provider);
|
||||
console.log(await provider.getBlockNumber());
|
||||
/ *
|
||||
provider.on("block", (b) => {
|
||||
console.log("BB", b);
|
||||
});
|
||||
console.log(await provider.getTransactionReceipt("0xa5ded92f548e9f362192f9ab7e5b3fbc9b5a919a868e29247f177d49ce38de6e"));
|
||||
|
||||
provider.once("0xa5ded92f548e9f362192f9ab7e5b3fbc9b5a919a868e29247f177d49ce38de6e", (tx) => {
|
||||
console.log("TT", tx);
|
||||
});
|
||||
* /
|
||||
try {
|
||||
console.log(await provider.getBlock(100));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(await provider.getBlock(13821768));
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
})();
|
||||
*/
|
555
src.ts/providers/provider-fallback.ts
Normal file
555
src.ts/providers/provider-fallback.ts
Normal file
@ -0,0 +1,555 @@
|
||||
|
||||
import { hexlify } from "../utils/data.js";
|
||||
|
||||
import { AbstractProvider } from "./abstract-provider.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Network } from "./network.js"
|
||||
|
||||
import type { Frozen } from "../utils/index.js";
|
||||
|
||||
import type { PerformActionRequest } from "./abstract-provider.js";
|
||||
import type { Networkish } from "./network.js"
|
||||
|
||||
//const BN_0 = BigInt("0");
|
||||
const BN_1 = BigInt("1");
|
||||
const BN_2 = BigInt("2");
|
||||
|
||||
function shuffle<T = any>(array: Array<T>): void {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
const tmp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = tmp;
|
||||
}
|
||||
}
|
||||
|
||||
function stall(duration: number) {
|
||||
return new Promise((resolve) => { setTimeout(resolve, duration); });
|
||||
}
|
||||
|
||||
function getTime(): number { return (new Date()).getTime(); }
|
||||
|
||||
export interface FallbackProviderConfig {
|
||||
|
||||
// The provider
|
||||
provider: AbstractProvider;
|
||||
|
||||
// How long to wait for a response before getting impatient
|
||||
// and ispatching the next provider
|
||||
stallTimeout?: number;
|
||||
|
||||
// Lower values are dispatched first
|
||||
priority?: number;
|
||||
|
||||
// How much this provider contributes to the quorum
|
||||
weight?: number;
|
||||
};
|
||||
|
||||
const defaultConfig = { stallTimeout: 400, priority: 1, weight: 1 };
|
||||
|
||||
// We track a bunch of extra stuff that might help debug problems or
|
||||
// optimize infrastructure later on.
|
||||
export interface FallbackProviderState extends Required<FallbackProviderConfig> {
|
||||
|
||||
// The most recent blockNumber this provider has reported (-2 if none)
|
||||
blockNumber: number;
|
||||
|
||||
// The number of total requests ever sent to this provider
|
||||
requests: number;
|
||||
|
||||
// The number of responses that errored
|
||||
errorResponses: number;
|
||||
|
||||
// The number of responses that occured after the result resolved
|
||||
lateResponses: number;
|
||||
|
||||
// How many times syncing was required to catch up the expected block
|
||||
outOfSync: number;
|
||||
|
||||
// The number of requests which reported unsupported operation
|
||||
unsupportedEvents: number;
|
||||
|
||||
// A rolling average (5% current duration) for response time
|
||||
rollingDuration: number;
|
||||
|
||||
// The ratio of quorum-agreed results to total
|
||||
score: number;
|
||||
}
|
||||
|
||||
interface Config extends FallbackProviderState {
|
||||
_updateNumber: null | Promise<any>;
|
||||
_network: null | Frozen<Network>;
|
||||
_totalTime: number;
|
||||
}
|
||||
|
||||
const defaultState = {
|
||||
blockNumber: -2, requests: 0, lateResponses: 0, errorResponses: 0,
|
||||
outOfSync: -1, unsupportedEvents: 0, rollingDuration: 0, score: 0,
|
||||
_network: null, _updateNumber: null, _totalTime: 0
|
||||
};
|
||||
|
||||
|
||||
async function waitForSync(config: Config, blockNumber: number): Promise<void> {
|
||||
while (config.blockNumber < 0 || config.blockNumber < blockNumber) {
|
||||
if (!config._updateNumber) {
|
||||
config._updateNumber = (async () => {
|
||||
const blockNumber = await config.provider.getBlockNumber();
|
||||
if (blockNumber > config.blockNumber) {
|
||||
config.blockNumber = blockNumber;
|
||||
}
|
||||
config._updateNumber = null;
|
||||
})();
|
||||
}
|
||||
await config._updateNumber;
|
||||
config.outOfSync++;
|
||||
}
|
||||
}
|
||||
|
||||
export type FallbackProviderOptions = {
|
||||
// How many providers must agree on a value before reporting
|
||||
// back the response
|
||||
quorum: number;
|
||||
|
||||
// How many providers must have reported the same event
|
||||
// for it to be emitted
|
||||
eventQuorum: number;
|
||||
|
||||
// How many providers to dispatch each event to simultaneously.
|
||||
// Set this to 0 to use getLog polling, which implies eventQuorum
|
||||
// is equal to quorum.
|
||||
eventWorkers: number;
|
||||
};
|
||||
|
||||
type RunningState = {
|
||||
config: Config;
|
||||
staller: null | Promise<void>;
|
||||
didBump: boolean;
|
||||
perform: null | Promise<any>;
|
||||
done: boolean;
|
||||
result: { result: any } | { error: Error }
|
||||
}
|
||||
|
||||
// Normalizes a result to a string that can be used to compare against
|
||||
// other results using normal string equality
|
||||
function normalize(network: Frozen<Network>, value: any, req: PerformActionRequest): string {
|
||||
switch (req.method) {
|
||||
case "chainId":
|
||||
return logger.getBigInt(value).toString();
|
||||
case "getBlockNumber":
|
||||
return logger.getNumber(value).toString();
|
||||
case "getGasPrice":
|
||||
return logger.getBigInt(value).toString();
|
||||
case "getBalance":
|
||||
return logger.getBigInt(value).toString();
|
||||
case "getTransactionCount":
|
||||
return logger.getNumber(value).toString();
|
||||
case "getCode":
|
||||
return hexlify(value);
|
||||
case "getStorageAt":
|
||||
return hexlify(value);
|
||||
case "getBlock":
|
||||
if (req.includeTransactions) {
|
||||
return JSON.stringify(network.formatter.blockWithTransactions(value));
|
||||
}
|
||||
return JSON.stringify(network.formatter.block(value));
|
||||
case "getTransaction":
|
||||
return JSON.stringify(network.formatter.transactionResponse(value));
|
||||
case "getTransactionReceipt":
|
||||
return JSON.stringify(network.formatter.receipt(value));
|
||||
case "call":
|
||||
return hexlify(value);
|
||||
case "estimateGas":
|
||||
return logger.getBigInt(value).toString();
|
||||
case "getLogs":
|
||||
return JSON.stringify(value.map((v: any) => network.formatter.log(v)));
|
||||
}
|
||||
|
||||
return logger.throwError("unsupported method", "UNSUPPORTED_OPERATION", {
|
||||
operation: `_perform(${ JSON.stringify(req.method) })`
|
||||
});
|
||||
}
|
||||
|
||||
type TallyResult = {
|
||||
result: any;
|
||||
normal: string;
|
||||
weight: number;
|
||||
};
|
||||
|
||||
// This strategy picks the highest wieght result, as long as the weight is
|
||||
// equal to or greater than quorum
|
||||
function checkQuorum(quorum: number, results: Array<TallyResult>): any {
|
||||
const tally: Map<string, { weight: number, result: any }> = new Map();
|
||||
for (const { result, normal, weight } of results) {
|
||||
const t = tally.get(normal) || { result, weight: 0 };
|
||||
t.weight += weight;
|
||||
tally.set(normal, t);
|
||||
}
|
||||
|
||||
let bestWeight = 0;
|
||||
let bestResult = undefined;
|
||||
|
||||
for (const { weight, result } of tally.values()) {
|
||||
if (weight >= quorum && weight > bestWeight) {
|
||||
bestWeight = weight;
|
||||
bestResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
return bestResult;
|
||||
}
|
||||
|
||||
/*
|
||||
function getMean(results: Array<TallyResult>): bigint {
|
||||
const total = results.reduce((a, r) => (a + BigInt(r.result)), BN_0);
|
||||
return total / BigInt(results.length);
|
||||
}
|
||||
*/
|
||||
|
||||
function getMedian(results: Array<TallyResult>): bigint {
|
||||
// Get the sorted values
|
||||
const values = results.map((r) => BigInt(r.result));
|
||||
values.sort((a, b) => ((a < b) ? -1: (b > a) ? 1: 0));
|
||||
|
||||
const mid = values.length / 2;
|
||||
|
||||
// Odd-length; take the middle value
|
||||
if (values.length % 2) { return values[mid]; }
|
||||
|
||||
// Even length; take the ceiling of the mean of the center two values
|
||||
return (values[mid - 1] + values[mid] + BN_1) / BN_2;
|
||||
}
|
||||
|
||||
function getFuzzyMode(quorum: number, results: Array<TallyResult>): undefined | number {
|
||||
if (quorum === 1) { return logger.getNumber(getMedian(results), "%internal"); }
|
||||
|
||||
const tally: Map<number, { result: number, weight: number }> = new Map();
|
||||
const add = (result: number, weight: number) => {
|
||||
const t = tally.get(result) || { result, weight: 0 };
|
||||
t.weight += weight;
|
||||
tally.set(result, t);
|
||||
};
|
||||
|
||||
for (const { weight, result } of results) {
|
||||
const r = logger.getNumber(result);
|
||||
add(r - 1, weight);
|
||||
add(r, weight);
|
||||
add(r + 1, weight);
|
||||
}
|
||||
|
||||
let bestWeight = 0;
|
||||
let bestResult = undefined;
|
||||
|
||||
for (const { weight, result } of tally.values()) {
|
||||
// Use this result, if this result meets quorum and has either:
|
||||
// - a better weight
|
||||
// - or equal weight, but the result is larger
|
||||
if (weight >= quorum && (weight > bestWeight || (bestResult != null && weight === bestWeight && result > bestResult))) {
|
||||
bestWeight = weight;
|
||||
bestResult = result;
|
||||
}
|
||||
}
|
||||
|
||||
return bestResult;
|
||||
}
|
||||
|
||||
export class FallbackProvider extends AbstractProvider {
|
||||
//readonly providerConfigs!: ReadonlyArray<Required<Readonly<ProviderConfig>>>;
|
||||
|
||||
readonly quorum: number;
|
||||
readonly eventQuorum: number;
|
||||
readonly eventWorkers: number;
|
||||
|
||||
readonly #configs: Array<Config>;
|
||||
|
||||
#height: number;
|
||||
#initialSyncPromise: null | Promise<void>;
|
||||
|
||||
constructor(providers: Array<AbstractProvider | FallbackProviderConfig>, network?: Networkish) {
|
||||
super(network);
|
||||
this.#configs = providers.map((p) => {
|
||||
if (p instanceof AbstractProvider) {
|
||||
return Object.assign({ provider: p }, defaultConfig, defaultState );
|
||||
} else {
|
||||
return Object.assign({ }, defaultConfig, p, defaultState );
|
||||
}
|
||||
});
|
||||
|
||||
this.#height = -2;
|
||||
this.#initialSyncPromise = null;
|
||||
|
||||
this.quorum = 2; //Math.ceil(providers.length / 2);
|
||||
this.eventQuorum = 1;
|
||||
this.eventWorkers = 1;
|
||||
|
||||
if (this.quorum > this.#configs.reduce((a, c) => (a + c.weight), 0)) {
|
||||
logger.throwArgumentError("quorum exceed provider wieght", "quorum", this.quorum);
|
||||
}
|
||||
}
|
||||
|
||||
// @TOOD: Copy these and only return public values
|
||||
get providerConfigs(): Array<FallbackProviderState> {
|
||||
return this.#configs.slice();
|
||||
}
|
||||
|
||||
async _detectNetwork() {
|
||||
return Network.from(logger.getBigInt(await this._perform({ method: "chainId" }))).freeze();
|
||||
}
|
||||
|
||||
// @TODO: Add support to select providers to be the event subscriber
|
||||
//_getSubscriber(sub: Subscription): Subscriber {
|
||||
// throw new Error("@TODO");
|
||||
//}
|
||||
|
||||
// Grab the next (random) config that is not already part of configs
|
||||
#getNextConfig(configs: Array<Config>): null | Config {
|
||||
// Shuffle the states, sorted by priority
|
||||
const allConfigs = this.#configs.slice();
|
||||
shuffle(allConfigs);
|
||||
allConfigs.sort((a, b) => (b.priority - a.priority));
|
||||
|
||||
for (const config of allConfigs) {
|
||||
if (configs.indexOf(config) === -1) { return config; }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Adds a new runner (if available) to running.
|
||||
#addRunner(running: Set<RunningState>, req: PerformActionRequest): null | RunningState {
|
||||
const config = this.#getNextConfig(Array.from(running).map((r) => r.config));
|
||||
if (config == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result: any = { };
|
||||
|
||||
const runner: RunningState = {
|
||||
config, result, didBump: false, done: false,
|
||||
perform: null, staller: null
|
||||
};
|
||||
|
||||
const now = getTime();
|
||||
|
||||
runner.perform = (async () => {
|
||||
try {
|
||||
config.requests++;
|
||||
result.result = await config.provider._perform(req);
|
||||
} catch (error) {
|
||||
config.errorResponses++;
|
||||
result.error = error;
|
||||
}
|
||||
|
||||
if (runner.done) { config.lateResponses++; }
|
||||
|
||||
const dt = (getTime() - now);
|
||||
config._totalTime += dt;
|
||||
|
||||
config.rollingDuration = 0.95 * config.rollingDuration + 0.05 * dt;
|
||||
|
||||
runner.perform = null;
|
||||
})();
|
||||
|
||||
runner.staller = (async () => {
|
||||
await stall(config.stallTimeout);
|
||||
runner.staller = null;
|
||||
})();
|
||||
|
||||
running.add(runner);
|
||||
return runner;
|
||||
}
|
||||
|
||||
// Initializes the blockNumber and network for each runner and
|
||||
// blocks until initialized
|
||||
async #initialSync(): Promise<void> {
|
||||
let initialSync = this.#initialSyncPromise;
|
||||
if (!initialSync) {
|
||||
const promises: Array<Promise<any>> = [ ];
|
||||
this.#configs.forEach((config) => {
|
||||
promises.push(waitForSync(config, 0));
|
||||
promises.push((async () => {
|
||||
config._network = await config.provider.getNetwork();
|
||||
})());
|
||||
});
|
||||
|
||||
this.#initialSyncPromise = initialSync = (async () => {
|
||||
// Wait for all providers to have a block number and network
|
||||
await Promise.all(promises);
|
||||
|
||||
// Check all the networks match
|
||||
let chainId: null | bigint = null;
|
||||
for (const config of this.#configs) {
|
||||
const network = <Frozen<Network>>(config._network);
|
||||
if (chainId == null) {
|
||||
chainId = network.chainId;
|
||||
} else if (network.chainId !== chainId) {
|
||||
logger.throwError("cannot mix providers on different networks", "UNSUPPORTED_OPERATION", {
|
||||
operation: "new FallbackProvider"
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
await initialSync
|
||||
}
|
||||
|
||||
|
||||
async #checkQuorum(running: Set<RunningState>, req: PerformActionRequest): Promise<any> {
|
||||
// Get all the result objects
|
||||
const results: Array<TallyResult> = [ ];
|
||||
for (const runner of running) {
|
||||
if ("result" in runner.result) {
|
||||
const result = runner.result.result;
|
||||
results.push({
|
||||
result,
|
||||
normal: normalize(<Frozen<Network>>(runner.config._network), result, req),
|
||||
weight: runner.config.weight
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Are there enough results to event meet quorum?
|
||||
if (results.reduce((a, r) => (a + r.weight), 0) < this.quorum) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (req.method) {
|
||||
case "getBlockNumber": {
|
||||
// We need to get the bootstrap block height
|
||||
if (this.#height === -2) {
|
||||
const height = Math.ceil(logger.getNumber(getMedian(this.#configs.map((c) => ({
|
||||
result: c.blockNumber,
|
||||
normal: logger.getNumber(c.blockNumber).toString(),
|
||||
weight: c.weight
|
||||
}))), "%internal"));
|
||||
this.#height = height;
|
||||
}
|
||||
|
||||
const mode = getFuzzyMode(this.quorum, results);
|
||||
if (mode === undefined) { return undefined; }
|
||||
if (mode > this.#height) { this.#height = mode; }
|
||||
return this.#height;
|
||||
}
|
||||
|
||||
case "getGasPrice":
|
||||
case "estimateGas":
|
||||
return getMedian(results);
|
||||
|
||||
case "getBlock":
|
||||
// Pending blocks are mempool dependant and already
|
||||
// quite untrustworthy
|
||||
if ("blockTag" in req && req.blockTag === "pending") {
|
||||
return results[0].result;
|
||||
}
|
||||
return checkQuorum(this.quorum, results);
|
||||
|
||||
case "chainId":
|
||||
case "getBalance":
|
||||
case "getTransactionCount":
|
||||
case "getCode":
|
||||
case "getStorageAt":
|
||||
case "getTransaction":
|
||||
case "getTransactionReceipt":
|
||||
case "getLogs":
|
||||
return checkQuorum(this.quorum, results);
|
||||
|
||||
case "call":
|
||||
// @TODO: Check errors
|
||||
return checkQuorum(this.quorum, results);
|
||||
|
||||
case "broadcastTransaction":
|
||||
throw new Error("TODO");
|
||||
}
|
||||
|
||||
return logger.throwError("unsupported method", "UNSUPPORTED_OPERATION", {
|
||||
operation: `_perform(${ JSON.stringify((<any>req).method) })`
|
||||
});
|
||||
}
|
||||
|
||||
async #waitForQuorum(running: Set<RunningState>, req: PerformActionRequest): Promise<any> {
|
||||
if (running.size === 0) { throw new Error("no runners?!"); }
|
||||
|
||||
// Any promises that are interesting to watch for; an expired stall
|
||||
// or a successful perform
|
||||
const interesting: Array<Promise<void>> = [ ];
|
||||
|
||||
//const results: Array<any> = [ ];
|
||||
//const errors: Array<Error> = [ ];
|
||||
let newRunners = 0;
|
||||
for (const runner of running) {
|
||||
// @TODO: use runner.perfom != null
|
||||
/*
|
||||
if ("result" in runner.result) {
|
||||
results.push(runner.result.result);
|
||||
} else if ("error" in runner.result) {
|
||||
errors.push(runner.result.error);
|
||||
}
|
||||
*/
|
||||
// No responses, yet; keep an eye on it
|
||||
if (runner.perform) {
|
||||
interesting.push(runner.perform);
|
||||
}
|
||||
|
||||
// Still stalling...
|
||||
if (runner.staller) {
|
||||
interesting.push(runner.staller);
|
||||
continue;
|
||||
}
|
||||
|
||||
// This runner has already triggered another runner
|
||||
if (runner.didBump) { continue; }
|
||||
|
||||
// Got a response (result or error) or stalled; kick off another runner
|
||||
runner.didBump = true;
|
||||
newRunners++;
|
||||
}
|
||||
|
||||
// Check for quorum
|
||||
/*
|
||||
console.log({ results, errors } );
|
||||
if (results.length >= this.quorum) {
|
||||
return results[0];
|
||||
}
|
||||
|
||||
if (errors.length >= this.quorum) {
|
||||
return errors[0];
|
||||
}
|
||||
*/
|
||||
const value = await this.#checkQuorum(running, req);
|
||||
if (value !== undefined) {
|
||||
if (value instanceof Error) { throw value; }
|
||||
return value;
|
||||
}
|
||||
|
||||
// Add any new runners, because a staller timed out or a result
|
||||
// or error response came in.
|
||||
for (let i = 0; i < newRunners; i++) {
|
||||
this.#addRunner(running, req)
|
||||
}
|
||||
|
||||
if (interesting.length === 0) {
|
||||
throw new Error("quorum not met");
|
||||
// return logger.throwError("failed to meet quorum", "", {
|
||||
// });
|
||||
}
|
||||
|
||||
// Wait for someone to either complete its perform or trigger a stall
|
||||
await Promise.race(interesting);
|
||||
|
||||
return await this.#waitForQuorum(running, req);
|
||||
}
|
||||
|
||||
async _perform<T = any>(req: PerformActionRequest): Promise<T> {
|
||||
await this.#initialSync();
|
||||
|
||||
// Bootstrap enough to meet quorum
|
||||
const running: Set<RunningState> = new Set();
|
||||
for (let i = 0; i < this.quorum; i++) {
|
||||
this.#addRunner(running, req);
|
||||
}
|
||||
|
||||
const result = this.#waitForQuorum(running, req);
|
||||
for (const runner of running) { runner.done = true; }
|
||||
return result;
|
||||
}
|
||||
}
|
89
src.ts/providers/provider-infura.ts
Normal file
89
src.ts/providers/provider-infura.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { FetchRequest } from "../utils/fetch.js";
|
||||
|
||||
import { showThrottleMessage } from "./community.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Network } from "./network.js";
|
||||
import { JsonRpcProvider } from "./provider-jsonrpc.js";
|
||||
|
||||
import type { AbstractProvider } from "./abstract-provider.js";
|
||||
import type { CommunityResourcable } from "./community.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
const defaultProjectId = "84842078b09946638c03157f83405213";
|
||||
|
||||
function getHost(name: string): string {
|
||||
switch(name) {
|
||||
case "homestead":
|
||||
return "mainnet.infura.io";
|
||||
case "ropsten":
|
||||
return "ropsten.infura.io";
|
||||
case "rinkeby":
|
||||
return "rinkeby.infura.io";
|
||||
case "kovan":
|
||||
return "kovan.infura.io";
|
||||
case "goerli":
|
||||
return "goerli.infura.io";
|
||||
case "matic":
|
||||
return "polygon-mainnet.infura.io";
|
||||
case "maticmum":
|
||||
return "polygon-mumbai.infura.io";
|
||||
case "optimism":
|
||||
return "optimism-mainnet.infura.io";
|
||||
case "optimism-kovan":
|
||||
return "optimism-kovan.infura.io";
|
||||
case "arbitrum":
|
||||
return "arbitrum-mainnet.infura.io";
|
||||
case "arbitrum-rinkeby":
|
||||
return "arbitrum-rinkeby.infura.io";
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("unsupported network", "network", name);
|
||||
}
|
||||
|
||||
|
||||
export class InfuraProvider extends JsonRpcProvider implements CommunityResourcable {
|
||||
readonly projectId!: string;
|
||||
readonly projectSecret!: null | string;
|
||||
|
||||
constructor(_network: Networkish = "homestead", projectId?: null | string, projectSecret?: null | string) {
|
||||
const network = Network.from(_network);
|
||||
if (projectId == null) { projectId = defaultProjectId; }
|
||||
if (projectSecret == null) { projectSecret = null; }
|
||||
|
||||
const request = InfuraProvider.getRequest(network, projectId, projectSecret);
|
||||
super(request, network, { staticNetwork: network });
|
||||
|
||||
defineProperties<InfuraProvider>(this, { projectId, projectSecret });
|
||||
}
|
||||
|
||||
_getProvider(chainId: number): AbstractProvider {
|
||||
try {
|
||||
return new InfuraProvider(chainId, this.projectId, this.projectSecret);
|
||||
} catch (error) { }
|
||||
return super._getProvider(chainId);
|
||||
}
|
||||
|
||||
static getRequest(network: Network, projectId?: null | string, projectSecret?: null | string): FetchRequest {
|
||||
if (projectId == null) { projectId = defaultProjectId; }
|
||||
if (projectSecret == null) { projectSecret = null; }
|
||||
|
||||
const request = new FetchRequest(`https:/\/${ getHost(network.name) }/v3/${ projectId }`);
|
||||
request.allowGzip = true;
|
||||
if (projectSecret) { request.setCredentials("", projectSecret); }
|
||||
|
||||
if (projectId === defaultProjectId) {
|
||||
request.retryFunc = async (request, response, attempt) => {
|
||||
showThrottleMessage("InfuraProvider");
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
return request;
|
||||
}
|
||||
|
||||
isCommunityResource(): boolean {
|
||||
return (this.projectId === defaultProjectId);
|
||||
}
|
||||
}
|
3
src.ts/providers/provider-ipcsocket-browser.ts
Normal file
3
src.ts/providers/provider-ipcsocket-browser.ts
Normal file
@ -0,0 +1,3 @@
|
||||
const IpcSocketProvider = undefined;
|
||||
|
||||
export { IpcSocketProvider };
|
98
src.ts/providers/provider-ipcsocket.ts
Normal file
98
src.ts/providers/provider-ipcsocket.ts
Normal file
@ -0,0 +1,98 @@
|
||||
|
||||
import { connect } from "net";
|
||||
import { SocketProvider } from "./provider-socket.js";
|
||||
|
||||
import type { Socket } from "net";
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
// @TODO: Is this sufficient? Is this robust? Will newlines occur between
|
||||
// all payloads and only between payloads?
|
||||
function splitBuffer(data: Buffer): { messages: Array<string>, remaining: Buffer } {
|
||||
const messages: Array<string> = [ ];
|
||||
|
||||
let lastStart = 0;
|
||||
while (true) {
|
||||
const nl = data.indexOf(10, lastStart);
|
||||
if (nl === -1) { break; }
|
||||
messages.push(data.subarray(lastStart, nl).toString().trim());
|
||||
lastStart = nl + 1;
|
||||
}
|
||||
|
||||
return { messages, remaining: data.subarray(lastStart) };
|
||||
}
|
||||
|
||||
export class IpcSocketProvider extends SocketProvider {
|
||||
#socket: Socket;
|
||||
get socket(): Socket { return this.#socket; }
|
||||
|
||||
constructor(path: string, network?: Networkish) {
|
||||
super(network);
|
||||
this.#socket = connect(path);
|
||||
|
||||
this.socket.on("ready", () => { this._start(); });
|
||||
|
||||
let response = Buffer.alloc(0);
|
||||
this.socket.on("data", (data) => {
|
||||
response = Buffer.concat([ response, data ]);
|
||||
const { messages, remaining } = splitBuffer(response);
|
||||
messages.forEach((message) => {
|
||||
this._processMessage(message);
|
||||
});
|
||||
response = remaining;
|
||||
});
|
||||
|
||||
this.socket.on("end", () => {
|
||||
this.emit("close");
|
||||
this.socket.destroy();
|
||||
this.socket.end();
|
||||
});
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
this.socket.destroy();
|
||||
this.socket.end();
|
||||
}
|
||||
|
||||
async _write(message: string): Promise<void> {
|
||||
console.log(">>>", message);
|
||||
this.socket.write(message);
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
import { defineProperties } from "@ethersproject/properties";
|
||||
|
||||
import { SocketLike, SocketProvider } from "./provider-socket.js";
|
||||
|
||||
import type { Socket } from "net";
|
||||
|
||||
export class SocketWrapper implements SocketLike {
|
||||
#socket: Socket;
|
||||
|
||||
constructor(path: string) {
|
||||
this.#socket = connect(path);
|
||||
}
|
||||
|
||||
send(data: string): void {
|
||||
this.#socket.write(data, () => { });
|
||||
}
|
||||
|
||||
addEventListener(event: string, listener: (data: string) => void): void {
|
||||
//this.#socket.on(event, (value: ) => {
|
||||
//});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
}
|
||||
}
|
||||
|
||||
export class IpcProvider extends SocketProvider {
|
||||
readonly path!: string;
|
||||
|
||||
constructor(path: string) {
|
||||
super(new SocketWrapper(path));
|
||||
defineProperties<IpcProvider>(this, { path });
|
||||
}
|
||||
}
|
||||
*/
|
853
src.ts/providers/provider-jsonrpc.ts
Normal file
853
src.ts/providers/provider-jsonrpc.ts
Normal file
@ -0,0 +1,853 @@
|
||||
// @TODO:
|
||||
// - Add the batching API
|
||||
|
||||
// https://playground.open-rpc.org/?schemaUrl=https://raw.githubusercontent.com/ethereum/eth1.0-apis/assembled-spec/openrpc.json&uiSchema%5BappBar%5D%5Bui:splitView%5D=true&uiSchema%5BappBar%5D%5Bui:input%5D=false&uiSchema%5BappBar%5D%5Bui:examplesDropdown%5D=false
|
||||
|
||||
import { resolveAddress } from "../address/index.js";
|
||||
import { hexlify } from "../utils/data.js";
|
||||
import { FetchRequest } from "../utils/fetch.js";
|
||||
import { toQuantity } from "../utils/maths.js";
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { TypedDataEncoder } from "../hash/typed-data.js";
|
||||
import { toUtf8Bytes } from "../utils/utf8.js";
|
||||
import { accessListify } from "../transaction/index.js";
|
||||
|
||||
import { AbstractProvider, UnmanagedSubscriber } from "./abstract-provider.js";
|
||||
import { AbstractSigner } from "./abstract-signer.js";
|
||||
import { Network } from "./network.js";
|
||||
import { FilterIdEventSubscriber, FilterIdPendingSubscriber } from "./subscriber-filterid.js";
|
||||
|
||||
import type { TypedDataDomain, TypedDataField } from "../hash/index.js";
|
||||
import type { TransactionLike } from "../transaction/index.js";
|
||||
|
||||
import type { PerformActionRequest, Subscriber, Subscription } from "./abstract-provider.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
import type { Provider, TransactionRequest, TransactionResponse } from "./provider.js";
|
||||
import type { Signer } from "./signer.js";
|
||||
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
|
||||
//function copy<T = any>(value: T): T {
|
||||
// return JSON.parse(JSON.stringify(value));
|
||||
//}
|
||||
|
||||
const Primitive = "bigint,boolean,function,number,string,symbol".split(/,/g);
|
||||
//const Methods = "getAddress,then".split(/,/g);
|
||||
function deepCopy<T = any>(value: T): T {
|
||||
if (value == null || Primitive.indexOf(typeof(value)) >= 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Keep any Addressable
|
||||
if (typeof((<any>value).getAddress) === "function") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) { return <any>(value.map(deepCopy)); }
|
||||
|
||||
if (typeof(value) === "object") {
|
||||
return Object.keys(value).reduce((accum, key) => {
|
||||
accum[key] = (<any>value)[key];
|
||||
return accum;
|
||||
}, <any>{ });
|
||||
}
|
||||
|
||||
throw new Error(`should not happen: ${ value } (${ typeof(value) })`);
|
||||
}
|
||||
|
||||
function getLowerCase(value: string): string {
|
||||
if (value) { return value.toLowerCase(); }
|
||||
return value;
|
||||
}
|
||||
|
||||
interface Pollable {
|
||||
pollingInterval: number;
|
||||
}
|
||||
|
||||
function isPollable(value: any): value is Pollable {
|
||||
return (value && typeof(value.pollingInterval) === "number");
|
||||
}
|
||||
|
||||
export type JsonRpcPayload = {
|
||||
id: number;
|
||||
method: string;
|
||||
params: Array<any> | Record<string, any>;
|
||||
jsonrpc: "2.0";
|
||||
};
|
||||
|
||||
export type JsonRpcResult = {
|
||||
id: number;
|
||||
result: any;
|
||||
};
|
||||
|
||||
export type JsonRpcError = {
|
||||
id: number;
|
||||
error: {
|
||||
code: number;
|
||||
message?: string;
|
||||
data?: any;
|
||||
}
|
||||
};
|
||||
|
||||
export type JsonRpcOptions = {
|
||||
// Whether to immediately fallback onto useing the polling strategy; otherwise
|
||||
// attempt to use filters first, falling back onto polling if filter returns failure
|
||||
polling?: boolean;
|
||||
|
||||
// Whether to check the network on each call; only set this to true when a backend
|
||||
// **cannot** change otherwise catastrophic errors can occur
|
||||
staticNetwork?: null | Network;
|
||||
|
||||
// How long to wait before draining the payload queue
|
||||
batchStallTime?: number;
|
||||
|
||||
// Maximum estimated size (in bytes) to allow in a batch
|
||||
batchMaxSize?: number;
|
||||
|
||||
// Maximum number of payloads to send per batch; if set to 1, non-batching requests
|
||||
// are made.
|
||||
batchMaxCount?: number;
|
||||
};
|
||||
|
||||
const defaultOptions = {
|
||||
polling: false,
|
||||
staticNetwork: null,
|
||||
|
||||
batchStallTime: 10, // 10ms
|
||||
batchMaxSize: (1 << 20), // 1Mb
|
||||
batchMaxCount: 100 // 100 requests
|
||||
}
|
||||
|
||||
export interface JsonRpcTransactionRequest {
|
||||
from?: string;
|
||||
to?: string;
|
||||
data?: string;
|
||||
|
||||
chainId?: string;
|
||||
type?: string;
|
||||
gas?: string;
|
||||
|
||||
gasPrice?: string;
|
||||
maxFeePerGas?: string;
|
||||
maxPriorityFeePerGas?: string;
|
||||
|
||||
nonce?: string;
|
||||
value?: string;
|
||||
|
||||
accessList?: Array<{ address: string, storageKeys: Array<string> }>;
|
||||
}
|
||||
|
||||
// @TODO: Unchecked Signers
|
||||
|
||||
export class JsonRpcSigner extends AbstractSigner<JsonRpcApiProvider> {
|
||||
address!: string;
|
||||
|
||||
constructor(provider: JsonRpcApiProvider, address: string) {
|
||||
super(provider);
|
||||
defineProperties<JsonRpcSigner>(this, { address });
|
||||
}
|
||||
|
||||
connect(provider: null | Provider): Signer {
|
||||
return logger.throwError("cannot reconnect JsonRpcSigner", "UNSUPPORTED_OPERATION", {
|
||||
operation: "signer.connect"
|
||||
});
|
||||
}
|
||||
|
||||
async getAddress(): Promise<string> {
|
||||
return this.address;
|
||||
}
|
||||
|
||||
// JSON-RPC will automatially fill in nonce, etc. so we just check from
|
||||
async populateTransaction(tx: TransactionRequest): Promise<TransactionLike<string>> {
|
||||
return await this.populateCall(tx);
|
||||
}
|
||||
|
||||
//async getNetwork(): Promise<Frozen<Network>> {
|
||||
// return await this.provider.getNetwork();
|
||||
//}
|
||||
|
||||
//async estimateGas(tx: TransactionRequest): Promise<bigint> {
|
||||
// return await this.provider.estimateGas(tx);
|
||||
//}
|
||||
|
||||
//async call(tx: TransactionRequest): Promise<string> {
|
||||
// return await this.provider.call(tx);
|
||||
//}
|
||||
|
||||
//async resolveName(name: string | Addressable): Promise<null | string> {
|
||||
// return await this.provider.resolveName(name);
|
||||
//}
|
||||
|
||||
//async getNonce(blockTag?: BlockTag): Promise<number> {
|
||||
// return await this.provider.getTransactionCountOf(this.address);
|
||||
//}
|
||||
|
||||
// Returns just the hash of the transaction after sent, which is what
|
||||
// the bare JSON-RPC API does;
|
||||
async sendUncheckedTransaction(_tx: TransactionRequest): Promise<string> {
|
||||
const tx = deepCopy(_tx);
|
||||
|
||||
const promises: Array<Promise<void>> = [];
|
||||
|
||||
// Make sure the from matches the sender
|
||||
if (tx.from) {
|
||||
const _from = tx.from;
|
||||
promises.push((async () => {
|
||||
const from = await resolveAddress(_from, this.provider);
|
||||
if (from == null || from.toLowerCase() !== this.address.toLowerCase()) {
|
||||
logger.throwArgumentError("from address mismatch", "transaction", _tx);
|
||||
}
|
||||
tx.from = from;
|
||||
})());
|
||||
} else {
|
||||
tx.from = this.address;
|
||||
}
|
||||
|
||||
// The JSON-RPC for eth_sendTransaction uses 90000 gas; if the user
|
||||
// wishes to use this, it is easy to specify explicitly, otherwise
|
||||
// we look it up for them.
|
||||
if (tx.gasLimit == null) {
|
||||
promises.push((async () => {
|
||||
tx.gasLimit = await this.provider.estimateGas({ ...tx, from: this.address});
|
||||
})());
|
||||
}
|
||||
|
||||
// The address may be an ENS name or Addressable
|
||||
if (tx.to != null) {
|
||||
const _to = tx.to;
|
||||
promises.push((async () => {
|
||||
tx.to = await resolveAddress(_to, this.provider);
|
||||
})());
|
||||
}
|
||||
|
||||
// Wait until all of our properties are filled in
|
||||
if (promises.length) { await Promise.all(promises); }
|
||||
|
||||
const hexTx = this.provider.getRpcTransaction(tx);
|
||||
|
||||
return this.provider.send("eth_sendTransaction", [ hexTx ]);
|
||||
}
|
||||
|
||||
async sendTransaction(tx: TransactionRequest): Promise<TransactionResponse> {
|
||||
// This cannot be mined any earlier than any recent block
|
||||
const blockNumber = await this.provider.getBlockNumber();
|
||||
|
||||
// Send the transaction
|
||||
const hash = await this.sendUncheckedTransaction(tx);
|
||||
|
||||
// Unfortunately, JSON-RPC only provides and opaque transaction hash
|
||||
// for a response, and we need the actual transaction, so we poll
|
||||
// for it; it should show up very quickly
|
||||
return await (new Promise((resolve, reject) => {
|
||||
const timeouts = [ 1000, 100 ];
|
||||
const checkTx = async () => {
|
||||
// Try getting the transaction
|
||||
const tx = await this.provider.getTransaction(hash);
|
||||
if (tx != null) {
|
||||
resolve(this.provider._wrapTransaction(tx, hash, blockNumber));
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait another 4 seconds
|
||||
this.provider._setTimeout(() => { checkTx(); }, timeouts.pop() || 4000);
|
||||
};
|
||||
checkTx();
|
||||
}));
|
||||
}
|
||||
|
||||
async signTransaction(_tx: TransactionRequest): Promise<string> {
|
||||
const tx = deepCopy(_tx);
|
||||
|
||||
// Make sure the from matches the sender
|
||||
if (tx.from) {
|
||||
const from = await resolveAddress(tx.from, this.provider);
|
||||
if (from == null || from.toLowerCase() !== this.address.toLowerCase()) {
|
||||
return logger.throwArgumentError("from address mismatch", "transaction", _tx);
|
||||
}
|
||||
tx.from = from;
|
||||
} else {
|
||||
tx.from = this.address;
|
||||
}
|
||||
|
||||
const hexTx = this.provider.getRpcTransaction(tx);
|
||||
return await this.provider.send("eth_sign_Transaction", [ hexTx ]);
|
||||
}
|
||||
|
||||
|
||||
async signMessage(_message: string | Uint8Array): Promise<string> {
|
||||
const message = ((typeof(_message) === "string") ? toUtf8Bytes(_message): _message);
|
||||
return await this.provider.send("personal_sign", [
|
||||
hexlify(message), this.address.toLowerCase() ]);
|
||||
}
|
||||
|
||||
async signTypedData(domain: TypedDataDomain, types: Record<string, Array<TypedDataField>>, _value: Record<string, any>): Promise<string> {
|
||||
const value = deepCopy(_value);
|
||||
|
||||
// Populate any ENS names (in-place)
|
||||
const populated = await TypedDataEncoder.resolveNames(domain, types, value, async (value: string) => {
|
||||
const address = await resolveAddress(value);
|
||||
if (address == null) {
|
||||
return logger.throwArgumentError("TypedData does not support null address", "value", value);
|
||||
}
|
||||
return address;
|
||||
});
|
||||
|
||||
return await this.provider.send("eth_signTypedData_v4", [
|
||||
this.address.toLowerCase(),
|
||||
JSON.stringify(TypedDataEncoder.getPayload(populated.domain, types, populated.value))
|
||||
]);
|
||||
}
|
||||
|
||||
async unlock(password: string): Promise<boolean> {
|
||||
return this.provider.send("personal_unlockAccount", [
|
||||
this.address.toLowerCase(), password, null ]);
|
||||
}
|
||||
|
||||
// https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign
|
||||
async _legacySignMessage(_message: string | Uint8Array): Promise<string> {
|
||||
const message = ((typeof(_message) === "string") ? toUtf8Bytes(_message): _message);
|
||||
return await this.provider.send("eth_sign", [
|
||||
this.address.toLowerCase(), hexlify(message) ]);
|
||||
}
|
||||
}
|
||||
|
||||
type ResolveFunc = (result: JsonRpcResult) => void;
|
||||
type RejectFunc = (error: Error) => void;
|
||||
|
||||
type Payload = { payload: JsonRpcPayload, resolve: ResolveFunc, reject: RejectFunc };
|
||||
|
||||
export class JsonRpcApiProvider extends AbstractProvider {
|
||||
|
||||
#options: Required<JsonRpcOptions>;
|
||||
|
||||
#nextId: number;
|
||||
#payloads: Array<Payload>;
|
||||
#drainTimer: null | NodeJS.Timer;
|
||||
|
||||
constructor(network?: Networkish, options?: JsonRpcOptions) {
|
||||
super(network);
|
||||
|
||||
this.#nextId = 1;
|
||||
this.#options = Object.assign({ }, defaultOptions, options || { });
|
||||
|
||||
this.#payloads = [ ];
|
||||
this.#drainTimer = null;
|
||||
|
||||
// This could be relaxed in the future to just check equivalent networks
|
||||
const staticNetwork = this._getOption("staticNetwork");
|
||||
if (staticNetwork && staticNetwork !== network) {
|
||||
logger.throwArgumentError("staticNetwork MUST match network object", "options", options);
|
||||
}
|
||||
}
|
||||
|
||||
_getOption<K extends keyof JsonRpcOptions>(key: K): JsonRpcOptions[K] {
|
||||
return this.#options[key];
|
||||
}
|
||||
|
||||
// @TODO: Merge this into send
|
||||
//prepareRequest(method: string, params: Array<any>): JsonRpcPayload {
|
||||
// return {
|
||||
// method, params, id: (this.#nextId++), jsonrpc: "2.0"
|
||||
// };
|
||||
//}
|
||||
/*
|
||||
async send<T = any>(method: string, params: Array<any>): Promise<T> {
|
||||
// @TODO: This should construct and queue the payload
|
||||
throw new Error("sub-class must implement this");
|
||||
}
|
||||
*/
|
||||
|
||||
#scheduleDrain(): void {
|
||||
if (this.#drainTimer) { return; }
|
||||
|
||||
this.#drainTimer = setTimeout(() => {
|
||||
this.#drainTimer = null;
|
||||
|
||||
const payloads = this.#payloads;
|
||||
this.#payloads = [ ];
|
||||
|
||||
while (payloads.length) {
|
||||
|
||||
// Create payload batches that satisfy our batch constraints
|
||||
const batch = [ <Payload>(payloads.shift()) ];
|
||||
while (payloads.length) {
|
||||
if (batch.length === this.#options.batchMaxCount) { break; }
|
||||
batch.push(<Payload>(payloads.shift()));
|
||||
const bytes = JSON.stringify(batch.map((p) => p.payload));
|
||||
if (bytes.length > this.#options.batchMaxSize) {
|
||||
payloads.unshift(<Payload>(batch.pop()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Process the result to each payload
|
||||
(async () => {
|
||||
const payload = ((batch.length === 1) ? batch[0].payload: batch.map((p) => p.payload));
|
||||
|
||||
this.emit("debug", { action: "sendRpcPayload", payload });
|
||||
|
||||
try {
|
||||
const result = await this._send(payload);
|
||||
this.emit("debug", { action: "receiveRpcResult", result });
|
||||
|
||||
// Process results in batch order
|
||||
for (const { resolve, reject, payload } of batch) {
|
||||
|
||||
// Find the matching result
|
||||
const resp = result.filter((r) => (r.id === payload.id))[0];
|
||||
|
||||
// No result; the node failed us in unexpected ways
|
||||
if (resp == null) {
|
||||
return reject(new Error("@TODO: no result"));
|
||||
}
|
||||
|
||||
// The response is an error
|
||||
if ("error" in resp) {
|
||||
return reject(this.getRpcError(payload, resp));
|
||||
|
||||
}
|
||||
|
||||
// All good; send the result
|
||||
resolve(resp.result);
|
||||
}
|
||||
|
||||
} catch (error: any) {
|
||||
this.emit("debug", { action: "receiveRpcError", error });
|
||||
|
||||
for (const { reject } of batch) {
|
||||
// @TODO: augment the error with the payload
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, this.#options.batchStallTime);
|
||||
}
|
||||
|
||||
// Sub-classes should **NOT** override this
|
||||
send(method: string, params: Array<any> | Record<string, any>): Promise<any> {
|
||||
// @TODO: cache chainId?? purge on switch_networks
|
||||
|
||||
const id = this.#nextId++;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.#payloads.push({
|
||||
resolve, reject,
|
||||
payload: { method, params, id, jsonrpc: "2.0" }
|
||||
});
|
||||
});
|
||||
|
||||
// If there is not a pending drainTimer, set one
|
||||
this.#scheduleDrain();
|
||||
|
||||
return <Promise<JsonRpcResult>>promise;
|
||||
}
|
||||
|
||||
// Sub-classes MUST override this
|
||||
_send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>> {
|
||||
return logger.throwError("sub-classes must override _send", "UNSUPPORTED_OPERATION", {
|
||||
operation: "jsonRpcApiProvider._send"
|
||||
});
|
||||
}
|
||||
|
||||
async getSigner(address: number | string = 0): Promise<JsonRpcSigner> {
|
||||
|
||||
const accountsPromise = this.send("eth_accounts", [ ]);
|
||||
|
||||
// Account index
|
||||
if (typeof(address) === "number") {
|
||||
const accounts = <Array<string>>(await accountsPromise);
|
||||
if (address > accounts.length) { throw new Error("no such account"); }
|
||||
return new JsonRpcSigner(this, accounts[address]);
|
||||
}
|
||||
|
||||
const [ network, accounts ] = await Promise.all([ this.getNetwork(), accountsPromise ]);
|
||||
|
||||
// Account address
|
||||
address = network.formatter.address(address);
|
||||
for (const account of accounts) {
|
||||
if (network.formatter.address(account) === account) {
|
||||
return new JsonRpcSigner(this, account);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("invalid account");
|
||||
}
|
||||
|
||||
// Sub-classes can override this; it detects the *actual* network we
|
||||
// are connected to
|
||||
async _detectNetwork(): Promise<Network> {
|
||||
// We have a static network (like INFURA)
|
||||
const network = this._getOption("staticNetwork");
|
||||
if (network) { return network; }
|
||||
|
||||
return Network.from(logger.getBigInt(await this._perform({ method: "chainId" })));
|
||||
}
|
||||
|
||||
_getSubscriber(sub: Subscription): Subscriber {
|
||||
// Pending Filters aren't availble via polling
|
||||
if (sub.type === "pending") { return new FilterIdPendingSubscriber(this); }
|
||||
|
||||
if (sub.type === "event") {
|
||||
return new FilterIdEventSubscriber(this, sub.filter);
|
||||
}
|
||||
|
||||
// Orphaned Logs are handled automatically, by the filter, since
|
||||
// logs with removed are emitted by it
|
||||
if (sub.type === "orphan" && sub.filter.orphan === "drop-log") {
|
||||
return new UnmanagedSubscriber("orphan");
|
||||
}
|
||||
|
||||
return super._getSubscriber(sub);
|
||||
}
|
||||
|
||||
// Normalize a JSON-RPC transaction
|
||||
getRpcTransaction(tx: TransactionRequest): JsonRpcTransactionRequest {
|
||||
const result: JsonRpcTransactionRequest = {};
|
||||
|
||||
// JSON-RPC now requires numeric values to be "quantity" values
|
||||
["chainId", "gasLimit", "gasPrice", "type", "maxFeePerGas", "maxPriorityFeePerGas", "nonce", "value"].forEach((key) => {
|
||||
if ((<any>tx)[key] == null) { return; }
|
||||
let dstKey = key;
|
||||
if (key === "gasLimit") { dstKey = "gas"; }
|
||||
(<any>result)[dstKey] = toQuantity(logger.getBigInt((<any>tx)[key], `tx.${ key }`));
|
||||
});
|
||||
|
||||
// Make sure addresses and data are lowercase
|
||||
["from", "to", "data"].forEach((key) => {
|
||||
if ((<any>tx)[key] == null) { return; }
|
||||
(<any>result)[key] = hexlify((<any>tx)[key]);
|
||||
});
|
||||
|
||||
// Normalize the access list object
|
||||
if (tx.accessList) {
|
||||
result["accessList"] = accessListify(tx.accessList);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get the necessary paramters for making a JSON-RPC request
|
||||
getRpcRequest(req: PerformActionRequest): null | { method: string, args: Array<any> } {
|
||||
switch (req.method) {
|
||||
case "chainId":
|
||||
return { method: "eth_chainId", args: [ ] };
|
||||
|
||||
case "getBlockNumber":
|
||||
return { method: "eth_blockNumber", args: [ ] };
|
||||
|
||||
case "getGasPrice":
|
||||
return { method: "eth_gasPrice", args: [] };
|
||||
|
||||
case "getBalance":
|
||||
return {
|
||||
method: "eth_getBalance",
|
||||
args: [ getLowerCase(req.address), req.blockTag ]
|
||||
};
|
||||
|
||||
case "getTransactionCount":
|
||||
return {
|
||||
method: "eth_getTransactionCount",
|
||||
args: [ getLowerCase(req.address), req.blockTag ]
|
||||
};
|
||||
|
||||
case "getCode":
|
||||
return {
|
||||
method: "eth_getCode",
|
||||
args: [ getLowerCase(req.address), req.blockTag ]
|
||||
};
|
||||
|
||||
case "getStorageAt":
|
||||
return {
|
||||
method: "eth_getStorageAt",
|
||||
args: [
|
||||
getLowerCase(req.address),
|
||||
("0x" + req.position.toString(16)),
|
||||
req.blockTag
|
||||
]
|
||||
};
|
||||
|
||||
case "broadcastTransaction":
|
||||
return {
|
||||
method: "eth_sendRawTransaction",
|
||||
args: [ req.signedTransaction ]
|
||||
};
|
||||
|
||||
case "getBlock":
|
||||
if ("blockTag" in req) {
|
||||
return {
|
||||
method: "eth_getBlockByNumber",
|
||||
args: [ req.blockTag, !!req.includeTransactions ]
|
||||
};
|
||||
} else if ("blockHash" in req) {
|
||||
return {
|
||||
method: "eth_getBlockByHash",
|
||||
args: [ req.blockHash, !!req.includeTransactions ]
|
||||
};
|
||||
}
|
||||
break;
|
||||
|
||||
case "getTransaction":
|
||||
return {
|
||||
method: "eth_getTransactionByHash",
|
||||
args: [ req.hash ]
|
||||
};
|
||||
|
||||
case "getTransactionReceipt":
|
||||
return {
|
||||
method: "eth_getTransactionReceipt",
|
||||
args: [ req.hash ]
|
||||
};
|
||||
|
||||
case "call":
|
||||
return {
|
||||
method: "eth_call",
|
||||
args: [ this.getRpcTransaction(req.transaction), req.blockTag ]
|
||||
};
|
||||
|
||||
case "estimateGas": {
|
||||
return {
|
||||
method: "eth_estimateGas",
|
||||
args: [ this.getRpcTransaction(req.transaction) ]
|
||||
};
|
||||
}
|
||||
|
||||
case "getLogs":
|
||||
if (req.filter && req.filter.address != null) {
|
||||
if (Array.isArray(req.filter.address)) {
|
||||
req.filter.address = req.filter.address.map(getLowerCase);
|
||||
} else {
|
||||
req.filter.address = getLowerCase(req.filter.address);
|
||||
}
|
||||
}
|
||||
return { method: "eth_getLogs", args: [ req.filter ] };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error {
|
||||
console.log("getRpcError", payload, error);
|
||||
return new Error(`JSON-RPC badness; @TODO: ${ error }`);
|
||||
/*
|
||||
if (payload.method === "eth_call") {
|
||||
const result = spelunkData(error);
|
||||
if (result) {
|
||||
// @TODO: Extract errorSignature, errorName, errorArgs, reason if
|
||||
// it is Error(string) or Panic(uint25)
|
||||
return logger.makeError("execution reverted during JSON-RPC call", "CALL_EXCEPTION", {
|
||||
data: result.data,
|
||||
transaction: args[0]
|
||||
});
|
||||
}
|
||||
|
||||
return logger.makeError("missing revert data during JSON-RPC call", "CALL_EXCEPTION", {
|
||||
data: "0x", transaction: args[0], info: { error }
|
||||
});
|
||||
}
|
||||
|
||||
if (method === "eth_estimateGas") {
|
||||
// @TODO: Spelunk, and adapt the above to allow missing data.
|
||||
// Then throw an UNPREDICTABLE_GAS exception
|
||||
}
|
||||
|
||||
const message = JSON.stringify(spelunkMessage(error));
|
||||
|
||||
if (message.match(/insufficient funds|base fee exceeds gas limit/)) {
|
||||
return logger.makeError("insufficient funds for intrinsic transaction cost", "INSUFFICIENT_FUNDS", {
|
||||
transaction: args[0]
|
||||
});
|
||||
}
|
||||
|
||||
if (message.match(/nonce/) && message.match(/too low/)) {
|
||||
return logger.makeError("nonce has already been used", "NONCE_EXPIRED", {
|
||||
transaction: args[0]
|
||||
});
|
||||
}
|
||||
|
||||
// "replacement transaction underpriced"
|
||||
if (message.match(/replacement transaction/) && message.match(/underpriced/)) {
|
||||
return logger.makeError("replacement fee too low", "REPLACEMENT_UNDERPRICED", {
|
||||
transaction: args[0]
|
||||
});
|
||||
}
|
||||
|
||||
if (message.match(/only replay-protected/)) {
|
||||
return logger.makeError("legacy pre-eip-155 transactions not supported", "UNSUPPORTED_OPERATION", {
|
||||
operation: method, info: { transaction: args[0] }
|
||||
});
|
||||
}
|
||||
|
||||
if (method === "estimateGas" && message.match(/gas required exceeds allowance|always failing transaction|execution reverted/)) {
|
||||
return logger.makeError("cannot estimate gas; transaction may fail or may require manual gas limit", "UNPREDICTABLE_GAS_LIMIT", {
|
||||
transaction: args[0]
|
||||
});
|
||||
}
|
||||
|
||||
return error;
|
||||
*/
|
||||
}
|
||||
|
||||
async _perform(req: PerformActionRequest): Promise<any> {
|
||||
// Legacy networks do not like the type field being passed along (which
|
||||
// is fair), so we delete type if it is 0 and a non-EIP-1559 network
|
||||
if (req.method === "call" || req.method === "estimateGas") {
|
||||
let tx = req.transaction;
|
||||
if (tx && tx.type != null && logger.getBigInt(tx.type)) {
|
||||
// If there are no EIP-1559 properties, it might be non-EIP-a559
|
||||
if (tx.maxFeePerGas == null && tx.maxPriorityFeePerGas == null) {
|
||||
const feeData = await this.getFeeData();
|
||||
if (feeData.maxFeePerGas == null && feeData.maxPriorityFeePerGas == null) {
|
||||
// Network doesn't know about EIP-1559 (and hence type)
|
||||
req = Object.assign({ }, req, {
|
||||
transaction: Object.assign({ }, tx, { type: undefined })
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const request = this.getRpcRequest(req);
|
||||
|
||||
if (request != null) {
|
||||
return await this.send(request.method, request.args);
|
||||
|
||||
/*
|
||||
@TODO: Add debug output to send
|
||||
this.emit("debug", { type: "sendRequest", request });
|
||||
try {
|
||||
const result =
|
||||
//console.log("RR", result);
|
||||
this.emit("debug", { type: "getResponse", result });
|
||||
return result;
|
||||
} catch (error) {
|
||||
this.emit("debug", { type: "getError", error });
|
||||
throw error;
|
||||
//throw this.getRpcError(request.method, request.args, <Error>error);
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
return super._perform(req);
|
||||
}
|
||||
}
|
||||
|
||||
export class JsonRpcProvider extends JsonRpcApiProvider {
|
||||
#connect: FetchRequest;
|
||||
|
||||
#pollingInterval: number;
|
||||
|
||||
constructor(url?: string | FetchRequest, network?: Networkish, options?: JsonRpcOptions) {
|
||||
if (url == null) { url = "http:/\/localhost:8545"; }
|
||||
super(network, options);
|
||||
|
||||
if (typeof(url) === "string") {
|
||||
this.#connect = new FetchRequest(url);
|
||||
} else {
|
||||
this.#connect = url.clone();
|
||||
}
|
||||
|
||||
this.#pollingInterval = 4000;
|
||||
}
|
||||
|
||||
async _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult>> {
|
||||
// Configure a POST connection for the requested method
|
||||
const request = this.#connect.clone();
|
||||
request.body = JSON.stringify(payload);
|
||||
|
||||
const response = await request.send();
|
||||
response.assertOk();
|
||||
|
||||
let resp = response.bodyJson;
|
||||
if (!Array.isArray(resp)) { resp = [ resp ]; }
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
get pollingInterval(): number { return this.#pollingInterval; }
|
||||
set pollingInterval(value: number) {
|
||||
if (!Number.isInteger(value) || value < 0) { throw new Error("invalid interval"); }
|
||||
this.#pollingInterval = value;
|
||||
this._forEachSubscriber((sub) => {
|
||||
if (isPollable(sub)) {
|
||||
sub.pollingInterval = this.#pollingInterval;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// This class should only be used when it is not possible for the
|
||||
// underlying network to change, such as with INFURA. If you are
|
||||
// using MetaMask or some other client which allows users to change
|
||||
// their network DO NOT USE THIS. Bad things will happen.
|
||||
/*
|
||||
export class StaticJsonRpcProvider extends JsonRpcProvider {
|
||||
readonly network!: Network;
|
||||
|
||||
constructor(url: string | ConnectionInfo, network?: Network, options?: JsonRpcOptions) {
|
||||
super(url, network, options);
|
||||
defineProperties<StaticJsonRpcProvider>(this, { network });
|
||||
}
|
||||
|
||||
async _detectNetwork(): Promise<Network> {
|
||||
return this.network;
|
||||
}
|
||||
}
|
||||
*/
|
||||
/*
|
||||
function spelunkData(value: any): null | { message: string, data: string } {
|
||||
if (value == null) { return null; }
|
||||
|
||||
// These *are* the droids we're looking for.
|
||||
if (typeof(value.message) === "string" && value.message.match("reverted") && isHexString(value.data)) {
|
||||
return { message: value.message, data: value.data };
|
||||
}
|
||||
|
||||
// Spelunk further...
|
||||
if (typeof(value) === "object") {
|
||||
for (const key in value) {
|
||||
const result = spelunkData(value[key]);
|
||||
if (result) { return result; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Might be a JSON string we can further descend...
|
||||
if (typeof(value) === "string") {
|
||||
try {
|
||||
return spelunkData(JSON.parse(value));
|
||||
} catch (error) { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function _spelunkMessage(value: any, result: Array<string>): void {
|
||||
if (value == null) { return; }
|
||||
|
||||
// These *are* the droids we're looking for.
|
||||
if (typeof(value.message) === "string") {
|
||||
result.push(value.message);
|
||||
}
|
||||
|
||||
// Spelunk further...
|
||||
if (typeof(value) === "object") {
|
||||
for (const key in value) {
|
||||
_spelunkMessage(value[key], result);
|
||||
}
|
||||
}
|
||||
|
||||
// Might be a JSON string we can further descend...
|
||||
if (typeof(value) === "string") {
|
||||
try {
|
||||
return _spelunkMessage(JSON.parse(value), result);
|
||||
} catch (error) { }
|
||||
}
|
||||
}
|
||||
|
||||
function spelunkMessage(value: any): Array<string> {
|
||||
const result: Array<string> = [ ];
|
||||
_spelunkMessage(value, result);
|
||||
return result;
|
||||
}
|
||||
*/
|
99
src.ts/providers/provider-pocket.ts
Normal file
99
src.ts/providers/provider-pocket.ts
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
import { defineProperties } from "../utils/properties.js";
|
||||
import { FetchRequest } from "../web/index.js";
|
||||
|
||||
import { showThrottleMessage } from "./community.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { Network } from "./network.js";
|
||||
import { JsonRpcProvider } from "./provider-jsonrpc.js";
|
||||
|
||||
import type { ConnectionInfo, ThrottleRetryFunc } from "../web/index.js";
|
||||
|
||||
import type { CommunityResourcable } from "./community.js";
|
||||
import type { Networkish } from "./network.js";
|
||||
|
||||
|
||||
|
||||
// These are load-balancer-based application IDs
|
||||
const defaultAppIds: Record<string, string> = {
|
||||
homestead: "6004bcd10040261633ade990",
|
||||
ropsten: "6004bd4d0040261633ade991",
|
||||
rinkeby: "6004bda20040261633ade994",
|
||||
goerli: "6004bd860040261633ade992",
|
||||
};
|
||||
|
||||
function getHost(name: string): string {
|
||||
switch(name) {
|
||||
case "homestead":
|
||||
return "eth-mainnet.gateway.pokt.network";
|
||||
case "ropsten":
|
||||
return "eth-ropsten.gateway.pokt.network";
|
||||
case "rinkeby":
|
||||
return "eth-rinkeby.gateway.pokt.network";
|
||||
case "goerli":
|
||||
return "eth-goerli.gateway.pokt.network";
|
||||
}
|
||||
|
||||
return logger.throwArgumentError("unsupported network", "network", name);
|
||||
}
|
||||
|
||||
function normalizeApiKey(network: Network, _appId?: null | string, applicationSecretKey?: null | string, loadBalancer?: boolean): { applicationId: string, applicationSecretKey: null | string, loadBalancer: boolean, community: boolean } {
|
||||
loadBalancer = !!loadBalancer;
|
||||
|
||||
let community = false;
|
||||
let applicationId = _appId;
|
||||
if (applicationId == null) {
|
||||
applicationId = defaultAppIds[network.name];
|
||||
if (applicationId == null) {
|
||||
logger.throwArgumentError("network does not support default applicationId", "applicationId", _appId);
|
||||
}
|
||||
loadBalancer = true;
|
||||
community = true;
|
||||
} else if (applicationId === defaultAppIds[network.name]) {
|
||||
loadBalancer = true;
|
||||
community = true;
|
||||
}
|
||||
if (applicationSecretKey == null) { applicationSecretKey = null; }
|
||||
|
||||
return { applicationId, applicationSecretKey, community, loadBalancer };
|
||||
}
|
||||
|
||||
export class PocketProvider extends JsonRpcProvider implements CommunityResourcable {
|
||||
readonly applicationId!: string;
|
||||
readonly applicationSecretKey!: null | string;
|
||||
readonly loadBalancer!: boolean;
|
||||
|
||||
constructor(_network: Networkish = "homestead", _appId?: null | string, _secretKey?: null | string, _loadBalancer?: boolean) {
|
||||
const network = Network.from(_network);
|
||||
const { applicationId, applicationSecretKey, loadBalancer } = normalizeApiKey(network, _appId, _secretKey, _loadBalancer);
|
||||
|
||||
const connection = PocketProvider.getConnection(network, applicationId, applicationSecretKey, loadBalancer);
|
||||
super(connection, network, { staticNetwork: network });
|
||||
|
||||
defineProperties<PocketProvider>(this, { applicationId, applicationSecretKey, loadBalancer });
|
||||
}
|
||||
|
||||
static getConnection(network: Network, _appId?: null | string, _secretKey?: null | string, _loadBalancer?: boolean): ConnectionInfo {
|
||||
const { applicationId, applicationSecretKey, community, loadBalancer } = normalizeApiKey(network, _appId, _secretKey, _loadBalancer);
|
||||
|
||||
let url = `https:/\/${ getHost(network.name) }/v1/`;
|
||||
if (loadBalancer) { url += "lb/"; }
|
||||
url += applicationId;
|
||||
|
||||
const request = new FetchRequest(url);
|
||||
request.allowGzip = true;
|
||||
if (applicationSecretKey) { request.setCredentials("", applicationSecretKey); }
|
||||
|
||||
const throttleRetry: ThrottleRetryFunc = async (request, response, attempt) => {
|
||||
if (community) { showThrottleMessage("PocketProvider"); }
|
||||
return true;
|
||||
};
|
||||
|
||||
return { request, throttleRetry };
|
||||
}
|
||||
|
||||
isCommunityResource(): boolean {
|
||||
return (this.applicationId === defaultAppIds[this.network.name]);
|
||||
}
|
||||
}
|
||||
*/
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user