Skip to content

Commit

Permalink
Add grpc chain client (#17)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
3 people authored Sep 17, 2021
1 parent 181d57a commit 124f8d1
Show file tree
Hide file tree
Showing 428 changed files with 72,977 additions and 15,278 deletions.
1 change: 1 addition & 0 deletions .github/workflows/publish-to-pypi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ name: Publish injective-py to TestPyPI and PyPI
on:
release:
types: [created]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build-n-publish:
Expand Down
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
recursive-include pyinjective/proto *.py
include pyinjective/denoms.ini
29 changes: 19 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
all:

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

gen-client: copy-proto
python -m grpc_tools.protoc -I./exchange_api/pb/ \
--python_out=./exchange_api/ \
--grpc_python_out=./exchange_api/ \
$(shell find ./exchange_api/pb -type f -name '*.proto')

SRC_PROTO_FILES = $(shell find ../injective-exchange/api/gen/grpc -type f -name '*.proto')
@for dir in $(PROTO_DIRS); do \
mkdir -p ./pyinjective/$${dir}; \
python3 -m grpc_tools.protoc \
-I proto \
--python_out=./pyinjective/proto \
--grpc_python_out=./pyinjective/proto \
$$(find $${dir} -type file -name '*.proto'); \
done; \
rm -rf proto
echo "import os\nimport sys\n\nsys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))" > pyinjective/proto/__init__.py

copy-proto:
mkdir -p exchange_api/pb/
@for file in $(SRC_PROTO_FILES) ; do \
cp "$${file}" exchange_api/pb/ ;\
done
rm -rf pyinjective/proto
mkdir -p proto/exchange
cp -r ../injective-core/proto/injective proto/
cp -r ../injective-core/third_party/proto/ proto/
@for file in $(EXCHANGE_PROTO_FILES); do \
cp "$${file}" proto/exchange/; \
done

.PHONY: all gen gen-client copy-proto
45 changes: 37 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ sudo apt install python3.X-dev
sudo dnf install python3-devel
```
### Quick Start
Installation
```bash
pip install injective-py
```
Example usage
```python
import injective.chain_client
import injective.exchange_api
from pyinjective.composer import Composer as ProtoMsgComposer
from pyinjective.client import Client
from pyinjective.transaction import Transaction
from pyinjective.constant import Network
from pyinjective.wallet import PrivateKey, PublicKey, Address

```

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

### Development
1. Generate proto binding & build

To copy proto schemas and regenerate GRPC clients:
```
make gen
python -m build
```

```bash
$ pipenv shell
$ pipenv install --dev
1. Enable dev env
```
pipenv shell
pipenv install --dev
```

1. Install pkg
```
# from local build
pip uninstall injective-py
pip install injective-py --no-index --find-links /path/to/injective/sdk-python/dist
# from pypi
pip uninstall injective-py
pip install injective-py
```

$ make copy-proto
$ make gen
1. Fetch latest denom config
```
python pyinjective/fetch_metadata.py
```

1. Run an example
```
python examples/chain_client_examples/1_CosmosBankMsgSend.py
```

## License
Expand Down
216 changes: 53 additions & 163 deletions examples/chain_client_examples/1_CosmosBankMsgSend.py
Original file line number Diff line number Diff line change
@@ -1,176 +1,66 @@
# import sys
# sys.path.insert(0, '/Users/nam/desktop/injective/sdk-python/')

import asyncio
import aiohttp
import logging
import json
import base64
import ecdsa
import sha3
import grpc

from typing import Any, Dict, List
from injective.chain_client._wallet import (
generate_wallet,
privkey_to_address,
privkey_to_pubkey,
pubkey_to_address,
seed_to_privkey,
DEFAULT_BECH32_HRP,
)
from injective.chain_client._typings import SyncMode



MIN_GAS_PRICE = 500000000

class Transaction:

def __init__(
self,
*,
privkey: bytes,
account_num: int,
sequence: int,
fee: int,
gas: int,
fee_denom: str = "inj",
memo: str = "",
chain_id: str = "injective-888",
hrp: str = DEFAULT_BECH32_HRP,
sync_mode: SyncMode = "block",
) -> None:
self._privkey = privkey
self._account_num = account_num
self._sequence = sequence
self._fee = fee
self._fee_denom = fee_denom
self._gas = gas
self._memo = memo
self._chain_id = chain_id
self._hrp = hrp
self._sync_mode = sync_mode
self._msgs: List[dict] = []

def add_cosmos_bank_msg_send(self, recipient: str, amount: int, denom: str = "inj") -> None:
msg = {
"type": "cosmos-sdk/MsgSend",
"value": {
"from_address": privkey_to_address(self._privkey, hrp=self._hrp),
"to_address": recipient,
"amount": [{"denom": denom, "amount": str(amount)}],
},
}
self._msgs.append(msg)

def get_signed(self) -> str:
pubkey = privkey_to_pubkey(self._privkey)
base64_pubkey = base64.b64encode(pubkey).decode("utf-8")
signed_tx = {
"tx": {
"msg": self._msgs,
"fee": {
"gas": str(self._gas),
"amount": [{"denom": self._fee_denom, "amount": str(self._fee)}],
},
"memo": self._memo,
"signatures": [
{
"signature": self._sign(),
"pub_key": {"type": "injective/PubKeyEthSecp256k1", "value": base64_pubkey},
"account_number": str(self._account_num),
"sequence": str(self._sequence),
}
],
},
"mode": self._sync_mode,
}
return json.dumps(signed_tx, separators=(",", ":"))

def _sign(self) -> str:
message_str = json.dumps(
self._get_sign_message(), separators=(",", ":"), sort_keys=True)
message_bytes = message_str.encode("utf-8")

privkey = ecdsa.SigningKey.from_string(
self._privkey, curve=ecdsa.SECP256k1)
signature_compact_keccak = privkey.sign_deterministic(
message_bytes, hashfunc=sha3.keccak_256, sigencode=ecdsa.util.sigencode_string_canonize
)
signature_base64_str = base64.b64encode(
signature_compact_keccak).decode("utf-8")
return signature_base64_str

def _get_sign_message(self) -> Dict[str, Any]:
return {
"chain_id": self._chain_id,
"account_number": str(self._account_num),
"fee": {
"gas": str(self._gas),
"amount": [{"amount": str(self._fee), "denom": self._fee_denom}],
},
"memo": self._memo,
"sequence": str(self._sequence),
"msgs": self._msgs,
}
from pyinjective.composer import Composer as ProtoMsgComposer
from pyinjective.client import Client
from pyinjective.transaction import Transaction
from pyinjective.constant import Network
from pyinjective.wallet import PrivateKey, PublicKey, Address

async def main() -> None:
sender_pk = seed_to_privkey(
"physical page glare junk return scale subject river token door mirror title"
)
sender_acc_addr = privkey_to_address(sender_pk)
print("Sender Account:", sender_acc_addr)

acc_num, acc_seq = await get_account_num_seq(sender_acc_addr)

tx = Transaction(
privkey=sender_pk,
account_num=acc_num,
sequence=acc_seq,
gas=200000,
fee=200000 * MIN_GAS_PRICE,
sync_mode="block",
# select network: localhost, testnet, mainnet
network = Network.testnet()

# initialize grpc client
client = Client(network.grpc_endpoint, insecure=True)

# load account
priv_key = PrivateKey.from_hex("f9db9bf330e23cb7839039e944adef6e9df447b90b503d5b4464c90bea9022f3")
pub_key = priv_key.to_public_key()
address = pub_key.to_address()

# prepare tx msg
msg = ProtoMsgComposer.MsgSend(
from_address=address.to_acc_bech32(),
to_address='inj14au322k9munkmx5wrchz9q30juf5wjgz2cfqku',
amount=1000000000000000000,
denom='inj'
)
tx.add_cosmos_bank_msg_send(
recipient="inj1qy69k458ppmj45c3vqwcd6wvlcuvk23x0hsz58",
amount=10000000000000000,
denom="inj",
acc_num, acc_seq = await address.get_num_seq(network.lcd_endpoint)
gas_price = 500000000
gas_limit = 200000
fee = [ProtoMsgComposer.Coin(
amount=str(gas_price * gas_limit),
denom=network.fee_denom,
)]

# build tx
tx = (
Transaction()
.with_messages(msg)
.with_sequence(acc_seq)
.with_account_num(acc_num)
.with_chain_id(network.chain_id)
.with_gas(gas_limit)
.with_fee(fee)
.with_memo("")
.with_timeout_height(0)
)

tx_json = tx.get_signed()

print("Signed Tx:", tx_json)
print("Sent Tx:", await post_tx(tx_json))

async def get_account_num_seq(address: str) -> (int, int):
async with aiohttp.ClientSession() as session:
async with session.request(
'GET', 'http://staking-lcd-testnet.injective.network/cosmos/auth/v1beta1/accounts/' + address,
headers={'Accept-Encoding': 'application/json'},
) as response:
if response.status != 200:
print(await response.text())
raise ValueError("HTTP response status", response.status)

resp = json.loads(await response.text())
acc = resp['account']['base_account']
return acc['account_number'], acc['sequence']

async def post_tx(tx_json: str):
async with aiohttp.ClientSession() as session:
async with session.request(
'POST', 'http://staking-lcd-testnet.injective.network/txs', data=tx_json,
headers={'Content-Type': 'application/json'},
) as response:
if response.status != 200:
print(await response.text())
raise ValueError("HTTP response status", response.status)
# build signed tx
sign_doc = tx.get_sign_doc(pub_key)
sig = priv_key.sign(sign_doc.SerializeToString())
tx_raw_bytes = tx.get_tx_data(sig, pub_key)

resp = json.loads(await response.text())
if 'code' in resp:
print("Response:", resp)
raise ValueError('sdk error %d: %s' % (resp['code'], resp['raw_log']))
# broadcast tx: send_tx_async_mode, send_tx_sync_mode, send_tx_block_mode
res = client.send_tx_block_mode(tx_raw_bytes)

return resp['txhash']
# print tx response
print(res)

if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.get_event_loop().run_until_complete(main())
asyncio.get_event_loop().run_until_complete(main())
Loading

0 comments on commit 124f8d1

Please sign in to comment.