/**
 *  The available providers should suffice for most developers purposes,
 *  but the [[AbstractProvider]] class has many features which enable
 *  sub-classing it for specific purposes.
 *
 *  @_section: api/providers/abstract-provider: Subclassing Provider  [abstract-provider]
 */

// @TODO
// Event coalescence
//   When we register an event with an async value (e.g. address is a Signer
//   or ENS name), we need to add it immeidately for the Event API, but also
//   need time to resolve the address. Upon resolving the address, we need to
//   migrate the listener to the static event. We also need to maintain a map
//   of Signer/ENS name to address so we can sync respond to listenerCount.

import { getAddress, resolveAddress } from "../address/index.js";
import { ZeroAddress } from "../constants/index.js";
import { Contract } from "../contract/index.js";
import { namehash } from "../hash/index.js";
import { Transaction } from "../transaction/index.js";
import {
    concat, dataLength, dataSlice, hexlify, isHexString,
    getBigInt, getBytes, getNumber,
    isCallException, isError, makeError, assert, assertArgument,
    FetchRequest,
    toBeArray, toQuantity,
    defineProperties, EventPayload, resolveProperties,
    toUtf8String
} from "../utils/index.js";

import { EnsResolver } from "./ens-resolver.js";
import {
    formatBlock, formatLog, formatTransactionReceipt, formatTransactionResponse
} from "./format.js";
import { Network } from "./network.js";
import { copyRequest, Block, FeeData, Log, TransactionReceipt, TransactionResponse } from "./provider.js";
import {
    PollingBlockSubscriber, PollingEventSubscriber, PollingOrphanSubscriber, PollingTransactionSubscriber
} from "./subscriber-polling.js";

import type { Addressable, AddressLike } from "../address/index.js";
import type { BigNumberish, BytesLike } from "../utils/index.js";
import type { Listener } from "../utils/index.js";

import type { Networkish } from "./network.js";
import type { FetchUrlFeeDataNetworkPlugin } from "./plugins-network.js";
//import type { MaxPriorityFeePlugin } from "./plugins-network.js";
import type {
    BlockParams, LogParams, TransactionReceiptParams,
    TransactionResponseParams
} from "./formatting.js";

import type {
    BlockTag, EventFilter, Filter, FilterByBlockHash, OrphanFilter,
    PreparedTransactionRequest, Provider, ProviderEvent,
    TransactionRequest
} from "./provider.js";

type Timer = ReturnType<typeof setTimeout>;


// Constants
const BN_2 = BigInt(2);

const MAX_CCIP_REDIRECTS = 10;

function isPromise<T = any>(value: any): value is Promise<T> {
    return (value && typeof(value.then) === "function");
}

function getTag(prefix: string, value: any): string {
    return prefix + ":" + JSON.stringify(value, (k, v) => {
        if (v == null) { return "null"; }
        if (typeof(v) === "bigint") { return `bigint:${ v.toString() }`}
        if (typeof(v) === "string") { return v.toLowerCase(); }

        // Sort object keys
        if (typeof(v) === "object" && !Array.isArray(v)) {
            const keys = Object.keys(v);
            keys.sort();
            return keys.reduce((accum, key) => {
                accum[key] = v[key];
                return accum;
            }, <any>{ });
        }

        return v;
    });
}

/**
 *  The types of additional event values that can be emitted for the
 *  ``"debug"`` event.
 */
export type DebugEventAbstractProvider = {
    action: "sendCcipReadFetchRequest",
    request: FetchRequest
    index: number
    urls: Array<string>
} | {
    action: "receiveCcipReadFetchResult",
    request: FetchRequest,
    result: any
} | {
    action: "receiveCcipReadFetchError",
    request: FetchRequest,
    result: any
} | {
    action: "sendCcipReadCall",
    transaction: { to: string, data: string }
} | {
    action: "receiveCcipReadCallResult",
    transaction: { to: string, data: string }
    result: string
} | {
    action: "receiveCcipReadCallError",
    transaction: { to: string, data: string }
    error: Error
};


/**
 *  The value passed to the [[AbstractProvider-_getSubscriber]] method.
 *
 *  Only developers sub-classing [[AbstractProvider[[ will care about this,
 *  if they are modifying a low-level feature of how subscriptions operate.
 */
export type Subscription = {
    type: "block" | "close" | "debug" | "error" | "network" | "pending",
    tag: string
} | {
    type: "transaction",
    tag: string,
    hash: string
} | {
    type: "event",
    tag: string,
    filter: EventFilter
} | {
    type: "orphan",
    tag: string,
    filter: OrphanFilter
};

/**
 *  A **Subscriber** manages a subscription.
 *
 *  Only developers sub-classing [[AbstractProvider[[ will care about this,
 *  if they are modifying a low-level feature of how subscriptions operate.
 */
export interface Subscriber {
    /**
     *  Called initially when a subscriber is added the first time.
     */
    start(): void;

    /**
     *  Called when there are no more subscribers to the event.
     */
    stop(): void;

    /**
     *  Called when the subscription should pause.
     *
     *  If %%dropWhilePaused%%, events that occur while paused should not
     *  be emitted [[resume]].
     */
    pause(dropWhilePaused?: boolean): void;

    /**
     *  Resume a paused subscriber.
     */
    resume(): void;

    /**
     *  The frequency (in ms) to poll for events, if polling is used by
     *  the subscriber.
     *
     *  For non-polling subscribers, this must return ``undefined``.
     */
    pollingInterval?: number;
}

/**
 *  An **UnmanagedSubscriber** is useful for events which do not require
 *  any additional management, such as ``"debug"`` which only requires
 *  emit in synchronous event loop triggered calls.
 */
export class UnmanagedSubscriber implements Subscriber {
    /**
     *  The name fof the event.
     */
    name!: string;

    /**
     *  Create a new UnmanagedSubscriber with %%name%%.
     */
    constructor(name: string) { defineProperties<UnmanagedSubscriber>(this, { name }); }

    start(): void { }
    stop(): void { }

    pause(dropWhilePaused?: boolean): void { }
    resume(): void { }
}

type Sub = {
    tag: string;
    nameMap: Map<string, string>
    addressableMap: WeakMap<Addressable, string>;
    listeners: Array<{ listener: Listener, once: boolean }>;
    // @TODO: get rid of this, as it is (and has to be)
    // tracked in subscriber
    started: boolean;
    subscriber: Subscriber;
};

function copy<T = any>(value: T): T {
    return JSON.parse(JSON.stringify(value));
}

function concisify(items: Array<string>): Array<string> {
    items = Array.from((new Set(items)).values())
    items.sort();
    return items;
}


