diff --git a/src.ts/_tests/create-provider.ts b/src.ts/_tests/create-provider.ts index d10f175b2..580ee393b 100644 --- a/src.ts/_tests/create-provider.ts +++ b/src.ts/_tests/create-provider.ts @@ -6,7 +6,7 @@ import { InfuraProvider, // PocketProvider, -// FallbackProvider, + FallbackProvider, isError, } from "../index.js"; @@ -79,23 +79,19 @@ const ProviderCreators: Array = [ } }, */ -/* { name: "FallbackProvider", networks: ethNetworks, create: function(network: string) { const providers: Array = []; - for (const creator of ProviderCreators) { - if (creator.name === "FallbackProvider") { continue; } - if (creator.networks.indexOf(network) >= 0) { - const provider = creator.create(network); - if (provider) { providers.push(provider); } - } + for (const providerName of [ "AlchemyProvider", "AnkrProvider", "EtherscanProvider", "InfuraProvider" ]) { + const provider = getProvider(providerName, network); + if (provider) { providers.push(provider); } } + if (providers.length === 0) { throw new Error("UNSUPPORTED NETWORK"); } return new FallbackProvider(providers); } }, -*/ ]; export const providerNames = Object.freeze(ProviderCreators.map((c) => (c.name))); diff --git a/src.ts/_tests/test-contract.ts b/src.ts/_tests/test-contract.ts index 5f1c17e16..98a708ad5 100644 --- a/src.ts/_tests/test-contract.ts +++ b/src.ts/_tests/test-contract.ts @@ -1,40 +1,181 @@ -/* + import assert from "assert"; -import { connect } from "./create-provider.js"; +import { getProvider } from "./create-provider.js"; -import { Contract } from "../index.js"; +import { Contract, EventLog, Wallet } from "../index.js"; +import type { ContractEventPayload, ContractEventName, Log } from "../index.js"; describe("Test Contract", function() { - it("tests contract @TODO: expand", async function() { - const provider = connect("mainnet"); + const addr = "0x99417252Aad7B065940eBdF50d665Fb8879c5958"; + const abi = [ + "error CustomError1(uint256 code, string message)", - const contract = new Contract("dai.tokens.ethers.eth", [ - "function balanceOf(address) view returns (uint)" - ], provider); + "event EventUint256(uint256 indexed value)", + "event EventAddress(address indexed value)", + "event EventString(string value)", + "event EventBytes(bytes value)", - assert.equal(await contract.balanceOf("ricmoo.firefly.eth"), BigInt("6015089439794538201631")); + "function testCustomError1(bool pass, uint code, string calldata message) pure returns (uint256)", + "function testErrorString(bool pass, string calldata message) pure returns (uint256)", + "function testPanic(uint256 code) returns (uint256)", + "function testEvent(uint256 valueUint256, address valueAddress, string valueString, bytes valueBytes) public", + "function testCallAdd(uint256 a, uint256 b) pure returns (uint256 result)" + ]; + + it("tests contract calls", async function() { + this.timeout(10000); + + const provider = getProvider("InfuraProvider", "goerli"); + const contract = new Contract(addr, abi, provider); + + assert.equal(await contract.testCallAdd(4, 5), BigInt(9), "testCallAdd(4, 5)"); + assert.equal(await contract.testCallAdd(6, 0), BigInt(6), "testCallAdd(6, 0)"); + }); + + it("tests events", async function() { + this.timeout(60000); + + const provider = getProvider("InfuraProvider", "goerli"); + assert.ok(provider); + + const contract = new Contract(addr, abi, provider); + + const signer = new Wallet((process.env.FAUCET_PRIVATEKEY), provider); + const contractSigner = contract.connect(signer); + + const vUint256 = 42; + const vAddrName = "ethers.eth"; + const vAddr = "0x228568EA92aC5Bc281c1E30b1893735c60a139F1"; + const vString = "Hello"; + const vBytes = "0x12345678"; + + let hash: null | string = null; + + // Test running a listener for a specific event + const specificEvent = new Promise((resolve, reject) => { + contract.on("EventUint256", async (value, event) => { + // Triggered by someone else + if (hash == null || hash !== event.log.transactionHash) { return; } + + try { + assert.equal(event.filter, "EventUint256", "event.filter"); + assert.equal(event.fragment.name, "EventUint256", "event.fragment.name"); + assert.equal(event.log.address, addr, "event.log.address"); + assert.equal(event.args.length, 1, "event.args.length"); + assert.equal(event.args[0], BigInt(42), "event.args[0]"); + + const count = await contract.listenerCount("EventUint256"); + await event.removeListener(); + assert.equal(await contract.listenerCount("EventUint256"), count - 1, "decrement event count"); + + resolve(null); + } catch (e) { + event.removeListener(); + reject(e); + } + }); + }); + + // Test running a listener on all (i.e. "*") events + const allEvents = new Promise((resolve, reject) => { + const waitingFor: Record = { + EventUint256: vUint256, + EventAddress: vAddr, + EventString: vString, + EventBytes: vBytes + }; + + contract.on("*", (event: ContractEventPayload) => { + // Triggered by someone else + if (hash == null || hash !== event.log.transactionHash) { return; } + try { + const name = event.eventName; + + assert.equal(event.args[0], waitingFor[name], `${ name }`); + delete waitingFor[name]; + + if (Object.keys(waitingFor).length === 0) { + event.removeListener(); + resolve(null); + } + + } catch (error) { + reject(error); + } + }); + + }); + + // Send a transaction to trigger some events + const tx = await contractSigner.testEvent(vUint256, vAddr, vString, vBytes); + hash = tx.hash; + + const checkEvent = (filter: ContractEventName, event: EventLog | Log) => { + const values: Record = { + EventUint256: vUint256, + EventString: vString, + EventAddress: vAddr, + EventBytes: vBytes + }; + + assert.ok(event instanceof EventLog, `queryFilter(${ filter }):isEventLog`); + + const name = event.eventName; + + assert.equal(event.address, addr, `queryFilter(${ filter }):address`); + assert.equal(event.args[0], values[name], `queryFilter(${ filter }):args[0]`); + }; + + const checkEventFilter = async (filter: ContractEventName) => { + const events = (await contract.queryFilter(filter, -10)).filter((e) => (e.transactionHash === hash)); + assert.equal(events.length, 1, `queryFilter(${ filter }).length`); + checkEvent(filter, events[0]); + return events[0]; + }; + + const receipt = await tx.wait(); + + // Check the logs in the receipt + for (const log of receipt.logs) { checkEvent("receipt", log); } + + // Various options for queryFilter + await checkEventFilter("EventUint256"); + await checkEventFilter([ "EventUint256" ]); + await checkEventFilter([ [ "EventUint256" ] ]); + await checkEventFilter("EventUint256(uint)"); + await checkEventFilter([ "EventUint256(uint)" ]); + await checkEventFilter([ [ "EventUint256(uint)" ] ]); + await checkEventFilter([ [ "EventUint256", "EventUint256(uint)" ] ]); + await checkEventFilter("0x85c55bbb820e6d71c71f4894e57751de334b38c421f9c170b0e66d32eafea337"); + + // Query by Event + await checkEventFilter(contract.filters.EventUint256); + + // Query by Deferred Topic Filter; address + await checkEventFilter(contract.filters.EventUint256(vUint256)); + + // Query by Deferred Topic Filter; address + await checkEventFilter(contract.filters.EventAddress(vAddr)); + + // Query by Deferred Topic Filter; ENS name => address + await checkEventFilter(contract.filters.EventAddress(vAddrName)); + + // Multiple Methods + { + const filter = [ [ "EventUint256", "EventString" ] ]; + const events = (await contract.queryFilter(filter, -10)).filter((e) => (e.transactionHash === hash)); + assert.equal(events.length, 2, `queryFilter(${ filter }).length`); + + for (const event of events) { checkEvent(filter, event); } + } + + await specificEvent; + await allEvents; }); }); -*/ -/* -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() { @@ -48,8 +189,7 @@ describe("Test Contract Calls", function() { contract["foo(string)"].fragment }); }); -*/ -/* + describe("Test Contract Interface", function() { it("builds contract interfaces", async function() { this.timeout(60000); @@ -105,14 +245,3 @@ describe("Test Contract Interface", function() { }); }); */ -/* -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") }`); - }); -}); -*/ diff --git a/src.ts/_tests/test-hash.ts b/src.ts/_tests/test-hash.ts index 41605996f..f62d36b1b 100644 --- a/src.ts/_tests/test-hash.ts +++ b/src.ts/_tests/test-hash.ts @@ -2,7 +2,8 @@ import assert from "assert"; import { hashMessage, - solidityPackedKeccak256, solidityPackedSha256 + solidityPacked, solidityPackedKeccak256, solidityPackedSha256, + isError } from "../index.js"; import { loadTests } from "./utils.js" @@ -157,4 +158,21 @@ describe("Test Solidity Hash functions", function() { assert.equal(solidityPackedSha256(test.types, test.values), test.sha256); }); } -}) + + const badTypes = [ + { types: [ "uint5" ], values: [ 1 ] }, + { types: [ "bytes0" ], values: [ "0x" ] }, + { types: [ "blorb" ], values: [ false ] }, + ]; + + for (const { types, values } of badTypes) { + it("fails on invalid type", function() { + assert.throws(function() { + const result = solidityPacked(types, values); + console.log(result); + }, function (error) { + return (isError(error, "INVALID_ARGUMENT") && error.argument === "type"); + }); + }); + } +}); diff --git a/src.ts/_tests/test-providers-send.ts b/src.ts/_tests/test-providers-send.ts new file mode 100644 index 000000000..1005ec5d8 --- /dev/null +++ b/src.ts/_tests/test-providers-send.ts @@ -0,0 +1,48 @@ +import assert from "assert"; + +import { Wallet } from "../index.js"; + +import { getProvider, providerNames } from "./create-provider.js"; + + +describe("Sends Transactions", function() { + + const cleanup: Array<() => void> = [ ]; + after(function() { + for (const func of cleanup) { func(); } + }); + + const wallet = new Wallet((process.env.FAUCET_PRIVATEKEY)); + + const networkName = "goerli"; + for (const providerName of providerNames) { + const provider = getProvider(providerName, networkName); + if (provider == null) { continue; } + + // Shutdown socket-based provider, otherwise its socket will prevent + // this process from exiting + if ((provider).destroy) { cleanup.push(() => { (provider).destroy(); }); } + + it(`tests sending: ${ providerName }.`, async function() { + this.timeout(60000); + + const w = wallet.connect(provider); + + const dustAddr = Wallet.createRandom().address; + + const tx = await w.sendTransaction({ + to: dustAddr, + value: 42, + type: 2 + }); + + const receipt = await provider.waitForTransaction(tx.hash); //tx.wait(); + console.log(receipt); + + const balance = await provider.getBalance(dustAddr); + assert.equal(balance, BigInt(42), "target balance after send"); + }); + } + + +}); diff --git a/src.ts/_tests/test-utils-units.ts b/src.ts/_tests/test-utils-units.ts new file mode 100644 index 000000000..fc7eca1fd --- /dev/null +++ b/src.ts/_tests/test-utils-units.ts @@ -0,0 +1,73 @@ +import assert from "assert"; + +import { loadTests } from "./utils.js"; + +import { formatEther, formatUnits, parseEther, parseUnits } from "../index.js"; + +import type { TestCaseUnit } from "./types.js"; + + +describe("Tests unit conversion", function() { + const tests = loadTests("units"); + + const units = [ + { unit: "ether", format: "ether_format", decimals: 18 }, + { unit: "kwei", format: "kwei_format", decimals: 3 }, + { unit: "mwei", format: "mwei_format", decimals: 6 }, + { unit: "gwei", format: "gwei_format", decimals: 9 }, + { unit: "szabo", format: "szabo_format", decimals: 12 }, + { unit: "finney", format: "finney_format", decimals: 15 }, + ]; + + for (const { unit, format, decimals } of units) { + + for (const test of tests) { + const str = ((test)[format]); + if (str == null) { continue; } + + it(`converts wei to ${ unit } string: ${ test.name }`, function() { + const wei = BigInt(test.wei); + if (decimals === 18) { + assert.equal(formatEther(wei), str, "formatEther"); + assert.equal(formatUnits(wei), str, "formatUnits"); + } + assert.equal(formatUnits(wei, unit), str, `formatUnits(${ unit })`); + assert.equal(formatUnits(wei, decimals), str, `formatUnits(${ decimals })`); + }); + } + + for (const test of tests) { + const str = ((test)[format]); + if (str == null) { continue; } + + it(`converts ${ format } string to wei: ${ test.name }`, function() { + const wei = BigInt(test.wei); + if (decimals === 18) { + assert.equal(parseEther(str), wei, "parseEther"); + assert.equal(parseUnits(str), wei, "parseUnits"); + } + assert.equal(parseUnits(str, unit), wei, `parseUnits(${ unit })`); + assert.equal(parseUnits(str, decimals), wei, `parseUnits(${ decimals })`); + }); + } + + } +}); + +describe("Tests bad unit conversion", function() { + it("fails to convert non-string value", function() { + assert.throws(() => { + parseUnits(3, "ether"); + }, (error: any) => { + return error.message.startsWith("value must be a string"); + }); + }); + + it("fails to convert unknown unit", function() { + assert.throws(() => { + parseUnits("3", "foobar"); + }, (error: any) => { + return error.message.startsWith("invalid unit"); + }); + }); +}); diff --git a/src.ts/_tests/test-utils-utf8.ts b/src.ts/_tests/test-utils-utf8.ts new file mode 100644 index 000000000..67ac9caff --- /dev/null +++ b/src.ts/_tests/test-utils-utf8.ts @@ -0,0 +1,152 @@ +import assert from "assert"; + +import { + toUtf8Bytes, toUtf8CodePoints, toUtf8String, + Utf8ErrorFuncs +} from "../index.js"; + +export type TestCaseBadString = { + name: string, + bytes: Uint8Array, + ignore: string, + replace: string, + error: string +}; + +export type TestCaseCodePoints = { + name: string; + text: string; + codepoints: Array; +}; + +describe("Tests UTF-8 bad strings", function() { + + const tests: Array = [ + { + name: "unexpected continue", + bytes: new Uint8Array([ 0x41, 0x80, 0x42, 0x43 ]), + ignore: "ABC", + replace: "A\ufffdBC", + error: "UNEXPECTED_CONTINUE" + }, + { + name: "bad prefix", + bytes: new Uint8Array([ 0x41, 0xf8, 0x42, 0x43 ]), + ignore: "ABC", + replace: "A\ufffdBC", + error: "BAD_PREFIX" + }, + { + name: "bad prefix (multiple)", + bytes: new Uint8Array([ 0x41, 0xf8, 0x88, 0x88, 0x42, 0x43 ]), + ignore: "ABC", + replace: "A\ufffdBC", + error: "BAD_PREFIX" + }, + { + name: "OVERRUN", + bytes: new Uint8Array([ 0x41, 0x42, 0xe2, 0x82 /* 0xac */ ]), + ignore: "AB", + replace: "AB\ufffd", + error: "OVERRUN" + }, + { + name: "missing continue", + bytes: new Uint8Array([ 0x41, 0x42, 0xe2, 0xe2, 0x82, 0xac, 0x43 ]), + ignore: "AB\u20acC", + replace: "AB\ufffd\u20acC", + error: "MISSING_CONTINUE" + }, + { + name: "out-of-range", + bytes: new Uint8Array([ 0x41, 0x42, 0xf7, 0xbf, 0xbf, 0xbf, 0x43 ]), + ignore: "ABC", + replace: "AB\ufffdC", + error: "OUT_OF_RANGE" + }, + { + name: "UTF-16 surrogate (low)", + bytes: new Uint8Array([ 0x41, 0x42, 0xed, 0xa0, 0x80, 0x43 ]), + ignore: "ABC", + replace: "AB\ufffdC", + error: "UTF16_SURROGATE" + }, + { + name: "UTF-16 surrogate (high)", + bytes: new Uint8Array([ 0x41, 0x42, 0xed, 0xbf, 0xbf, 0x43 ]), + ignore: "ABC", + replace: "AB\ufffdC", + error: "UTF16_SURROGATE" + }, + { + name: "overlong", + bytes: new Uint8Array([ 0xf0, 0x82, 0x82, 0xac ]), + ignore: "", + replace: "\u20ac", + error: "OVERLONG" + } + ]; + + for (const { name, bytes, ignore, replace, error } of tests) { + it(`correctly handles ${ name }: replace strategy`, function() { + const result = toUtf8String(bytes, Utf8ErrorFuncs.replace); + assert.equal(result, replace); + }); + + it(`correctly handles ${ name }: ignore strategy`, function() { + const result = toUtf8String(bytes, Utf8ErrorFuncs.ignore); + assert.equal(result, ignore); + }); + + it(`correctly handles ${ name }: error strategy`, function() { + assert.throws(() => { + const result = toUtf8String(bytes); + console.log(result); + }, (e: any) => { + return (e.message.indexOf(error) >= 0); + }); + }); + } + + it("fails to get UTF-8 bytes from incomplete surrogate", function() { + assert.throws(() => { + const text = String.fromCharCode(0xd800);; + const result = toUtf8Bytes(text); + console.log(result); + }, (error: any) => { + return (error.message.startsWith("invalid surrogate pair")); + }); + }); + + it("fails to get UTF-8 bytes from invalid surrogate pair", function() { + assert.throws(() => { + const text = String.fromCharCode(0xd800, 0xdbff);; + const result = toUtf8Bytes(text); + console.log(result); + }, (error: any) => { + return (error.message.startsWith("invalid surrogate pair")); + }); + }); +}); + +describe("Tests UTF-8 bad strings", function() { + + const tests: Array = [ + { + name: "the Euro symbol", + text: "AB\u20acC", + codepoints: [ 0x41, 0x42, 0x20ac, 0x43 ] + }, + ]; + + for (const { name, text, codepoints } of tests) { + it(`expands strings to codepoints: ${ name }`, function() { + const result = toUtf8CodePoints(text); + assert.equal(result.length, codepoints.length, "codepoints.length"); + for (let i = 0; i < result.length; i++) { + assert.equal(result[i], codepoints[i], `codepoints[${ i }]`); + } + }); + } + +}); diff --git a/src.ts/_tests/types.ts b/src.ts/_tests/types.ts index be9d18b58..bb6800b18 100644 --- a/src.ts/_tests/types.ts +++ b/src.ts/_tests/types.ts @@ -150,6 +150,24 @@ export interface TestCaseSolidityHash { ///////////////////////////// // rlp +export interface TestCaseUnit { + name: string; + wei: string; + ethers: string; + ether_format: string; + + kwei?: string; + mwei?: string; + gwei?: string; + szabo?: string; + finney?: string; + finney_format?: string; + szabo_format?: string; + gwei_format?: string; + mwei_format?: string; + kwei_format?: string; +} + export type NestedHexString = string | Array; export interface TestCaseRlp { diff --git a/src.ts/_tests/utils.ts b/src.ts/_tests/utils.ts index 95bc703ba..c253bca72 100644 --- a/src.ts/_tests/utils.ts +++ b/src.ts/_tests/utils.ts @@ -42,6 +42,8 @@ export interface MochaRunnable { const ATTEMPTS = 5; export async function retryIt(name: string, func: (this: MochaRunnable) => Promise): Promise { + //const errors: Array = [ ]; + it(name, async function() { this.timeout(ATTEMPTS * 5000); @@ -53,15 +55,20 @@ export async function retryIt(name: string, func: (this: MochaRunnable) => Promi if (error.message === "sync skip; aborting execution") { // Skipping a test; let mocha handle it throw error; + } else if (error.code === "ERR_ASSERTION") { // Assertion error; let mocha scold us throw error; + } else { + //errors.push(error); + if (i === ATTEMPTS - 1) { - stats.pushRetry(i, name, error); + throw error; + //stats.pushRetry(i, name, error); } else { await stall(500 * (1 << i)); - stats.pushRetry(i, name, null); + //stats.pushRetry(i, name, null); } } } @@ -72,6 +79,7 @@ export async function retryIt(name: string, func: (this: MochaRunnable) => Promi }); } +/* export interface StatSet { name: string; retries: Array<{ message: string, error: null | Error }>; @@ -80,11 +88,11 @@ export interface StatSet { const _guard = { }; export class Stats { - #stats: Array; +// #stats: Array; constructor(guard: any) { if (guard !== _guard) { throw new Error("private constructor"); } - this.#stats = [ ]; +// this.#stats = [ ]; } #currentStats(): StatSet { @@ -124,3 +132,4 @@ export class Stats { } export const stats = new Stats(_guard); +*/