Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[wip] feat: multicall context manager #1033

Closed
wants to merge 1 commit into from

Conversation

omarish
Copy link
Contributor

@omarish omarish commented Apr 4, 2021

What I did

This is a rough first draft at a multicall context manager. It doesn't work yet but sharing it here to get feedback before I go deeper into implementation.

Related issue: #1011

How I did it

How to verify it

What Remains

  • Thread safety
  • Making it so that using the MulticallContextManager (multicall) enables the middleware

Checklist

  • I have confirmed that my PR passes all linting checks
  • I have included test cases
  • I have updated the documentation
  • I have added an entry to the changelog

@banteg
Copy link
Collaborator

banteg commented Apr 5, 2021

I've played with it quite a bit too, a few thoughts:

Multicall as middleware won't work, it requires patching web3 internals too deeply, and possibly replacing web3 provider.

A better way might be yielding a caller decorator from the context manager which intercepts function calls, generates the calldata using encode_input and stores a decode_output reference. This could potentially also work with automatic flushing of the queue as was originally envisioned since you have the idea of the argument types before yielding futures.

What I haven't solved so far is keeping the API the same. I don't like that it adds this .result() requirement.

This is what I got so far:

from concurrent.futures import Future
from contextlib import contextmanager
from functools import partial


class Caller:
    def __init__(self, queue, contract=None, func=None):
        self.queue = queue
        self.contract = contract
        self.func = func

    def __getattr__(self, name):
        return Caller(self.queue, self.contract, getattr(self.contract, name))

    def __call__(self, *args, **kwds):
        future = Future()
        future.call = [str(self.contract), self.func.encode_input(*args, **kwds)]
        future.decode_output = self.func.decode_output
        self.queue.append(future)
        return future


@contextmanager
def multicall_context():
    """
    with multicall_context() as caller:
        response = caller(contract).func(args)

    response.result()
    """
    queue = []
    yield partial(Caller, queue)  # caller(contract).func(args).result()

    block, results = multicall.aggregate.call([request.call for request in queue])

    for request, result in zip(queue, results):
        request.set_result(request.decode_output(result))

A full example:

def main():
    vault = Contract("0x5dbcF33D8c2E976c6b560249878e6F1491Bca25c")

    with multicall_context() as caller:
        assets = caller(vault).balance()  # can't call result before exiting
        pps = caller(vault).getPricePerFullShare()

    print(assets.result().to('ether') * pps.result().to('ether'))

@spinoch
Copy link

spinoch commented Apr 6, 2021

What I haven't solved so far is keeping the API the same. I don't like that it adds this .result() requirement.

Could use ContextManager.__exit__ to take care of that (?)

@banteg banteg mentioned this pull request May 5, 2021
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants