Initial code drop for v6-beta-exports.

This commit is contained in:
Richard Moore 2022-09-05 16:14:43 -04:00
parent 8278dcb1ca
commit f5336c19b1
184 changed files with 22459 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules/**
output/**
misc/**

26
.npmignore Normal file
View 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
View File

@ -0,0 +1,3 @@
{
"type": "commonjs"
}

131
package.json Normal file
View 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
View 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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function unescapeColor(text) {
return text.replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/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+>&gt;&gt; <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+>&gt;&gt; <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
View 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
}) ],
}
];

View 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);

View 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);
}

View 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");
}

View 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
View 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);
});
}
});

View File

@ -0,0 +1,132 @@
import assert from "assert";
import { loadTests } from "./utils.js";
import type {
TestCaseAccount,
TestCaseCreate,
TestCaseCreate2
} from "./types.js";
import {
getAddress, getIcapAddress,
getCreateAddress, getCreate2Address
} from "../index.js";
describe("computes checksum address", function() {
const tests = loadTests<TestCaseAccount>("accounts");
for (const test of tests) {
it(`computes the checksum address: ${ 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);
});
});
});

View 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") }`);
});
});
*/

View File

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

View File

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

View File

@ -0,0 +1,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
View 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
View 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"
},
*/

View 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");
});
}
});

View 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);
}
});
}
});

View 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");
});
});
});

View 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 ]");
});
});
}
})

View 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");
});
}
});

View 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
View 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
View 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
View File

@ -0,0 +1 @@
export const version = "6.0.0-beta-exports.0";

95
src.ts/abi/abi-coder.ts Normal file
View 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
View 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));
}

View 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));
}
}

View 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));
}
}

View File

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

208
src.ts/abi/coders/array.ts Normal file
View 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);
}
}

View File

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

View 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));
}
}

View 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
View File

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

View 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;
}
}

View 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));
}
}

View 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

File diff suppressed because it is too large Load Diff

38
src.ts/abi/index.ts Normal file
View 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
View 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
View 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
View 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
View 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);
}

View 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
View 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";

View File

@ -0,0 +1,6 @@
/**
* A constant for the zero address.
*/
export const ZeroAddress = "0x0000000000000000000000000000000000000000";

View File

@ -0,0 +1,5 @@
/**
* A constant for the zero hash.
*/
export const ZeroHash = "0x0000000000000000000000000000000000000000000000000000000000000000";

14
src.ts/constants/index.ts Normal file
View 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";

View 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,
};

View 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
View 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() { }

View 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
View File

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

83
src.ts/contract/types.ts Normal file
View 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;
};

View 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();
}
}

View 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
View File

@ -0,0 +1,4 @@
export {
createHash, createHmac, pbkdf2Sync, randomBytes
} from "crypto";

26
src.ts/crypto/hmac.ts Normal file
View 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
View 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
View 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
View 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
View 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);

View 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
View 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
View 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
View 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;
}
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,6 @@
import * as ethers from "./ethers.js";
export { ethers };
export * from "./ethers.js";

File diff suppressed because it is too large Load Diff

View 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);
}
}

View 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 };

View 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("==========================");
}

View 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>;
}

View 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);
}

View 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(".");
}
}
}

View 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
View 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
View 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);
}
}

View 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;
}

View 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);
}
}

View 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;
}
}

View 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);
}
}

View 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 });
}
}

View 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);
}
})();
*/

View 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;
}
}

View 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);
}
}

View File

@ -0,0 +1,3 @@
const IpcSocketProvider = undefined;
export { IpcSocketProvider };

View 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 });
}
}
*/

View 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;
}
*/

View 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