async function getSubscription(_event: ProviderEvent, provider: AbstractProvider): Promise<Subscription> {
    if (_event == null) { throw new Error("invalid event"); }

    // Normalize topic array info an EventFilter
    if (Array.isArray(_event)) { _event = { topics: _event }; }

    if (typeof(_event) === "string") {
        switch (_event) {
            case "block": case "pending": case "debug": case "error": case "network": {
                return { type: _event, tag: _event };
            }
        }
    }

    if (isHexString(_event, 32)) {
        const hash = _event.toLowerCase();
        return { type: "transaction", tag: getTag("tx", { hash }), hash };
    }

    if ((<any>_event).orphan) {
        const event = <OrphanFilter>_event;
        // @TODO: Should lowercase and whatnot things here instead of copy...
        return { type: "orphan", tag: getTag("orphan", event), filter: copy(event) };
    }

    if (((<any>_event).address || (<any>_event).topics)) {
        const event = <EventFilter>_event;

        const filter: any = {
            topics: ((event.topics || []).map((t) => {
                if (t == null) { return null; }
                if (Array.isArray(t)) {
                    return concisify(t.map((t) => t.toLowerCase()));
                }
                return t.toLowerCase();
            }))
        };

        if (event.address) {
            const addresses: Array<string> = [ ];
            const promises: Array<Promise<void>> = [ ];

            const addAddress = (addr: AddressLike) => {
                if (isHexString(addr)) {
                    addresses.push(addr);
                } else {
                    promises.push((async () => {
                        addresses.push(await resolveAddress(addr, provider));
                    })());
                }
            }

            if (Array.isArray(event.address)) {
                event.address.forEach(addAddress);
            } else {
                addAddress(event.address);
            }
            if (promises.length) { await Promise.all(promises); }
            filter.address = concisify(addresses.map((a) => a.toLowerCase()));
        }

        return { filter, tag: getTag("event", filter), type: "event" };
    }

    assertArgument(false, "unknown ProviderEvent", "event", _event);
}

function getTime(): number { return (new Date()).getTime(); }

/**
 *  An **AbstractPlugin** is used to provide additional internal services
 *  to an [[AbstractProvider]] without adding backwards-incompatible changes
 *  to method signatures or other internal and complex logic.
 */
export interface AbstractProviderPlugin {
    /**
     *  The reverse domain notation of the plugin.
     */
    readonly name: string;

    /**
     *  Creates a new instance of the plugin, connected to %%provider%%.
     */
    connect(provider: AbstractProvider): AbstractProviderPlugin;
}

/**
 *  A normalized filter used for [[PerformActionRequest]] objects.
 */
export type PerformActionFilter = {
    address?: string | Array<string>;
    topics?: Array<null | string | Array<string>>;
    fromBlock?: BlockTag;
    toBlock?: BlockTag;
} | {
    address?: string | Array<string>;
    topics?: Array<null | string | Array<string>>;
    blockHash?: string;
};

/**
 *  A normalized transactions used for [[PerformActionRequest]] objects.
 */
export interface PerformActionTransaction extends PreparedTransactionRequest {
    /**
     *  The ``to`` address of the transaction.
     */
    to?: string;

    /**
     *  The sender of the transaction.
     */
    from?: string;
}

/**
 *  The [[AbstractProvider]] methods will normalize all values and pass this
 *  type to [[AbstractProvider-_perform]].
 */
export type PerformActionRequest = {
    method: "broadcastTransaction",
    signedTransaction: string
} | {
    method: "call",
    transaction: PerformActionTransaction, blockTag: BlockTag
} | {
    method: "chainId"
} | {
    method: "estimateGas",
    transaction: PerformActionTransaction
} | {
    method: "getBalance",
    address: string, blockTag: BlockTag
} | {
    method: "getBlock",
    blockTag: BlockTag, includeTransactions: boolean
} | {
    method: "getBlock",
    blockHash: string, includeTransactions: boolean
} | {
    method: "getBlockNumber"
} | {
    method: "getCode",
    address: string, blockTag: BlockTag
} | {
    method: "getGasPrice"
} | {
    method: "getLogs",
    filter: PerformActionFilter
} | {
    method: "getStorage",
    address: string, position: bigint, blockTag: BlockTag
} | {
    method: "getTransaction",
    hash: string
} | {
    method: "getTransactionCount",
    address: string, blockTag: BlockTag
} | {
    method: "getTransactionReceipt",
    hash: string
} | {
    method: "getTransactionResult",
    hash: string
};

type _PerformAccountRequest = {
    method: "getBalance" | "getTransactionCount" | "getCode"
} | {
    method: "getStorage", position: bigint
}

/**
 *  Options for configuring some internal aspects of an [[AbstractProvider]].
 *
 *  **``cacheTimeout``** - how long to cache a low-level ``_perform``
 *  for, based on input parameters. This reduces the number of calls
 *  to getChainId and getBlockNumber, but may break test chains which
 *  can perform operations (internally) synchronously. Use ``-1`` to
 *  disable, ``0`` will only buffer within the same event loop and
 *  any other value is in ms. (default: ``250``)
 */
export type AbstractProviderOptions = {
    cacheTimeout?: number;
    pollingInterval?: number;
};

const defaultOptions = {
    cacheTimeout: 250,
    pollingInterval: 4000
};

type CcipArgs = {
    sender: string;
    urls: Array<string>;
    calldata: string;
    selector: string;
    extraData: string;
    errorArgs: Array<any>
};

/**
 *  An **AbstractProvider** provides a base class for other sub-classes to
 *  implement the [[Provider]] API by normalizing input arguments and
 *  formatting output results as well as tracking events for consistent
 *  behaviour on an eventually-consistent network.
 */
export class AbstractProvider implements Provider {

    #subs: Map<string, Sub>;
    #plugins: Map<string, AbstractProviderPlugin>;

    // null=unpaused, true=paused+dropWhilePaused, false=paused
    #pausedState: null | boolean;

    #destroyed: boolean;

    #networkPromise: null | Promise<Network>;
    readonly #anyNetwork: boolean;

    #performCache: Map<string, Promise<any>>;

    // The most recent block number if running an event or -1 if no "block" event
    #lastBlockNumber: number;

    #nextTimer: number;
    #timers: Map<number, { timer: null | Timer, func: () => void, time: number }>;

    #disableCcipRead: boolean;

    #options: Required<AbstractProviderOptions>;

