ApeWorX / ape

The smart contract development tool for Pythonistas, Data Scientists, and Security Professionals
https://apeworx.io
Apache License 2.0
892 stars 131 forks source link

ProviderNotConnectedError trying to connect to multiple networks in Python [APE-616] #1296

Closed PoCk3T-SPAI closed 9 months ago

PoCk3T-SPAI commented 1 year ago

Environment information

$ ape --version
0.6.2

$ ape plugins list
Installed Plugins:
  tokens       0.6.0
  alchemy      0.6.0
  trezor       0.6.0
  ledger       0.6.0
  ens          0.6.0
  infura       0.6.0
  hardhat      0.6.0
  etherscan    0.6.0
  foundry      0.6.0
  vyper        0.6.1
  avalanche    0.1.0a3
  solidity     0.6.0
  bsc          0.6.0
  polygon      0.6.0
  template     0.6.0
$ cat ape-config.yaml
dependencies:
  - name: OpenZeppelin
    github: OpenZeppelin/openzeppelin-contracts
    version: 4.8.1    

  - name: Chainlink
    github: smartcontractkit/chainlink
    branch: master
    contracts_folder: contracts/src/v0.8

solidity:
  import_remapping:
    - "@openzeppelin=OpenZeppelin/4.8.1"
    - "@chainlink=Chainlink/master"

What went wrong?

Please include information like:

NETWORKS = [ "polygon:mumbai:infura", "ethereum:goerli:infura" ]

app = FastAPI() account = accounts.load(os.environ["WALLET_NAME"]) account.set_autosign(True, passphrase=os.environ["WALLET_PASSPHRASE"])

@app.get(path="/networks/list") async def list_networks(): connected_networks = [] for network in NETWORKS: with networks.parse_network_choice(network) as provider: provider.connect() ecosystem_name = provider.network.ecosystem.name network_name = provider.network.name provider_name = provider.name connected_networks.append(network)

click.echo(f"You are connected to network '{ecosystem_name}:{network_name}:{provider_name}'.")

return connected_networks

def main():
uvicorn.run(app, host="0.0.0.0", port=os.environ.get("PORT", 8080))

- full output of the error you received

ERROR: Exception in ASGI application Traceback (most recent call last): File "/home/harambe/.local/lib/python3.9/site-packages/uvicorn/protocols/http/h11_impl.py", line 407, in run_asgi result = await app( # type: ignore[func-returns-value] File "/home/harambe/.local/lib/python3.9/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in call return await self.app(scope, receive, send) File "/home/harambe/.local/lib/python3.9/site-packages/fastapi/applications.py", line 271, in call await super().call(scope, receive, send) File "/home/harambe/.local/lib/python3.9/site-packages/starlette/applications.py", line 118, in call await self.middleware_stack(scope, receive, send) File "/home/harambe/.local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 184, in call raise exc File "/home/harambe/.local/lib/python3.9/site-packages/starlette/middleware/errors.py", line 162, in call await self.app(scope, receive, _send) File "/home/harambe/.local/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 79, in call raise exc File "/home/harambe/.local/lib/python3.9/site-packages/starlette/middleware/exceptions.py", line 68, in call await self.app(scope, receive, sender) File "/home/harambe/.local/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in call raise e File "/home/harambe/.local/lib/python3.9/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in call await self.app(scope, receive, send) File "/home/harambe/.local/lib/python3.9/site-packages/starlette/routing.py", line 706, in call await route.handle(scope, receive, send) File "/home/harambe/.local/lib/python3.9/site-packages/starlette/routing.py", line 276, in handle await self.app(scope, receive, send) File "/home/harambe/.local/lib/python3.9/site-packages/starlette/routing.py", line 66, in app response = await func(request) File "/home/harambe/.local/lib/python3.9/site-packages/fastapi/routing.py", line 237, in app raw_response = await run_endpoint_function( File "/home/harambe/.local/lib/python3.9/site-packages/fastapi/routing.py", line 163, in run_endpoint_function return await dependant.call(**values) File "/home/harambe/project/scripts/deploy.py", line 22, in list_networks with networks.parse_network_choice(network) as provider: File "/home/harambe/.local/lib/python3.9/site-packages/ape/api/networks.py", line 541, in enter return self.push_provider() File "/home/harambe/.local/lib/python3.9/site-packages/ape/api/networks.py", line 553, in push_provider raise ProviderNotConnectedError() ape.exceptions.ProviderNotConnectedError: Not connected to a network provider.



### How can it be fixed?

No idea, but I've researched, seen https://github.com/ApeWorX/ape/issues/1055, and I'm not sure how it was resolved since some discussions might have happened on Discord.

Also, I do not want to run ape on a specific network, I'm trying to automate the deployment of smart contracts on multiple networks when receiving an HTTP API request to do so.

Is what I'm trying to do even possible?
Let me know if you have any thoughts :)
PoCk3T-SPAI commented 1 year ago

