Add Swig fee-transfer filtering across TypeScript, Go, and Python SDKs#1
Conversation
|
Hey @maxsch-xmint thanks for putting this together. Here are some issues that stand out. Issues
The safety check compares sourceAddr/destAddr (which are token account addresses) against signerAddresses and payTo (which are owner/wallet addresses). These will never match because an ATA is a PDA derived from the owner — it's a different address entirely. Go (swig.go:157-164): TypeScript (utils.ts:1302-1306): Python (swig.py:776-780): Same issue. To actually catch malicious fee transfers draining signer/merchant funds, you need to either:
As-is, the safety check is effectively a no-op — it will never trigger, making the ErrSuspiciousFeeTransfer error dead code.
The filter loop starts at i = 2 and the result always places instructions[0] and instructions[1] as compute budget prefix: result := []solana.CompiledInstruction{instructions[0], instructions[1], *merchantTransfer} If a Swig transaction has 0 or 1 compute budget instructions, or has them in a different order, this will silently include non-compute-budget
The new code in scheme.ts:1057-1070 fetches ALT data from an RPC node during verification: Why are the ALTs needed? Are we being too permissive here? This introduces a network dependency into what was previously a pure/offline verification path. If the RPC is slow or unavailable, verification fails. This is a behavioral change worth flagging — consider whether this should be documented or whether the ALT data could be passed through the payload instead.
More importantly, you need to check both ATAs (SPL Token and Token-2022) since the merchant transfer could use either program. The Python implementation correctly does this (swig.py:758-759), but TypeScript only returns one.
Go uses "invalid_exact_solana_payload_suspicious_fee_transfer" but Python and TypeScript use "invalid_exact_svm_payload_suspicious_fee_transfer". The Go
The change from: ...means the errorCode field now contains raw error messages instead of well-known constants. Any downstream consumers keying on specific error codes
Extra indentation was introduced on this comment line.
And: These bypass type safety entirely. The functions should accept the actual types or use proper type narrowing. |
Description
Add
filterFeeTransfersto the Swig normalizer in all three SDKs (TypeScript, Go, Python) so x402 can skip internal fee/routing transfers that Swig injects and only validate the actual merchant payment.How it works
When a Swig transaction contains multiple SPL
TransferCheckedinstructions, the filter:TransferChecked(discriminator byte12).suspicious_fee_transferany transfer whose source or destination is a signer address or the merchant owner.The normalizer now receives a
NormalizationContext(asset mint, payTo address, signer addresses) from the facilitator and passes it through to the filter.Files changed
Core filter logic
typescript/packages/mechanisms/svm/src/utils.ts—filterFeeTransfers,tryDecodeTransferCheckedgo/pkg/mechanisms/svm/swig.go—FilterFeeTransfers,tryDecodeTransferCheckedpython/python_x402/swig.py—filter_fee_transfers,try_decode_transfer_checkedNormalizer — accept context & call filter
typescript/packages/mechanisms/svm/src/normalizer.tsgo/pkg/mechanisms/svm/normalizer.gopython/python_x402/normalizer.pyFacilitator — supply NormalizationContext
typescript/packages/mechanisms/svm/src/schemes/exact/facilitator/scheme.tsgo/pkg/mechanisms/svm/schemes/exact/facilitator/scheme.gopython/python_x402/exact/facilitator.pyTest plan
vitestunit tests with real Swig transaction fixturesgo testwith equivalent fixturespytestwith equivalent fixturesTests
Tests
typescript/packages/mechanisms/svm/test/unit/swig-fee-transfer.test.tsgo/pkg/mechanisms/svm/swig_fee_transfer_test.gopython/tests/test_swig_fee_transfer.pyChecklist