Skip to content

Commit 8d9aa17

Browse files
committed
add support for custom HTTP headers on proxy and faucet API requests
1 parent 3c99638 commit 8d9aa17

10 files changed

Lines changed: 376 additions & 170 deletions

File tree

CLI.md

Lines changed: 267 additions & 130 deletions
Large diffs are not rendered by default.

multiversx_sdk_cli/cli.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import multiversx_sdk_cli.cli_wallet
3131
import multiversx_sdk_cli.version
3232
from multiversx_sdk_cli import config, errors, utils, ux
33-
from multiversx_sdk_cli.cli_shared import set_proxy_from_config_if_not_provided
33+
from multiversx_sdk_cli.cli_shared import parse_proxy_headers, set_proxy_from_config_if_not_provided
3434
from multiversx_sdk_cli.config_env import get_address_hrp
3535
from multiversx_sdk_cli.constants import LOG_LEVELS, SDK_PATH
3636

@@ -81,6 +81,7 @@ def _do_main(cli_args: list[str]):
8181
parser.print_help()
8282
else:
8383
set_proxy_from_config_if_not_provided(args)
84+
config.set_proxy_headers(parse_proxy_headers(getattr(args, "proxy_headers", None)))
8485
args.func(args)
8586

8687

multiversx_sdk_cli/cli_faucet.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ def setup_parser(args: list[str], subparsers: Any) -> Any:
2929
cli_shared.add_wallet_args(args, sub)
3030
sub.add_argument("--chain", choices=["D", "T"], help="the chain identifier")
3131
sub.add_argument("--api", type=str, help="custom api url for the native auth client")
32+
sub.add_argument(
33+
"--api-headers",
34+
nargs="+",
35+
metavar="KEY=VALUE",
36+
help="extra HTTP headers for API requests, e.g. 'Api-Key=mytoken'"
37+
)
3238
sub.add_argument("--wallet-url", type=str, help="custom wallet url to call the faucet from")
3339
sub.set_defaults(func=faucet)
3440

@@ -40,7 +46,8 @@ def faucet(args: Any):
4046
account = cli_shared.prepare_account(args)
4147
wallet, api = get_wallet_and_api_urls(args)
4248

43-
config = NativeAuthClientConfig(origin=wallet, api_url=api)
49+
extra_headers = cli_shared.parse_proxy_headers(getattr(args, "api_headers", None))
50+
config = NativeAuthClientConfig(origin=wallet, api_url=api, extra_request_headers=extra_headers or None)
4451
client = NativeAuthClient(config)
4552

4653
init_token = client.initialize()

multiversx_sdk_cli/cli_get.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from multiversx_sdk import ProxyNetworkProvider, Token, TokenComputer
77

