Skip to content

Commit f06600a

Browse files
authored
Merge pull request #415 from InjectiveLabs/ic-1055/create-python-sdk-final-release-for-v120-testnet
[IC-1055] create-python-sdk-final-release-for-v120-testnet
2 parents c3d1dd1 + 54470d4 commit f06600a

38 files changed

Lines changed: 2216 additions & 1103 deletions

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [v1.15.0]
6+
### Added
7+
- Added support in v2 Composer for the new exchange module MsgBatchLiquidatePositions message, including the `liquidate_position_data` and `msg_batch_liquidate_positions` composer helpers and a corresponding example script
8+
- Exposed `OrderType`, `OracleType`, and `CrossMarginEligibility` proto enums as `IntEnum` class attributes on the v2 `Composer` (`Composer.ORDER_TYPE`, `Composer.ORACLE_TYPE`, `Composer.CROSS_MARGIN_ELIGIBILITY`) for IDE discoverability and type safety. The `order_type` and `oracle_type` parameters in composer methods now accept either the string name or an integer / enum value (backward-compatible); `cross_margin_eligibility` (newly introduced this release) accepts only the `Composer.CROSS_MARGIN_ELIGIBILITY` enum.
9+
10+
### Changed
11+
- Updated all compiled protos for compatibility with Injective core v1.20.0 and Indexer v1.19.41
12+
513
## [1.14.1] - 2026-04-29
614
### Changed
715
- Update Python version limitation to ">=3.10,<3.15" to support Python v1.14

