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