Skip to content

Commit 3165d58

Browse files
authored
Merge pull request #317 from InjectiveLabs/feat/add_ibc_transfer_support
feat/add_ibc_transfer_support
2 parents 1fae35f + 3d9761d commit 3165d58

13 files changed

Lines changed: 561 additions & 2 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ All notable changes to this project will be documented in this file.
55
## [1.5.0] - 9999-99-99
66
### Added
77
- Added support for all queries in the chain 'tendermint' module
8+
- Added support for all queries in the IBC Transfer module
89

910
### Changed
1011
- Refactored cookies management logic to use all gRPC calls' responses to update the current cookies
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import asyncio
2+
import os
3+
from decimal import Decimal
4+
5+
import dotenv
6+
7+
from pyinjective.async_client import AsyncClient
8+
from pyinjective.core.broadcaster import MsgBroadcasterWithPk
9+
from pyinjective.core.network import Network
10+
from pyinjective.wallet import PrivateKey
11+
12+
13+
async def main() -> None:
14+
dotenv.load_dotenv()
15+
configured_private_key = os.getenv("INJECTIVE_PRIVATE_KEY")
16+
17+
# select network: local, testnet, mainnet
18+
network = Network.testnet()
19+
20+
# initialize grpc client
21+
client = AsyncClient(network)
22+
await client.initialize_tokens_from_chain_denoms()
23+
composer = await client.composer()
24+
await client.sync_timeout_height()
25+
26+
message_broadcaster = MsgBroadcasterWithPk.new_using_simulation(
27+
network=network,
28+
private_key=configured_private_key,
29+
)
30+
31+
# load account
32+
priv_key = PrivateKey.from_hex(configured_private_key)
33+
pub_key = priv_key.to_public_key()
34+
address = pub_key.to_address()
35+
await client.fetch_account(address.to_acc_bech32())
36+
37+
source_port = "transfer"
38+
source_channel = "channel-126"
39+
token_amount = composer.create_coin_amount(amount=Decimal("0.1"), token_name="INJ")
40+
sender = address.to_acc_bech32()
41+
receiver = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r"
42+
timeout_height = 10
43+
44+
# prepare tx msg
45+
message = composer.msg_ibc_transfer(
46+
source_port=source_port,
47+
source_channel=source_channel,
48+
token_amount=token_amount,
49+
sender=sender,
50+
receiver=receiver,
51+
timeout_height=timeout_height,
52+
)
53+
54+
# broadcast the transaction
55+
result = await message_broadcaster.broadcast([message])
56+
print("---Transaction Response---")
57+
print(result)
58+
59+
60+
if __name__ == "__main__":
61+
asyncio.get_event_loop().run_until_complete(main())
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import asyncio
2+
from hashlib import sha256
3+
4+
from google.protobuf import symbol_database
5+
6+
from pyinjective.async_client import AsyncClient
7+
from pyinjective.core.network import Network
8+
9+
10+
async def main() -> None:
11+
network = Network.testnet()
12+
client = AsyncClient(network)
13+
14+
path = "transfer/channel-126"
15+
base_denom = "uluna"
16+
full_path = f"{path}/{base_denom}"
17+
path_hash = sha256(full_path.encode()).hexdigest()
18+
trace_hash = f"ibc/{path_hash}"
19+
20+
denom_trace = await client.fetch_denom_trace(hash=trace_hash)
21+
print(denom_trace)
22+
23+
24+
if __name__ == "__main__":
25+
symbol_db = symbol_database.Default()
26+
asyncio.get_event_loop().run_until_complete(main())
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import asyncio
2+
3+
from google.protobuf import symbol_database
4+
5+
from pyinjective.async_client import AsyncClient
6+
from pyinjective.client.model.pagination import PaginationOption
7+
from pyinjective.core.network import Network
8+
9+
10+
async def main() -> None:
11+
network = Network.testnet()
12+
client = AsyncClient(network)
13+
14+
pagination = PaginationOption(skip=2, limit=4)
15+
16+
denom_traces = await client.fetch_denom_traces(pagination=pagination)
17+
print(denom_traces)
18+
19+
20+
if __name__ == "__main__":
21+
symbol_db = symbol_database.Default()
22+
asyncio.get_event_loop().run_until_complete(main())
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import asyncio
2+
3+
from google.protobuf import symbol_database
4+
5+
from pyinjective.async_client import AsyncClient
6+
from pyinjective.core.network import Network
7+
8+
9+
async def main() -> None:
10+
network = Network.testnet()
11+
client = AsyncClient(network)
12+
13+
path = "transfer/channel-126"
14+
base_denom = "uluna"
15+
full_path = f"{path}/{base_denom}"
16+
17+
denom_hash = await client.fetch_denom_hash(trace=full_path)
18+
print(denom_hash)
19+
20+
21+
if __name__ == "__main__":
22+
symbol_db = symbol_database.Default()
23+
asyncio.get_event_loop().run_until_complete(main())
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import asyncio
2+
3+
from google.protobuf import symbol_database
4+
5+
from pyinjective.async_client import AsyncClient
6+
from pyinjective.core.network import Network
7+
8+
9+
async def main() -> None:
10+
network = Network.testnet()
11+
client = AsyncClient(network)
12+
13+
port_id = "transfer"
14+
channel_id = "channel-126"
15+
16+
escrow_address = await client.fetch_escrow_address(port_id=port_id, channel_id=channel_id)
17+
print(escrow_address)
18+
19+
20+
if __name__ == "__main__":
21+
symbol_db = symbol_database.Default()
22+
asyncio.get_event_loop().run_until_complete(main())
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import asyncio
2+
3+
from google.protobuf import symbol_database
4+
5+
from pyinjective.async_client import AsyncClient
6+
from pyinjective.core.network import Network
7+
8+
9+
async def main() -> None:
10+
network = Network.testnet()
11+
client = AsyncClient(network)
12+
13+
base_denom = "uluna"
14+
15+
escrow = await client.fetch_total_escrow_for_denom(denom=base_denom)
16+
print(escrow)
17+
18+
19+
if __name__ == "__main__":
20+
symbol_db = symbol_database.Default()
21+
asyncio.get_event_loop().run_until_complete(main())