    /**
     *  Create a new **AbstractProvider** connected to %%network%%, or
     *  use the various network detection capabilities to discover the
     *  [[Network]] if necessary.
     */
    constructor(_network?: "any" | Networkish, options?: AbstractProviderOptions) {
        this.#options = Object.assign({ }, defaultOptions, options || { });

        if (_network === "any") {
            this.#anyNetwork = true;
            this.#networkPromise = null;
        } else if (_network) {
            const network = Network.from(_network);
            this.#anyNetwork = false;
            this.#networkPromise = Promise.resolve(network);
            setTimeout(() => { this.emit("network", network, null); }, 0);
        } else {
            this.#anyNetwork = false;
            this.#networkPromise = null;
        }

        this.#lastBlockNumber = -1;

        this.#performCache = new Map();

        this.#subs = new Map();
        this.#plugins = new Map();
        this.#pausedState = null;

        this.#destroyed = false;

        this.#nextTimer = 1;
        this.#timers = new Map();

        this.#disableCcipRead = false;
    }

    get pollingInterval(): number { return this.#options.pollingInterval; }

    /**
     *  Returns ``this``, to allow an **AbstractProvider** to implement
     *  the [[ContractRunner]] interface.
     */
    get provider(): this { return this; }

    /**
     *  Returns all the registered plug-ins.
     */
    get plugins(): Array<AbstractProviderPlugin> {
        return Array.from(this.#plugins.values());
    }

    /**
     *  Attach a new plug-in.
     */
    attachPlugin(plugin: AbstractProviderPlugin): this {
        if (this.#plugins.get(plugin.name)) {
            throw new Error(`cannot replace existing plugin: ${ plugin.name } `);
        }
        this.#plugins.set(plugin.name,  plugin.connect(this));
        return this;
    }

    /**
     *  Get a plugin by name.
     */
    getPlugin<T extends AbstractProviderPlugin = AbstractProviderPlugin>(name: string): null | T {
        return <T>(this.#plugins.get(name)) || null;
    }

    /**
     *  Prevent any CCIP-read operation, regardless of whether requested
     *  in a [[call]] using ``enableCcipRead``.
     */
    get disableCcipRead(): boolean { return this.#disableCcipRead; }
    set disableCcipRead(value: boolean) { this.#disableCcipRead = !!value; }

    // Shares multiple identical requests made during the same 250ms
    async #perform<T = any>(req: PerformActionRequest): Promise<T> {
        const timeout = this.#options.cacheTimeout;

        // Caching disabled
        if (timeout < 0) { return await this._perform(req); }

        // Create a tag
        const tag = getTag(req.method, req);

        let perform = this.#performCache.get(tag);
        if (!perform) {
            perform = this._perform(req);

            this.#performCache.set(tag, perform);

            setTimeout(() => {
                if (this.#performCache.get(tag) === perform) {
                    this.#performCache.delete(tag);
                }
            }, timeout);
        }

        return await perform;
    }

    /**
     *  Resolves to the data for executing the CCIP-read operations.
     */
    async ccipReadFetch(tx: PerformActionTransaction, calldata: string, urls: Array<string>): Promise<null | string> {
        if (this.disableCcipRead || urls.length === 0 || tx.to == null) { return null; }

        const sender = tx.to.toLowerCase();
        const data = calldata.toLowerCase();

        const errorMessages: Array<string> = [ ];

        for (let i = 0; i < urls.length; i++) {
            const url = urls[i];

            // URL expansion
            const href = url.replace("{sender}", sender).replace("{data}", data);

            // If no {data} is present, use POST; otherwise GET
            //const json: string | null = (url.indexOf("{data}") >= 0) ? null: JSON.stringify({ data, sender });

            //const result = await fetchJson({ url: href, errorPassThrough: true }, json, (value, response) => {
            //    value.status = response.statusCode;
            //    return value;
            //});
            const request = new FetchRequest(href);
            if (url.indexOf("{data}") === -1) {
                request.body = { data, sender };
            }

            this.emit("debug", { action: "sendCcipReadFetchRequest", request, index: i, urls });

            let errorMessage = "unknown error";

            const resp = await request.send();
            try {
                 const result = resp.bodyJson;
                 if (result.data) {
                     this.emit("debug", { action: "receiveCcipReadFetchResult", request, result });
                     return result.data;
                 }
                 if (result.message) { errorMessage = result.message; }
                 this.emit("debug", { action: "receiveCcipReadFetchError", request, result });
            } catch (error) { }

            // 4xx indicates the result is not present; stop
            assert(resp.statusCode < 400 || resp.statusCode >= 500, `response not found during CCIP fetch: ${ errorMessage }`,
                "OFFCHAIN_FAULT", { reason: "404_MISSING_RESOURCE", transaction: tx, info: { url, errorMessage } });

            // 5xx indicates server issue; try the next url
            errorMessages.push(errorMessage);
        }

        assert(false, `error encountered during CCIP fetch: ${ errorMessages.map((m) => JSON.stringify(m)).join(", ") }`, "OFFCHAIN_FAULT", {
            reason: "500_SERVER_ERROR",
            transaction: tx, info: { urls, errorMessages }
        });
    }

    /**
     *  Provides the opportunity for a sub-class to wrap a block before
     *  returning it, to add additional properties or an alternate
     *  sub-class of [[Block]].
     */
    _wrapBlock(value: BlockParams, network: Network): Block {
        return new Block(formatBlock(value), this);
    }

    /**
     *  Provides the opportunity for a sub-class to wrap a log before
     *  returning it, to add additional properties or an alternate
     *  sub-class of [[Log]].
     */
    _wrapLog(value: LogParams, network: Network): Log {
        return new Log(formatLog(value), this);
    }

    /**
     *  Provides the opportunity for a sub-class to wrap a transaction
     *  receipt before returning it, to add additional properties or an
     *  alternate sub-class of [[TransactionReceipt]].
     */
    _wrapTransactionReceipt(value: TransactionReceiptParams, network: Network): TransactionReceipt {
        return new TransactionReceipt(formatTransactionReceipt(value), this);
    }

    /**
     *  Provides the opportunity for a sub-class to wrap a transaction
     *  response before returning it, to add additional properties or an
     *  alternate sub-class of [[TransactionResponse]].
     */
    _wrapTransactionResponse(tx: TransactionResponseParams, network: Network): TransactionResponse {
        return new TransactionResponse(formatTransactionResponse(tx), this);
    }

    /**
     *  Resolves to the Network, forcing a network detection using whatever
     *  technique the sub-class requires.
     *
     *  Sub-classes **must** override this.
     */
    _detectNetwork(): Promise<Network> {
        assert(false, "sub-classes must implement this", "UNSUPPORTED_OPERATION", {
            operation: "_detectNetwork"
        });
    }

    /**
     *  Sub-classes should use this to perform all built-in operations. All
     *  methods sanitizes and normalizes the values passed into this.
     *
     *  Sub-classes **must** override this.
     */
    async _perform<T = any>(req: PerformActionRequest): Promise<T> {
        assert(false, `unsupported method: ${ req.method }`, "UNSUPPORTED_OPERATION", {
            operation: req.method,
            info: req
        });
    }

    // State

    async getBlockNumber(): Promise<number> {
        const blockNumber = getNumber(await this.#perform({ method: "getBlockNumber" }), "%response");
        if (this.#lastBlockNumber >= 0) { this.#lastBlockNumber = blockNumber; }
        return blockNumber;
    }

    /**
     *  Returns or resolves to the address for %%address%%, resolving ENS
     *  names and [[Addressable]] objects and returning if already an
     *  address.
     */
    _getAddress(address: AddressLike): string | Promise<string> {
        return resolveAddress(address, this);
    }

    /**
     *  Returns or resolves to a valid block tag for %%blockTag%%, resolving
     *  negative values and returning if already a valid block tag.
     */
    _getBlockTag(blockTag?: BlockTag): string | Promise<string> {
        if (blockTag == null) { return "latest"; }

        switch (blockTag) {
            case "earliest":
                return "0x0";
            case "latest": case "pending": case "safe": case "finalized":
                return blockTag;
        }


        if (isHexString(blockTag)) {
            if (isHexString(blockTag, 32)) { return blockTag; }
            return toQuantity(blockTag);
        }

        if (typeof(blockTag) === "bigint") {
            blockTag = getNumber(blockTag, "blockTag");
        }

        if (typeof(blockTag) === "number") {
            if (blockTag >= 0) { return toQuantity(blockTag); }
            if (this.#lastBlockNumber >= 0) { return toQuantity(this.#lastBlockNumber + blockTag); }
            return this.getBlockNumber().then((b) => toQuantity(b + <number>blockTag));
        }

        assertArgument(false, "invalid blockTag", "blockTag", blockTag);
    }

    /**
     *  Returns or resolves to a filter for %%filter%%, resolving any ENS
     *  names or [[Addressable]] object and returning if already a valid
     *  filter.
     */
    _getFilter(filter: Filter | FilterByBlockHash): PerformActionFilter | Promise<PerformActionFilter> {

        // Create a canonical representation of the topics
        const topics = (filter.topics || [ ]).map((t) => {
            if (t == null) { return null; }
            if (Array.isArray(t)) {
                return concisify(t.map((t) => t.toLowerCase()));
            }
            return t.toLowerCase();
        });

        const blockHash = ("blockHash" in filter) ? filter.blockHash: undefined;

        const resolve = (_address: Array<string>, fromBlock?: string, toBlock?: string) => {
            let address: undefined | string | Array<string> = undefined;
            switch (_address.length) {
                case 0: break;
                case 1:
                    address = _address[0];
                    break;
                default:
                    _address.sort();
                    address = _address;
            }

            if (blockHash) {
                if (fromBlock != null || toBlock != null) {
                    throw new Error("invalid filter");
                }
            }

            const filter = <any>{ };
            if (address) { filter.address = address; }
            if (topics.length) { filter.topics = topics; }
            if (fromBlock) { filter.fromBlock = fromBlock; }
            if (toBlock) { filter.toBlock = toBlock; }
            if (blockHash) { filter.blockHash = blockHash; }

            return filter;
        };

        // Addresses could be async (ENS names or Addressables)
        let address: Array<string | Promise<string>> = [ ];
        if (filter.address) {
            if (Array.isArray(filter.address)) {
                for (const addr of filter.address) { address.push(this._getAddress(addr)); }
            } else {
                address.push(this._getAddress(filter.address));
            }
        }

        let fromBlock: undefined | string | Promise<string> = undefined;
        if ("fromBlock" in filter) { fromBlock = this._getBlockTag(filter.fromBlock); }

        let toBlock: undefined | string | Promise<string> = undefined;
        if ("toBlock" in filter) { toBlock = this._getBlockTag(filter.toBlock); }

        if (address.filter((a) => (typeof(a) !== "string")).length ||
            (fromBlock != null && typeof(fromBlock) !== "string") ||
            (toBlock != null && typeof(toBlock) !== "string")) {

            return Promise.all([ Promise.all(address), fromBlock, toBlock ]).then((result) => {
                return resolve(result[0], result[1], result[2]);
            });
        }

        return resolve(<Array<string>>address, fromBlock, toBlock);
    }

    /**
     *  Returns or resovles to a transaction for %%request%%, resolving
     *  any ENS names or [[Addressable]] and returning if already a valid
     *  transaction.
     */
    _getTransactionRequest(_request: TransactionRequest): PerformActionTransaction | Promise<PerformActionTransaction> {
        const request = <PerformActionTransaction>copyRequest(_request);

        const promises: Array<Promise<void>> = [ ];
        [ "to", "from" ].forEach((key) => {
            if ((<any>request)[key] == null) { return; }

            const addr = resolveAddress((<any>request)[key], this);
            if (isPromise(addr)) {
                promises.push((async function() { (<any>request)[key] = await addr; })());
            } else {
                (<any>request)[key] = addr;
            }
        });

        if (request.blockTag != null) {
            const blockTag = this._getBlockTag(request.blockTag);
            if (isPromise(blockTag)) {
                promises.push((async function() { request.blockTag = await blockTag; })());
            } else {
                request.blockTag = blockTag;
            }
        }

        if (promises.length) {
            return (async function() {
                await Promise.all(promises);
                return request;
            })();
        }

        return request;
    }

    async getNetwork(): Promise<Network> {

        // No explicit network was set and this is our first time
        if (this.#networkPromise == null) {

            // Detect the current network (shared with all calls)
            const detectNetwork = this._detectNetwork().then((network) => {
                this.emit("network", network, null);
                return network;
            }, (error) => {
                // Reset the networkPromise on failure, so we will try again
                if (this.#networkPromise === detectNetwork) {
                    this.#networkPromise = null;
                }
                throw error;
            });

            this.#networkPromise = detectNetwork;
            return (await detectNetwork).clone();
        }

        const networkPromise = this.#networkPromise;

        const [ expected, actual ] = await Promise.all([
            networkPromise,          // Possibly an explicit Network
            this._detectNetwork()    // The actual connected network
        ]);

        if (expected.chainId !== actual.chainId) {
            if (this.#anyNetwork) {
                // The "any" network can change, so notify listeners
                this.emit("network", actual, expected);

                // Update the network if something else hasn't already changed it
                if (this.#networkPromise === networkPromise) {
                    this.#networkPromise = Promise.resolve(actual);
                }
            } else {
                // Otherwise, we do not allow changes to the underlying network
                assert(false, `network changed: ${ expected.chainId } => ${ actual.chainId } `, "NETWORK_ERROR", {
                    event: "changed"
                });
            }
        }

        return expected.clone();
    }

    async getFeeData(): Promise<FeeData> {
        const network = await this.getNetwork();

        const getFeeDataFunc = async () => {
            const { _block, gasPrice } = await resolveProperties({
                _block: this.#getBlock("latest", false),
                gasPrice: ((async () => {
                    try {
                        const gasPrice = await this.#perform({ method: "getGasPrice" });
                        return getBigInt(gasPrice, "%response");
                    } catch (error) { }
                    return null
                })())
            });

            let maxFeePerGas: null | bigint = null;
            let maxPriorityFeePerGas: null | bigint = null;

            // These are the recommended EIP-1559 heuristics for fee data
            const block = this._wrapBlock(_block, network);
            if (block && block.baseFeePerGas) {
                maxPriorityFeePerGas = BigInt("1000000000");
                maxFeePerGas = (block.baseFeePerGas * BN_2) + maxPriorityFeePerGas;
            }

            return new FeeData(gasPrice, maxFeePerGas, maxPriorityFeePerGas);
        };

        // Check for a FeeDataNetWorkPlugin
        const plugin = <FetchUrlFeeDataNetworkPlugin>network.getPlugin("org.ethers.plugins.network.FetchUrlFeeDataPlugin");
        if (plugin) {
            const req = new FetchRequest(plugin.url);
            const feeData = await plugin.processFunc(getFeeDataFunc, this, req);
            return new FeeData(feeData.gasPrice, feeData.maxFeePerGas, feeData.maxPriorityFeePerGas);
        }

        return await getFeeDataFunc();
    }


    async estimateGas(_tx: TransactionRequest): Promise<bigint> {
        let tx = this._getTransactionRequest(_tx);
        if (isPromise(tx)) { tx = await tx; }
        return getBigInt(await this.#perform({
            method: "estimateGas", transaction: tx
        }), "%response");
    }

    async #call(tx: PerformActionTransaction, blockTag: string, attempt: number): Promise<string> {
        assert (attempt < MAX_CCIP_REDIRECTS, "CCIP read exceeded maximum redirections", "OFFCHAIN_FAULT", {
             reason: "TOO_MANY_REDIRECTS",
             transaction: Object.assign({ }, tx, { blockTag, enableCcipRead: true })
         });

         // This came in as a PerformActionTransaction, so to/from are safe; we can cast
         const transaction = <PerformActionTransaction>copyRequest(tx);

         try {
             return hexlify(await this._perform({ method: "call", transaction, blockTag }));

         } catch (error: any) {
             // CCIP Read OffchainLookup
             if (!this.disableCcipRead && isCallException(error) && error.data && attempt >= 0 && blockTag === "latest" && transaction.to != null && dataSlice(error.data, 0, 4) === "0x556f1830") {
                 const data = error.data;

                 const txSender = await resolveAddress(transaction.to, this);

                 // Parse the CCIP Read Arguments
                 let ccipArgs: CcipArgs;
                 try {
                     ccipArgs = parseOffchainLookup(dataSlice(error.data, 4));
                 } catch (error: any) {
                     assert(false, error.message, "OFFCHAIN_FAULT", {
                         reason: "BAD_DATA", transaction, info: { data } });
                 }

                 // Check the sender of the OffchainLookup matches the transaction
                 assert(ccipArgs.sender.toLowerCase() === txSender.toLowerCase(),
                     "CCIP Read sender mismatch", "CALL_EXCEPTION", {
                         action: "call",
                         data,
                         reason: "OffchainLookup",
                         transaction: <any>transaction, // @TODO: populate data?
                         invocation: null,
                         revert: {
                             signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
                             name: "OffchainLookup",
                             args: ccipArgs.errorArgs
                         }
                     });

                 const ccipResult = await this.ccipReadFetch(transaction, ccipArgs.calldata, ccipArgs.urls);
                 assert(ccipResult != null, "CCIP Read failed to fetch data", "OFFCHAIN_FAULT", {
                     reason: "FETCH_FAILED", transaction, info: { data: error.data, errorArgs: ccipArgs.errorArgs } });

                 const tx = {
                     to: txSender,
                     data: concat([ ccipArgs.selector, encodeBytes([ ccipResult, ccipArgs.extraData ]) ])
                 };

                 this.emit("debug", { action: "sendCcipReadCall", transaction: tx });
                 try {
                     const result = await this.#call(tx, blockTag, attempt + 1);
                     this.emit("debug", { action: "receiveCcipReadCallResult", transaction: Object.assign({ }, tx), result });
                     return result;
                 } catch (error) {
                     this.emit("debug", { action: "receiveCcipReadCallError", transaction: Object.assign({ }, tx), error });
                     throw error;
                 }
             }

             throw error;
         }
    }

    async #checkNetwork<T>(promise: Promise<T>): Promise<T> {
        const { value } = await resolveProperties({
            network: this.getNetwork(),
            value: promise
        });
        return value;
    }

    async call(_tx: TransactionRequest): Promise<string> {
        const { tx, blockTag } = await resolveProperties({
            tx: this._getTransactionRequest(_tx),
            blockTag: this._getBlockTag(_tx.blockTag)
        });

        return await this.#checkNetwork(this.#call(tx, blockTag, _tx.enableCcipRead ? 0: -1));
    }

    // Account
    async #getAccountValue(request: _PerformAccountRequest, _address: AddressLike, _blockTag?: BlockTag): Promise<any> {
        let address: string | Promise<string> = this._getAddress(_address);
        let blockTag: string | Promise<string> = this._getBlockTag(_blockTag);

        if (typeof(address) !== "string" || typeof(blockTag) !== "string") {
            [ address, blockTag ] = await Promise.all([ address, blockTag ]);
        }

        return await this.#checkNetwork(this.#perform(Object.assign(request, { address, blockTag })));
    }

    async getBalance(address: AddressLike, blockTag?: BlockTag): Promise<bigint> {
        return getBigInt(await this.#getAccountValue({ method: "getBalance" }, address, blockTag), "%response");
    }

    async getTransactionCount(address: AddressLike, blockTag?: BlockTag): Promise<number> {
        return getNumber(await this.#getAccountValue({ method: "getTransactionCount" }, address, blockTag), "%response");
    }

    async getCode(address: AddressLike, blockTag?: BlockTag): Promise<string> {
        return hexlify(await this.#getAccountValue({ method: "getCode" }, address, blockTag));
    }

    async getStorage(address: AddressLike, _position: BigNumberish, blockTag?: BlockTag): Promise<string> {
        const position = getBigInt(_position, "position");
        return hexlify(await this.#getAccountValue({ method: "getStorage", position }, address, blockTag));
    }

    // Write
    async broadcastTransaction(signedTx: string): Promise<TransactionResponse> {
        const { blockNumber, hash, network } = await resolveProperties({
             blockNumber: this.getBlockNumber(),
             hash: this._perform({
                 method: "broadcastTransaction",
                 signedTransaction: signedTx
             }),
             network: this.getNetwork()
        });

        const tx = Transaction.from(signedTx);
        if (tx.hash !== hash) {
            throw new Error("@TODO: the returned hash did not match");
        }

        return this._wrapTransactionResponse(<any>tx, network).replaceableTransaction(blockNumber);
    }

    async #getBlock(block: BlockTag | string, includeTransactions: boolean): Promise<any> {
        // @TODO: Add CustomBlockPlugin check

        if (isHexString(block, 32)) {
            return await this.#perform({
                method: "getBlock", blockHash: block, includeTransactions
            });
        }

        let blockTag = this._getBlockTag(block);
        if (typeof(blockTag) !== "string") { blockTag = await blockTag; }

        return await this.#perform({
            method: "getBlock", blockTag, includeTransactions
        });
    }

    // Queries
    async getBlock(block: BlockTag | string, prefetchTxs?: boolean): Promise<null | Block> {
        const { network, params } = await resolveProperties({
            network: this.getNetwork(),
            params: this.#getBlock(block, !!prefetchTxs)
        });
        if (params == null) { return null; }

        return this._wrapBlock(params, network);
    }

    async getTransaction(hash: string): Promise<null | TransactionResponse> {
        const { network, params } = await resolveProperties({
            network: this.getNetwork(),
            params: this.#perform({ method: "getTransaction", hash })
        });
        if (params == null) { return null; }

        return this._wrapTransactionResponse(params, network);
    }

    async getTransactionReceipt(hash: string): Promise<null | TransactionReceipt> {
        const { network, params } = await resolveProperties({
            network: this.getNetwork(),
            params: this.#perform({ method: "getTransactionReceipt", hash })
        });
        if (params == null) { return null; }

        // Some backends did not backfill the effectiveGasPrice into old transactions
        // in the receipt, so we look it up manually and inject it.
        if (params.gasPrice == null && params.effectiveGasPrice == null) {
            const tx = await this.#perform({ method: "getTransaction", hash });
            if (tx == null) { throw new Error("report this; could not find tx or effectiveGasPrice"); }
            params.effectiveGasPrice = tx.gasPrice;
        }

        return this._wrapTransactionReceipt(params, network);
    }

    async getTransactionResult(hash: string): Promise<null | string> {
        const { result } = await resolveProperties({
            network: this.getNetwork(),
            result: this.#perform({ method: "getTransactionResult", hash })
        });
        if (result == null) { return null; }
        return hexlify(result);
    }

    // Bloom-filter Queries
    async getLogs(_filter: Filter | FilterByBlockHash): Promise<Array<Log>> {
        let filter = this._getFilter(_filter);
        if (isPromise(filter)) { filter = await filter; }

        const { network, params } = await resolveProperties({
            network: this.getNetwork(),
            params: this.#perform<Array<LogParams>>({ method: "getLogs", filter })
        });

        return params.map((p) => this._wrapLog(p, network));
    }

    // ENS
    _getProvider(chainId: number): AbstractProvider {
        assert(false, "provider cannot connect to target network", "UNSUPPORTED_OPERATION", {
            operation: "_getProvider()"
        });
    }

    async getResolver(name: string): Promise<null | EnsResolver> {
        return await EnsResolver.fromName(this, name);
    }

    async getAvatar(name: string): Promise<null | string> {
        const resolver = await this.getResolver(name);
        if (resolver) { return await resolver.getAvatar(); }
        return null;
    }

    async resolveName(name: string): Promise<null | string>{
        const resolver = await this.getResolver(name);
        if (resolver) { return await resolver.getAddress(); }
        return null;
    }

    async lookupAddress(address: string): Promise<null | string> {
        address = getAddress(address);
        const node = namehash(address.substring(2).toLowerCase() + ".addr.reverse");

        try {

            const ensAddr = await EnsResolver.getEnsAddress(this);
            const ensContract = new Contract(ensAddr, [
                "function resolver(bytes32) view returns (address)"
            ], this);

            const resolver = await ensContract.resolver(node);
            if (resolver == null || resolver === ZeroAddress) { return null; }

            const resolverContract = new Contract(resolver, [
                "function name(bytes32) view returns (string)"
            ], this);
            const name = await resolverContract.name(node);

            // Failed forward resolution
            const check = await this.resolveName(name);
            if (check !== address) { return null; }

            return name;

        } catch (error) {
            // No data was returned from the resolver
            if (isError(error, "BAD_DATA") && error.value === "0x") {
                return null;
            }

            // Something reerted
            if (isError(error, "CALL_EXCEPTION")) { return null; }

            throw error;
        }

        return null;
    }

    async waitForTransaction(hash: string, _confirms?: null | number, timeout?: null | number): Promise<null | TransactionReceipt> {
        const confirms = (_confirms != null) ? _confirms: 1;
        if (confirms === 0) { return this.getTransactionReceipt(hash); }

        return new Promise(async (resolve, reject) => {
            let timer: null | Timer = null;

            const listener = (async (blockNumber: number) => {
                try {
                    const receipt = await this.getTransactionReceipt(hash);
                    if (receipt != null) {
                        if (blockNumber - receipt.blockNumber + 1 >= confirms) {
                            resolve(receipt);
                            //this.off("block", listener);
                            if (timer) {
                                clearTimeout(timer);
                                timer = null;
                            }
                            return;
                        }
                    }
                } catch (error) {
                    console.log("EEE", error);
                }
                this.once("block", listener);
            });

            if (timeout != null) {
                timer = setTimeout(() => {
                    if (timer == null) { return; }
                    timer = null;
                    this.off("block", listener);
                    reject(makeError("timeout", "TIMEOUT", { reason: "timeout" }));
                }, timeout);
            }

            listener(await this.getBlockNumber());
        });
    }

    async waitForBlock(blockTag?: BlockTag): Promise<Block> {
        assert(false, "not implemented yet", "NOT_IMPLEMENTED", {
            operation: "waitForBlock"
        });
    }

    /**
     *  Clear a timer created using the [[_setTimeout]] method.
     */
    _clearTimeout(timerId: number): void {
        const timer = this.#timers.get(timerId);
        if (!timer) { return; }
        if (timer.timer) { clearTimeout(timer.timer); }
        this.#timers.delete(timerId);
    }

    /**
     *  Create a timer that will execute %%func%% after at least %%timeout%%
     *  (in ms). If %%timeout%% is unspecified, then %%func%% will execute
     *  in the next event loop.
     *
     *  [Pausing](AbstractProvider-paused) the provider will pause any
     *  associated timers.
     */
    _setTimeout(_func: () => void, timeout?: number): number {
        if (timeout == null) { timeout = 0; }
        const timerId = this.#nextTimer++;
        const func = () => {
            this.#timers.delete(timerId);
            _func();
        };

        if (this.paused) {
            this.#timers.set(timerId, { timer: null, func, time: timeout });
        } else {
            const timer = setTimeout(func, timeout);
            this.#timers.set(timerId, { timer, func, time: getTime() });
        }

        return timerId;
    }

    /**
     *  Perform %%func%% on each subscriber.
     */
    _forEachSubscriber(func: (s: Subscriber) => void): void {
        for (const sub of this.#subs.values()) {
            func(sub.subscriber);
        }
    }

    /**
     *  Sub-classes may override this to customize subscription
     *  implementations.
     */
    _getSubscriber(sub: Subscription): Subscriber {
        switch (sub.type) {
            case "debug":
            case "error":
            case "network":
                return new UnmanagedSubscriber(sub.type);
            case "block": {
                const subscriber = new PollingBlockSubscriber(this);
                subscriber.pollingInterval = this.pollingInterval;
                return subscriber;
            }
            case "event":
                return new PollingEventSubscriber(this, sub.filter);
            case "transaction":
                return new PollingTransactionSubscriber(this, sub.hash);
            case "orphan":
                return new PollingOrphanSubscriber(this, sub.filter);
        }

        throw new Error(`unsupported event: ${ sub.type }`);
    }

    /**
     *  If a [[Subscriber]] fails and needs to replace itself, this
     *  method may be used.
     *
     *  For example, this is used for providers when using the
     *  ``eth_getFilterChanges`` method, which can return null if state
     *  filters are not supported by the backend, allowing the Subscriber
     *  to swap in a [[PollingEventSubscriber]].
     */
    _recoverSubscriber(oldSub: Subscriber, newSub: Subscriber): void {
        for (const sub of this.#subs.values()) {
            if (sub.subscriber === oldSub) {
                if (sub.started) { sub.subscriber.stop(); }
                sub.subscriber = newSub;
                if (sub.started) { newSub.start(); }
                if (this.#pausedState != null) { newSub.pause(this.#pausedState); }
                break;
            }
        }
    }

    async #hasSub(event: ProviderEvent, emitArgs?: Array<any>): Promise<null | Sub> {
        let sub = await getSubscription(event, this);
        // This is a log that is removing an existing log; we actually want
        // to emit an orphan event for the removed log
        if (sub.type === "event" && emitArgs && emitArgs.length > 0 && emitArgs[0].removed === true) {
            sub = await getSubscription({ orphan: "drop-log", log: emitArgs[0] }, this);
        }
        return this.#subs.get(sub.tag) || null;
    }

    async #getSub(event: ProviderEvent): Promise<Sub> {
        const subscription = await getSubscription(event, this);

        // Prevent tampering with our tag in any subclass' _getSubscriber
        const tag = subscription.tag;

        let sub = this.#subs.get(tag);
        if (!sub) {
            const subscriber = this._getSubscriber(subscription);

            const addressableMap = new WeakMap();
            const nameMap = new Map();
            sub = { subscriber, tag, addressableMap, nameMap, started: false, listeners: [ ] };
            this.#subs.set(tag, sub);
        }

        return sub;
    }

