ethers-io / ethers.js

Complete Ethereum library and wallet implementation in JavaScript.
https://ethers.org/
MIT License
7.95k stars 1.84k forks source link

decoded event/calldata es6 proxies dissapear / hard to use #4523

Open peersky opened 10 months ago

peersky commented 10 months ago

Ethers Version

5.7.2

Search Terms

ES6 proxy,event,calldata,parsed data dissapears

Describe the Problem

Whenever you call or parse event from chain with ethers, if returned is object, and ethers had ABI provided - it will return object of type array with object keys per ABI description added as ES6 Proxies. This allows us to do something like myContract['myStateMethod'].stateVariable instead of myContract['myStateMethod'][0]

However this becomes a hell if you want to use ethersjs in say frontend code (And I do have reasons to choose it over wagmi).

In such case caching libraries such as react-query will simply lose all of these proxies during refetch.

Converting ES6 proxies in to objects is a hell of a job and cannot be addressed properly outside of the ethersjs itself.

Proposed solution: instead of proxies return proper objects.

Code Snippet

No response

Contract ABI

No response

Errors

No response

Environment

Browser (Chrome, Safari, etc), React Native/Expo/JavaScriptCore

Environment (Other)

No response

ricmoo commented 10 months ago

There aren’t any Proxy objects in v5. It is designed for ES3, for which Proxy isn’t present.

In v6 though, contract uses Procy, which is required to be able to trap arbitrary names and signatures for lookup (since it may need to further refine on input parameters).

There should be a way to wrap a proxy; I believe Vue uses something called markRaw which will protect it.

But in v5, there definitely isn’t any semblance of Proxy…

peersky commented 10 months ago

Hmm maybe Im missing something but In any way I had to write code that converts arrays with non-number keys in to objects to counter issue I was facing, because react-query refetch would wipe out all proxies. Im using tanstack react query with React, so no Vue methods availible.

ricmoo commented 10 months ago

Hmmm.. In v5, they aren't a Proxy, but they are an Array with properties set. So, something like:

a = [ 1, 2, 3 ];
a.first = 1;
a.second = 2;
a.third = 3;

// In the event of a deferred error, like a bad string
a = [ "a", "b"]
Object.defineProperty(a, 2, { get: () => { throw new Error("invalid string"); } } );
a.first = "a";
b.second = "b";
Object.defineProperty(a, "third", { get: () => { throw new Error("invalid string"); } } );

Maybe some aspect of that was confusing React?

This is also the reason Result objects moved to Proxy in v6. Is there a standard way for React components to make themselves serializable, btw?

jxom commented 10 months ago

Wagmi had encountered this issue in the past when we integrated with Ethers. It is because arrays with assigned named properties (or vice-versa) aren’t serializable in JS itself (it serializes to a “plain” array and omits the named properties). To overcome this issue, we also had to store a reference to the ABI signature and parse + decode the serialized plain array: https://github.com/wevm/wagmi/blob/fc10ebe659dd5f3b7a8e00581f094652280a779b/packages/core/src/utils/parseContractResult.ts

Usage with React Query here: https://github.com/wevm/wagmi/blob/fc10ebe659dd5f3b7a8e00581f094652280a779b/packages/react/src/hooks/contracts/useContractRead.ts#L161

peersky commented 9 months ago

I solved this in my application level by wrapping any ethers js decoding in to recursive function that returns objects instead of mixed array/object. Im also doing hard copies from all values to be double safe there are no proxy objects left.

It might be not the most elegant solution, I haven't solved typescript typings completely, and technically breaks the rpc specifications (must return arrays), but it works for me and I can use it further in my code with great comfort:

export const deproxify = <T extends any>(object: T) => {
    if (typeof object == "string") return object;
    if (
        !!object &&
        Object.prototype.hasOwnProperty.call(object, "_isBigNumber")
    ) {
        return ethers.BigNumber.from((" " + object.toString()).slice(1)) as T;
    }
    if (!object) return object;
    let isMixedArrayObject =
        Array.isArray(object) &&
        Object.keys(object).some((key) => isNaN(Number(key)));
    let result = Array.isArray(object) && !isMixedArrayObject ? [] : {};
    Object.keys(object).forEach((key) => {
        if (isMixedArrayObject && !isNaN(Number(key))) {
            // do nothing -> remove array elements and create object
        }
        // else - make sure we do hard copy of values
        else if (typeof object[key] === "string")
            result[key] = (" " + object[key]).slice(1);
        else if (typeof object[key] === "number")
            result[key] = Number(object[key]);
        else if (typeof object[key] === "boolean")
            result[key] = Boolean(object[key]);
        else if (object[key] === null) result[key] = null;
        else if (object[key] === undefined) result[key] = undefined;
        else if (object[key]?._isBigNumber) {
            result[key] = ethers.BigNumber.from(
                (" " + object[key].toString()).slice(1)
            );
        } else result[key] = deproxify(object[key]);
    });
    return result as T;
};