Skip to content
This repository has been archived by the owner on Feb 13, 2024. It is now read-only.

Commit

Permalink
Adding an option to use different rpc http and ws addresses for one c…
Browse files Browse the repository at this point in the history
…luster url
  • Loading branch information
ochaloup committed Jan 21, 2022
1 parent 1967a63 commit 4c0ce60
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 60 deletions.
8 changes: 8 additions & 0 deletions docs/CommonParameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ For example, to switch to the public Solana endpoint you would specify:
---cluster-url https://api.mainnet-beta.solana.com
```

There is an optional possibility to provide two parameters to the `--cluster-url` argument. Then the first parameter defines HTTP url of the RPC node
and the second one defines WS url of the RPC node.

For example, if you want to place order via one RPC node while loading market data via websocket connection from a different node
```
--cluster-url https://localhost:80 wss://localhost:443
```

There are several different RPC node providers now who provide free or premium RPC node connectivity. Use the URL they send you here.

**Note** This parameter is unusual in that it can be specified multiple times. This allows you to provide multiple RPC nodes and have the program switch to the next node if a node displays problems. For example, you can specify `--cluster-url` 3 times (with 3 different RPC URLs) and if one node starts rate-limiting you, the program will automatically switch to using the next node you specified. You'll only see exceptions if all 3 nodes have problems at the same time.
Expand Down
1 change: 1 addition & 0 deletions mango/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .cache import PriceCache as PriceCache
from .cache import RootBankCache as RootBankCache
from .client import BetterClient as BetterClient
from .client import ClusterUrlData as ClusterUrlData
from .client import BlockhashNotFoundException as BlockhashNotFoundException
from .client import ClientException as ClientException
from .client import CompoundException as CompoundException
Expand Down
112 changes: 71 additions & 41 deletions mango/client.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions mango/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
from solana.publickey import PublicKey
from solana.rpc.commitment import Commitment

from .client import BetterClient
from .client import BetterClient, ClusterUrlData
from .constants import MangoConstants
from .instructionreporter import InstructionReporter, CompoundInstructionReporter
from .instrumentlookup import InstrumentLookup
Expand All @@ -37,7 +37,7 @@
# A `Context` object to manage Solana connection and Mango configuration.
#
class Context:
def __init__(self, name: str, cluster_name: str, cluster_urls: typing.Sequence[str], skip_preflight: bool,
def __init__(self, name: str, cluster_name: str, cluster_urls: typing.Sequence[ClusterUrlData], skip_preflight: bool,
commitment: str, encoding: str, blockhash_cache_duration: int,
stale_data_pauses_before_retry: typing.Sequence[float], mango_program_address: PublicKey,
serum_program_address: PublicKey, group_name: str, group_address: PublicKey,
Expand Down
40 changes: 30 additions & 10 deletions mango/contextbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from decimal import Decimal
from solana.publickey import PublicKey

from .client import BetterClient
from .client import BetterClient, ClusterUrlData
from .constants import MangoConstants, DATA_PATH
from .context import Context
from .idsjsonmarketlookup import IdsJsonMarketLookup
Expand Down Expand Up @@ -53,6 +53,25 @@
# A `ContextBuilder` class to allow building `Context` objects without introducing circular dependencies.
#
class ContextBuilder:

# Utility class for parsing cluster-url command line parameter
#
class ParseClusterUrls(argparse.Action):
cluster_urls: typing.List[ClusterUrlData] = []

def __call__(self, parser: argparse.ArgumentParser, namespace: object, values: typing.Any, option_string: typing.Optional[str] = None) -> None:
if values:
if len(values) == 1:
self.cluster_urls.append(ClusterUrlData(rpc=values[0]))
elif len(values) == 2:
self.cluster_urls.append(ClusterUrlData(rpc=values[0], ws=values[1]))
else:
raise parser.error(
'Argument --cluster-url permits maximal two parameters. The first one configures HTTP connection url, the second one '
'configures the WS connection url. Example: --cluster-url https://localhost:8181 wss://localhost:8282'
)
setattr(namespace, self.dest, self.cluster_urls)

# Configuring a `Context` is a common operation for command-line programs and can involve a
# lot of duplicate code.
#
Expand All @@ -63,8 +82,8 @@ def add_command_line_parameters(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--name", type=str, default="Mango Explorer",
help="Name of the program (used in reports and alerts)")
parser.add_argument("--cluster-name", type=str, default=None, help="Solana RPC cluster name")
parser.add_argument("--cluster-url", type=str, action="append", default=[],
help="Solana RPC cluster URL (can be specified multiple times to provide failover when one errors)")
parser.add_argument("--cluster-url", nargs='*', type=str, action=ContextBuilder.ParseClusterUrls, default=[],
help="Solana RPC cluster URL (can be specified multiple times to provide failover when one errors; optional second parameter value defines websocket connection)")
parser.add_argument("--group-name", type=str, default=None, help="Mango group name")
parser.add_argument("--group-address", type=PublicKey, default=None, help="Mango group address")
parser.add_argument("--mango-program-address", type=PublicKey, default=None, help="Mango program address")
Expand Down Expand Up @@ -98,7 +117,7 @@ def add_command_line_parameters(parser: argparse.ArgumentParser) -> None:
def from_command_line_parameters(args: argparse.Namespace) -> Context:
name: typing.Optional[str] = args.name
cluster_name: typing.Optional[str] = args.cluster_name
cluster_urls: typing.Optional[typing.Sequence[str]] = args.cluster_url
cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = args.cluster_url
group_name: typing.Optional[str] = args.group_name
group_address: typing.Optional[PublicKey] = args.group_address
mango_program_address: typing.Optional[PublicKey] = args.mango_program_address
Expand Down Expand Up @@ -151,7 +170,7 @@ def from_group_name(context: Context, group_name: str) -> Context:
@staticmethod
def forced_to_devnet(context: Context) -> Context:
cluster_name: str = "devnet"
cluster_url: str = MangoConstants["cluster_urls"][cluster_name]
cluster_url: ClusterUrlData = ClusterUrlData(rpc=MangoConstants["cluster_urls"][cluster_name])
fresh_context = copy.copy(context)
fresh_context.client = BetterClient.from_configuration(context.name,
cluster_name,
Expand All @@ -168,7 +187,7 @@ def forced_to_devnet(context: Context) -> Context:
@staticmethod
def forced_to_mainnet_beta(context: Context) -> Context:
cluster_name: str = "mainnet"
cluster_url: str = MangoConstants["cluster_urls"][cluster_name]
cluster_url: ClusterUrlData = ClusterUrlData(rpc=MangoConstants["cluster_urls"][cluster_name])
fresh_context = copy.copy(context)
fresh_context.client = BetterClient.from_configuration(context.name,
cluster_name,
Expand All @@ -184,7 +203,8 @@ def forced_to_mainnet_beta(context: Context) -> Context:

@staticmethod
def build(name: typing.Optional[str] = None, cluster_name: typing.Optional[str] = None,
cluster_urls: typing.Optional[typing.Sequence[str]] = None, skip_preflight: bool = False,
cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = None,
skip_preflight: bool = False,
commitment: typing.Optional[str] = None, encoding: typing.Optional[str] = None,
blockhash_cache_duration: typing.Optional[int] = None,
stale_data_pauses_before_retry: typing.Optional[typing.Sequence[float]] = None,
Expand Down Expand Up @@ -213,13 +233,13 @@ def __public_key_or_none(address: typing.Optional[str]) -> typing.Optional[Publi
actual_blockhash_cache_duration: int = blockhash_cache_duration or 0
actual_stale_data_pauses_before_retry: typing.Sequence[float] = stale_data_pauses_before_retry or []

actual_cluster_urls: typing.Optional[typing.Sequence[str]] = cluster_urls
actual_cluster_urls: typing.Optional[typing.Sequence[ClusterUrlData]] = cluster_urls
if actual_cluster_urls is None or len(actual_cluster_urls) == 0:
cluster_url_from_environment: typing.Optional[str] = os.environ.get("CLUSTER_URL")
if cluster_url_from_environment is not None and cluster_url_from_environment != "":
actual_cluster_urls = [cluster_url_from_environment]
actual_cluster_urls = [ClusterUrlData(rpc=cluster_url_from_environment)]
else:
actual_cluster_urls = [MangoConstants["cluster_urls"][actual_cluster]]
actual_cluster_urls = [ClusterUrlData(rpc=MangoConstants["cluster_urls"][actual_cluster])]

actual_skip_preflight: bool = skip_preflight
actual_group_name: str = group_name or os.environ.get("GROUP_NAME") or default_group_data["name"]
Expand Down
4 changes: 2 additions & 2 deletions mango/websocketsubscription.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def build_request(self) -> str:
raise NotImplementedError("WebSocketSubscription.build_request() is not implemented on the base type.")

def open(self,) -> None:
websocket_url: str = self.context.client.cluster_url.replace("https", "wss", 1)
websocket_url: str = self.context.client.cluster_ws_url

def on_open(sock: websocket.WebSocketApp) -> None:
sock.send(self.build_request())
Expand Down Expand Up @@ -256,7 +256,7 @@ def __init__(self, context: Context, ping_interval: int = 10) -> None:
self._pong_subscription: typing.Optional[Disposable] = None

def open(self) -> None:
websocket_url = self.context.client.cluster_url.replace("https", "wss", 1)
websocket_url = self.context.client.cluster_ws_url
ws: ReconnectingWebsocket = ReconnectingWebsocket(websocket_url, self.open_handler)
ws.item.subscribe(on_next=self.on_item) # type: ignore[call-arg]
ws.ping_interval = self.ping_interval
Expand Down
7 changes: 5 additions & 2 deletions tests/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def get_minimum_balance_for_rent_exemption(size, *args: typing.Any, **kwargs: ty

class MockClient(mango.BetterClient):
def __init__(self) -> None:
rpc = mango.RPCCaller("fake", "http://localhost", [], mango.SlotHolder(), mango.InstructionReporter())
rpc = mango.RPCCaller("fake", "http://localhost", "ws://localhost", [], mango.SlotHolder(), mango.InstructionReporter())
compound = mango.CompoundRPCCaller("fake", [rpc])
super().__init__(MockCompatibleClient(), "test", "local", Commitment("processed"),
False, "base64", 0, compound)
Expand All @@ -46,7 +46,10 @@ def fake_seeded_public_key(seed: str) -> PublicKey:
def fake_context() -> mango.Context:
context = mango.Context(name="Mango Test",
cluster_name="test",
cluster_urls=["http://localhost", "http://localhost"],
cluster_urls=[
mango.ClusterUrlData(rpc="http://localhost"),
mango.ClusterUrlData(rpc="http://localhost")
],
skip_preflight=False,
commitment="processed",
encoding="base64",
Expand Down
4 changes: 2 additions & 2 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

class FakeRPCCaller(mango.RPCCaller):
def __init__(self) -> None:
super().__init__("Fake", "https://localhost", [0.1, 0.2], mango.SlotHolder(), mango.InstructionReporter())
super().__init__("Fake", "https://localhost", "wss://localhost", [0.1, 0.2], mango.SlotHolder(), mango.InstructionReporter())
self.called = False

def make_request(self, method: RPCMethod, *params: typing.Any) -> RPCResponse:
Expand All @@ -25,7 +25,7 @@ def make_request(self, method: RPCMethod, *params: typing.Any) -> RPCResponse:

class RaisingRPCCaller(mango.RPCCaller):
def __init__(self) -> None:
super().__init__("Fake", "https://localhost", [0.1, 0.2], mango.SlotHolder(), mango.InstructionReporter())
super().__init__("Fake", "https://localhost", "wss://localhost", [0.1, 0.2], mango.SlotHolder(), mango.InstructionReporter())
self.called = False

def make_request(self, method: RPCMethod, *params: typing.Any) -> RPCResponse:
Expand Down
3 changes: 2 additions & 1 deletion tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def test_new_from_cluster() -> None:
context_has_default_values(mango.ContextBuilder.default())
derived = mango.ContextBuilder.build(cluster_name="devnet")
assert derived.client.cluster_name == "devnet"
assert derived.client.cluster_url == "https://mango.devnet.rpcpool.com"
assert derived.client.cluster_rpc_url == "https://mango.devnet.rpcpool.com"
assert derived.client.cluster_ws_url == "wss://mango.devnet.rpcpool.com"
assert derived.mango_program_address == PublicKey("4skJ85cdxQAFVKbcGgfun8iZPL7BadVYXG3kGEGkufqA")
assert derived.serum_program_address == PublicKey("DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY")
assert derived.group_name == "devnet.2"
Expand Down

0 comments on commit 4c0ce60

Please sign in to comment.