diff --git a/v4-client-py-v2/dydx_v4_client/indexer/rest/modules/markets.py b/v4-client-py-v2/dydx_v4_client/indexer/rest/modules/markets.py index 79153352..f72c9d54 100644 --- a/v4-client-py-v2/dydx_v4_client/indexer/rest/modules/markets.py +++ b/v4-client-py-v2/dydx_v4_client/indexer/rest/modules/markets.py @@ -80,6 +80,7 @@ async def get_perpetual_market_candles( dict: The candle data. """ uri = f"/v4/candles/perpetualMarkets/{market}" + return await self.get( uri, params={ diff --git a/v4-client-py-v2/dydx_v4_client/indexer/rest/noble_client.py b/v4-client-py-v2/dydx_v4_client/indexer/rest/noble_client.py index 714a151a..07f360db 100644 --- a/v4-client-py-v2/dydx_v4_client/indexer/rest/noble_client.py +++ b/v4-client-py-v2/dydx_v4_client/indexer/rest/noble_client.py @@ -1,35 +1,20 @@ +import hashlib from typing import List, Optional - import grpc -from ecdsa.util import sigencode_string_canonize -from v4_proto.cosmos.auth.v1beta1 import query_pb2_grpc as auth -from v4_proto.cosmos.auth.v1beta1.auth_pb2 import BaseAccount -from v4_proto.cosmos.auth.v1beta1.query_pb2 import QueryAccountRequest +from bech32 import convertbits, bech32_encode +from google.protobuf.any_pb2 import Any from v4_proto.cosmos.bank.v1beta1 import query_pb2 as bank_query +from v4_proto.cosmos.bank.v1beta1 import tx_pb2 as bank_tx from v4_proto.cosmos.bank.v1beta1 import query_pb2_grpc as bank_query_grpc -from v4_proto.cosmos.base.abci.v1beta1.abci_pb2 import TxResponse from v4_proto.cosmos.base.v1beta1.coin_pb2 import Coin -from v4_proto.cosmos.crypto.secp256k1.keys_pb2 import PubKey -from v4_proto.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode from v4_proto.cosmos.tx.v1beta1 import service_pb2_grpc from v4_proto.cosmos.tx.v1beta1.service_pb2 import ( - BroadcastMode, - BroadcastTxRequest, SimulateRequest, + BroadcastTxRequest, + BroadcastMode, ) -from v4_proto.cosmos.tx.v1beta1.tx_pb2 import ( - AuthInfo, - Fee, - ModeInfo, - SignDoc, - SignerInfo, - Tx, - TxBody, -) - -from dydx_v4_client.config import GAS_MULTIPLIER -from dydx_v4_client.node.builder import as_any -from dydx_v4_client.wallet import from_mnemonic +from v4_proto.cosmos.tx.v1beta1.tx_pb2 import Tx, TxBody, AuthInfo, Fee +from dydx_v4_client.wallet import from_mnemonic, Wallet class NobleClient: @@ -73,189 +58,108 @@ async def connect(self, mnemonic: str): if not mnemonic: raise ValueError("Mnemonic not provided") private_key = from_mnemonic(mnemonic) - self.wallet = private_key + self.wallet = Wallet(private_key, 0, 0) self.channel = grpc.secure_channel( self.rest_endpoint, grpc.ssl_channel_credentials(), ) - async def get_account_balances( - self, address: str - ) -> bank_query.QueryAllBalancesResponse: + async def get_account_balances(self) -> List[Coin]: """ - Retrieves the account balances for the specified address. - - Args: - address (str): The account address. + Retrieves the account balances for the connected wallet. Returns: - bank_query.QueryAllBalancesResponse: The response containing the account balances. + List[Coin]: A list of Coin objects representing the account balances. Raises: - ValueError: If the client channel is not initialized. + ValueError: If the client is not connected. """ - if self.channel is None: - raise ValueError("NobleClient channel not initialized") + if not self.is_connected: + raise ValueError("Client is not connected") + + address = self.wallet.address stub = bank_query_grpc.QueryStub(self.channel) - return stub.AllBalances(bank_query.QueryAllBalancesRequest(address=address)) + request = bank_query.QueryAllBalancesRequest(address=address) + response = stub.AllBalances(request) + return response.balances - async def get_account_balance( - self, address: str, denom: str - ) -> bank_query.QueryBalanceResponse: + async def simulate_transfer_native_token(self, amount: str, recipient: str) -> Fee: """ - Retrieves the account balance for the specified address and denomination. + Simulates a transfer of native tokens. Args: - address (str): The account address. - denom (str): The balance denomination. + amount (str): The amount of tokens to transfer. + recipient (str): The recipient's address. Returns: - bank_query.QueryBalanceResponse: The response containing the account balance. + Fee: The estimated fee for the transaction. Raises: - ValueError: If the client channel is not initialized. + ValueError: If the client is not connected. """ - if self.channel is None: - raise ValueError("NobleClient channel not initialized") - stub = bank_query_grpc.QueryStub(self.channel) - return stub.Balance( - bank_query.QueryBalanceRequest(address=address, denom=denom) - ) + if not self.is_connected: + raise ValueError("Client is not connected") - async def get_account(self, address: str) -> BaseAccount: - """ - Retrieves the account information for the specified address. + # Create a MsgSend transaction + msg_send = bank_tx.MsgSend( + from_address=self.wallet.address, + to_address=recipient, + amount=[Coin(amount=amount, denom="uusdc")], + ) + any_msg = Any() + any_msg.Pack(msg_send) - Args: - address (str): The account address. + tx_body = TxBody(messages=[any_msg]) + auth_info = AuthInfo() + tx = Tx(body=tx_body, auth_info=auth_info) - Returns: - BaseAccount: The account information. + stub = service_pb2_grpc.ServiceStub(self.channel) + simulate_request = SimulateRequest(tx=tx) + simulate_response = stub.Simulate(simulate_request) - Raises: - ValueError: If the client channel is not initialized. - Exception: If the account unpacking fails. - """ - if self.channel is None: - raise ValueError("NobleClient channel not initialized") - account = BaseAccount() - response = auth.QueryStub(self.channel).Account( - QueryAccountRequest(address=address) + return Fee( + amount=simulate_response.gas_info.fee.amount, + gas_limit=simulate_response.gas_info.gas_used, ) - if not response.account.Unpack(account): - raise Exception("Failed to unpack account") - return account - async def send( - self, - messages: List[dict], - gas_price: str = "0.025uusdc", - memo: Optional[str] = None, - ) -> TxResponse: + async def transfer_native(self, amount: str, recipient: str) -> str: """ - Sends a transaction with the specified messages. + Transfers native tokens to the specified recipient. Args: - messages (List[dict]): The list of transaction messages. - gas_price (str): The gas price for the transaction (default: "0.025uusdc"). - memo (Optional[str]): The transaction memo. + amount (str): The amount of tokens to transfer. + recipient (str): The recipient's address. Returns: - TxResponse: The transaction response. + str: The transaction hash. Raises: - ValueError: If the client channel or wallet is not initialized. + ValueError: If the client is not connected. """ - if self.channel is None: - raise ValueError("NobleClient channel not initialized") - if self.wallet is None: - raise ValueError("NobleClient wallet not initialized") + if not self.is_connected: + raise ValueError("Client is not connected") - # Simulate to get the gas estimate - fee = await self.simulate_transaction( - messages, gas_price, memo or self.default_client_memo + # Create a MsgSend transaction + msg_send = bank_tx.MsgSend( + from_address=self.wallet.address, + to_address=recipient, + amount=[Coin(amount=amount, denom="uusdc")], ) + any_msg = Any() + any_msg.Pack(msg_send) - # Sign and broadcast the transaction - signer_info = SignerInfo( - public_key=as_any( - PubKey(key=self.wallet.get_verifying_key().to_string("compressed")) - ), - mode_info=ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT)), - sequence=self.get_account( - self.wallet.get_verifying_key().to_string() - ).sequence, - ) - body = TxBody(messages=messages, memo=memo or self.default_client_memo) - auth_info = AuthInfo(signer_infos=[signer_info], fee=fee) - signature = self.wallet.sign( - SignDoc( - body_bytes=body.SerializeToString(), - auth_info_bytes=auth_info.SerializeToString(), - account_number=self.get_account( - self.wallet.get_verifying_key().to_string() - ).account_number, - chain_id=self.chain_id, - ).SerializeToString(), - sigencode=sigencode_string_canonize, - ) + tx_body = TxBody(messages=[any_msg], memo=self.default_client_memo) + fee = await self.simulate_transfer_native_token(amount, recipient) - tx = Tx(body=body, auth_info=auth_info, signatures=[signature]) - request = BroadcastTxRequest( - tx_bytes=tx.SerializeToString(), mode=BroadcastMode.BROADCAST_MODE_SYNC - ) - return service_pb2_grpc.ServiceStub(self.channel).BroadcastTx(request) - - async def simulate_transaction( - self, - messages: List[dict], - gas_price: str = "0.025uusdc", - memo: Optional[str] = None, - ) -> Fee: - """ - Simulates a transaction to estimate the gas fee. - - Args: - messages (List[dict]): The list of transaction messages. - gas_price (str): The gas price for the transaction (default: "0.025uusdc"). - memo (Optional[str]): The transaction memo. + auth_info = AuthInfo(fee=fee) + tx = Tx(body=tx_body, auth_info=auth_info) + signed_tx = self.wallet.sign_tx(tx) - Returns: - Fee: The estimated gas fee. - - Raises: - ValueError: If the client channel or wallet is not initialized. - """ - if self.channel is None: - raise ValueError("NobleClient channel not initialized") - if self.wallet is None: - raise ValueError("NobleClient wallet not initialized") - - # Get simulated response - signer_info = SignerInfo( - public_key=as_any( - PubKey(key=self.wallet.get_verifying_key().to_string("compressed")) - ), - mode_info=ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT)), - sequence=self.get_account( - self.wallet.get_verifying_key().to_string() - ).sequence, - ) - body = TxBody(messages=messages, memo=memo or self.default_client_memo) - auth_info = AuthInfo(signer_infos=[signer_info], fee=Fee(gas_limit=0)) - request = SimulateRequest( - tx=Tx(body=body, auth_info=auth_info), + stub = service_pb2_grpc.ServiceStub(self.channel) + broadcast_request = BroadcastTxRequest( + tx_bytes=signed_tx.SerializeToString(), + mode=BroadcastMode.BROADCAST_MODE_BLOCK, ) - response = service_pb2_grpc.ServiceStub(self.channel).Simulate(request) + broadcast_response = stub.BroadcastTx(broadcast_request) - # Calculate and return the fee - gas_limit = int(response.gas_info.gas_used * GAS_MULTIPLIER) - return Fee( - amount=[ - Coin( - amount=str(int(gas_limit * float(gas_price.split("u")[0]))), - denom=gas_price.split("u")[1], - ) - ], - gas_limit=gas_limit, - ) + return broadcast_response.tx_response.txhash diff --git a/v4-client-py-v2/dydx_v4_client/network.py b/v4-client-py-v2/dydx_v4_client/network.py index e0ae9266..8cff7dff 100644 --- a/v4-client-py-v2/dydx_v4_client/network.py +++ b/v4-client-py-v2/dydx_v4_client/network.py @@ -62,7 +62,7 @@ def make_config( ) TESTNET = make_testnet() TESTNET_FAUCET = "https://faucet.v4testnet.dydx.exchange" -TESTNET_NOBLE = "https://rpc.testnet.noble.strange.love" +TESTNET_NOBLE = "https://noble-testnet-rpc.polkachu.com" local_node = partial( diff --git a/v4-client-py-v2/tests/indexer/rest/test_noble_client.py b/v4-client-py-v2/tests/indexer/rest/test_noble_client.py index d4006da4..d3bea562 100644 --- a/v4-client-py-v2/tests/indexer/rest/test_noble_client.py +++ b/v4-client-py-v2/tests/indexer/rest/test_noble_client.py @@ -1,5 +1,6 @@ import pytest -from v4_proto.cosmos.base.abci.v1beta1.abci_pb2 import TxResponse + +from dydx_v4_client.node.fee import Coin, Fee @pytest.mark.asyncio @@ -8,43 +9,52 @@ async def test_is_connected(noble_client): @pytest.mark.asyncio -@pytest.mark.skip(reason="This test is not implemented") -async def test_ibc_transfer(node_client, noble_client): - message = { - "source_port": "transfer", - "source_channel": "channel-0", - "token": {"denom": "usdc", "amount": "1000"}, - "sender": noble_client.wallet.get_verifying_key().to_string(), - "receiver": "cosmos1...", - "timeout_height": 0, - "timeout_timestamp": 0, - } - tx_response = await node_client.ibc_transfer([message]) - assert isinstance(tx_response, TxResponse) - assert tx_response.code == 0 +async def test_get_address(noble_client): + address = noble_client.wallet.address + + assert isinstance(address, str) + assert len(address) == 43 + assert address.startswith("dydx") + + +@pytest.mark.asyncio +@pytest.mark.skip(reason="NobleClient testnet is not available") +async def test_get_account_balances(noble_client): + balances = await noble_client.get_account_balances() + + assert isinstance(balances, list) + assert len(balances) > 0 + assert all(isinstance(coin, Coin) for coin in balances) + + # Check if there's a balance for the native token (uusdc) + uusdc_balance = next((coin for coin in balances if coin.denom == "uusdc"), None) + assert uusdc_balance is not None + assert int(uusdc_balance.amount) > 0 @pytest.mark.asyncio -@pytest.mark.skip(reason="This test is not implemented") -async def test_send(noble_client): - message = { - "depositor": noble_client.wallet.get_verifying_key().to_string(), - "amount": {"denom": "usdc", "amount": "1000"}, - } - tx_response = await noble_client.send([message]) - assert isinstance(tx_response, TxResponse) - assert tx_response.code == 0 +@pytest.mark.skip(reason="NobleClient testnet is not available") +async def test_simulate_transfer_native_token(noble_client, test_address): + amount = "1000000" # 1 USDC (assuming 6 decimal places) + recipient = "dydx15ndn9c895f8ntck25qughtuck9spv2d9svw5qx" + + fee = await noble_client.simulate_transfer_native_token(amount, recipient) + + assert isinstance(fee, Fee) + assert len(fee.amount) > 0 + assert fee.gas_limit > 0 + + fee_amount = sum(int(coin.amount) for coin in fee.amount) + assert fee_amount < int(amount) * 0.01 @pytest.mark.asyncio -@pytest.mark.skip(reason="This test is not implemented") -async def test_simulate_transaction(noble_client, node_client): - message = { - "depositor": noble_client.wallet.get_verifying_key().to_string(), - "amount": {"denom": "usdc", "amount": "1000"}, - } - fee = await noble_client.simulate_transaction([message]) - assert isinstance(fee, dict) - assert fee["gas_limit"] > 0 - assert len(fee["amount"]) == 1 - assert "usdc" in fee["amount"][0]["denom"] +@pytest.mark.skip(reason="NobleClient testnet is not available") +async def test_transfer_native_token(noble_client): + amount = "1000000" # 1 USDC (6 decimal places) + recipient = "dydx15ndn9c895f8ntck25qughtuck9spv2d9svw5qx" + + tx_hash = await noble_client.transfer_native(amount, recipient) + + assert isinstance(tx_hash, str) + assert len(tx_hash) == 64