Skip to content

Commit 124f8d1

Browse files
nhannamsiuachilleas-kalalbertchon
authored
Add grpc chain client (#17)
* Update Makefile to collect and generate all assets following pyband layout * Include temp action on this branch * Init implementation for grpc chain client * WIP refactoring the pkg * WIP refactoring the pkg * WIP refactoring the pkg * WIP refactoring the pkg * WIP refactoring the pkg * Construct base components for pkg * Add subaccount, accunt num_seq getter, adapt signing hash fuction * Add timeout height option to tx * Add network constants * Update example with new pkg layout * Add import lines to proto pkg * add exchange grpc endpoint to client, update exchange_api usage * Refactor backend price, quantity calculation * Refactor derivative limit order example * refactor examples * Add denoms.ini and refactor constant.py * Update readme and remove temp action hook * wip: example client * Add back all utils conversion methods * Fetch all markets metadata and save to denoms.ini * Revert client.py changes, add composer class to build proto msgs * Update chain client examples * Update example import usage * Remove trigger price from composer methods * Prepare to gen proto files * generate complete proto files Co-authored-by: Achilleas <[email protected]> Co-authored-by: Albert Chon <[email protected]>
1 parent 181d57a commit 124f8d1

File tree

428 files changed

+72977
-15278
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

428 files changed

+72977
-15278
lines changed

.github/workflows/publish-to-pypi.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ name: Publish injective-py to TestPyPI and PyPI
44
on:
55
release:
66
types: [created]
7+
78
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
89
jobs:
910
build-n-publish:

MANIFEST.in

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
recursive-include pyinjective/proto *.py
2+
include pyinjective/denoms.ini

Makefile

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
11
all:
22

3+
EXCHANGE_PROTO_FILES=$(shell find ../injective-exchange/api/gen/grpc -type f -name '*.proto')
4+
PROTO_DIRS=$(shell find ./proto -path -prune -o -name '*.proto' -print0 | xargs -0 -n1 dirname | sort | uniq)
35
gen: gen-client
46

57
gen-client: copy-proto
6-
python -m grpc_tools.protoc -I./exchange_api/pb/ \
7-
--python_out=./exchange_api/ \
8-
--grpc_python_out=./exchange_api/ \
9-
$(shell find ./exchange_api/pb -type f -name '*.proto')
10-
11-
SRC_PROTO_FILES = $(shell find ../injective-exchange/api/gen/grpc -type f -name '*.proto')
8+
@for dir in $(PROTO_DIRS); do \
9+
mkdir -p ./pyinjective/$${dir}; \
10+
python3 -m grpc_tools.protoc \
11+
-I proto \
12+
--python_out=./pyinjective/proto \
13+
--grpc_python_out=./pyinjective/proto \
14+
$$(find $${dir} -type file -name '*.proto'); \
15+
done; \
16+
rm -rf proto
17+
echo "import os\nimport sys\n\nsys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))" > pyinjective/proto/__init__.py
1218

1319
copy-proto:
14-
mkdir -p exchange_api/pb/
15-
@for file in $(SRC_PROTO_FILES) ; do \
16-
cp "$${file}" exchange_api/pb/ ;\
17-
done
20+
rm -rf pyinjective/proto
21+
mkdir -p proto/exchange
22+
cp -r ../injective-core/proto/injective proto/
23+
cp -r ../injective-core/third_party/proto/ proto/
24+
@for file in $(EXCHANGE_PROTO_FILES); do \
25+
cp "$${file}" proto/exchange/; \
26+
done
1827

1928
.PHONY: all gen gen-client copy-proto

README.md

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@ sudo apt install python3.X-dev
1111
sudo dnf install python3-devel
1212
```
1313
### Quick Start
14+
Installation
1415
```bash
1516
pip install injective-py
1617
```
18+
Example usage
1719
```python
18-
import injective.chain_client
19-
import injective.exchange_api
20+
from pyinjective.composer import Composer as ProtoMsgComposer
21+
from pyinjective.client import Client
22+
from pyinjective.transaction import Transaction
23+
from pyinjective.constant import Network
24+
from pyinjective.wallet import PrivateKey, PublicKey, Address
25+
2026
```
2127

2228
### Usage
@@ -42,15 +48,38 @@ WARNING: Additional context: user = True home = None root = None prefix
4248
```
4349

4450
### Development
51+
1. Generate proto binding & build
4552

46-
To copy proto schemas and regenerate GRPC clients:
53+
```
54+
make gen
55+
python -m build
56+
```
4757

48-
```bash
49-
$ pipenv shell
50-
$ pipenv install --dev
58+
1. Enable dev env
59+
```
60+
pipenv shell
61+
pipenv install --dev
62+
```
63+
64+
1. Install pkg
65+
```
66+
# from local build
67+
pip uninstall injective-py
68+
pip install injective-py --no-index --find-links /path/to/injective/sdk-python/dist
69+
70+
# from pypi
71+
pip uninstall injective-py
72+
pip install injective-py
73+
```
5174

52-
$ make copy-proto
53-
$ make gen
75+
1. Fetch latest denom config
76+
```
77+
python pyinjective/fetch_metadata.py
78+
```
79+
80+
1. Run an example
81+
```
82+
python examples/chain_client_examples/1_CosmosBankMsgSend.py
5483
```
5584

5685
## License
Lines changed: 53 additions & 163 deletions
Original file line numberDiff line numberDiff line change
@@ -1,176 +1,66 @@
1+
# import sys
2+
# sys.path.insert(0, '/Users/nam/desktop/injective/sdk-python/')
3+
14
import asyncio
2-
import aiohttp
35
import logging
4-
import json
5-
import base64
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-
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-
def add_cosmos_bank_msg_send(self, recipient: str, amount: int, denom: str = "inj") -> None:
54-
msg = {
55-
"type": "cosmos-sdk/MsgSend",
56-
"value": {
57-
"from_address": privkey_to_address(self._privkey, hrp=self._hrp),
58-
"to_address": recipient,
59-
"amount": [{"denom": denom, "amount": str(amount)}],
60-
},
61-
}
62-
self._msgs.append(msg)
63-
64-
def get_signed(self) -> str:
65-
pubkey = privkey_to_pubkey(self._privkey)
66-
base64_pubkey = base64.b64encode(pubkey).decode("utf-8")
67-
signed_tx = {
68-
"tx": {
69-
"msg": self._msgs,
70-
"fee": {
71-
"gas": str(self._gas),
72-
"amount": [{"denom": self._fee_denom, "amount": str(self._fee)}],
73-
},
74-
"memo": self._memo,
75-
"signatures": [
76-
{
77-
"signature": self._sign(),
78-
"pub_key": {"type": "injective/PubKeyEthSecp256k1", "value": base64_pubkey},
79-
"account_number": str(self._account_num),
80-
"sequence": str(self._sequence),
81-
}
82-
],
83-
},
84-
"mode": self._sync_mode,
85-
}
86-
return json.dumps(signed_tx, separators=(",", ":"))
876

88-
def _sign(self) -> str:
89-
message_str = json.dumps(
90-
self._get_sign_message(), separators=(",", ":"), sort_keys=True)
91-
message_bytes = message_str.encode("utf-8")
92-
93-
privkey = ecdsa.SigningKey.from_string(
94-
self._privkey, curve=ecdsa.SECP256k1)
95-
signature_compact_keccak = privkey.sign_deterministic(
96-
message_bytes, hashfunc=sha3.keccak_256, sigencode=ecdsa.util.sigencode_string_canonize
97-
)
98-
signature_base64_str = base64.b64encode(
99-
signature_compact_keccak).decode("utf-8")
100-
return signature_base64_str
101-
102-
def _get_sign_message(self) -> Dict[str, Any]:
103-
return {
104-
"chain_id": self._chain_id,
105-
"account_number": str(self._account_num),
106-
"fee": {
107-
"gas": str(self._gas),
108-
"amount": [{"amount": str(self._fee), "denom": self._fee_denom}],
109-
},
110-
"memo": self._memo,
111-
"sequence": str(self._sequence),
112-
"msgs": self._msgs,
113-
}
7+
from pyinjective.composer import Composer as ProtoMsgComposer
8+
from pyinjective.client import Client
9+
from pyinjective.transaction import Transaction
10+
from pyinjective.constant import Network
11+
from pyinjective.wallet import PrivateKey, PublicKey, Address
11412

11513
async def main() -> None:
116-
sender_pk = seed_to_privkey(
117-
"physical page glare junk return scale subject river token door mirror title"
118-
)
119-
sender_acc_addr = privkey_to_address(sender_pk)
120-
print("Sender Account:", sender_acc_addr)
121-
122-
acc_num, acc_seq = await get_account_num_seq(sender_acc_addr)
123-
124-
tx = Transaction(
125-
privkey=sender_pk,
126-
account_num=acc_num,
127-
sequence=acc_seq,
128-
gas=200000,
129-
fee=200000 * MIN_GAS_PRICE,
130-
sync_mode="block",
14+
# select network: localhost, testnet, mainnet
15+
network = Network.testnet()
16+
17+
# initialize grpc client
18+
client = Client(network.grpc_endpoint, insecure=True)
19+
20+
# load account
21+
priv_key = PrivateKey.from_hex("f9db9bf330e23cb7839039e944adef6e9df447b90b503d5b4464c90bea9022f3")
22+
pub_key = priv_key.to_public_key()
23+
address = pub_key.to_address()
24+
25+
# prepare tx msg
26+
msg = ProtoMsgComposer.MsgSend(
27+
from_address=address.to_acc_bech32(),
28+
to_address='inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku',
29+
amount=1000000000000000000,
30+
denom='inj'
13131
)
132-
tx.add_cosmos_bank_msg_send(
133-
recipient="inj1qy69k458ppmj45c3vqwcd6wvlcuvk23x0hsz58",
134-
amount=10000000000000000,
135-
denom="inj",
32+
acc_num, acc_seq = await address.get_num_seq(network.lcd_endpoint)
33+
gas_price = 500000000
34+
gas_limit = 200000
35+
fee = [ProtoMsgComposer.Coin(
36+
amount=str(gas_price * gas_limit),
37+
denom=network.fee_denom,
38+
)]
39+
40+
# build tx
41+
tx = (
42+
Transaction()
43+
.with_messages(msg)
44+
.with_sequence(acc_seq)
45+
.with_account_num(acc_num)
46+
.with_chain_id(network.chain_id)
47+
.with_gas(gas_limit)
48+
.with_fee(fee)
49+
.with_memo("")
50+
.with_timeout_height(0)
13651
)
13752

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)
53+
# build signed tx
54+
sign_doc = tx.get_sign_doc(pub_key)
55+
sig = priv_key.sign(sign_doc.SerializeToString())
56+
tx_raw_bytes = tx.get_tx_data(sig, pub_key)
16657

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']))
58+
# broadcast tx: send_tx_async_mode, send_tx_sync_mode, send_tx_block_mode
59+
res = client.send_tx_block_mode(tx_raw_bytes)
17160

172-
return resp['txhash']
61+
# print tx response
62+
print(res)
17363

17464
if __name__ == "__main__":
17565
logging.basicConfig(level=logging.INFO)
176-
asyncio.get_event_loop().run_until_complete(main())
66+
asyncio.get_event_loop().run_until_complete(main())

0 commit comments

Comments
 (0)