Closing and answering the solution to myself:

fubuloubu commented 1 year ago

I'm reopening this one because it seems like our documentation needs Improvement for how to use multi-chain environments inside scripts especially creating FastAPI apps

PoCk3T-SPAI commented 1 year ago

@fubuloubu thank you for all your efforts and contribution on ApeWorX/ape issues, not everyone post or comment but I'm sure a lot of people like me appreciate your feedbacks, it helps building trust with the devs and the community a lot; I'm very grateful for it.

Here's a working version of FastAPI + multi-network connections established when receiving an HTTP REST API call:

# ApeX/ape dependencies
from ape import accounts, project, networks
import click

# Web server dependencies
import uvicorn
from fastapi import FastAPI
import os
app = FastAPI()

TESTNETS_IN_APE = [
    networks.ethereum.goerli.use_provider("infura"),
    networks.bsc.testnet.use_provider("geth"),
    networks.avalanche.fuji.use_provider("geth"),
    networks.polygon.mumbai.use_provider("geth"),
    networks.fantom.testnet.use_provider("geth"),
    networks.optimism.goerli.use_provider("alchemy"),
    networks.arbitrum.goerli.use_provider("alchemy"),    
]

def main():
    uvicorn.run(app, host="0.0.0.0", port=os.environ.get("PORT", 8080))

@app.get(path="/networks/list/testnets")
async def list_testnet_networks():
    initial_list = []
    for _provider in TESTNETS_IN_APE:
        try:
            with _provider as provider:            
                initial_list.append({"ecosystem_name": provider.network.ecosystem.name, "network_name": provider.network.name, "provider_name": provider.name})    
                click.echo(f"Connected successfully to network '{provider.network.ecosystem.name}:{provider.network.name}:{provider.name}'.")
        except Exception as e:
            click.echo(f"Failed to connect: {e}")
    return initial_list

Feel free to use this snippet in any form or shape for your future documentation effort.

One small thing that also took time for to connect the dots on: Alchemy and Infura don't support all networks, but 'GETH' is not a bad word, it's actually just a way to connect directly to the public RPC endpoints these L1 and L2 projects maintain, or that some other Web3 infra. provider maintain. Here's my ape-config.yaml config for reference:

geth:
  bsc:
    testnet:
      uri: https://bsc-testnet.public.blastapi.io 
    mainnet:
      uri: https://bsc-mainnet.public.blastapi.io 
  avalanche:
    fuji:
      uri: https://api.avax-test.network/ext/bc/C/rpc # https://ava-testnet.public.blastapi.io/ext/bc/C/rpc
    mainnet:
      uri: https://api.avax.network/ext/bc/C/rpc # https://ava-mainnet.public.blastapi.io/ext/bc/C/rpc
  fantom:
    testnet:
      uri: https://fantom-testnet.public.blastapi.io
    opera:
      uri: https://fantom-mainnet.public.blastapi.io
  polygon:
    mumbai:
      uri: https://polygon-testnet.public.blastapi.io
    mainnet:
      uri: https://polygon-mainnet.public.blastapi.io

in case it also helps with the documentation efforts..

fubuloubu commented 1 year ago

Thanks for the feedback! Yeah, we are looking much more deeply into FastAPI-based applications for a product idea we have, so this will help us debug some issues with this type of setup and help support you in this use case better.

PoCk3T-SPAI commented 1 year ago

Nice! Well, then, the only thing left standing between the current design of ape and its ability to be turned into a REST API micro-service for Web3 tasks = its limitation importing accounts (private key or mnemonic) through CLI only, whereas the preferred way would be through Python (so that the private key can be passed through a secret manager, like that)

fubuloubu commented 1 year ago

Nice! Well, then, the only thing left standing between the current design of ape and its ability to be turned into a REST API micro-service for Web3 tasks = its limitation importing accounts (private key or mnemonic) through CLI only, whereas the preferred way would be through Python (so that the private key can be passed through a secret manager, like that)

