Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## [v1.15.0]
### Added
- 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
- 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.

### Changed
- Updated all compiled protos for compatibility with Injective core v1.20.0 and Indexer v1.19.41

## [1.14.1] - 2026-04-29
### Changed
- Update Python version limitation to ">=3.10,<3.15" to support Python v1.14
Expand Down
179 changes: 179 additions & 0 deletions MAINTAINERS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Maintainers Guide

This document describes maintenance workflows for SDK contributors and maintainers.

---

## Prerequisites

The following tools must be installed before running any maintenance commands.

| Tool | Purpose | Install (macOS) |
|------|---------|-----------------|
| `buf` | Proto code generation | `brew install bufbuild/buf/buf` |
| `git` | Repository operations | `brew install git` |
| `make` | Task runner | bundled with Xcode CLT |
| `poetry` | Python packaging | [python-poetry.org/docs](https://python-poetry.org/docs/#installation) |
| Python 3.10+ | Runtime | `brew install python` |

> **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.

---

## Regenerating the proto bindings

The generated Python bindings in `pyinjective/proto/` are produced from `.proto` source files pulled from several upstream repositories.

### Step 1 — Update version references

Two files control which upstream versions are used:

**`Makefile`** — controls the injective-indexer gRPC proto files:

```makefile
clone-injective-indexer:
git clone https://github.com/InjectiveLabs/injective-indexer.git -b <tag> --depth 1 --single-branch
```

Update the `-b` tag to the desired `injective-indexer` release (e.g. `v1.19.0`).

**`buf.gen.yaml`** — controls all other proto sources via the `inputs:` block. Bump the relevant tags for:

- `injective-core` (most common change)
- `ibc-go`, `wasmd`, `cometbft`, `cosmos-sdk` (when a protocol upgrade requires it)

Example `buf.gen.yaml` inputs entry to update:

```yaml
- git_repo: https://github.com/InjectiveLabs/injective-core
tag: v1.19.0
subdir: proto
```

### Step 2 — Run generation

```bash
make gen
```

This single command runs the full pipeline:

```mermaid
flowchart LR
bumpVersions["Bump tags in Makefile + buf.gen.yaml"] --> makeGen["make gen"]
makeGen --> cloneAll["clone-all\n(injective-indexer)"]
cloneAll --> copyProto["copy-proto\n(.proto → proto/exchange/)"]
copyProto --> bufGen["buf generate\n(buf.gen.yaml inputs)"]
bufGen --> cleanup["remove proto/\nand injective-indexer/"]
cleanup --> fixImports["fix-generated-proto-imports"]
fixImports --> review["git diff + pytest"]
```

**What each step does:**

1. `clone-all` — shallow-clones the `injective-indexer` repository at the configured tag.
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/`.
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/`.
4. Cleanup — removes the temporary `proto/` and `injective-indexer/` directories.
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`.

### Step 3 — Verify and commit

```bash
git diff pyinjective/proto/ # review the generated diff
poetry run pytest -v # run the full test suite
```

Commit the `Makefile`, `buf.gen.yaml`, and all updated `pyinjective/proto/**` files together in a single commit.

### Troubleshooting

| Problem | Fix |
|---------|-----|
| Stale `injective-indexer/` or `proto/` directories from a failed previous run | `make clean-all` |
| `buf generate` fails with auth errors on private BSR repos | Log in with `buf registry login` |
| Import errors after generation | Rerun `make fix-generated-proto-imports` manually to isolate the issue |

---

## Regenerating `pyinjective/ofac.json`

The `pyinjective/ofac.json` file is the local snapshot of the OFAC and restricted-wallet list used by `OfacChecker`. Its upstream source is:

```
https://raw.githubusercontent.com/InjectiveLabs/injective-lists/refs/heads/master/json/wallets/ofacAndRestricted.json
```

Refresh the snapshot with:

```bash
make gen-ofac
```

This calls `poetry run python pyinjective/ofac.py`, which downloads the latest list from the URL above and overwrites `pyinjective/ofac.json`.

**When to refresh:** before each release, and whenever the upstream `injective-lists` repository publishes a significant update to the wallet list.

Commit the updated `pyinjective/ofac.json` together with other release preparation changes.

---

## Bumping the package version

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.

```toml
[tool.poetry]
name = "injective-py"
version = "1.15.0" # ← update this before releasing
```

**Versioning conventions used in this project:**

| Suffix | Meaning | Example |
|--------|---------|---------|
| `X.Y.Z` | Stable production release | `1.15.0` |
| `X.Y.Z-rcN` | Release candidate | `1.15.0-rc1` |

Keep the `pyproject.toml` version bump in the same commit as the corresponding `CHANGELOG.md` entry so both are always in sync.

---

## Release workflow

Publishing to PyPI is fully automated via [`.github/workflows/release.yml`](.github/workflows/release.yml).

### How the workflow is triggered

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.

### What the workflow does

1. Checks out the repository at the commit the GitHub Release points to.
2. Installs Python and Poetry on `ubuntu-latest`.
3. Runs `poetry publish --build`, which builds the distribution and pushes it to PyPI using the `PYPI_API_TOKEN` repository secret.

> **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).

### Operational notes

- The `PYPI_API_TOKEN` secret must remain valid on the repository. Rotating it when expired is a maintainer responsibility.
- 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.
- 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.

---

## Release checklist

Follow these steps in order when cutting a new SDK release:

1. **Bump proto versions** — update the `injective-indexer` tag in `Makefile` and the relevant tags in `buf.gen.yaml`.
2. **Regenerate protos** — run `make gen` and verify `git diff pyinjective/proto/` looks correct.
3. **Refresh OFAC list** — run `make gen-ofac`.
4. **Bump package version** — update `version` in `pyproject.toml` following the `X.Y.Z` / `X.Y.Z-rcN` convention.
5. **Update CHANGELOG** — add a release entry to `CHANGELOG.md` in the same commit as the version bump.
6. **Run tests** — `poetry run pytest -v` must pass locally.
7. **Open a PR** — get the changes reviewed and merged into the target branch.
8. **Create a git tag** — tag the merge commit with the release version (e.g. `v1.15.0`).
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.
10. **Verify** — monitor the `Publish Python distribution to PyPI` action in the Actions tab until it completes successfully.
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ clean-all:
$(call clean_repos)

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

clone-all: clone-injective-indexer

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

gen-ofac:
poetry run python pyinjective/ofac.py

tests:
poetry run pytest -v

.PHONY: all gen gen-client copy-proto tests
.PHONY: all gen gen-client copy-proto gen-ofac tests
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ Upgrade `pip` to the latest version, if you see these warnings:
poetry run pytest -v
```

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

---

## Async client (exchange V2)
Expand Down
8 changes: 1 addition & 7 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,8 @@ inputs:
tag: v1.0.1-inj.7
- git_repo: https://github.com/InjectiveLabs/cosmos-sdk
tag: v0.50.14-inj.9
# - git_repo: https://github.com/InjectiveLabs/wasmd
# branch: v0.51.x-inj
# subdir: proto
# - git_repo: https://github.com/InjectiveLabs/hyperlane-cosmos
# tag: v1.0.1-inj
# subdir: proto
- git_repo: https://github.com/InjectiveLabs/injective-core
tag: v1.19.0
tag: v1.20.0-alpha.3
subdir: proto
# - git_repo: https://github.com/InjectiveLabs/injective-core
# branch: c-655/add_chainlink_data_streams_oracle
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def main() -> None:
new_maintenance_margin_ratio=Decimal("0.085"),
new_reduce_margin_ratio=Decimal("3.5"),
new_open_notional_cap=composer.uncapped_open_notional_cap(),
cross_margin_eligibility=composer.CROSS_MARGIN_ELIGIBILITY.CM_ELIGIBILITY_INELIGIBLE,
)

# broadcast the transaction
Expand Down
87 changes: 87 additions & 0 deletions examples/chain_client/exchange/31_MsgBatchLiquidatePositions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import asyncio
import json
import os
import uuid
from decimal import Decimal

import dotenv

from pyinjective.async_client_v2 import AsyncClient
from pyinjective.core.broadcaster import MsgBroadcasterWithPk
from pyinjective.core.network import Network
from pyinjective.wallet import PrivateKey


async def main() -> None:
dotenv.load_dotenv()
private_key_in_hexa = os.getenv("INJECTIVE_PRIVATE_KEY")

Comment on lines +17 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an explicit private key presence check before use.

If INJECTIVE_PRIVATE_KEY is unset, this example fails later with a less actionable error. Fail fast with a clear message.

Proposed fix
 async def main() -> None:
     dotenv.load_dotenv()
     private_key_in_hexa = os.getenv("INJECTIVE_PRIVATE_KEY")
+    if not private_key_in_hexa:
+        raise ValueError("INJECTIVE_PRIVATE_KEY is not set")

Also applies to: 30-33, 38-38

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/chain_client/exchange/31_MsgBatchLiquidatePositions.py` around lines
17 - 18, The code reads INJECTIVE_PRIVATE_KEY into private_key_in_hexa without
validating it; add an explicit presence check immediately after the getenv (for
variable private_key_in_hexa) and fail fast with a clear error (raise a
RuntimeError or call sys.exit(1) with a message like "INJECTIVE_PRIVATE_KEY
environment variable is required") so the script stops with an actionable
message; apply the same presence check pattern for the other similar getenv
usages around the 30-33 and 38 lines in this example.

# select network: local, testnet, mainnet
network = Network.testnet()

# initialize grpc client
client = AsyncClient(network)
composer = await client.composer()

gas_price = await client.current_chain_gas_price()
# adjust gas price to make it valid even if it changes between the time it is requested and the TX is broadcasted
gas_price = int(gas_price * 1.1)

message_broadcaster = MsgBroadcasterWithPk.new_using_gas_heuristics(
network=network,
private_key=private_key_in_hexa,
gas_price=gas_price,
client=client,
composer=composer,
)

priv_key = PrivateKey.from_hex(private_key_in_hexa)
pub_key = priv_key.to_public_key()
address = pub_key.to_address()
subaccount_id = address.get_subaccount_id(index=0)

market_id = "0x17ef48032cb24375ba7c2e39f384e56433bcab20cbee9a7357e4cba2eb00abe6"
fee_recipient = "inj1hkhdaj2a2clmq5jq6mspsggqs32vynpk228q3r"
cid = str(uuid.uuid4())

order = composer.derivative_order(
market_id=market_id,
subaccount_id=subaccount_id,
fee_recipient=fee_recipient,
price=Decimal("39.01"), # This should be the liquidation price
quantity=Decimal("0.147"),
margin=Decimal("5.73447"),
order_type="SELL",
cid=cid,
)

# Build individual liquidation entries; order is optional per-entry
liquidation_with_order = composer.liquidate_position_data(
subaccount_id="0x156df4d5bc8e7dd9191433e54bd6a11eeb390921000000000000000000000000",
market_id=market_id,
order=order,
)
liquidation_without_order = composer.liquidate_position_data(
subaccount_id="0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1000000000000000000000000",
market_id=market_id,
)

# prepare tx msg
msg = composer.msg_batch_liquidate_positions(
sender=address.to_acc_bech32(),
liquidations=[liquidation_with_order, liquidation_without_order],
)

# broadcast the transaction
result = await message_broadcaster.broadcast([msg])
print("---Transaction Response---")
print(json.dumps(result, indent=2))

gas_price = await client.current_chain_gas_price()
# adjust gas price to make it valid even if it changes between the time it is requested and the TX is broadcasted
gas_price = int(gas_price * 1.1)
message_broadcaster.update_gas_price(gas_price=gas_price)


if __name__ == "__main__":
asyncio.get_event_loop().run_until_complete(main())
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ async def main() -> None:
min_quantity_tick_size=Decimal("0.01"),
min_notional=Decimal("1"),
open_notional_cap=composer.uncapped_open_notional_cap(),
cross_margin_eligible=False,
)

# broadcast the transaction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ async def main() -> None:
taker_fee_rate=Decimal("0.001"),
initial_margin_ratio=Decimal("0.33"),
maintenance_margin_ratio=Decimal("0.095"),
reduce_margin_ratio=Decimal("3"),
min_price_tick_size=Decimal("0.001"),
min_quantity_tick_size=Decimal("0.01"),
min_notional=Decimal("1"),
open_notional_cap=composer.uncapped_open_notional_cap(),
cross_margin_eligible=False,
)

# broadcast the transaction
Expand Down
Loading
Loading