1+ import pytest
2+ import aiohttp
3+ import json
4+ import ecdsa
5+ import sha3
6+ import grpc
7+ import base64
8+
9+ from typing import Any , Dict , List
10+ from injective .chain_client ._wallet import (
11+ generate_wallet ,
12+ privkey_to_address ,
13+ privkey_to_pubkey ,
14+ pubkey_to_address ,
15+ seed_to_privkey ,
16+ DEFAULT_BECH32_HRP ,
17+ )
18+
19+ from injective .chain_client ._typings import SyncMode
20+ import injective .exchange_api .injective_accounts_rpc_pb2 as accounts_rpc_pb
21+ import injective .exchange_api .injective_accounts_rpc_pb2_grpc as accounts_rpc_grpc
22+
23+ MIN_GAS_PRICE = 500000000
24+
25+ class Transaction :
26+
27+ def __init__ (
28+ self ,
29+ * ,
30+ privkey : bytes ,
31+ account_num : int ,
32+ sequence : int ,
33+ fee : int ,
34+ gas : int ,
35+ fee_denom : str = "inj" ,
36+ memo : str = "" ,
37+ chain_id : str = "injective-888" ,
38+ hrp : str = DEFAULT_BECH32_HRP ,
39+ sync_mode : SyncMode = "block" ,
40+ ) -> None :
41+ self ._privkey = privkey
42+ self ._account_num = account_num
43+ self ._sequence = sequence
44+ self ._fee = fee
45+ self ._fee_denom = fee_denom
46+ self ._gas = gas
47+ self ._memo = memo
48+ self ._chain_id = chain_id
49+ self ._hrp = hrp
50+ self ._sync_mode = sync_mode
51+ self ._msgs : List [dict ] = []
52+
53+
54+ def add_cosmos_bank_msg_send (self , recipient : str , amount : int , denom : str = "inj" ) -> None :
55+ msg = {
56+ "type" : "cosmos-sdk/MsgSend" ,
57+ "value" : {
58+ "from_address" : privkey_to_address (self ._privkey , hrp = self ._hrp ),
59+ "to_address" : recipient ,
60+ "amount" : [{"denom" : denom , "amount" : str (amount )}],
61+ },
62+ }
63+ self ._msgs .append (msg )
64+
65+ # Injective • Exchange Module
66+
67+ def add_exchange_msg_deposit (self , subaccount : str , amount : int , denom : str = "inj" ) -> None :
68+ msg = {
69+ "type" : "exchange/MsgDeposit" ,
70+ "value" : {
71+ "sender" : privkey_to_address (self ._privkey , hrp = self ._hrp ),
72+ "subaccount_id" : subaccount ,
73+ "amount" : {"denom" : denom , "amount" : str (amount )},
74+ },
75+ }
76+ self ._msgs .append (msg )
77+
78+ def get_signed (self ) -> str :
79+ pubkey = privkey_to_pubkey (self ._privkey )
80+ base64_pubkey = base64 .b64encode (pubkey ).decode ("utf-8" )
81+ signed_tx = {
82+ "tx" : {
83+ "msg" : self ._msgs ,
84+ "fee" : {
85+ "gas" : str (self ._gas ),
86+ "amount" : [{"denom" : self ._fee_denom , "amount" : str (self ._fee )}],
87+ },
88+ "memo" : self ._memo ,
89+ "signatures" : [
90+ {
91+ "signature" : self ._sign (),
92+ "pub_key" : {"type" : "injective/PubKeyEthSecp256k1" , "value" : base64_pubkey },
93+ "account_number" : str (self ._account_num ),
94+ "sequence" : str (self ._sequence ),
95+ }
96+ ],
97+ },
98+ "mode" : self ._sync_mode ,
99+ }
100+ return json .dumps (signed_tx , separators = ("," , ":" ))
101+
102+ def _sign (self ) -> str :
103+ message_str = json .dumps (
104+ self ._get_sign_message (), separators = ("," , ":" ), sort_keys = True )
105+ message_bytes = message_str .encode ("utf-8" )
106+
107+ privkey = ecdsa .SigningKey .from_string (
108+ self ._privkey , curve = ecdsa .SECP256k1 )
109+ signature_compact_keccak = privkey .sign_deterministic (
110+ message_bytes , hashfunc = sha3 .keccak_256 , sigencode = ecdsa .util .sigencode_string_canonize
111+ )
112+ signature_base64_str = base64 .b64encode (
113+ signature_compact_keccak ).decode ("utf-8" )
114+ return signature_base64_str
115+
116+ def _get_sign_message (self ) -> Dict [str , Any ]:
117+ return {
118+ "chain_id" : self ._chain_id ,
119+ "account_number" : str (self ._account_num ),
120+ "fee" : {
121+ "gas" : str (self ._gas ),
122+ "amount" : [{"amount" : str (self ._fee ), "denom" : self ._fee_denom }],
123+ },
124+ "memo" : self ._memo ,
125+ "sequence" : str (self ._sequence ),
126+ "msgs" : self ._msgs ,
127+ }
128+
129+
130+ async def get_account_num_seq (address : str ) -> (int , int ):
131+ async with aiohttp .ClientSession () as session :
132+ async with session .request (
133+ 'GET' , 'http://staking-lcd-testnet.injective.network/cosmos/auth/v1beta1/accounts/' + address ,
134+ headers = {'Accept-Encoding' : 'application/json' },
135+ ) as response :
136+ if response .status != 200 :
137+ print (await response .text ())
138+ raise ValueError ("HTTP response status" , response .status )
139+
140+ resp = json .loads (await response .text ())
141+ acc = resp ['account' ]['base_account' ]
142+ return acc ['account_number' ], acc ['sequence' ]
143+
144+ async def post_tx (tx_json : str ):
145+ async with aiohttp .ClientSession () as session :
146+ async with session .request (
147+ 'POST' , 'http://staking-lcd-testnet.injective.network/txs' , data = tx_json ,
148+ headers = {'Content-Type' : 'application/json' },
149+ ) as response :
150+ if response .status != 200 :
151+ print (await response .text ())
152+ raise ValueError ("HTTP response status" , response .status )
153+
154+ resp = json .loads (await response .text ())
155+ if 'code' in resp :
156+ print ("Response:" , resp )
157+ raise ValueError ('sdk error %d: %s' % (resp ['code' ], resp ['raw_log' ]))
158+
159+ return resp ['txhash' ]
160+
161+ @pytest .fixture
162+ async def msg_send ():
163+ sender_pk = seed_to_privkey (
164+ "physical page glare junk return scale subject river token door mirror title"
165+ )
166+ sender_acc_addr = privkey_to_address (sender_pk )
167+ print ("Sender Account:" , sender_acc_addr )
168+
169+ acc_num , acc_seq = await get_account_num_seq (sender_acc_addr )
170+
171+ async with grpc .aio .insecure_channel ('testnet-sentry0.injective.network:9910' ) as channel :
172+ accounts_rpc = accounts_rpc_grpc .InjectiveAccountsRPCStub (channel )
173+ account_addr = "inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku"
174+
175+ subacc = await accounts_rpc .SubaccountsList (accounts_rpc_pb .SubaccountsListRequest (account_address = account_addr ))
176+ for sub in subacc .subaccounts :
177+ print ("Primary subaccount:" , sub )
178+ break
179+
180+ tx = Transaction (
181+ privkey = sender_pk ,
182+ account_num = acc_num ,
183+ sequence = acc_seq ,
184+ gas = 200000 ,
185+ fee = 200000 * MIN_GAS_PRICE ,
186+ sync_mode = "block" ,
187+ )
188+ tx .add_cosmos_bank_msg_send (
189+ recipient = "inj1qy69k458ppmj45c3vqwcd6wvlcuvk23x0hsz58" ,
190+ amount = 10000000000000000 ,
191+ denom = "inj" ,
192+ )
193+ tx .add_exchange_msg_deposit (
194+ subaccount = sub ,
195+ amount = 10000000000000000 ,
196+ denom = "inj" ,
197+ )
198+
199+ tx_json = tx .get_signed ()
200+ tx_result = await post_tx (tx_json )
201+
202+ print ("Signed Tx:" , tx_json )
203+ print ("Sent Tx:" , tx_result )
204+
205+ return len (tx_result )
206+
207+ @pytest .mark .asyncio
208+ async def test_msg_send (msg_send ):
209+ assert msg_send == 64
0 commit comments