You can very easily create a plugin for a custom account type, that can import secrets from various managed key systems (Vault, AWS, GCloud, Fireblocks, etc.). This is definitely a lot safer than using plaintext private keys (even accessed through a secret manager), so I would suggest doing that for your preferred platform of choice.

It's very easy to create an account plugin, just implemented a few methods from this class. Here is an example of an account plugin: https://github.com/ApeWorX/ape-trezor

sabotagebeats commented 1 year ago
$ ape --version
0.6.2

I was able to get this to work with the similar code to the OP: $cat ape-config.yaml:

dependencies:
  - name: OpenZeppelin
    github: OpenZeppelin/openzeppelin-contracts
    version: 4.8.1    

solidity:
  import_remapping:
    - openzeppelin=OpenZeppelin/4.8.1

default_ecosystem: polygon

polygon:
  default_network: mumbai 
  mumbai:
    default_provider: alchemy

$cat scripts/deploy.py:

$ cat scripts/deploy.py 
import uvicorn  # type: ignore  
from fastapi import FastAPI     
import os
from ape import accounts, project, networks 
from ape.cli import NetworkBoundCommand, network_option

NETWORKS = [
    "polygon:mumbai:alchemy",
    "ethereum:goerli:alchemy"
]

app = FastAPI()
account = accounts.load(os.environ["WALLET_NAME"])
account.set_autosign(True, passphrase=os.environ["WALLET_PASSPHRASE"])

@app.get(path="/networks/list")
async def list_networks():
    connected_networks = []
    for network in NETWORKS:
        with networks.parse_network_choice(network) as provider:
            provider.connect()
            ecosystem_name = provider.network.ecosystem.name
            network_name = provider.network.name
            provider_name = provider.name
            connected_networks.append(network)
            print(f"You are connected to network '{ecosystem_name}:{network_name}:{provider_name}'.")
    return connected_networks

def main():        
    uvicorn.run(app, host="0.0.0.0", port=os.environ.get("PORT", 8080))
$ curl http://127.0.0.1:8080/networks/list
["polygon:mumbai:alchemy","ethereum:goerli:alchemy"]
$ ape run deploy
WARNING: Danger! This account will now sign any transaction it's given.
INFO:     Started server process [12403]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)
You are connected to network 'polygon:mumbai:alchemy'.
You are connected to network 'ethereum:goerli:alchemy'

This also works with the default network provider in ape-config.yaml commented out.

However this doesn't work if the default network provider is different from the network provider you are using in scripts/deploy.py:

ape/config.yaml:

...
default_ecosystem: polygon

polygon:
  default_network: mumbai 
  mumbai:
    default_provider: infura

scripts/deploy.py:

...
NETWORKS = [
    "polygon:mumbai:alchemy",
    "ethereum:goerli:alchemy"
]
...
    for network in NETWORKS:
        with networks.parse_network_choice(network) as provider:

results in

$ ape run deploy
ERROR: (ProviderNotConnectedError) Not connected to a network provider.
fubuloubu commented 1 year ago

@sabotagebeats follow up on this: "it seems like networks.parse_network_choice just doesn't work how it's supposed to" and I think we can close this with the doc update you mentioned here: "However this doesn't work if the default network provider is different from the network provider you are using in scripts/deploy.py"

antazoey commented 9 months ago

Updates!

  1. tip: If you use the CLI approach for scripting, you can opt out of the initial network connection! The main-method approach for scripts automatically connects for legacy reasons but the cli approach requires you to set it up that way. more info!.

the main method approach will connect to a network before the main method starts executing, similar to brownie.

  1. That being said, it should have still worked even with an initial connection. I installed Ape 0.6.2 and all the same plugins but for some reason I am not able to reproduce. 🤷🏻‍♀️ I don't have a working Docker setup right now, but I tried what I could. However, I ran into a bunch of other weird problems on this version with networks, so there definitely were bugs. Even the error messages are much better now in the 0.7.0 range. I am pretty sure the bugs have been fixed but don't know for sure because I couldn't reproduce your exact problem.

For example, I know there was a bug when calling .connect() yourself when you didnt need to on some providers, I wouldnt be surprised if that was largely related.

  1. I updated the docs, inspired from your code, providing a multi-chain example and am wondering if we can have that close this issue. PR here: https://github.com/ApeWorX/ape/pull/1876

Thank you! And sorry for being late on this one.