MAINTAINERS.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Maintainers Guide
2+
3+
This document describes maintenance workflows for SDK contributors and maintainers.
4+
5+
---
6+
7+
## Prerequisites
8+
9+
The following tools must be installed before running any maintenance commands.
10+
11+
| Tool | Purpose | Install (macOS) |
12+
|------|---------|-----------------|
13+
| `buf` | Proto code generation | `brew install bufbuild/buf/buf` |
14+
| `git` | Repository operations | `brew install git` |
15+
| `make` | Task runner | bundled with Xcode CLT |
16+
| `poetry` | Python packaging | [python-poetry.org/docs](https://python-poetry.org/docs/#installation) |
17+
| Python 3.10+ | Runtime | `brew install python` |
18+
19+
> **macOS only**: The `fix-generated-proto-imports` step inside `make gen` uses the BSD `sed -i ""` syntax. On Linux, `sed -i ""` must be replaced with `sed -i`. All maintainers are expected to run proto generation on macOS or adapt the command in the `Makefile` accordingly.
20+
21+
---
22+
23+
## Regenerating the proto bindings
24+
25+
The generated Python bindings in `pyinjective/proto/` are produced from `.proto` source files pulled from several upstream repositories.
26+
27+
### Step 1 — Update version references
28+
29+
Two files control which upstream versions are used:
30+
31+
**`Makefile`** — controls the injective-indexer gRPC proto files:
32+
33+
```makefile
34+
clone-injective-indexer:
35+
git clone https://github.com/InjectiveLabs/injective-indexer.git -b <tag> --depth 1 --single-branch
36+
```
37+
38+
Update the `-b` tag to the desired `injective-indexer` release (e.g. `v1.19.0`).
39+
40+
**`buf.gen.yaml`** controls all other proto sources via the `inputs:` block. Bump the relevant tags for:
41+
42+
- `injective-core` (most common change)
43+
- `ibc-go`, `wasmd`, `cometbft`, `cosmos-sdk` (when a protocol upgrade requires it)
44+
45+
Example `buf.gen.yaml` inputs entry to update:
46+
47+
```yaml
48+
- git_repo: https://github.com/InjectiveLabs/injective-core
49+
tag: v1.19.0
50+
subdir: proto
51+
```
52+
53+
### Step 2 — Run generation
54+
55+
```bash
56+
make gen
57+
```
58+
59+
This single command runs the full pipeline:
60+
61+
```mermaid
62+
flowchart LR
63+
bumpVersions["Bump tags in Makefile + buf.gen.yaml"] --> makeGen["make gen"]
64+
makeGen --> cloneAll["clone-all\n(injective-indexer)"]
65+
cloneAll --> copyProto["copy-proto\n(.proto → proto/exchange/)"]
66+
copyProto --> bufGen["buf generate\n(buf.gen.yaml inputs)"]
67+
bufGen --> cleanup["remove proto/\nand injective-indexer/"]
68+
cleanup --> fixImports["fix-generated-proto-imports"]
69+
fixImports --> review["git diff + pytest"]
70+
```
71+
72+
**What each step does:**
73+
74+
1. `clone-all` — shallow-clones the `injective-indexer` repository at the configured tag.
75+
2. `copy-proto` — deletes the previous `pyinjective/proto/` tree, then copies all `.proto` files from the cloned indexer's `api/gen/grpc` directory into `proto/exchange/`.
76+
3. `buf generate` — runs the `buf` tool against `buf.gen.yaml`, pulling proto sources from the BSR (Buf Schema Registry) and the `git_repo` inputs, then emitting Python and gRPC stubs into `pyinjective/proto/`.
77+
4. Cleanup — removes the temporary `proto/` and `injective-indexer/` directories.
78+
5. `fix-generated-proto-imports` — rewrites bare import paths (e.g. `from cosmos`) in every generated `.py` file to their package-qualified equivalents (e.g. `from pyinjective.proto.cosmos`). This step covers the modules listed in `PROTO_MODULES` in the `Makefile` plus `google.api`.
79+
80+
### Step 3 — Verify and commit
81+
82+
```bash
83+
git diff pyinjective/proto/ # review the generated diff
84+
poetry run pytest -v # run the full test suite
85+
```
86+
87+
Commit the `Makefile`, `buf.gen.yaml`, and all updated `pyinjective/proto/**` files together in a single commit.
88+
89+
### Troubleshooting
90+
91+
| Problem | Fix |
92+
|---------|-----|
93+
| Stale `injective-indexer/` or `proto/` directories from a failed previous run | `make clean-all` |
94+
| `buf generate` fails with auth errors on private BSR repos | Log in with `buf registry login` |
95+
| Import errors after generation | Rerun `make fix-generated-proto-imports` manually to isolate the issue |
96+
97+
---
98+
99+
## Regenerating `pyinjective/ofac.json`
100+
101+
The `pyinjective/ofac.json` file is the local snapshot of the OFAC and restricted-wallet list used by `OfacChecker`. Its upstream source is:
102+
103+
```
104+
https://raw.githubusercontent.com/InjectiveLabs/injective-lists/refs/heads/master/json/wallets/ofacAndRestricted.json
105+
```
106+
107+
Refresh the snapshot with:
108+
109+
```bash
110+
make gen-ofac
111+
```
112+
113+
This calls `poetry run python pyinjective/ofac.py`, which downloads the latest list from the URL above and overwrites `pyinjective/ofac.json`.
114+
115+
**When to refresh:** before each release, and whenever the upstream `injective-lists` repository publishes a significant update to the wallet list.
116+
117+
Commit the updated `pyinjective/ofac.json` together with other release preparation changes.
118+
119+
---
120+
121+
## Bumping the package version
122+
123+
The `version` field in `pyproject.toml` is the exact string that Poetry uses as the package name on PyPI when publishing. Every published release must have a unique version.
124+
125+
```toml
126+
[tool.poetry]
127+
name = "injective-py"
128+
version = "1.15.0" # ← update this before releasing
129+
```
130+
131+
**Versioning conventions used in this project:**
132+
133+
| Suffix | Meaning | Example |
134+
|--------|---------|---------|
135+
| `X.Y.Z` | Stable production release | `1.15.0` |
136+
| `X.Y.Z-rcN` | Release candidate | `1.15.0-rc1` |
137+
138+
Keep the `pyproject.toml` version bump in the same commit as the corresponding `CHANGELOG.md` entry so both are always in sync.
139+
140+
---
141+
142+
## Release workflow
143+
144+
Publishing to PyPI is fully automated via [`.github/workflows/release.yml`](.github/workflows/release.yml).
145+
146+
### How the workflow is triggered
147+
148+
The workflow fires on GitHub **`release: published`** events. This means it runs only when a maintainer explicitly publishes a GitHub Release — not on plain tag pushes or branch merges.
149+
150+
### What the workflow does
151+
152+
1. Checks out the repository at the commit the GitHub Release points to.
153+
2. Installs Python and Poetry on `ubuntu-latest`.
154+
3. Runs `poetry publish --build`, which builds the distribution and pushes it to PyPI using the `PYPI_API_TOKEN` repository secret.
155+
156+
> **Important:** The `pyproject.toml` version in the commit targeted by the GitHub Release is what gets published. If the version was not bumped before creating the release, the wrong version will be pushed to PyPI (and PyPI will reject a re-upload of an already-existing version).
157+
158+
### Operational notes
159+
160+
- The `PYPI_API_TOKEN` secret must remain valid on the repository. Rotating it when expired is a maintainer responsibility.
161+
- The release workflow does **not** run tests. Tests run automatically on every PR and merge via `run-tests.yml` and `pre-commit.yml` — ensure the branch is green before cutting a release.
162+
- GitHub pre-releases (marked as such in the UI) still trigger `release: published`, so `-rc*` versions are published to PyPI the same way as stable ones.
163+
164+
---
165+
166+
## Release checklist
167+
168+
Follow these steps in order when cutting a new SDK release:
169+
170+
1. **Bump proto versions** — update the `injective-indexer` tag in `Makefile` and the relevant tags in `buf.gen.yaml`.
171+
2. **Regenerate protos** — run `make gen` and verify `git diff pyinjective/proto/` looks correct.
172+
3. **Refresh OFAC list** — run `make gen-ofac`.
173+
4. **Bump package version** — update `version` in `pyproject.toml` following the `X.Y.Z` / `X.Y.Z-rcN` convention.
174+
5. **Update CHANGELOG** — add a release entry to `CHANGELOG.md` in the same commit as the version bump.
175+
6. **Run tests**`poetry run pytest -v` must pass locally.
176+
7. **Open a PR** — get the changes reviewed and merged into the target branch.
177+
8. **Create a git tag** — tag the merge commit with the release version (e.g. `v1.15.0`).
178+
9. **Publish a GitHub Release** — point it at the tag, paste the `CHANGELOG.md` entry as release notes, and click **Publish release**. The CI workflow takes care of building and uploading the package to PyPI automatically.
179+
10. **Verify** — monitor the `Publish Python distribution to PyPI` action in the Actions tab until it completes successfully.

Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ clean-all:
2626
$(call clean_repos)
2727

2828
clone-injective-indexer:
29-
git clone https://github.com/InjectiveLabs/injective-indexer.git -b v1.19.0 --depth 1 --single-branch
29+
git clone https://github.com/InjectiveLabs/injective-indexer.git -b v1.19.41 --depth 1 --single-branch
3030

3131
clone-all: clone-injective-indexer
3232

@@ -35,7 +35,10 @@ copy-proto:
3535
mkdir -p proto/exchange
3636
find ./injective-indexer/api/gen/grpc -type f -name "*.proto" -exec cp {} ./proto/exchange/ \;
3737

38+
gen-ofac:
39+
poetry run python pyinjective/ofac.py
40+
3841
tests:
3942
poetry run pytest -v
4043

41-
.PHONY: all gen gen-client copy-proto tests
44+
.PHONY: all gen gen-client copy-proto gen-ofac tests

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ Upgrade `pip` to the latest version, if you see these warnings:
7070
poetry run pytest -v
7171
```
7272

73+
> **Maintainers:** see [MAINTAINERS.md](MAINTAINERS.md) for how to regenerate proto bindings, refresh `pyinjective/ofac.json`, and cut a new release.
74+
7375
---
7476

7577
## Async client (exchange V2)

buf.gen.yaml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,8 @@ inputs:
1919
tag: v1.0.1-inj.7
2020
- git_repo: https://github.com/InjectiveLabs/cosmos-sdk
2121
tag: v0.50.14-inj.9
22-
# - git_repo: https://github.com/InjectiveLabs/wasmd
23-
# branch: v0.51.x-inj
24-
# subdir: proto
25-
# - git_repo: https://github.com/InjectiveLabs/hyperlane-cosmos
26-
# tag: v1.0.1-inj
27-
# subdir: proto
2822
- git_repo: https://github.com/InjectiveLabs/injective-core
29-
tag: v1.19.0
23+
tag: v1.20.0-alpha.3
3024
subdir: proto
3125
# - git_repo: https://github.com/InjectiveLabs/injective-core
3226
# branch: c-655/add_chainlink_data_streams_oracle

examples/chain_client/exchange/25_MsgUpdateDerivativeMarket.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ async def main() -> None:
5353
new_maintenance_margin_ratio=Decimal("0.085"),
5454
new_reduce_margin_ratio=Decimal("3.5"),
5555
new_open_notional_cap=composer.uncapped_open_notional_cap(),
56+
cross_margin_eligibility=composer.CROSS_MARGIN_ELIGIBILITY.CM_ELIGIBILITY_INELIGIBLE,
5657
)
5758

5859
# broadcast the transaction
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import asyncio
2+
import json
3+
import os
4+
import uuid
5+
from decimal import Decimal
6+
7+
import dotenv
8+
9+
from pyinjective.async_client_v2 import AsyncClient
10+
from pyinjective.core.broadcaster import MsgBroadcasterWithPk
11+
from pyinjective.core.network import Network
12+
from pyinjective.wallet import PrivateKey
13+
14+
15+
async def main() -> None:
16+
dotenv.load_dotenv()
17+
private_key_in_hexa = os.getenv("INJECTIVE_PRIVATE_KEY")
18+
19+
# select network: local, testnet, mainnet
20+
network = Network.testnet()
21+
22+
# initialize grpc client
23+
client = AsyncClient(network)
24+
composer = await client.composer()
25+
26+
gas_price = await client.current_chain_gas_price()
27+
# adjust gas price to make it valid even if it changes between the time it is requested and the TX is broadcasted
28+
gas_price = int(gas_price * 1.1)
29+
30+
message_broadcaster = MsgBroadcasterWithPk.new_using_gas_heuristics(
31+
network=network,
32+
private_key=private_key_in_hexa,
33+
gas_price=gas_price,
34+
client=client,
35+
composer=composer,
36+
)
37+
38+
priv_key = PrivateKey.from_hex(private_key_in_hexa)
39+
pub_key = priv_key.to_public_key()
40+
address = pub_key.to_address()
41+
subaccount_id = address.get_subaccount_id(index=0)
42+
43+
market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6"
44+
fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r"
45+
cid = str(uuid.uuid4())
46+
47+
order = composer.derivative_order(
48+
market_id=market_id,
49+
subaccount_id=subaccount_id,
50+
fee_recipient=fee_recipient,
51+
price=Decimal("39.01"), # This should be the liquidation price
52+
quantity=Decimal("0.147"),
53+
margin=Decimal("5.73447"),
54+
order_type="SELL",
55+
cid=cid,
56+
)
57+
58+
# Build individual liquidation entries; order is optional per-entry
59+
liquidation_with_order = composer.liquidate_position_data(
60+
subaccount_id="0x156df4d5bc8e7dd9191433e54bd6a11eeb390921000000000000000000000000",
61+
market_id=market_id,
62+
order=order,
63+
)
64+
liquidation_without_order = composer.liquidate_position_data(
65+
subaccount_id="0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000",
66+
market_id=market_id,
67+
)
68+
69+
# prepare tx msg
70+
msg = composer.msg_batch_liquidate_positions(
71+
sender=address.to_acc_bech32(),
72+
liquidations=[liquidation_with_order, liquidation_without_order],
73+
)
74+
75+
# broadcast the transaction
76+
result = await message_broadcaster.broadcast([msg])
77+
print("---Transaction Response---")
78+
print(json.dumps(result, indent=2))
79+
80+
gas_price = await client.current_chain_gas_price()
81+
# adjust gas price to make it valid even if it changes between the time it is requested and the TX is broadcasted
82+
gas_price = int(gas_price * 1.1)
83+
message_broadcaster.update_gas_price(gas_price=gas_price)
84+
85+
86+
if __name__ == "__main__":
87+
asyncio.get_event_loop().run_until_complete(main())

examples/chain_client/exchange/4_MsgInstantPerpetualMarketLaunch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ async def main() -> None:
5959
min_quantity_tick_size=Decimal("0.01"),
6060
min_notional=Decimal("1"),
6161
open_notional_cap=composer.uncapped_open_notional_cap(),
62+
cross_margin_eligible=False,
6263
)
6364

6465
# broadcast the transaction

examples/chain_client/exchange/5_MsgInstantExpiryFuturesMarketLaunch.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,12 @@ async def main() -> None:
5555
taker_fee_rate=Decimal("0.001"),
5656
initial_margin_ratio=Decimal("0.33"),
5757
maintenance_margin_ratio=Decimal("0.095"),
58+
reduce_margin_ratio=Decimal("3"),
5859
min_price_tick_size=Decimal("0.001"),
5960
min_quantity_tick_size=Decimal("0.01"),
6061
min_notional=Decimal("1"),
6162
open_notional_cap=composer.uncapped_open_notional_cap(),
63+
cross_margin_eligible=False,
6264
)
6365

6466
# broadcast the transaction

0 commit comments

Comments
 (0)