Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
90e5772
Add WASM cryptography module (port of go-sdk/wasm)
nullPointerEnjoyer Sep 16, 2026
aff0e05
Add node JSON-RPC client (port of go-sdk/node)
nullPointerEnjoyer Sep 16, 2026
9e2e692
Add indexer REST client (port of go-sdk/indexer)
nullPointerEnjoyer Sep 16, 2026
32c62a2
Add wallet JSON-RPC client (port of go-sdk/wallet)
nullPointerEnjoyer Sep 16, 2026
9b3b9ad
Add top-level client and examples
nullPointerEnjoyer Sep 16, 2026
6f665fb
Add documentation and security polish
nullPointerEnjoyer Sep 17, 2026
dd60634
Deduplicate node/wallet RPC core into shared BaseJSONRPCClient
nullPointerEnjoyer Sep 17, 2026
643bf0a
Collapse indexer _core pass-through into _http
nullPointerEnjoyer Sep 17, 2026
519730f
Remove dead Transaction re-export and redundant noqa markers
nullPointerEnjoyer Sep 17, 2026
0d7dc86
Add deletion log for DRY refactor session
nullPointerEnjoyer Sep 17, 2026
4c43e6b
Add pytest-cov with coverage config
nullPointerEnjoyer Sep 17, 2026
6035097
Fix externref table slot double-dealloc in wasm array calls
nullPointerEnjoyer Sep 17, 2026
bb44337
Add WASM test suite: 95.5% total coverage, all edge cases
nullPointerEnjoyer Sep 17, 2026
8be7bc6
Add CI: lint, typecheck, test matrix, build with coverage gate
nullPointerEnjoyer Sep 17, 2026
fa16e45
Fix setup-uv action pin: use real v5.4.2 commit SHA
nullPointerEnjoyer Sep 17, 2026
9a343ef
Add AI code review workflow (port of rust-sdk code-review.yml)
nullPointerEnjoyer Sep 17, 2026
87efd2b
Revert ocr_version pin: breaks the action's CLI install (ocr not found)
nullPointerEnjoyer Sep 17, 2026
e19c5fe
Address all OCR (open-code-review) findings on PR #1
nullPointerEnjoyer Sep 17, 2026
3d86367
Address round-2 OCR findings
nullPointerEnjoyer Sep 17, 2026
a21b639
Address round-3 OCR findings
nullPointerEnjoyer Sep 17, 2026
6549bc1
Add publish workflow (TestPyPI dry-run on tag push, PyPI on GitHub Re…
nullPointerEnjoyer Sep 18, 2026
70a3781
Address round-4 OCR findings
nullPointerEnjoyer Sep 18, 2026
760dcfc
Address round-5 OCR findings
nullPointerEnjoyer Sep 18, 2026
6ae940e
Address round-6 OCR findings
nullPointerEnjoyer Sep 18, 2026
7792cf0
Address round-7 OCR findings
nullPointerEnjoyer Sep 18, 2026
6805b95
Address round-8 OCR findings
nullPointerEnjoyer Sep 18, 2026
6ac9ab2
Address round-9 OCR findings
nullPointerEnjoyer Sep 18, 2026
b8cef42
Address round-10 OCR findings
nullPointerEnjoyer Sep 18, 2026
121dedf
Address round-11 OCR findings
nullPointerEnjoyer Sep 18, 2026
e5a533d
Address residual findings from OCR thread triage
nullPointerEnjoyer Sep 18, 2026
f8346b2
Narrow example error handling; document datetime semantics
nullPointerEnjoyer Sep 18, 2026
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
121 changes: 121 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

# Least privilege: the workflow only reads the repo.
permissions:
contents: read

# Cancel superseded runs on rapid pushes.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
name: Lint & typecheck
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Install Python
run: uv python install 3.12

- name: Sync dependencies (locked)
run: uv sync --locked --group dev --python 3.12

- name: Ruff lint
run: uv run ruff check .

# Scoped to Python paths: repo-wide format would also reformat the
# illustrative snippets inside docs/*.md and README.md.
- name: Ruff format
run: uv run ruff format --check mintlayer/ tests/ examples/

- name: Mypy
run: uv run mypy mintlayer/
Comment thread
nullPointerEnjoyer marked this conversation as resolved.

test:
name: Test (Python ${{ matrix.python }})
runs-on: ubuntu-latest
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
python: ["3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Install Python ${{ matrix.python }}
run: uv python install ${{ matrix.python }}

- name: Sync dependencies (locked)
run: uv sync --locked --group dev --python ${{ matrix.python }}

# WASM init costs ~400 ms per Client; the session-scoped fixture keeps
# the suite to ~2 min. Coverage gate: total must stay above 80%.
- name: Run tests with coverage
run: >
uv run pytest tests/ -q
--cov --cov-report=term-missing
--cov-fail-under=80

build:
name: Build wheel
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Install Python
run: uv python install 3.12

- name: Build sdist and wheel
run: uv build

- name: Smoke-test wheel contents
run: |
python3 - <<'EOF'
import zipfile, glob
wheels = glob.glob("dist/*.whl")
assert wheels, "no wheel produced by uv build"
wheel = wheels[0]
names = zipfile.ZipFile(wheel).namelist()
assert any(n.endswith("wasm_wrappers_bg.wasm") for n in names), "wasm binary missing"
assert any(n.endswith("wasm_wrappers_bg.wasm.sha256") for n in names), "sha256 pin missing"
assert any(n == "mintlayer/__init__.py" for n in names), "package missing"
print(f"{wheel}: {len(names)} entries OK")
EOF

all-green:
name: All checks passed
runs-on: ubuntu-latest
needs: [lint, test, build]
steps:
- run: echo "All CI jobs green"
36 changes: 36 additions & 0 deletions .github/workflows/code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: AI Code Review

on:
pull_request:
types: [opened, synchronize, reopened]

permissions:
contents: read
pull-requests: write
Comment thread
nullPointerEnjoyer marked this conversation as resolved.

concurrency:
group: ocr-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
review:
# Fork PRs cannot read secrets; skip them cleanly.
if: github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: alibaba/open-code-review@494bf1c8d7a19196ab166960a06fef38d69a1d16 # v1.12.0
with:
llm_url: https://api.z.ai/api/coding/paas/v4
llm_auth_token: ${{ secrets.OCR_LLM_TOKEN }}
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
llm_model: glm-5.3-flash
llm_use_anthropic: false
# GLM-5.3 family rejects thinking.type=disabled, which is the
# action's default extra_body — this override is required.
llm_extra_body: '{"thinking": {"type": "enabled"}}'
llm_reasoning_effort: low
incremental: 'true'
route_severity_below: 'low'
max_tokens_budget: '500000'
review_task_timeout: '15'
stream_progress: 'true'
196 changes: 196 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
name: Publish

# Release flow:
# 1. Push a tag (git tag v0.1.0 && git push origin v0.1.0)
# -> build + verify -> publish to TestPyPI (dry-run).
# 2. Verify the TestPyPI install, then cut a GitHub Release for the
# SAME, already-existing tag -> publish to PyPI.
#
# Note: creating a Release for a NEW tag fires both events (GitHub creates
# the tag first); the event guards below keep each target correct in that
# case too, but the intended flow above keeps TestPyPI as a manual gate.

on:
push:
tags: ["v*"]
release:
types: [published]

# id-token / attestations intentionally omitted: publishing authenticates
# with an API token stored in the environment secrets. PEP 740 attestations
# require OIDC trusted publishing; re-enable both if the project migrates
# to trusted publishing (which would also eliminate the long-lived token).
permissions:
contents: read

# Never cancel a publish in flight.
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

jobs:
build:
name: Build & verify distribution
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
persist-credentials: false

- uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
enable-cache: true
cache-dependency-glob: uv.lock

- name: Install Python
run: uv python install 3.12

- name: Sync dependencies (locked)
run: uv sync --locked --group dev --python 3.12

- name: Ruff lint
run: uv run ruff check .

- name: Ruff format
run: uv run ruff format --check mintlayer/ tests/ examples/

- name: Mypy
run: uv run mypy mintlayer/

- name: Run tests with coverage
run: >
uv run pytest tests/ -q
--cov --cov-report=term-missing
--cov-fail-under=80

- name: Verify tag matches package version
run: |
version="$(uv run --no-sync python -c 'import mintlayer; print(mintlayer.__version__)')"
echo "package version: $version"
if [ "v${version}" != "${GITHUB_REF_NAME}" ]; then
echo "::error::tag ${GITHUB_REF_NAME} does not match package version v${version}"
exit 1
fi

- name: Build sdist and wheel
run: uv build

- name: Twine metadata check
run: uvx twine check dist/*

- name: Smoke-test wheel in a clean venv
run: |
uv venv /tmp/smoke-venv
uv pip install --python /tmp/smoke-venv/bin/python dist/*.whl
/tmp/smoke-venv/bin/python - <<'EOF'
import mintlayer
from mintlayer.wasm import Client as WasmClient, MAINNET

assert mintlayer.__version__
w = WasmClient()
# Keys carry a one-byte WASM ABI status prefix: priv=33, pub=34.
priv = w.make_private_key()
assert len(priv) == 33 and any(priv), f"bad private key length {len(priv)}"
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
pub = w.public_key_from_private_key(priv)
assert pub[1:2] in (b"\x02", b"\x03"), "pubkey not compressed SEC1"
addr = w.pubkey_to_pubkeyhash_address(pub, MAINNET)
assert isinstance(addr, str) and addr.startswith("mtc1"), f"bad address {addr!r}"
print(f"wheel OK: version={mintlayer.__version__} address={addr[:12]}...")
EOF

- name: Upload distributions
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: dist
path: dist/
if-no-files-found: error

publish-testpypi:
name: Publish to TestPyPI
if: github.event_name == 'push'
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
environment: testpypi
steps:
- name: Download distributions
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: dist
path: dist/

- name: Publish
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
attestations: false

publish-pypi:
name: Publish to PyPI
if: github.event_name == 'release'
needs: build
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
runs-on: ubuntu-latest
timeout-minutes: 10
environment: pypi
# Only this job needs cross-run artifact download access; build runs
# project code and must not carry it.
permissions:
contents: read
actions: read
steps:
# Rebuilds are not guaranteed byte-reproducible (wheel zip timestamps),
# so instead of building again we publish the EXACT dist/ artifact that
# the tag-push run verified and uploaded to TestPyPI.
- name: Locate the tag-push publish run
id: tagrun
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="${GITHUB_REF_NAME#refs/tags/}"
run_id="$(gh run list -R "$GITHUB_REPOSITORY" --workflow publish.yml --event push \
--json databaseId,headBranch,conclusion \
--jq ".[] | select(.headBranch == \"$tag\" and .conclusion == \"success\") | .databaseId" | head -1)"
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
if [ -z "$run_id" ]; then
echo "::error::no successful tag-push publish run for $tag - push the tag first and let the TestPyPI dry-run finish"
exit 1
fi
echo "run_id=$run_id" >> "$GITHUB_OUTPUT"

- name: Download the TestPyPI-verified artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
rm -rf dist
gh run download "${{ steps.tagrun.outputs.run_id }}" -n dist -D dist -R "$GITHUB_REPOSITORY"
ls -la dist/

# Integrity check for the cross-run download: only publish bytes that
# are provably identical to what TestPyPI publicly serves for this
# version (the dry-run's uploads are the trust anchor).
- name: Verify artifacts match TestPyPI
run: |
python3 - <<'EOF'
import hashlib, json, os, pathlib, sys, urllib.request

version = os.environ["GITHUB_REF_NAME"].lstrip("v")
url = f"https://test.pypi.org/pypi/mintlayer/{version}/json"
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
with urllib.request.urlopen(url, timeout=30) as resp:
remote = {u["filename"]: u["digests"]["sha256"] for u in json.load(resp)["urls"]}
if not remote:
sys.exit(f"version {version} not found on TestPyPI - cut the tag first")
for dist in sorted(pathlib.Path("dist").iterdir()):
digest = hashlib.sha256(dist.read_bytes()).hexdigest()
if dist.name not in remote:
sys.exit(f"{dist.name} is not published on TestPyPI for version {version}")
if remote[dist.name] != digest:
sys.exit(f"{dist.name} does not match the TestPyPI dry-run artifact")
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
print("artifacts match the TestPyPI dry-run")
EOF

- name: Publish
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_API_TOKEN }}
Comment thread
nullPointerEnjoyer marked this conversation as resolved.
attestations: false
18 changes: 16 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ build/
.mypy_cache/
.ruff_cache/
.pytest_cache/
.coverage
htmlcov/
.wrangler/
uv.lock

# secrets
.env*
!.env.example
*.pem
*.key
*.seed
*.env
*.p12
*.pfx
id_rsa*
id_ed25519*
id_ecdsa*
secrets/
.coverage*
Loading
Loading