Binding to Meta Classes: Types whose properties are defined at runtime

I’ve been chipping away at working on bindings for the popular Web3 library Ethers.js

I’ve run into a roadblock that has to do with a class setting its properties at runtime.

Docs: Contract

The Contract return type looks like this

Contract {
...

  functions: {
    'function1()': [Function (anonymous)],
    'function2()': [Function (anonymous)],
  },
  address: '0xB5d592f85ab2D955c25720EbE6FF8D4d1E1Be300',
  resolvedAddress: Promise { <pending> },
  function1(): [Function (anonymous)],
  function2(): [Function (anonymous)],

...
}

The Contract object is a meta-class which defines the contracts functions at runtime and uses the ContractFunction type for the functions

Any tips on modeling this scenario properly in rescript?

Hi @YKW would you be able to provide an example in JS how this is created and used?

Edit: I’m assuming function1() and function2() were dynamically added at runtime, is that right?

@kevanstannard
Here’s a simplified example calling the name and symbol methods on a token


const ethers = require("ethers")
const abi = [
  {
    "constant": true,
    "inputs": [],
    "name": "name",
    "outputs": [
      {
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
  {
    "constant": true,
    "inputs": [],
    "name": "symbol",
    "outputs": [
      {
        "name": "",
        "type": "string"
      }
    ],
    "payable": false,
    "stateMutability": "view",
    "type": "function"
  },
]

const provider = ethers.getDefaultProvider("https://rpc.gnosischain.com/")

const getContract = (contractAddress) => {
  const contract = new ethers.Contract(contractAddress, abi, provider));
  return contract;
};

const contract = await getContract("0xB5d592f85ab2D955c25720EbE6FF8D4d1E1Be30")
const name = await contract.name().call();
const symbol = await contract.symbol().call();

Are the input/output types always string? primitive? Jsonable?

@mouton

Here is the BaseContract in Typescript and its accompanying Contract that fills in the functions at runtime

Output of Contract is a JavaScript object containing the functions described in the abi parameter. I’m not quite sure what you’re asking for. Tell me if this helps.

Edit: I see now you are talking about the abi. Here is the abi specification docs Contract ABI Specification — Solidity 0.8.11 documentation

Hi @YKW the dynamic functions are a good challenge.

I don’t know a good solution, but I’ll offer some thoughts that might help.

Declare an ABI module with the input and output definitions. I’ve left as open objects for now, but would be good if they could be converted to records.

module ABI = {
  type input = {"name": string, "type": string}
  type output = {"name": string, "type": string}
  type def = {
    "constant": bool,
    "inputs": array<input>,
    "name": string,
    "outputs": array<output>,
    "payable": bool,
    "stateMutability": string,
    "type": string,
  }
}

Declare Provider and Contract modules:

module Provider = {
  type t
  @module("ethers") external getDefaultProvider: string => t = "getDefaultProvider"
}

module Contract = {
  type t
  @module("ethers") @new
  external makeContract: (string, array<ABI.def>, Provider.t) => Promise.t<t> = "Contract"
}

Consumers of your bindings would declare a custom ABI module, here’s an example:


module MyABI = {
  let config: array<ABI.def> = [
    {
      "constant": true,
      "inputs": [],
      "name": "name",
      "outputs": [
        {
          "name": "",
          "type": "string",
        },
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function",
    },
    {
      "constant": true,
      "inputs": [],
      "name": "symbol",
      "outputs": [
        {
          "name": "",
          "type": "string",
        },
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function",
    },
  ]

  let name: Contract.t => Promise.t<string> = %raw(`
    (contract) => contract.name().call()
  `)

  let symbol: Contract.t => Promise.t<string> = %raw(`
    (contract) => contract.symbol().call()
  `)
}

Edit: For function with args, something like:

let other: (Contract.t, string, int) => Promise.t<string> = %raw(`
  (contract, a, b) => contract.other().call(undefined, a, b)
`)

This provides the ABI configuration and the corresponding functions for that configuration.

I’m seeing this as another kind of binding - writing bindings for the configuration.

The functions themselves are %raw() mappings. Not sure if there is an easier way considering variable input and output types.

Using all of this would be something like this:

let provider = Provider.getDefaultProvider("https://rpc.gnosischain.com/")

let getContract = (contractAddress, config) => {
  Contract.makeContract(contractAddress, config, provider)
}

getContract("0xB5d592f85ab2D955c25720EbE6FF8D4d1E1Be30", MyABI.config)
->Promise.then(contract => {
  let name = contract->MyABI.name
  let symbol = contract->MyABI.symbol

  Promise.all2((name, symbol))->Promise.then(((name, symbol)) => {
    Js.log2(name, symbol)
    Promise.resolve()
  })
})
->ignore

Not a perfect solution, but maybe some ideas that can help?

Edit: It looks like the TypeScript bindings don’t attempt to type the contract functions:

export type ContractFunction<T = any> = (...args: Array<any>) => Promise<T>;

As far as I’m aware, there is no equivalent in ReScript.

I like this solution, writing a custom ABI module makes a lot of sense.

As far as the raw JS, it’s not ideal. maybe I could rewrite the js to return something a little easier to work with.

Edit: Using this logic, I guess it could be possible to create custom Contract modules with the appropriate typed functions inside of them. Since the ABI is essentially static. Only problem is if the ABI changes, then the module would have to manually be changed. Would be great if we could generate function types from the ABI

1 Like

Hey @YKW - keen to hear what you are up to!

I’ve been using rescript (or reason) for basically 100% of my ethereum work for the last 2 and a half years (starting with https://github.com/wildcards-world/ui/) :slight_smile:

I’ve been bad at documenting and creating libraries etc for everything. But lets setup a call and I can give you a demo of everything that we are currently working on at https://float.capital (which is built with rescript) - for example all of our hardhat tests are written completely in rescript with a pretty cool unit testing framework (that is built around smock). Keen to see what you are up to also!

1 Like

Also, just to mention, we use a custom codegen tool that reads the contract ABI and generates the rescript interface for the contract - so it isn’t dynamic in the code. Yet to open-source it, no reason not to besides it is hard-coded into our project and time is at a premium!!

1 Like