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