    async on(event: ProviderEvent, listener: Listener): Promise<this> {
        const sub = await this.#getSub(event);
        sub.listeners.push({ listener, once: false });
        if (!sub.started) {
            sub.subscriber.start();
            sub.started = true;
            if (this.#pausedState != null) { sub.subscriber.pause(this.#pausedState); }
        }
        return this;
    }

    async once(event: ProviderEvent, listener: Listener): Promise<this> {
        const sub = await this.#getSub(event);
        sub.listeners.push({ listener, once: true });
        if (!sub.started) {
            sub.subscriber.start();
            sub.started = true;
            if (this.#pausedState != null) { sub.subscriber.pause(this.#pausedState); }
        }
        return this;
    }

    async emit(event: ProviderEvent, ...args: Array<any>): Promise<boolean> {
        const sub = await this.#hasSub(event, args);
        // If there is not subscription or if a recent emit removed
        // the last of them (which also deleted the sub) do nothing
        if (!sub || sub.listeners.length === 0) { return false; };

        const count = sub.listeners.length;
        sub.listeners = sub.listeners.filter(({ listener, once }) => {
            const payload = new EventPayload(this, (once ? null: listener), event);
            try {
                listener.call(this, ...args, payload);
            } catch(error) { }
            return !once;
        });

        if (sub.listeners.length === 0) {
            if (sub.started) { sub.subscriber.stop(); }
            this.#subs.delete(sub.tag);
        }

        return (count > 0);
    }

    async listenerCount(event?: ProviderEvent): Promise<number> {
        if (event) {
            const sub = await this.#hasSub(event);
            if (!sub) { return 0; }
            return sub.listeners.length;
        }

        let total = 0;
        for (const { listeners } of this.#subs.values()) {
            total += listeners.length;
        }
        return total;
    }

    async listeners(event?: ProviderEvent): Promise<Array<Listener>> {
        if (event) {
            const sub = await this.#hasSub(event);
            if (!sub) { return  [ ]; }
            return sub.listeners.map(({ listener }) => listener);
        }
        let result: Array<Listener> = [ ];
        for (const { listeners } of this.#subs.values()) {
            result = result.concat(listeners.map(({ listener }) => listener));
        }
        return result;
    }

    async off(event: ProviderEvent, listener?: Listener): Promise<this> {
        const sub = await this.#hasSub(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 || sub.listeners.length === 0) {
            if (sub.started) { sub.subscriber.stop(); }
            this.#subs.delete(sub.tag);
        }

        return this;
    }

    async removeAllListeners(event?: ProviderEvent): Promise<this> {
        if (event) {
            const { tag, started, subscriber } = await this.#getSub(event);
            if (started) { subscriber.stop(); }
            this.#subs.delete(tag);
        } else {
            for (const [ tag, { started, subscriber } ] of this.#subs) {
                if (started) { subscriber.stop(); }
                this.#subs.delete(tag);
            }
        }
        return this;
    }

    // Alias for "on"
    async addListener(event: ProviderEvent, listener: Listener): Promise<this> {
       return await this.on(event, listener);
    }

    // Alias for "off"
    async removeListener(event: ProviderEvent, listener: Listener): Promise<this> {
       return this.off(event, listener);
    }

    /**
     *  If this provider has been destroyed using the [[destroy]] method.
     *
     *  Once destroyed, all resources are reclaimed, internal event loops
     *  and timers are cleaned up and no further requests may be sent to
     *  the provider.
     */
    get destroyed(): boolean {
        return this.#destroyed;
    }

    /**
     *  Sub-classes may use this to shutdown any sockets or release their
     *  resources and reject any pending requests.
     *
     *  Sub-classes **must** call ``super.destroy()``.
     */
    destroy(): void {
        // Stop all listeners
        this.removeAllListeners();

        // Shut down all tiemrs
        for (const timerId of this.#timers.keys()) {
            this._clearTimeout(timerId);
        }

        this.#destroyed = true;
    }

    /**
     *  Whether the provider is currently paused.
     *
     *  A paused provider will not emit any events, and generally should
     *  not make any requests to the network, but that is up to sub-classes
     *  to manage.
     *
     *  Setting ``paused = true`` is identical to calling ``.pause(false)``,
     *  which will buffer any events that occur while paused until the
     *  provider is unpaused.
     */
    get paused(): boolean { return (this.#pausedState != null); }
    set paused(pause: boolean) {
        if (!!pause === this.paused) { return; }

        if (this.paused) {
            this.resume();
        } else {
            this.pause(false);
        }
    }

    /**
     *  Pause the provider. If %%dropWhilePaused%%, any events that occur
     *  while paused are dropped, otherwise all events will be emitted once
     *  the provider is unpaused.
     */
    pause(dropWhilePaused?: boolean): void {
        this.#lastBlockNumber = -1;

        if (this.#pausedState != null) {
            if (this.#pausedState == !!dropWhilePaused) { return; }
            assert(false, "cannot change pause type; resume first", "UNSUPPORTED_OPERATION", {
                operation: "pause"
            });
        }

        this._forEachSubscriber((s) => s.pause(dropWhilePaused));
        this.#pausedState = !!dropWhilePaused;

        for (const timer of this.#timers.values()) {
            // Clear the timer
            if (timer.timer) { clearTimeout(timer.timer); }

            // Remaining time needed for when we become unpaused
            timer.time = getTime() - timer.time;
        }
    }

    /**
     *  Resume the provider.
     */
    resume(): void {
        if (this.#pausedState == null) { return; }

        this._forEachSubscriber((s) => s.resume());
        this.#pausedState = null;
        for (const timer of this.#timers.values()) {
            // Remaining time when we were paused
            let timeout = timer.time;
            if (timeout < 0) { timeout = 0; }

            // Start time (in cause paused, so we con compute remaininf time)
            timer.time = getTime();

            // Start the timer
            setTimeout(timer.func, timeout);
        }
    }
}


function _parseString(result: string, start: number): null | string {
    try {
        const bytes = _parseBytes(result, start);
        if (bytes) { return toUtf8String(bytes); }
    } catch(error) { }
    return null;
}

function _parseBytes(result: string, start: number): null | string {
    if (result === "0x") { return null; }
    try {
        const offset = getNumber(dataSlice(result, start, start + 32));
        const length = getNumber(dataSlice(result, offset, offset + 32));

        return dataSlice(result, offset + 32, offset + 32 + length);
    } catch (error) { }
    return null;
}

function numPad(value: number): Uint8Array {
    const result = toBeArray(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;
}

const empty: Uint8Array = new Uint8Array([ ]);

// ABI Encodes a series of (bytes, bytes, ...)
function encodeBytes(datas: Array<BytesLike>): string {
    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 = 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);
}

const zeros = "0x0000000000000000000000000000000000000000000000000000000000000000"
function parseOffchainLookup(data: string): CcipArgs {
    const result: CcipArgs = {
        sender: "", urls: [ ], calldata: "", selector: "", extraData: "", errorArgs: [ ]
    };

    assert(dataLength(data) >= 5 * 32, "insufficient OffchainLookup data", "OFFCHAIN_FAULT", {
        reason: "insufficient OffchainLookup data"
    });

    const sender = dataSlice(data, 0, 32);
    assert(dataSlice(sender, 0, 12) === dataSlice(zeros, 0, 12), "corrupt OffchainLookup sender", "OFFCHAIN_FAULT", {
        reason: "corrupt OffchainLookup sender"
    });
    result.sender = dataSlice(sender, 12);

    // Read the URLs from the response
    try {
        const urls: Array<string> = [];
        const urlsOffset = getNumber(dataSlice(data, 32, 64));
        const urlsLength = getNumber(dataSlice(data, urlsOffset, urlsOffset + 32));
        const urlsData = dataSlice(data, urlsOffset + 32);
        for (let u = 0; u < urlsLength; u++) {
            const url = _parseString(urlsData, u * 32);
            if (url == null) { throw new Error("abort"); }
            urls.push(url);
        }
        result.urls = urls;
    } catch (error) {
        assert(false, "corrupt OffchainLookup urls", "OFFCHAIN_FAULT", {
            reason: "corrupt OffchainLookup urls"
        });
    }

    // Get the CCIP calldata to forward
    try {
        const calldata = _parseBytes(data, 64);
        if (calldata == null) { throw new Error("abort"); }
        result.calldata = calldata;
    } catch (error) {
        assert(false, "corrupt OffchainLookup calldata", "OFFCHAIN_FAULT", {
            reason: "corrupt OffchainLookup calldata"
        });
    }

    // Get the callbackSelector (bytes4)
    assert(dataSlice(data, 100, 128) === dataSlice(zeros, 0, 28), "corrupt OffchainLookup callbaackSelector", "OFFCHAIN_FAULT", {
        reason: "corrupt OffchainLookup callbaackSelector"
    });
    result.selector = dataSlice(data, 96, 100);

    // Get the extra data to send back to the contract as context
    try {
        const extraData = _parseBytes(data, 128);
        if (extraData == null) { throw new Error("abort"); }
        result.extraData = extraData;
    } catch (error) {
        assert(false, "corrupt OffchainLookup extraData", "OFFCHAIN_FAULT", {
            reason: "corrupt OffchainLookup extraData"
        });
    }

    result.errorArgs = "sender,urls,calldata,selector,extraData".split(/,/).map((k) => (<any>result)[k])

    return result;
}
