-
Notifications
You must be signed in to change notification settings - Fork 0
Add Mintlayer Python SDK (port of go-sdk) #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 aff0e05
Add node JSON-RPC client (port of go-sdk/node)
nullPointerEnjoyer 9e2e692
Add indexer REST client (port of go-sdk/indexer)
nullPointerEnjoyer 32c62a2
Add wallet JSON-RPC client (port of go-sdk/wallet)
nullPointerEnjoyer 9b3b9ad
Add top-level client and examples
nullPointerEnjoyer 6f665fb
Add documentation and security polish
nullPointerEnjoyer dd60634
Deduplicate node/wallet RPC core into shared BaseJSONRPCClient
nullPointerEnjoyer 643bf0a
Collapse indexer _core pass-through into _http
nullPointerEnjoyer 519730f
Remove dead Transaction re-export and redundant noqa markers
nullPointerEnjoyer 0d7dc86
Add deletion log for DRY refactor session
nullPointerEnjoyer 4c43e6b
Add pytest-cov with coverage config
nullPointerEnjoyer 6035097
Fix externref table slot double-dealloc in wasm array calls
nullPointerEnjoyer bb44337
Add WASM test suite: 95.5% total coverage, all edge cases
nullPointerEnjoyer 8be7bc6
Add CI: lint, typecheck, test matrix, build with coverage gate
nullPointerEnjoyer fa16e45
Fix setup-uv action pin: use real v5.4.2 commit SHA
nullPointerEnjoyer 9a343ef
Add AI code review workflow (port of rust-sdk code-review.yml)
nullPointerEnjoyer 87efd2b
Revert ocr_version pin: breaks the action's CLI install (ocr not found)
nullPointerEnjoyer e19c5fe
Address all OCR (open-code-review) findings on PR #1
nullPointerEnjoyer 3d86367
Address round-2 OCR findings
nullPointerEnjoyer a21b639
Address round-3 OCR findings
nullPointerEnjoyer 6549bc1
Add publish workflow (TestPyPI dry-run on tag push, PyPI on GitHub Re…
nullPointerEnjoyer 70a3781
Address round-4 OCR findings
nullPointerEnjoyer 760dcfc
Address round-5 OCR findings
nullPointerEnjoyer 6ae940e
Address round-6 OCR findings
nullPointerEnjoyer 7792cf0
Address round-7 OCR findings
nullPointerEnjoyer 6805b95
Address round-8 OCR findings
nullPointerEnjoyer 6ac9ab2
Address round-9 OCR findings
nullPointerEnjoyer b8cef42
Address round-10 OCR findings
nullPointerEnjoyer 121dedf
Address round-11 OCR findings
nullPointerEnjoyer e5a533d
Address residual findings from OCR thread triage
nullPointerEnjoyer f8346b2
Narrow example error handling; document datetime semantics
nullPointerEnjoyer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/ | ||
|
|
||
| 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" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
|
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 }} | ||
|
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' | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)}" | ||
|
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 | ||
|
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)" | ||
|
nullPointerEnjoyer marked this conversation as resolved.
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" | ||
|
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") | ||
|
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 }} | ||
|
nullPointerEnjoyer marked this conversation as resolved.
|
||
| attestations: false | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.