Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
7d05f6c
feat(websocket): expose heartbeat events with improved implementation
0xferit Aug 10, 2025
f776336
"Claude PR Assistant workflow"
0xferit Aug 10, 2025
71e83c9
"Claude Code Review workflow"
0xferit Aug 10, 2025
f9807d3
feat: enforce POST_ONLY flag on all orders and funding offers
0xferit Aug 10, 2025
8f6c669
fix: close update-order bypass loophole
0xferit Aug 10, 2025
de37f13
chore: gitignore plans folder
0xferit Aug 10, 2025
658af09
fix(rest,ws): preserve flags on order update when omitted; enforce PO…
0xferit Aug 10, 2025
6e24bbe
fix(websocket): send flags in update_order payload when provided (kee…
0xferit Aug 10, 2025
f91eab9
fix: address critical issues from PR review
0xferit Aug 11, 2025
7edd591
fix: correct test assertions for keyword arguments
0xferit Aug 11, 2025
2b39722
fix: resolve linting and formatting issues
0xferit Aug 11, 2025
80ae75b
fix: apply black formatting to remaining files
0xferit Aug 11, 2025
1a77ace
fix: resolve all pre-commit hook failures
0xferit Aug 11, 2025
151ee99
fix: address critical issues from PR review
0xferit Aug 11, 2025
590205c
fix: preserve existing order flags on partial updates
0xferit Aug 11, 2025
2014806
fix(rest,ws,middleware): stop applying POST_ONLY to funding offers
0xferit Aug 11, 2025
51a480e
Add PyPI Trusted Publishing workflow
0xferit Aug 16, 2025
bdb1079
Add package documentation and publishing tools
0xferit Aug 16, 2025
b7b31be
Update tests for POST_ONLY enforcement
0xferit Aug 16, 2025
21df84f
Add audit documentation and package test script
0xferit Aug 16, 2025
3efd020
chore: remove unnecessary files and redundant workflows
0xferit Aug 16, 2025
125490b
feat: implement hardening recommendations for POST_ONLY enforcement
0xferit Aug 16, 2025
cc53b08
docs: consolidate all documentation into README.md
0xferit Aug 16, 2025
d4e5720
docs: remove misleading 'Requires flag management' row from compariso…
0xferit Aug 16, 2025
8b7e912
docs: fix comparison table - change 'API compatibility' to 'Drop-in r…
0xferit Aug 16, 2025
6a5cd5d
docs: remove confusing 'Drop-in replacement' row from comparison table
0xferit Aug 16, 2025
bf05f52
docs: make README concise and focused
0xferit Aug 16, 2025
49d90b9
chore: bump version to 3.0.5.post2
0xferit Aug 16, 2025
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
75 changes: 75 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
name: Publish to PyPI

on:
release:
types: [published]
workflow_dispatch: # Allow manual triggering for testing

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.9'

- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build wheel

- name: Build package
run: python -m build

- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: dist/

publish-to-pypi:
name: Publish to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/bitfinex-api-py-postonly/
permissions:
id-token: write # REQUIRED for trusted publishing

steps:
- name: Download distribution packages
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

publish-to-testpypi:
name: Publish to TestPyPI (Optional)
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' # Only on manual trigger
environment:
name: testpypi
url: https://test.pypi.org/project/bitfinex-api-py-postonly/
permissions:
id-token: write

steps:
- name: Download distribution packages
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/

- name: Publish to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
skip-existing: true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ pip-wheel-metadata/
.idea

venv/

# Planning documents
plans/
223 changes: 223 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Bitfinex API Python Fork - Safety Features

## Purpose
This fork adds two safety features to prevent accidental market orders and monitor connection health.

## Feature 1: Post-Only Order Enforcement
**Status:** ACTIVE
**Protection:** ALL orders automatically become post-only (maker-only)

### How It Works
- Flag 4096 (POST_ONLY) is automatically added to every order
- Cannot be disabled or bypassed
- Funding offers are not affected

### Enforcement Points
1. **REST API**: `submit_order()` and `update_order()` in `rest_auth_endpoints.py`
2. **Middleware**: All `order/submit` and `order/update` endpoints in `middleware.py`
3. **WebSocket**: Input validation in `bfx_websocket_inputs.py` and client-level in `bfx_websocket_client.py`

## Feature 2: Heartbeat Events
**Status:** ACTIVE
**Purpose:** Monitor WebSocket connection health

### Usage
```python
from bfxapi import Client

bfx = Client(api_key="...", api_secret="...")

@bfx.wss.on("heartbeat")
def on_heartbeat(subscription):
if subscription:
print(f"Public channel heartbeat: {subscription['channel']}")
else:
print("Authenticated connection heartbeat")

bfx.wss.run()
```

## Implementation Details

### Post-Only Enforcement - Technical Implementation

**Core Function** (`bfxapi/_utils/post_only_enforcement.py`):
```python
def enforce_post_only(flags: Optional[int], order_type: Optional[str] = None) -> int:
"""
Ensure POST_ONLY flag is set, preserving other flags.
Validates that order type is compatible with POST_ONLY.

Raises ValueError if order type contains 'MARKET' (incompatible with POST_ONLY).
"""
if order_type and 'MARKET' in order_type.upper():
raise ValueError(
f"Order type '{order_type}' is incompatible with POST_ONLY enforcement. "
"POST_ONLY only works with limit-style orders."
)
return POST_ONLY | (flags if flags is not None else 0)
```

**Hardening Features:**
- Rejects MARKET orders with clear error message
- Case-insensitive order type validation
- Preserves all existing flags using bitwise OR

**Layer 1 - REST API** (`rest_auth_endpoints.py`):
- Line 106: `flags = enforce_post_only(flags, order_type=type)` in `submit_order()` - validates order type
- Line 146: `flags = enforce_post_only(flags)` in `update_order()` - no type validation needed
- Funding offers explicitly skip enforcement (no flags sent)

**Layer 2 - Middleware** (`middleware.py`):
- Lines 66-77: Intercepts all `order/submit` and `order/update` endpoints
- Line 73: `enforce_post_only(flags, order_type)` for submit - validates order type
- Line 77: `enforce_post_only(flags)` for update
- Includes maintenance note for endpoint pattern updates
- Catch-all protection layer

**Layer 3 - WebSocket Input** (`bfx_websocket_inputs.py`):
- Line 32: `enforce_post_only(flags, order_type=type)` in `submit_order()` - validates order type
- Line 71: `enforce_post_only(flags)` in `update_order()`
- Funding offers skip enforcement (line 114)

**Layer 4 - WebSocket Client** (`bfx_websocket_client.py`):
- Lines 350-355: Final enforcement before sending to WebSocket
- Handles both "on" (new order) and "ou" (update order) events

### Heartbeat Events - Technical Implementation

**WebSocket Bucket** (`bfx_websocket_bucket.py`):
- Lines 69-72: Emits heartbeat events for public channels instead of discarding

**WebSocket Client** (`bfx_websocket_client.py`):
- Lines 267-268: Emits heartbeat events for authenticated channel (channel 0)

**Event Emitter** (`bfx_event_emitter.py`):
- Line 35: Added "heartbeat" to valid events list

## Verification for Auditors

### Quick Verification (< 1 minute)
```bash
# Count enforcement points in production code (excluding imports and definition)
grep -r "enforce_post_only(" bfxapi/ --include="*.py" | grep -v "def enforce" | grep -v test | wc -l
# Expected: 8 occurrences

# Verify POST_ONLY flag value
grep "POST_ONLY = 4096" bfxapi/constants/order_flags.py
# Expected: Match found

# Check all enforcement locations
grep -n "enforce_post_only" bfxapi/rest/_interfaces/rest_auth_endpoints.py
# Expected: Lines 106 and 146

grep -n "enforce_post_only" bfxapi/rest/_interface/middleware.py
# Expected: Lines 69 and 72

grep -n "enforce_post_only" bfxapi/websocket/_client/bfx_websocket_inputs.py
# Expected: Lines 32 and 71

grep -n "enforce_post_only" bfxapi/websocket/_client/bfx_websocket_client.py
# Expected: Lines 350 and 353
```

### Production Code Changes

**Post-Only Enforcement Files:**
```
bfxapi/_utils/post_only_enforcement.py # NEW: 37 lines (with validation)
bfxapi/constants/order_flags.py # NEW: 6 lines
bfxapi/rest/_interface/middleware.py # +13 lines (with notes)
bfxapi/rest/_interfaces/rest_auth_endpoints.py # +13 lines
bfxapi/websocket/_client/bfx_websocket_inputs.py # +15 lines
bfxapi/websocket/_client/bfx_websocket_client.py # +8 lines
tests/test_post_only_enforcement.py # 240+ lines (13 tests)
```

**Heartbeat Events Files:**
```
bfxapi/websocket/_client/bfx_websocket_bucket.py # +4 lines
bfxapi/websocket/_client/bfx_websocket_client.py # +4 lines
bfxapi/websocket/_event_emitter/bfx_event_emitter.py # +1 line
examples/websocket/heartbeat.py # NEW: 65 lines
README.md # +27 lines
```

### Code Delta Summary
- **Post-Only Enforcement**: ~92 lines of production code (with validation)
- **Heartbeat Events**: ~40 lines of production code
- **Core safety logic**: 37 lines (the `enforce_post_only` function with market order validation)
- **Tests**: 240+ lines, 13 comprehensive tests
- **No external dependencies added**

## PyPI Publishing

This package uses GitHub Actions with Trusted Publishing (OIDC) for secure, automated PyPI releases.

### Publishing Process

1. **Update version in setup.py**
2. **Create Git tag and push:**
```bash
git tag v3.0.5.post2
git push origin v3.0.5.post2
```
3. **Create GitHub Release** - triggers automatic PyPI upload via `.github/workflows/publish.yml`

### Configuration
- **PyPI Project**: bitfinex-api-py-postonly
- **Workflow**: `.github/workflows/publish.yml`
- **No API tokens** - Uses OIDC authentication

## Important Notes

⚠️ **This is a SAFETY fork, not for bypassing exchange rules**

- All orders will be maker-only (won't cross spread)
- MARKET orders are rejected with clear error message
- Monitor heartbeats to ensure connection is alive
- Funding offers are not affected by POST_ONLY enforcement
- The enforcement cannot be disabled without modifying the source code

## Example: Order Submission

```python
from bfxapi import Client

bfx = Client(api_key="...", api_secret="...")

# This will ALWAYS be post-only (flag 4096 added automatically)
order = bfx.rest.auth.submit_order(
type="EXCHANGE LIMIT",
symbol="tBTCUSD",
amount=0.01,
price=50000
# No need to specify flags - POST_ONLY is forced
)

# Even if you try flags=0, POST_ONLY is still added
order = bfx.rest.auth.submit_order(
type="EXCHANGE LIMIT",
symbol="tBTCUSD",
amount=0.01,
price=50000,
flags=0 # Still becomes flags=4096 internally!
)

# Other flags are preserved
order = bfx.rest.auth.submit_order(
type="EXCHANGE LIMIT",
symbol="tBTCUSD",
amount=0.01,
price=50000,
flags=64 # HIDDEN flag - becomes 4160 (4096 | 64)
)
```

## Protection Levels

1. **Application Level**: `submit_order()` and `update_order()` force POST_ONLY
2. **Middleware Level**: Order submit/update REST calls force POST_ONLY
3. **WebSocket Level**: Order submit/update WS messages force POST_ONLY

Each level provides redundant protection to ensure no order can bypass the POST_ONLY requirement.
35 changes: 35 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Include important files
include README.md
include LICENSE
include NOTICE

# Include all Python source files
recursive-include bfxapi *.py

# EXCLUDE non-production files
exclude CLAUDE.md
exclude .gitignore
exclude .env
exclude *.pyc
exclude setup.cfg
exclude pyproject.toml

# EXCLUDE directories that should NOT be in package
prune tests
prune examples
prune .github
prune .git
prune .venv
prune venv
prune __pycache__
prune .pytest_cache
prune *.egg-info
prune build
prune dist
prune .cursor
prune plans

# Exclude any compiled Python files
global-exclude *.pyc
global-exclude *.pyo
global-exclude __pycache__
15 changes: 15 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
bitfinex-api-py-postonly
Copyright 2025 Ferit

This is a modified fork of bitfinex-api-py
Original Copyright: Bitfinex
Original Repository: https://github.com/bitfinexcom/bitfinex-api-py
Original License: Apache License 2.0

SAFETY MODIFICATIONS:
- ALL orders are automatically forced to be POST_ONLY (maker-only)
- WebSocket heartbeat events are exposed for connection monitoring

CRITICAL: This fork enforces POST_ONLY flag (4096) on ALL orders.
Orders will never cross the spread and will always be maker orders.
This cannot be disabled without modifying the source code.
Loading