pyinjective/async_client.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from pyinjective.core.market import BinaryOptionMarket, DerivativeMarket, SpotMarket
4040
from pyinjective.core.network import Network
4141
from pyinjective.core.token import Token
42+
from pyinjective.core.tx.grpc.ibc_transfer_grpc_api import IBCTransferGrpcApi
4243
from pyinjective.core.tx.grpc.tendermint_grpc_api import TendermintGrpcApi
4344
from pyinjective.core.tx.grpc.tx_grpc_api import TxGrpcApi
4445
from pyinjective.exceptions import NotFoundError
@@ -184,6 +185,10 @@ def __init__(
184185
channel=self.chain_channel,
185186
cookie_assistant=network.chain_cookie_assistant,
186187
)
188+
self.ibc_transfer_api = IBCTransferGrpcApi(
189+
channel=self.chain_channel,
190+
cookie_assistant=network.chain_cookie_assistant,
191+
)
187192
self.tendermint_api = TendermintGrpcApi(
188193
channel=self.chain_channel,
189194
cookie_assistant=network.chain_cookie_assistant,
@@ -3013,6 +3018,24 @@ async def listen_chain_stream_updates(
30133018
oracle_price_filter=oracle_price_filter,
30143019
)
30153020

3021+
# region IBC Apps module
3022+
async def fetch_denom_trace(self, hash: str) -> Dict[str, Any]:
3023+
return await self.ibc_transfer_api.fetch_denom_trace(hash=hash)
3024+
3025+
async def fetch_denom_traces(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]:
3026+
return await self.ibc_transfer_api.fetch_denom_traces(pagination=pagination)
3027+
3028+
async def fetch_denom_hash(self, trace: str) -> Dict[str, Any]:
3029+
return await self.ibc_transfer_api.fetch_denom_hash(trace=trace)
3030+
3031+
async def fetch_escrow_address(self, port_id: str, channel_id: str) -> Dict[str, Any]:
3032+
return await self.ibc_transfer_api.fetch_escrow_address(port_id=port_id, channel_id=channel_id)
3033+
3034+
async def fetch_total_escrow_for_denom(self, denom: str) -> Dict[str, Any]:
3035+
return await self.ibc_transfer_api.fetch_total_escrow_for_denom(denom=denom)
3036+
3037+
# endregion
3038+
30163039
async def composer(self):
30173040
return Composer(
30183041
network=self.network.string(),

pyinjective/composer.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from pyinjective.proto.cosmos.staking.v1beta1 import tx_pb2 as cosmos_staking_tx_pb
2323
from pyinjective.proto.cosmwasm.wasm.v1 import tx_pb2 as wasm_tx_pb
2424
from pyinjective.proto.exchange import injective_explorer_rpc_pb2 as explorer_pb2
25+
from pyinjective.proto.ibc.applications.transfer.v1 import tx_pb2 as ibc_transfer_tx_pb
26+
from pyinjective.proto.ibc.core.client.v1 import client_pb2 as ibc_core_client_pb
2527
from pyinjective.proto.injective.auction.v1beta1 import tx_pb2 as injective_auction_tx_pb
2628
from pyinjective.proto.injective.exchange.v1beta1 import (
2729
authz_pb2 as injective_authz_pb,
@@ -143,14 +145,14 @@ def Coin(self, amount: int, denom: str):
143145
warn("This method is deprecated. Use coin instead", DeprecationWarning, stacklevel=2)
144146
return base_coin_pb.Coin(amount=str(amount), denom=denom)
145147

146-
def coin(self, amount: int, denom: str):
148+
def coin(self, amount: int, denom: str) -> base_coin_pb.Coin:
147149
"""
148150
This method create an instance of Coin gRPC type, considering the amount is already expressed in chain format
149151
"""
150152
formatted_amount_string = str(int(amount))
151153
return base_coin_pb.Coin(amount=formatted_amount_string, denom=denom)
152154

153-
def create_coin_amount(self, amount: Decimal, token_name: str):
155+
def create_coin_amount(self, amount: Decimal, token_name: str) -> base_coin_pb.Coin:
154156
"""
155157
This method create an instance of Coin gRPC type, considering the amount is already expressed in chain format
156158
"""
@@ -2142,6 +2144,38 @@ def msg_update_distribution_params(self, authority: str, community_tax: str, wit
21422144
def msg_community_pool_spend(self, authority: str, recipient: str, amount: List[base_coin_pb.Coin]):
21432145
return cosmos_distribution_tx_pb.MsgCommunityPoolSpend(authority=authority, recipient=recipient, amount=amount)
21442146

2147+
# region IBC Transfer module
2148+
def msg_ibc_transfer(
2149+
self,
2150+
source_port: str,
2151+
source_channel: str,
2152+
token_amount: base_coin_pb.Coin,
2153+
sender: str,
2154+
receiver: str,
2155+
timeout_height: Optional[int] = None,
2156+
timeout_timestamp: Optional[int] = None,
2157+
memo: Optional[str] = None,
2158+
) -> ibc_transfer_tx_pb.MsgTransfer:
2159+
if timeout_height is None and timeout_timestamp is None:
2160+
raise ValueError("IBC Transfer error: Either timeout_height or timeout_timestamp must be provided")
2161+
parsed_timeout_height = None
2162+
if timeout_height:
2163+
parsed_timeout_height = ibc_core_client_pb.Height(
2164+
revision_number=timeout_height, revision_height=timeout_height
2165+
)
2166+
return ibc_transfer_tx_pb.MsgTransfer(
2167+
source_port=source_port,
2168+
source_channel=source_channel,
2169+
token=token_amount,
2170+
sender=sender,
2171+
receiver=receiver,
2172+
timeout_height=parsed_timeout_height,
2173+
timeout_timestamp=timeout_timestamp,
2174+
memo=memo,
2175+
)
2176+
2177+
# endregion
2178+
21452179
# data field format: [request-msg-header][raw-byte-msg-response]
21462180
# you need to figure out this magic prefix number to trim request-msg-header off the data
21472181
# this method handles only exchange responses
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from typing import Any, Callable, Dict, Optional
2+
3+
from grpc.aio import Channel
4+
5+
from pyinjective.client.model.pagination import PaginationOption
6+
from pyinjective.core.network import CookieAssistant
7+
from pyinjective.proto.ibc.applications.transfer.v1 import (
8+
query_pb2 as ibc_transfer_query,
9+
query_pb2_grpc as ibc_transfer_query_grpc,
10+
)
11+
from pyinjective.utils.grpc_api_request_assistant import GrpcApiRequestAssistant
12+
13+
14+
class IBCTransferGrpcApi:
15+
def __init__(self, channel: Channel, cookie_assistant: CookieAssistant):
16+
self._stub = ibc_transfer_query_grpc.QueryStub(channel)
17+
self._assistant = GrpcApiRequestAssistant(cookie_assistant=cookie_assistant)
18+
19+
async def fetch_params(self) -> Dict[str, Any]:
20+
request = ibc_transfer_query.QueryParamsRequest()
21+
response = await self._execute_call(call=self._stub.Params, request=request)
22+
23+
return response
24+
25+
async def fetch_denom_trace(self, hash: str) -> Dict[str, Any]:
26+
request = ibc_transfer_query.QueryDenomTraceRequest(hash=hash)
27+
response = await self._execute_call(call=self._stub.DenomTrace, request=request)
28+
29+
return response
30+
31+
async def fetch_denom_traces(self, pagination: Optional[PaginationOption] = None) -> Dict[str, Any]:
32+
if pagination is None:
33+
pagination = PaginationOption()
34+
request = ibc_transfer_query.QueryDenomTracesRequest(pagination=pagination.create_pagination_request())
35+
response = await self._execute_call(call=self._stub.DenomTraces, request=request)
36+
37+
return response
38+
39+
async def fetch_denom_hash(self, trace: str) -> Dict[str, Any]:
40+
request = ibc_transfer_query.QueryDenomHashRequest(trace=trace)
41+
response = await self._execute_call(call=self._stub.DenomHash, request=request)
42+
43+
return response
44+
45+
async def fetch_escrow_address(self, port_id: str, channel_id: str) -> Dict[str, Any]:
46+
request = ibc_transfer_query.QueryEscrowAddressRequest(port_id=port_id, channel_id=channel_id)
47+
response = await self._execute_call(call=self._stub.EscrowAddress, request=request)
48+
49+
return response
50+
51+
async def fetch_total_escrow_for_denom(self, denom: str) -> Dict[str, Any]:
52+
request = ibc_transfer_query.QueryTotalEscrowForDenomRequest(denom=denom)
53+
response = await self._execute_call(call=self._stub.TotalEscrowForDenom, request=request)
54+
55+
return response
56+
57+
async def _execute_call(self, call: Callable, request) -> Dict[str, Any]:
58+
return await self._assistant.execute_call(call=call, request=request)

0 commit comments

Comments
 (0)