Skip to content

Commit f4db6f2

Browse files
authored
Merge pull request #225 from InjectiveLabs/feat/message_broadcaster
Feat/message broadcaster
2 parents c9a01bb + 9cd21a8 commit f4db6f2

File tree

6 files changed

+377
-7
lines changed

6 files changed

+377
-7
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ make tests
8787
```
8888

8989
### Changelogs
90+
**0.7.1**
91+
* Include implementation of the MessageBroadcaster, to simplify the transaction creation and broadcasting process.
92+
9093
**0.7.0.6**
9194
* ADD SEI/USDT in metadata
9295

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import asyncio
2+
3+
from pyinjective.composer import Composer as ProtoMsgComposer
4+
from pyinjective.core.broadcaster import MsgBroadcasterWithPk
5+
from pyinjective.constant import Network
6+
from pyinjective.wallet import PrivateKey
7+
8+
9+
async def main() -> None:
10+
# select network: local, testnet, mainnet
11+
network = Network.testnet()
12+
composer = ProtoMsgComposer(network=network.string())
13+
private_key_in_hexa = "f9db9bf330e23cb7839039e944adef6e9df447b90b503d5b4464c90bea9022f3"
14+
15+
message_broadcaster = MsgBroadcasterWithPk.new_using_simulation(
16+
network=network,
17+
private_key=private_key_in_hexa,
18+
use_secure_connection=True
19+
)
20+
21+
priv_key = PrivateKey.from_hex(private_key_in_hexa)
22+
pub_key = priv_key.to_public_key()
23+
address = pub_key.to_address()
24+
subaccount_id = address.get_subaccount_id(index=0)
25+
26+
# prepare trade info
27+
fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r"
28+
29+
spot_market_id_create = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe"
30+
31+
spot_orders_to_create = [
32+
composer.SpotOrder(
33+
market_id=spot_market_id_create,
34+
subaccount_id=subaccount_id,
35+
fee_recipient=fee_recipient,
36+
price=3,
37+
quantity=55,
38+
is_buy=True,
39+
is_po=False
40+
),
41+
composer.SpotOrder(
42+
market_id=spot_market_id_create,
43+
subaccount_id=subaccount_id,
44+
fee_recipient=fee_recipient,
45+
price=300,
46+
quantity=55,
47+
is_buy=False,
48+
is_po=False
49+
),
50+
]
51+
52+
# prepare tx msg
53+
msg = composer.MsgBatchUpdateOrders(
54+
sender=address.to_acc_bech32(),
55+
spot_orders_to_create=spot_orders_to_create,
56+
)
57+
58+
# broadcast the transaction
59+
result = await message_broadcaster.broadcast([msg])
60+
print("---Transaction Response---")
61+
print(result)
62+
63+
if __name__ == "__main__":
64+
asyncio.get_event_loop().run_until_complete(main())
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import asyncio
2+
3+
from pyinjective.composer import Composer as ProtoMsgComposer
4+
from pyinjective.async_client import AsyncClient
5+
from pyinjective.core.broadcaster import MsgBroadcasterWithPk
6+
from pyinjective.constant import Network
7+
from pyinjective.wallet import PrivateKey, Address
8+
9+
10+
async def main() -> None:
11+
# select network: local, testnet, mainnet
12+
network = Network.testnet()
13+
composer = ProtoMsgComposer(network=network.string())
14+
15+
# initialize grpc client
16+
client = AsyncClient(network, insecure=False)
17+
await client.sync_timeout_height()
18+
19+
# load account
20+
private_key_in_hexa = "5d386fbdbf11f1141010f81a46b40f94887367562bd33b452bbaa6ce1cd1381e"
21+
priv_key = PrivateKey.from_hex(private_key_in_hexa)
22+
pub_key = priv_key.to_public_key()
23+
address = pub_key.to_address()
24+
25+
message_broadcaster = MsgBroadcasterWithPk.new_for_grantee_account_using_simulation(
26+
network=network,
27+
grantee_private_key=private_key_in_hexa,
28+
use_secure_connection=True
29+
)
30+
31+
# prepare tx msg
32+
market_id = "0x0611780ba69656949525013d947713300f56c37b6175e02f26bffa495c3208fe"
33+
granter_inj_address = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r"
34+
granter_address = Address.from_acc_bech32(granter_inj_address)
35+
granter_subaccount_id = granter_address.get_subaccount_id(index=0)
36+
37+
msg = composer.MsgCreateSpotLimitOrder(
38+
sender=granter_inj_address,
39+
market_id=market_id,
40+
subaccount_id=granter_subaccount_id,
41+
fee_recipient=address.to_acc_bech32(),
42+
price=7.523,
43+
quantity=0.01,
44+
is_buy=True,
45+
is_po=False
46+
)
47+
48+
# broadcast the transaction
49+
result = await message_broadcaster.broadcast([msg])
50+
print("---Transaction Response---")
51+
print(result)
52+
53+
if __name__ == "__main__":
54+
asyncio.get_event_loop().run_until_complete(main())

pyinjective/constant.py

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,13 +94,33 @@ def devnet(cls):
9494
)
9595

9696
@classmethod
97-
def testnet(cls):
97+
def testnet(cls, node="lb"):
98+
nodes = [
99+
"lb",
100+
"sentry",
101+
]
102+
if node not in nodes:
103+
raise ValueError('Must be one of {}'.format(nodes))
104+
105+
if node == 'lb':
106+
lcd_endpoint = 'https://k8s.testnet.lcd.injective.network',
107+
tm_websocket_endpoint = 'wss://k8s.testnet.tm.injective.network/websocket',
108+
grpc_endpoint = 'k8s.testnet.chain.grpc.injective.network:443',
109+
grpc_exchange_endpoint = 'k8s.testnet.exchange.grpc.injective.network:443',
110+
grpc_explorer_endpoint = 'k8s.testnet.explorer.grpc.injective.network:443',
111+
else:
112+
lcd_endpoint = 'https://testnet.lcd.injective.network'
113+
tm_websocket_endpoint = 'wss://testnet.tm.injective.network/websocket'
114+
grpc_endpoint = 'testnet.chain.grpc.injective.network'
115+
grpc_exchange_endpoint = 'testnet.exchange.grpc.injective.network'
116+
grpc_explorer_endpoint = 'testnet.explorer.grpc.injective.network'
117+
98118
return cls(
99-
lcd_endpoint='https://k8s.testnet.lcd.injective.network',
100-
tm_websocket_endpoint='wss://k8s.testnet.tm.injective.network/websocket',
101-
grpc_endpoint='k8s.testnet.chain.grpc.injective.network:443',
102-
grpc_exchange_endpoint='k8s.testnet.exchange.grpc.injective.network:443',
103-
grpc_explorer_endpoint='k8s.testnet.explorer.grpc.injective.network:443',
119+
lcd_endpoint=lcd_endpoint,
120+
tm_websocket_endpoint=tm_websocket_endpoint,
121+
grpc_endpoint=grpc_endpoint,
122+
grpc_exchange_endpoint=grpc_exchange_endpoint,
123+
grpc_explorer_endpoint=grpc_explorer_endpoint,
104124
chain_id='injective-888',
105125
fee_denom='inj',
106126
env='testnet'

pyinjective/core/broadcaster.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
from abc import ABC, abstractmethod
2+
from decimal import Decimal
3+
from typing import List, Optional
4+
5+
import math
6+
from google.protobuf import any_pb2
7+
8+
from pyinjective import PrivateKey, Transaction, PublicKey
9+
from pyinjective.async_client import AsyncClient
10+
from pyinjective.composer import Composer
11+
from pyinjective.constant import Network
12+
13+
14+
class BroadcasterAccountConfig(ABC):
15+
16+
@property
17+
@abstractmethod
18+
def trading_injective_address(self) -> str:
19+
...
20+
21+
@property
22+
@abstractmethod
23+
def trading_private_key(self) -> PrivateKey:
24+
...
25+
26+
@property
27+
@abstractmethod
28+
def trading_public_key(self) -> PublicKey:
29+
...
30+
31+
@abstractmethod
32+
def messages_prepared_for_transaction(self, messages: List[any_pb2.Any]) -> List[any_pb2.Any]:
33+
...
34+
35+
36+
class TransactionFeeCalculator (ABC):
37+
DEFAULT_GAS_PRICE = 500_000_000
38+
39+
@abstractmethod
40+
async def configure_gas_fee_for_transaction(
41+
self,
42+
transaction: Transaction,
43+
private_key: PrivateKey,
44+
public_key: PublicKey,
45+
):
46+
...
47+
48+
49+
class MsgBroadcasterWithPk:
50+
51+
def __init__(
52+
self,
53+
network: Network,
54+
account_config: BroadcasterAccountConfig,
55+
client: AsyncClient,
56+
composer: Composer,
57+
fee_calculator: TransactionFeeCalculator):
58+
self._network = network
59+
self._account_config = account_config
60+
self._client = client
61+
self._composer = composer
62+
self._fee_calculator = fee_calculator
63+
64+
@classmethod
65+
def new_using_simulation(
66+
cls,
67+
network: Network,
68+
private_key: str,
69+
use_secure_connection: bool = True,
70+
client: Optional[AsyncClient] = None,
71+
composer: Optional[Composer] = None,
72+
):
73+
client = client or AsyncClient(network=network, insecure=not(use_secure_connection))
74+
composer = composer or Composer(network=client.network.string())
75+
account_config = StandardAccountBroadcasterConfig(private_key=private_key)
76+
fee_calculator = SimulatedTransactionFeeCalculator(
77+
client=client,
78+
composer=composer
79+
)
80+
instance = cls(
81+
network=network,
82+
account_config=account_config,
83+
client=client,
84+
composer=composer,
85+
fee_calculator=fee_calculator,
86+
)
87+
return instance
88+
89+
@classmethod
90+
def new_for_grantee_account_using_simulation(
91+
cls,
92+
network: Network,
93+
grantee_private_key: str,
94+
use_secure_connection: bool = True,
95+
client: Optional[AsyncClient] = None,
96+
composer: Optional[Composer] = None,
97+
):
98+
client = client or AsyncClient(network=network, insecure=not (use_secure_connection))
99+
composer = composer or Composer(network=client.network.string())
100+
account_config = GranteeAccountBroadcasterConfig(grantee_private_key=grantee_private_key, composer=composer)
101+
fee_calculator = SimulatedTransactionFeeCalculator(
102+
client=client,
103+
composer=composer
104+
)
105+
instance = cls(
106+
network=network,
107+
account_config=account_config,
108+
client=client,
109+
composer=composer,
110+
fee_calculator=fee_calculator,
111+
)
112+
return instance
113+
114+
async def broadcast(self, messages: List[any_pb2.Any]):
115+
await self._client.sync_timeout_height()
116+
await self._client.get_account(self._account_config.trading_injective_address)
117+
118+
messages_for_transaction = self._account_config.messages_prepared_for_transaction(messages=messages)
119+
120+
transaction = Transaction()
121+
transaction.with_messages(*messages_for_transaction)
122+
transaction.with_sequence(self._client.get_sequence())
123+
transaction.with_account_num(self._client.get_number())
124+
transaction.with_chain_id(self._network.chain_id)
125+
126+
await self._fee_calculator.configure_gas_fee_for_transaction(
127+
transaction=transaction,
128+
private_key=self._account_config.trading_private_key,
129+
public_key=self._account_config.trading_public_key,
130+
)
131+
transaction.with_memo("")
132+
transaction.with_timeout_height(timeout_height=self._client.timeout_height)
133+
134+
sign_doc = transaction.get_sign_doc(self._account_config.trading_public_key)
135+
sig = self._account_config.trading_private_key.sign(sign_doc.SerializeToString())
136+
tx_raw_bytes = transaction.get_tx_data(sig, self._account_config.trading_public_key)
137+
138+
# broadcast tx: send_tx_async_mode, send_tx_sync_mode
139+
transaction_result = await self._client.send_tx_sync_mode(tx_raw_bytes)
140+
141+
return transaction_result
142+
143+
144+
class StandardAccountBroadcasterConfig(BroadcasterAccountConfig):
145+
146+
def __init__(self, private_key: str):
147+
self._private_key = PrivateKey.from_hex(private_key)
148+
self._public_key = self._private_key.to_public_key()
149+
self._address = self._public_key.to_address()
150+
151+
@property
152+
def trading_injective_address(self) -> str:
153+
return self._address.to_acc_bech32()
154+
155+
@property
156+
def trading_private_key(self) -> PrivateKey:
157+
return self._private_key
158+
159+
@property
160+
def trading_public_key(self) -> PublicKey:
161+
return self._public_key
162+
163+
def messages_prepared_for_transaction(self, messages: List[any_pb2.Any]) -> List[any_pb2.Any]:
164+
# For standard account the messages do not need to be addapted
165+
return messages
166+
167+
168+
class GranteeAccountBroadcasterConfig(BroadcasterAccountConfig):
169+
170+
def __init__(self, grantee_private_key: str, composer: Composer):
171+
self._grantee_private_key = PrivateKey.from_hex(grantee_private_key)
172+
self._grantee_public_key = self._grantee_private_key.to_public_key()
173+
self._grantee_address = self._grantee_public_key.to_address()
174+
self._composer = composer
175+
176+
@property
177+
def trading_injective_address(self) -> str:
178+
return self._grantee_address.to_acc_bech32()
179+
180+
@property
181+
def trading_private_key(self) -> PrivateKey:
182+
return self._grantee_private_key
183+
184+
@property
185+
def trading_public_key(self) -> PublicKey:
186+
return self._grantee_public_key
187+
188+
def messages_prepared_for_transaction(self, messages: List[any_pb2.Any]) -> List[any_pb2.Any]:
189+
exec_message = self._composer.MsgExec(
190+
grantee=self.trading_injective_address,
191+
msgs=messages,
192+
)
193+
194+
return [exec_message]
195+
196+
197+
class SimulatedTransactionFeeCalculator(TransactionFeeCalculator):
198+
199+
def __init__(self, client: AsyncClient, composer: Composer, gas_price: Optional[int] = None):
200+
self._client = client
201+
self._composer = composer
202+
self._gas_price = gas_price or self.DEFAULT_GAS_PRICE
203+
204+
async def configure_gas_fee_for_transaction(
205+
self,
206+
transaction: Transaction,
207+
private_key: PrivateKey,
208+
public_key: PublicKey,
209+
):
210+
sim_sign_doc = transaction.get_sign_doc(public_key)
211+
sim_sig = private_key.sign(sim_sign_doc.SerializeToString())
212+
sim_tx_raw_bytes = transaction.get_tx_data(sim_sig, public_key)
213+
214+
# simulate tx
215+
(sim_res, success) = await self._client.simulate_tx(sim_tx_raw_bytes)
216+
if not success:
217+
raise RuntimeError(f"Transaction simulation error: {sim_res}")
218+
219+
gas_limit = math.ceil(Decimal(str(sim_res.gas_info.gas_used)) * Decimal("1.1"))
220+
221+
fee = [
222+
self._composer.Coin(
223+
amount=self._gas_price * gas_limit,
224+
denom=self._client.network.fee_denom,
225+
)
226+
]
227+
228+
transaction.with_gas(gas=gas_limit)
229+
transaction.with_fee(fee=fee)

0 commit comments

Comments
 (0)