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

enable NobleClient native transfers #222

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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={
Expand Down
238 changes: 71 additions & 167 deletions v4-client-py-v2/dydx_v4_client/indexer/rest/noble_client.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion v4-client-py-v2/dydx_v4_client/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading
Loading