Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
28598ca
fix(go): x402/MPP server-SDK safety (allowlist, ATA policy, confirmat…
EfeDurmaz16 May 30, 2026
aeb229b
fix(lua): x402/MPP server-SDK safety and config hardening
EfeDurmaz16 May 30, 2026
3f77d22
fix(php): x402/MPP server-SDK safety and Laravel expiry validation
EfeDurmaz16 May 30, 2026
207ca16
fix(ruby): dev-store warning and x402 exact ATA-create rejection
EfeDurmaz16 May 30, 2026
a704ea3
docs: operability caveats and cross-SDK scoping notes
EfeDurmaz16 May 30, 2026
999374b
fix(ruby): bind x402 transfer program to canonical SPL set not extra.…
EfeDurmaz16 May 31, 2026
7fff2b7
fix(ruby): resolve testnet stablecoin mints to the devnet address
EfeDurmaz16 May 31, 2026
2ea98c1
fix(ruby): cap MPP charge and split amounts at u64
EfeDurmaz16 May 31, 2026
e901e52
fix(php): align x402 exact verifier with rust spine
EfeDurmaz16 May 31, 2026
93e7075
fix(php): add unconditional pinned-field backstop to mpp charge server
EfeDurmaz16 May 31, 2026
c59c784
fix(php): match rust on ata payer default and push compute-budget caps
EfeDurmaz16 May 31, 2026
77d2a6d
fix(lua): raise x402 compute-unit-price cap to 5_000_000 for rust parity
EfeDurmaz16 May 31, 2026
0386e3d
fix(lua): bind x402 transfer program to canonical token set
EfeDurmaz16 May 31, 2026
6b708de
fix(lua): decode x402 u64 amounts and prices exactly
EfeDurmaz16 May 31, 2026
c5b7cc0
test(lua): add x402 rust-parity regression coverage
EfeDurmaz16 May 31, 2026
4a881da
fix(go): align x402 client offer parsing with rust spine
EfeDurmaz16 May 31, 2026
abbb1bf
fix(go): emit canonical x402 verifier codes and bind echoed accepted
EfeDurmaz16 May 31, 2026
059480f
fix(lua): pin MPP SPL transferChecked decimals to method details
EfeDurmaz16 May 31, 2026
fee03f3
fix(lua): reject fee-payer token account as MPP SPL transfer source
EfeDurmaz16 May 31, 2026
f725037
fix(lua): match MPP inner CPI instructions on the confirmed path
EfeDurmaz16 May 31, 2026
ddfc687
test(lua): add MPP charge rust-parity regression coverage
EfeDurmaz16 May 31, 2026
1000182
fix(go): align mpp charge verifier with rust spine
EfeDurmaz16 May 31, 2026
f3eb5c1
Merge remote-tracking branch 'fork/fix/ruby-parity' into m144
EfeDurmaz16 May 31, 2026
8d22c7a
Merge remote-tracking branch 'fork/fix/php-parity' into m144
EfeDurmaz16 May 31, 2026
21498a1
Merge remote-tracking branch 'fork/fix/lua-parity' into m144
EfeDurmaz16 May 31, 2026
849dc2f
fix(ci): drop unused go x402 alias raw field and dead lua remainder a…
EfeDurmaz16 May 31, 2026
c89967c
style(go): gofmt x402.go after removing the unused alias raw field
EfeDurmaz16 May 31, 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
2 changes: 1 addition & 1 deletion go/Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,5 @@ check: build lint audit test-cover

# Boot the dual-protocol example server
serve-example port="4567":
go run ./examples/paykit-server
go run ./examples/simple-server

124 changes: 124 additions & 0 deletions go/protocols/mpp/internal_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package mpp

import (
"context"
"fmt"
"testing"

solana "github.com/gagliardetto/solana-go"
Expand All @@ -9,6 +11,23 @@ import (
"github.com/solana-foundation/pay-kit/go/signer"
)

// errSigner is a paykit.Signer stub whose Sign method always returns the given
// error, exercising the signerBridge.Sign error propagation branch.
type errSigner struct {
pubkey string
err error
raw []byte // when non-nil, Sign returns this slice without error
}

func (e *errSigner) Pubkey() paykit.Address { return paykit.Address(e.pubkey) }
func (e *errSigner) IsDemo() bool { return false }
func (e *errSigner) Sign(_ context.Context, _ []byte) ([]byte, error) {
if e.err != nil {
return nil, e.err
}
return e.raw, nil
}

func testCfg() paykit.Config {
demo := signer.Demo()
return paykit.Config{
Expand Down Expand Up @@ -165,6 +184,111 @@ func TestVerifyAndSettleRejectsGarbageCredential(t *testing.T) {
}
}

// TestSignerBridgeSignPropagatesSignerError proves that when the wrapped
// paykit.Signer.Sign returns an error, signerBridge.Sign surfaces it wrapped
// with the "signerBridge:" prefix.
func TestSignerBridgeSignPropagatesSignerError(t *testing.T) {
demo := signer.Demo()
bad := &errSigner{
pubkey: string(demo.Pubkey()),
err: fmt.Errorf("KMS unavailable"),
}
b := &signerBridge{signer: bad}
_, err := b.Sign([]byte("hello"))
if err == nil {
t.Fatal("expected an error from failing inner signer")
}
if err.Error() == "" {
t.Fatal("expected non-empty error message")
}
}

// TestSignerBridgeSignRejectsWrongLength proves that when the inner signer
// returns a raw byte slice that is not exactly 64 bytes, signerBridge.Sign
// returns an error rather than copying a truncated or oversized value into a
// solana.Signature.
func TestSignerBridgeSignRejectsWrongLength(t *testing.T) {
demo := signer.Demo()
short := &errSigner{
pubkey: string(demo.Pubkey()),
raw: make([]byte, 32), // 32 bytes — not 64
}
b := &signerBridge{signer: short}
_, err := b.Sign([]byte("hello"))
if err == nil {
t.Fatal("expected an error for a 32-byte (non-64) signature")
}
}

// TestChallengeHeadersReturnsNilOnBadRecipient proves that ChallengeHeaders
// returns nil (rather than panicking) when the gate's PayTo address is not a
// valid Solana pubkey, which causes serverFor -> server.New to fail.
func TestChallengeHeadersReturnsNilOnBadRecipient(t *testing.T) {
a := &Adapter{cfg: testCfg()}
gate := &paykit.Gate{
Amount: paykit.MustParseUSD("0.10"),
PayTo: paykit.Address("!!!not-a-valid-pubkey"),
}
if headers := a.ChallengeHeaders(gate); headers != nil {
t.Errorf("expected nil for ChallengeHeaders with bad recipient, got %v", headers)
}
}

// TestVerifyAndSettleReturnsErrOnBadRecipient proves VerifyAndSettle wraps
// the serverFor failure in a PaymentError (code="invalid_proof") when the
// gate's PayTo address is not a valid Solana pubkey.
func TestVerifyAndSettleReturnsErrOnBadRecipient(t *testing.T) {
a := &Adapter{cfg: testCfg()}
gate := &paykit.Gate{
Amount: paykit.MustParseUSD("0.10"),
PayTo: paykit.Address("!!!not-a-valid-pubkey"),
}
_, err := a.VerifyAndSettle(&paykit.AdapterRequest{
Gate: gate,
Authorization: "Payment bm90LWEtY3JlZGVudGlhbA==",
})
if err == nil {
t.Fatal("expected an error for a gate with an invalid recipient")
}
}

// TestChargeOptionsIncludesFeeWithinAndFeeOnTopSplits proves that chargeOptions
// appends paycore.Split entries for both FeeWithin and FeeOnTop fees declared
// on the gate. This exercises the two range-loop bodies in chargeOptions that
// the existing AcceptsEntry tests do not reach.
func TestChargeOptionsIncludesFeeWithinAndFeeOnTopSplits(t *testing.T) {
a := &Adapter{cfg: testCfg()}
gate := &paykit.Gate{
Amount: paykit.MustParseUSD("10.00"),
FeeWithin: paykit.Fees{paykit.Address("PLATFORM"): paykit.MustParseUSD("0.30")},
FeeOnTop: paykit.Fees{paykit.Address("GATEWAY"): paykit.MustParseUSD("0.50")},
}
opts := a.chargeOptions(gate)
if len(opts.Splits) != 2 {
t.Fatalf("expected 2 splits in chargeOptions, got %d: %+v", len(opts.Splits), opts.Splits)
}
recipients := make(map[string]bool)
for _, s := range opts.Splits {
recipients[s.Recipient] = true
}
if !recipients["PLATFORM"] || !recipients["GATEWAY"] {
t.Errorf("chargeOptions splits missing expected recipients: %v", opts.Splits)
}
}

// TestPriceCoinUsesExplicitSettlementWhenPresent proves that priceCoin returns
// the first explicit settlement stablecoin rather than falling back to the
// adapter config when the Price was built with an explicit settlement.
func TestPriceCoinUsesExplicitSettlementWhenPresent(t *testing.T) {
a := &Adapter{cfg: testCfg()} // cfg.Stablecoins = [USDC]
// Build a price with an explicit USDT settlement; priceCoin must return
// USDT (the explicit settlement) rather than USDC (the config default).
priceUSDT := paykit.MustParseUSD("0.30", paykit.USDT)
if got := a.priceCoin(priceUSDT); got != "USDT" {
t.Errorf("priceCoin: got %q want USDT", got)
}
}

func TestVerifyAndSettleReachesCredentialVerification(t *testing.T) {
cfg := testCfg()
cfg.RPCURL = "http://127.0.0.1:1" // unreachable; charge build tolerates it
Expand Down
Loading
Loading