Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
705fc1e
style(python): ruff-format existing solana_mpp tests
EfeDurmaz16 May 29, 2026
0c86702
refactor(python): use Decimal money math and explicit dict typing in …
EfeDurmaz16 May 29, 2026
2f3575c
feat(python): add pay_kit PayCore foundation
EfeDurmaz16 May 29, 2026
0270a30
feat(python): add pay_kit value objects, signer, and operator
EfeDurmaz16 May 29, 2026
dd318a7
feat(python): add pay_kit gate, pricing, and configuration
EfeDurmaz16 May 29, 2026
53ff1c9
feat(python): add pay_kit MPP and x402-exact protocol adapters
EfeDurmaz16 May 29, 2026
a2c9b8e
feat(python): add pay_kit preflight and middleware core
EfeDurmaz16 May 29, 2026
06e17fc
feat(python): add pay_kit framework shims and public API
EfeDurmaz16 May 29, 2026
7763511
chore(python): wire pay_kit into pyproject
EfeDurmaz16 May 29, 2026
b3eb1c4
test(python): add pay_kit test suite
EfeDurmaz16 May 29, 2026
2f95987
feat(harness): register pay-kit-python interop server
EfeDurmaz16 May 29, 2026
7bf4fd6
docs(python): document pay_kit, add framework examples and CI job
EfeDurmaz16 May 29, 2026
9adc3a7
refactor(python): strict-type the pay_kit package
EfeDurmaz16 May 29, 2026
c6bd5cd
ci(python): point pyright at the active interpreter
EfeDurmaz16 May 29, 2026
e0fcf0e
docs(python): correct the pay_kit repo-layout tree
EfeDurmaz16 May 29, 2026
90565aa
ci(python): fix the canonical python.yml for pay_kit, drop ci.yml dup…
EfeDurmaz16 May 29, 2026
b1d6c94
docs(python): align the README with the sibling SDKs
EfeDurmaz16 May 29, 2026
96798b8
refactor(python): reorganize pay_kit to mirror the rust crate layout
EfeDurmaz16 May 29, 2026
71d11b3
refactor(python): lift shared infra into _paycore so protocols don't …
EfeDurmaz16 May 29, 2026
47b1020
fix(python): align x402 Lighthouse program id with the rust spine
EfeDurmaz16 May 29, 2026
979dd0a
feat(python): implement SPL-token MPP client charge transfers
EfeDurmaz16 May 29, 2026
e179f50
fix(php,lua): align x402 Lighthouse program id with the rust spine
EfeDurmaz16 May 29, 2026
ca55dfe
refactor(python): split x402 into exact/{verify,types} + client packages
EfeDurmaz16 May 29, 2026
1052641
feat(python): x402 exact client (challenge parse + payment build + tr…
EfeDurmaz16 May 29, 2026
e810f2e
test(python): x402 exact client unit suite
EfeDurmaz16 May 29, 2026
80fc3f7
feat(harness): register python-x402 exact interop client
EfeDurmaz16 May 29, 2026
90b442f
test(harness): add x402-exact token-2022 + ATA-precreated scenarios
EfeDurmaz16 May 29, 2026
bf225d4
ci(python): wire python-x402 client into the interop matrix
EfeDurmaz16 May 29, 2026
73d2a23
Revert "test(harness): add x402-exact token-2022 + ATA-precreated sce…
EfeDurmaz16 May 29, 2026
8a3568a
ci(python): scope the python-x402 interop run to the basic happy path
EfeDurmaz16 May 29, 2026
a91c638
docs(python): drop mpp module path from x402 transport docstring
EfeDurmaz16 May 29, 2026
104099f
fix(python): add SolanaRpc.get_latest_blockhash for the x402 client b…
EfeDurmaz16 May 29, 2026
533ee29
docs(python): document and example the x402 exact client
EfeDurmaz16 May 29, 2026
18a5a9b
fix(python): align x402 client ComputeUnitLimit with the rust spine (…
EfeDurmaz16 May 29, 2026
cdf4048
fix(python/x402): confirm before success, drop ATA-create, mandatory …
EfeDurmaz16 May 30, 2026
1f76061
fix(python): share replay store per config, charge fee-on-top total, …
EfeDurmaz16 May 30, 2026
0b861e4
fix(python): close mainnet x402 demo-signer bypass, reject bad amounts
EfeDurmaz16 May 30, 2026
02811a3
fix(python): accept x402 Lighthouse guard in any optional slot
EfeDurmaz16 May 30, 2026
8631f6e
refactor(python): split mpp server charge into decode and verify modules
EfeDurmaz16 May 30, 2026
adc457f
fix(python-x402): match rust client field precedence and fee-payer to…
EfeDurmaz16 May 31, 2026
7a24050
fix(python-mpp): match rust charge client tx layout and fee-payer slot
EfeDurmaz16 May 31, 2026
37e48b5
fix(python-mpp): enforce required ATA-creation set in charge allowlist
EfeDurmaz16 May 31, 2026
627005d
fix(python): cast envelope resource to typed dict for pyright
EfeDurmaz16 May 31, 2026
53a24f5
fix(python-x402): keep echoed accepted clean so rust verifier matches
EfeDurmaz16 May 31, 2026
c69acfb
fix(python): enforce transferChecked decimals and add compute-budget …
EfeDurmaz16 Jun 1, 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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,9 @@ jobs:
- name: Verify committed gen files are up to date
working-directory: .
run: |
if ! git diff --quiet -- rust/src/server/html/ go/protocols/mpp/server/html/ lua/mpp/server/html_assets/ python/src/solana_mpp/server/html/; then
if ! git diff --quiet -- rust/src/server/html/ go/protocols/mpp/server/html/ lua/mpp/server/html_assets/ python/src/pay_kit/protocols/mpp/server/html/; then
echo "::error::Generated files are out of date. Run 'just html-build' and commit the results."
git diff --stat -- rust/src/server/html/ go/protocols/mpp/server/html/ lua/mpp/server/html_assets/ python/src/solana_mpp/server/html/
git diff --stat -- rust/src/server/html/ go/protocols/mpp/server/html/ lua/mpp/server/html_assets/ python/src/pay_kit/protocols/mpp/server/html/
exit 1
fi
- name: Upload HTML build artifacts
Expand All @@ -137,7 +137,7 @@ jobs:
rust/src/server/html/
go/protocols/mpp/server/html/
lua/mpp/server/html_assets/
python/src/solana_mpp/server/html/
python/src/pay_kit/protocols/mpp/server/html/
typescript/packages/mpp/src/server/html-assets.gen.ts

test-rust:
Expand Down
40 changes: 35 additions & 5 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,28 @@ jobs:
cache: pip
- name: Install Python SDK
working-directory: python
# Framework extras bring fastapi/flask/django so the pay_kit shim tests
# import and pyright can resolve them; dev brings ruff, pyright, pytest.
run: |
python -m pip install --upgrade pip
pip install -e ".[dev]"
pip install -e ".[dev,fastapi,flask,django]"
- name: Lint with ruff
working-directory: python
run: ruff check src tests
- name: Type check with pyright
working-directory: python
run: pyright
# Point pyright at the interpreter the extras were installed into; it
# does not reliably auto-detect the active env on the runner.
run: pyright --pythonpath "$(python -c 'import sys; print(sys.executable)')"
- name: Run tests with coverage
working-directory: python
# Coverage gate: line coverage at 90%. Branch coverage gate is follow-up work, tracked in #108.
# Coverage gate: line coverage at 90% over pay_kit (the MPP wire layer
# now lives under pay_kit/protocols/mpp). preflight.py is omitted in
# pyproject (live-RPC paths). Branch coverage gate is follow-up work,
# tracked in #108.
run: |
pytest \
--cov=solana_mpp \
--cov=pay_kit \
--cov-report=term-missing \
--cov-report=json:coverage.json \
--cov-fail-under=90 \
Expand Down Expand Up @@ -93,7 +100,9 @@ jobs:
run: pnpm --filter @solana/mpp build
- name: Build Rust interop adapters
working-directory: rust
run: cargo build -p solana-mpp --bin interop_client --bin interop_server
run: |
cargo build -p solana-mpp --bin interop_client --bin interop_server
cargo build -p solana-x402 --bin interop_client --bin interop_server
- name: Install interop harness
working-directory: harness
run: pnpm install --frozen-lockfile
Expand All @@ -109,3 +118,24 @@ jobs:
MPP_INTEROP_CLIENTS: rust
MPP_INTEROP_SERVERS: python
run: pnpm exec vitest run test/e2e.test.ts
# x402 exact: drive the Python pay_kit x402 client (a real signed v0
# VersionedTransaction) against the full-settling rust and python x402
# servers. test/e2e.test.ts self-hosts surfnet and threads the funded
# client keypair into X402_INTEROP_CLIENT_SECRET_KEY. The matrix pairs
# every active client x active server, so the MPP_INTEROP_* selectors are
# also set to keep the default charge-enabled adapters (typescript, php,
# go) out of this x402 run. ts-x402 is excluded: its stub server expects a
# payload.challengeId and never broadcasts a real transaction, so it
# cannot settle a genuine signed tx (same reason rust-x402 skips it). The
# x402 client adapter imports pay_kit from python/src on sys.path (no
# extra install beyond the editable SDK above).
- name: Focused python-x402 -> rust/python x402 servers
working-directory: harness
env:
MPP_INTEROP_INTENTS: x402-exact
MPP_INTEROP_SCENARIOS: x402-exact-basic
MPP_INTEROP_CLIENTS: python-x402
MPP_INTEROP_SERVERS: rust-x402,python
X402_INTEROP_CLIENTS: python-x402
X402_INTEROP_SERVERS: rust-x402,python
run: pnpm exec vitest run test/e2e.test.ts --testTimeout 180000
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ cargo add solana-mpp
go get github.com/solana-foundation/pay-kit/go

# Python
pip install solana-mpp
pip install solana-pay-kit

# Ruby
cd ruby && bundle install
Expand Down Expand Up @@ -121,13 +121,16 @@ return result.withReceipt(Response.json({ data: '...' }))
<summary>Python</summary>

```python
from solana_mpp.server import Mpp, Config
from pay_kit import MemoryStore
from pay_kit.protocols.mpp.server import Config, Mpp

mpp = Mpp(Config(
recipient="RecipientPubkey...",
currency="EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
decimals=6,
html=True,
secret_key="...", # or set MPP_SECRET_KEY in the environment
store=MemoryStore(), # required; use FileReplayStore(path) for durable replay
))

challenge = mpp.charge("1.00") # 1 USDC
Expand Down
2 changes: 1 addition & 1 deletion docs/security/compute-budget-caps.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ this monorepo.
| Lua (#103) | `lua/mpp/methods/solana/instructions.lua:31` | `MAX_COMPUTE_UNIT_LIMIT` (pending PR #103 merge) |
| Lua (#103) | `lua/mpp/methods/solana/instructions.lua:32` | `MAX_COMPUTE_UNIT_PRICE_MICROLAMPORTS` (pending PR #103 merge) |
| Go (#101) | `go/protocols/mpp/server/server.go` (`maxComputeUnitLimit`) | pending PR #101 merge |
| Python (#106) | `python/src/solana_mpp/server/mpp.py` | pending PR #106 merge |
| Python (#106) | `python/src/pay_kit/protocols/mpp/server/charge.py` | pending PR #106 merge |

`harness/test/compute-budget-caps.test.ts` parses each file above
and asserts byte-identical literals against the canonical pair. Go and
Expand Down
4 changes: 2 additions & 2 deletions docs/security/fee-payer-drain.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Client crafts a transaction where the fee-payer is placed at a non-canonical sig

### 4. Tampered-Details Attack (Client-Supplied `methodDetails.feePayerKey`)

The MPP charge request carries `methodDetails.feePayerKey` (string, base58 pubkey; this is the canonical wire field across all SDKs, see [`typescript/packages/mpp/src/Methods.ts`](../../typescript/packages/mpp/src/Methods.ts) L62, [`go/paycore/solana.go`](../../go/paycore/solana.go) L115, [`python/src/solana_mpp/protocol/solana.py`](../../python/src/solana_mpp/protocol/solana.py) L120, [`rust/crates/mpp/src/protocol/solana.rs`](../../rust/crates/mpp/src/protocol/solana.rs) L394, [`php/src/Server/SolanaChargeTransactionVerifier.php`](../../php/src/Server/SolanaChargeTransactionVerifier.php) L304, [`ruby/lib/mpp/methods/solana/verifier.rb`](../../ruby/lib/mpp/methods/solana/verifier.rb), [`lua/mpp/server/init.lua`](../../lua/mpp/server/init.lua) L85). A malicious client supplies `methodDetails.feePayerKey = ATTACKER_PUBKEY` while the server's actual signing key is `SERVER_PUBKEY`. If the verifier trusts the client-supplied details field as the source of truth for "who is the fee-payer", it will validate guards (source != fee-payer, slot, etc.) against `ATTACKER_PUBKEY`. The real `SERVER_PUBKEY` then signs a transaction that drains itself.
The MPP charge request carries `methodDetails.feePayerKey` (string, base58 pubkey; this is the canonical wire field across all SDKs, see [`typescript/packages/mpp/src/Methods.ts`](../../typescript/packages/mpp/src/Methods.ts) L62, [`go/paycore/solana.go`](../../go/paycore/solana.go) L115, [`python/src/pay_kit/_paycore/solana.py`](../../python/src/pay_kit/_paycore/solana.py) L120, [`rust/crates/mpp/src/protocol/solana.rs`](../../rust/crates/mpp/src/protocol/solana.rs) L394, [`php/src/Server/SolanaChargeTransactionVerifier.php`](../../php/src/Server/SolanaChargeTransactionVerifier.php) L304, [`ruby/lib/mpp/methods/solana/verifier.rb`](../../ruby/lib/mpp/methods/solana/verifier.rb), [`lua/mpp/server/init.lua`](../../lua/mpp/server/init.lua) L85). A malicious client supplies `methodDetails.feePayerKey = ATTACKER_PUBKEY` while the server's actual signing key is `SERVER_PUBKEY`. If the verifier trusts the client-supplied details field as the source of truth for "who is the fee-payer", it will validate guards (source != fee-payer, slot, etc.) against `ATTACKER_PUBKEY`. The real `SERVER_PUBKEY` then signs a transaction that drains itself.

Source of truth MUST be the server-context fee-payer pubkey (the public key of the server's signer keypair), never a client-controlled field.

Expand Down Expand Up @@ -67,7 +67,7 @@ A passing fee-payer co-sign path is the conjunction of all four. Missing any one
| PHP | [`php/src/Server/SolanaChargeTransactionVerifier.php`](../../php/src/Server/SolanaChargeTransactionVerifier.php): `validateInstructionAllowlist` (L454), invoked from both push (L169) and pull (L216) paths in the same file |
| Ruby | [`ruby/lib/mpp/methods/solana/verifier.rb`](../../ruby/lib/mpp/methods/solana/verifier.rb): `validate_allowlist` (L191), `expected_fee_payer` (L100), source-vs-fee-payer guards at L128, L156, L158 |
| Lua | [`lua/mpp/server/solana_verify.lua`](../../lua/mpp/server/solana_verify.lua): `verify_instruction_allowlist` (L330), invoked from the main verify path at L140 |
| Python | `python/src/solana_mpp/server/mpp.py`: `_validate_instruction_allowlist` (lands with [#106](https://github.com/solana-foundation/mpp-sdk/pull/106)) |
| Python | `python/src/pay_kit/protocols/mpp/server/charge.py`: `_validate_instruction_allowlist` (lands with [#106](https://github.com/solana-foundation/mpp-sdk/pull/106)) |
| Go | `go/protocols/mpp/server/server.go`: allowlist branch inside `verifyTransaction` (lands with [#101](https://github.com/solana-foundation/mpp-sdk/pull/101)) |

The Rust path is the spine. PHP, Ruby, Lua, Python, and Go port the same four invariants with language-idiomatic surfaces.
Expand Down
Loading
Loading