88
from multiversx_sdk_cli import cli_shared
9+
from multiversx_sdk_cli.cli_shared import add_proxy_arg
910
from multiversx_sdk_cli.config import get_config_for_network_providers
1011
from multiversx_sdk_cli.config_env import MxpyEnv
1112
from multiversx_sdk_cli.errors import (
@@ -25,7 +26,7 @@ def setup_parser(subparsers: Any) -> Any:
2526
sub = cli_shared.add_command_subparser(subparsers, "get", "account", "Get info about an account.")
2627
_add_alias_arg(sub)
2728
_add_address_arg(sub)
28-
_add_proxy_arg(sub)
29+
add_proxy_arg(sub)
2930
sub.add_argument(
3031
"--balance",
3132
action="store_true",
@@ -40,22 +41,22 @@ def setup_parser(subparsers: Any) -> Any:
4041
)
4142
_add_alias_arg(sub)
4243
_add_address_arg(sub)
43-
_add_proxy_arg(sub)
44+
add_proxy_arg(sub)
4445
sub.set_defaults(func=get_storage)
4546

4647
sub = cli_shared.add_command_subparser(
4748
subparsers, "get", "storage-entry", "Get a specific storage entry (key-value pair) of an account."
4849
)
4950
_add_alias_arg(sub)
5051
_add_address_arg(sub)
51-
_add_proxy_arg(sub)
52+
add_proxy_arg(sub)
5253
sub.add_argument("--key", type=str, required=True, help="the storage key to read from")
5354
sub.set_defaults(func=get_key)
5455

5556
sub = cli_shared.add_command_subparser(subparsers, "get", "token", "Get a token of an account.")
5657
_add_alias_arg(sub)
5758
_add_address_arg(sub)
58-
_add_proxy_arg(sub)
59+
add_proxy_arg(sub)
5960
sub.add_argument(
6061
"--identifier",
6162
type=str,
@@ -65,16 +66,16 @@ def setup_parser(subparsers: Any) -> Any:
6566
sub.set_defaults(func=get_token)
6667

6768
sub = cli_shared.add_command_subparser(subparsers, "get", "transaction", "Get a transaction from the network.")
68-
_add_proxy_arg(sub)
69+
add_proxy_arg(sub)
6970
sub.add_argument("--hash", type=str, required=True, help="the transaction hash")
7071
sub.set_defaults(func=get_transaction)
7172

7273
sub = cli_shared.add_command_subparser(subparsers, "get", "network-config", "Get the network configuration.")
73-
_add_proxy_arg(sub)
74+
add_proxy_arg(sub)
7475
sub.set_defaults(func=get_network_config)
7576

7677
sub = cli_shared.add_command_subparser(subparsers, "get", "network-status", "Get the network status.")
77-
_add_proxy_arg(sub)
78+
add_proxy_arg(sub)
7879
sub.add_argument(
7980
"--shard",
8081
type=int,
@@ -96,10 +97,6 @@ def _add_address_arg(sub: Any):
9697
sub.add_argument("--address", type=str, help="the bech32 address")
9798

9899

99-
def _add_proxy_arg(sub: Any):
100-
sub.add_argument("--proxy", type=str, help="the proxy url")
101-
102-
103100
def get_account(args: Any):
104101
if args.alias and args.address:
105102
raise BadUsage("Provide either '--alias' or '--address'")

multiversx_sdk_cli/cli_shared.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,11 @@
5858
from multiversx_sdk_cli.signing_wrapper import SigningWrapper
5959
from multiversx_sdk_cli.simulation import Simulator
6060
from multiversx_sdk_cli.transactions import send_and_wait_for_result
61-
from multiversx_sdk_cli.utils import log_explorer_transaction
61+
from multiversx_sdk_cli.utils import log_explorer_transaction, parse_headers_list
6262
from multiversx_sdk_cli.ux import confirm_continuation
6363

6464
logger = logging.getLogger("cli_shared")
6565

66-
6766
trusted_cosigner_service_url_by_chain_id = {
6867
"1": "https://tools.multiversx.com/guardian",
6968
"D": "https://devnet-tools.multiversx.com/guardian",
@@ -118,11 +117,11 @@ def add_command_subparser(subparsers: Any, group: str, command: str, description
118117

119118

120119
def add_tx_args(
121-
args: list[str],
122-
sub: Any,
123-
with_nonce: bool = True,
124-
with_receiver: bool = True,
125-
with_data: bool = True,
120+
args: list[str],
121+
sub: Any,
122+
with_nonce: bool = True,
123+
with_receiver: bool = True,
124+
with_data: bool = True,
126125
):
127126
if with_nonce:
128127
sub.add_argument(
@@ -270,6 +269,21 @@ def add_relayed_v3_wallet_args(args: list[str], sub: Any):
270269

271270
def add_proxy_arg(sub: Any):
272271
sub.add_argument("--proxy", type=str, help="🔗 the URL of the proxy")
272+
sub.add_argument(
273+
"--proxy-headers",
274+
nargs="+",
275+
metavar="KEY=VALUE",
276+
help="custom HTTP headers for proxy requests, e.g. 'Api-Key=mytoken'",
277+
)
278+
279+
280+
def parse_proxy_headers(proxy_headers: Optional[list[str]]) -> dict[str, str]:
281+
if not proxy_headers:
282+
return {}
283+
for item in proxy_headers:
284+
if "=" not in item:
285+
raise ArgumentsNotProvidedError(f"Invalid proxy header (expected KEY=VALUE): {item!r}")
286+
return parse_headers_list(proxy_headers)
273287

274288

275289
def add_outfile_arg(sub: Any, what: str = ""):
@@ -302,7 +316,7 @@ def add_token_transfers_args(sub: Any):
302316
"--token-transfers",
303317
nargs="+",
304318
help="token transfers for transfer & execute, as [token, amount] "
305-
"E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000",
319+
"E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000",
306320
)
307321

308322

@@ -843,9 +857,9 @@ def initialize_gas_limit_estimator(args: Any) -> Union[GasLimitEstimator, None]:
843857

844858

845859
def set_options_for_hash_signing_if_needed(
846-
transaction: Transaction,
847-
guardian: Union[IAccount, None],
848-
relayer: Union[IAccount, None],
860+
transaction: Transaction,
861+
guardian: Union[IAccount, None],
862+
relayer: Union[IAccount, None],
849863
):
850864
transaction_computer = TransactionComputer()
851865

@@ -858,10 +872,10 @@ def set_options_for_hash_signing_if_needed(
858872

859873

860874
def alter_transaction_and_sign_again_if_needed(
861-
args: Any,
862-
tx: Transaction,
863-
sender: IAccount,
864-
guardian_and_relayer_data: GuardianRelayerData,
875+
args: Any,
876+
tx: Transaction,
877+
sender: IAccount,
878+
guardian_and_relayer_data: GuardianRelayerData,
865879
):
866880
initial_tx = deepcopy(tx)
867881

@@ -886,9 +900,9 @@ def alter_transaction_and_sign_again_if_needed(
886900

887901

888902
def _alter_version_and_options_if_provided(
889-
args: Any,
890-
initial_tx: Transaction,
891-
transaction: Transaction,
903+
args: Any,
904+
initial_tx: Transaction,
905+
transaction: Transaction,
892906
) -> bool:
893907
"""Alters the transaction version and options if they are provided in args.
894908
Returns True if any alteration was made, False otherwise.
@@ -913,9 +927,9 @@ def _alter_version_and_options_if_provided(
913927

914928

915929
def _sign_transaction(
916-
transaction: Transaction,
917-
sender: Optional[IAccount] = None,
918-
guardian_and_relayer_data: GuardianRelayerData = GuardianRelayerData(),
930+
transaction: Transaction,
931+
sender: Optional[IAccount] = None,
932+
guardian_and_relayer_data: GuardianRelayerData = GuardianRelayerData(),
919933
):
920934
signer = SigningWrapper()
921935
signer.sign_transaction(

multiversx_sdk_cli/cli_tokens.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
TokenType,
88
)
99

10-
from multiversx_sdk_cli import cli_shared
10+
from multiversx_sdk_cli import cli_shared, config
1111
from multiversx_sdk_cli.args_validation import (
1212
validate_broadcast_args,
1313
validate_chain_id_args,
@@ -828,7 +828,7 @@ def _initialize_controller(args: Any) -> TokenManagementController:
828828
proxy_url = args.proxy if args.proxy else ""
829829
return TokenManagementController(
830830
chain_id=chain_id,
831-
network_provider=ProxyNetworkProvider(proxy_url),
831+
network_provider=ProxyNetworkProvider(proxy_url, config=config.get_config_for_network_providers()),
832832
gas_limit_estimator=gas_estimator,
833833
)
834834

multiversx_sdk_cli/config.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
from functools import cache
33
from pathlib import Path
4-
from typing import Any
4+
from typing import Any, Optional
55

66
from multiversx_sdk import NetworkProviderConfig
77

@@ -192,5 +192,14 @@ def get_dependency_parent_directory(key: str) -> Path:
192192
return SDK_PATH / key
193193

194194

195+
_proxy_headers: dict[str, str] = {}
196+
197+
198+
def set_proxy_headers(headers: dict[str, str]) -> None:
199+
global _proxy_headers
200+
_proxy_headers = headers
201+
202+
195203
def get_config_for_network_providers() -> NetworkProviderConfig:
196-
return NetworkProviderConfig(client_name="mxpy")
204+
requests_options: Optional[dict[str, Any]] = {"headers": _proxy_headers} if _proxy_headers else None
205+
return NetworkProviderConfig(client_name="mxpy", requests_options=requests_options)

multiversx_sdk_cli/config_env.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
InvalidEnvironmentValue,
1212
UnknownEnvironmentError,
1313
)
14-
from multiversx_sdk_cli.utils import read_json_file, write_json_file
14+
from multiversx_sdk_cli.utils import parse_headers_list, read_json_file, write_json_file
1515

1616
LOCAL_ENV_PATH = Path("env.mxpy.json").resolve()
1717
GLOBAL_ENV_PATH = SDK_PATH / "env.mxpy.json"
@@ -21,6 +21,7 @@
2121
class MxpyEnv:
2222
address_hrp: str
2323
proxy_url: str
24+
proxy_headers: dict[str, str]
2425
explorer_url: str
2526
ask_confirmation: bool
2627

@@ -30,6 +31,7 @@ def from_active_env(cls) -> "MxpyEnv":
3031
return cls(
3132
address_hrp=get_address_hrp(),
3233
proxy_url=get_proxy_url(),
34+
proxy_headers=get_proxy_headers(),
3335
explorer_url=get_explorer_url(),
3436
ask_confirmation=get_confirmation_setting(),
3537
)
@@ -39,6 +41,7 @@ def get_defaults() -> dict[str, str]:
3941
return {
4042
"default_address_hrp": "erd",
4143
"proxy_url": "",
44+
"proxy_headers": "",
4245
"explorer_url": "",
4346
"ask_confirmation": "false",
4447
}
@@ -72,6 +75,16 @@ def get_proxy_url() -> str:
7275
return _get_env_value("proxy_url")
7376

7477

78+
@cache
79+
def get_proxy_headers() -> dict[str, str]:
80+
"""
81+
Returns the proxy headers for the active environment as a dict.
82+
Headers are stored as space-separated KEY=VALUE pairs (e.g. 'X-Api-Key=abc Authorization=Bearer token').
83+
"""
84+
raw = _get_env_value("proxy_headers")
85+
return parse_headers_list(raw.split()) if raw else {}
86+
87+
7588
@cache
7689
def get_explorer_url() -> str:
7790
"""

multiversx_sdk_cli/tests/test_proxy.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from multiversx_sdk import Address, ProxyNetworkProvider
1+
from multiversx_sdk import Address, NetworkProviderConfig, ProxyNetworkProvider
22

33
from multiversx_sdk_cli.cli import main
44
from multiversx_sdk_cli.config import get_config_for_network_providers
@@ -25,3 +25,22 @@ def test_query_contract():
2525
]
2626
)
2727
assert False if result else True
28+
29+
30+
def test_proxy_extra_headers():
31+
from unittest.mock import MagicMock, patch
32+
33+
config = NetworkProviderConfig(requests_options={"headers": {"x-custom-header": "test"}})
34+
proxy = ProxyNetworkProvider("", config=config)
35+
36+
def echo_headers(url, **kwargs):
37+
received_headers = kwargs.get("headers", {})
38+
mock_resp = MagicMock()
39+
mock_resp.status_code = 200
40+
mock_resp.json.return_value = {"data": {"headers": received_headers}, "error": "", "code": "successful"}
41+
return mock_resp
42+
43+
with patch("requests.Session.get", side_effect=echo_headers):
44+
response = proxy.do_get_generic("headers")
45+
headers = response.get("headers", {})
46+
assert headers["x-custom-header"] == "test"

multiversx_sdk_cli/utils.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,12 @@ def get_explorer_by_chain_id(chain_id: str) -> str:
183183
return explorers_by_chain_id[chain_id]
184184
except KeyError:
185185
return ""
186+
187+
188+
def parse_headers_list(items: list[str]) -> dict[str, str]:
189+
"""Parses a list of KEY=VALUE strings into a dict."""
190+
headers: dict[str, str] = {}
191+
for item in items:
192+
key, _, value = item.partition("=")
193+
headers[key.strip()] = value.strip()
194+
return headers

0 commit comments

Comments
 (0)