Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions changelog.d/1063-gonuts-v0.14.0.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- The wallet fork pin moves to gonuts-tollgate v0.14.0, picking up the forged-keyset-id refusal (a mint whose advertised keyset id does not match the id its published keys derive to is now rejected — tollgate #705), the refuse-swap-consumed-by-fees guard, pre-persisted swap intents with crash replay, and cached keyset fees. Test fakes now advertise keyset ids their keys actually derive to, as the new guard demands. ([#1063](https://github.com/OpenTollGate/tollgate-module-basic-go/pull/1063))
1 change: 1 addition & 0 deletions changelog.d/1077-wallet-init-keyset-self-heal.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed: wallet init no longer bricks into degraded mode when the mint has rotated its keysets since the last sync — with a reachable mint the keyset set is re-fetched (NUT-02) and boot proceeds; retired-keyset records, their derivation counters and any unspent proofs are preserved in the wallet instead of requiring the move-wallet.db-aside recovery ([#1077](https://github.com/OpenTollGate/tollgate-module-basic-go/pull/1077))
1 change: 1 addition & 0 deletions changelog.d/908-swap-fee-belt-refusal.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **A token whose value exactly equals its swap fee is refused cleanly, not bounced by the mint.** The wallet used to pass the below-fees guard when the token amount EQUALLED the fee, split zero, and POST a swap with an empty outputs array — the mint's raw NUT-03 400 ("Outputs are required and must be an array", the CU107 portal error) reached the payer instead of a fee verdict. gonuts-tollgate v0.13.1 refuses it locally (and any swap that reaches zero outputs), and the backend maps the refusal to the clear payment-error-below-swap-fee notice ([#908](https://github.com/OpenTollGate/tollgate-module-basic-go/pull/908))
6 changes: 3 additions & 3 deletions packaging/build-inputs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
"comment": "Immutable build inputs for reproducible tollgate-wrt artifacts. Every byte-affecting external input is pinned here. Update intentionally (see docs/reproducible-builds.md); never point portal.commit at a floating ref.",
"go": {
"version": "1.26.10",
"comment": "The toolchain that compiles the shipped binaries. SOURCE OF TRUTH: the official golang of the pinned OpenWrt SDK release's packages feed (see .openwrt_sdk.go_per_release) — the feed defines it, so our binaries are reproducible against the SDK's own toolchain; never a minor of our own choosing (#448). scripts/sdk-go-version.sh check enforces it against the live feeds; update re-derives it.",
"comment": "The toolchain that compiles the shipped binaries. SOURCE OF TRUTH: the official golang of the pinned OpenWrt SDK release's packages feed (see .openwrt_sdk.go_per_release) \u2014 the feed defines it, so our binaries are reproducible against the SDK's own toolchain; never a minor of our own choosing (#448). scripts/sdk-go-version.sh check enforces it against the live feeds; update re-derives it.",
"tarball_linux_amd64": {
"url": "https://go.dev/dl/go1.26.10.linux-amd64.tar.gz",
"sha256": "ccfe7ae822d2c2f41f4fe7c956199967a4dac22d0ee0d2b2bfdb440aab3b20c5"
}
},
"gonuts": {
"repo": "https://github.com/OpenTollGate/gonuts-tollgate",
"version": "v0.13.0",
"version": "v0.14.1",
"comment": "The wallet fork every Go module pins IDENTICALLY. tests/contract/check-gonuts-pin.py enumerates carriers by glob (never a hand list) and fails on any disagreement with this value; scripts/bump-gonuts.sh rewrites manifest and every carrier together. Before a release is cut, release-check verifies this is the fork's latest stable tag. Pseudo-version integration pins are for branches only and must not outlive their tag on main."
},
"node": {
Expand Down Expand Up @@ -71,7 +71,7 @@
},
"ipk": {
"release": "24.10.8",
"comment": "The ipk/opkg era for the installed base (feed-less 24.10 systems — fork #93/#110 evidence). Digests are the point-release tags from hub.docker.com/openwrt/sdk. The SDK packages STAGED PREBUILT binaries (the manifest-pinned Go compiles on the host), so the era's older feed Go (1.23.x) never compiles our code — go_per_release keeps it audited for awareness.",
"comment": "The ipk/opkg era for the installed base (feed-less 24.10 systems \u2014 fork #93/#110 evidence). Digests are the point-release tags from hub.docker.com/openwrt/sdk. The SDK packages STAGED PREBUILT binaries (the manifest-pinned Go compiles on the host), so the era's older feed Go (1.23.x) never compiles our code \u2014 go_per_release keeps it audited for awareness.",
"targets": {
"mediatek-filogic": {
"digest": "sha256:4109260d68a593972d0d0408e6e439472cd02df9a29a3a9383fd52bd97707a4a"
Expand Down
2 changes: 1 addition & 1 deletion scripts/token-recovery/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module token-recovery

go 1.26.0

require github.com/OpenTollGate/gonuts-tollgate v0.13.0
require github.com/OpenTollGate/gonuts-tollgate v0.14.1

require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
Expand Down
4 changes: 2 additions & 2 deletions scripts/token-recovery/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OpenTollGate/gonuts-tollgate v0.13.0 h1:BtxPOSzbSED8ZytVzhEVBXrYgHRlSHqcgoWdhG5evds=
github.com/OpenTollGate/gonuts-tollgate v0.13.0/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/OpenTollGate/gonuts-tollgate v0.14.1 h1:hLJkL81dMQcz+ZUQkHgbP2MvzxvIVESxTAVSvXiFY08=
github.com/OpenTollGate/gonuts-tollgate v0.14.1/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
Expand Down
2 changes: 1 addition & 1 deletion src/cli/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (

require (
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/OpenTollGate/gonuts-tollgate v0.13.0 // indirect
github.com/OpenTollGate/gonuts-tollgate v0.14.1 // indirect
github.com/OpenTollGate/tollgate-module-basic-go/src/lightning v0.0.0-00010101000000-000000000000 // indirect
github.com/OpenTollGate/tollgate-module-basic-go/src/tollwallet v0.0.0 // indirect
github.com/OpenTollGate/tollgate-module-basic-go/src/utils v0.0.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions src/cli/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OpenTollGate/gonuts-tollgate v0.13.0 h1:BtxPOSzbSED8ZytVzhEVBXrYgHRlSHqcgoWdhG5evds=
github.com/OpenTollGate/gonuts-tollgate v0.13.0/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/OpenTollGate/gonuts-tollgate v0.14.1 h1:hLJkL81dMQcz+ZUQkHgbP2MvzxvIVESxTAVSvXiFY08=
github.com/OpenTollGate/gonuts-tollgate v0.14.1/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
Expand Down
2 changes: 1 addition & 1 deletion src/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ require (
github.com/FactomProject/basen v0.0.0-20150613233007-fe3947df716e // indirect
github.com/FactomProject/btcutilecc v0.0.0-20130527213604-d3a63a5752ec // indirect
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
github.com/OpenTollGate/gonuts-tollgate v0.13.0 // indirect
github.com/OpenTollGate/gonuts-tollgate v0.14.1 // indirect
github.com/OpenTollGate/tollgate-module-basic-go/src/lightning v0.0.0-00010101000000-000000000000 // indirect
github.com/OpenTollGate/tollgate-module-basic-go/src/tollgate_protocol v0.0.0 // indirect
github.com/OpenTollGate/tollgate-module-basic-go/src/tollwallet v0.0.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions src/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OpenTollGate/gonuts-tollgate v0.13.0 h1:BtxPOSzbSED8ZytVzhEVBXrYgHRlSHqcgoWdhG5evds=
github.com/OpenTollGate/gonuts-tollgate v0.13.0/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/OpenTollGate/gonuts-tollgate v0.14.1 h1:hLJkL81dMQcz+ZUQkHgbP2MvzxvIVESxTAVSvXiFY08=
github.com/OpenTollGate/gonuts-tollgate v0.14.1/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
Expand Down
4 changes: 2 additions & 2 deletions src/merchant/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ module github.com/OpenTollGate/tollgate-module-basic-go/src/merchant
go 1.26.0

require (
github.com/OpenTollGate/gonuts-tollgate v0.13.0
github.com/OpenTollGate/gonuts-tollgate v0.14.1
github.com/OpenTollGate/tollgate-module-basic-go/src/config_manager v0.0.0
github.com/OpenTollGate/tollgate-module-basic-go/src/tollwallet v0.0.0
github.com/OpenTollGate/tollgate-module-basic-go/src/utils v0.0.0
github.com/OpenTollGate/tollgate-module-basic-go/src/valve v0.0.0
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
github.com/nbd-wtf/go-nostr v0.51.12
golang.org/x/time v0.15.0
)
Expand Down Expand Up @@ -78,7 +79,6 @@ require (
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/coder/websocket v1.8.13 // indirect
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down
4 changes: 2 additions & 2 deletions src/merchant/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw=
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk=
github.com/OpenTollGate/gonuts-tollgate v0.13.0 h1:BtxPOSzbSED8ZytVzhEVBXrYgHRlSHqcgoWdhG5evds=
github.com/OpenTollGate/gonuts-tollgate v0.13.0/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/OpenTollGate/gonuts-tollgate v0.14.1 h1:hLJkL81dMQcz+ZUQkHgbP2MvzxvIVESxTAVSvXiFY08=
github.com/OpenTollGate/gonuts-tollgate v0.14.1/go.mod h1:4er8keqQ4yg3Zuz0w1BuXclKSeKOk3SGqsEl4b0oI7M=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
Expand Down
4 changes: 4 additions & 0 deletions src/merchant/merchant.go
Original file line number Diff line number Diff line change
Expand Up @@ -1866,6 +1866,10 @@ func isBelowSwapFeeError(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "nothing to swap") ||
strings.Contains(msg, "no outputs provided") ||
// gonuts-tollgate v0.13.1's belt check (#752): a swap that reached
// zero outputs through any path is refused before the mint is
// asked — same fee-vs-value family, same clean notice.
strings.Contains(msg, "produced no outputs") ||
strings.Contains(msg, "swap fees")
}

Expand Down
7 changes: 5 additions & 2 deletions src/merchant/merchant_degraded_recovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,13 @@ func newSwitchableMintServer(t *testing.T) *switchableMintServer {
switch r.URL.Path {
case "/v1/keysets":
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"keysets":[{"id":"00ad268c4d1f5826","unit":"sat","active":true,"input_fee_ppk":0}]}`)
// 000f715baf5d4c2e is what DeriveKeysetId derives for this fake's
// amount-1 key (the generator point) — v0.14.0's #705 guard
// refuses ids the published keys do not derive to.
fmt.Fprint(w, `{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true,"input_fee_ppk":0}]}`)
case "/v1/keys":
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"keysets":[{"id":"00ad268c4d1f5826","unit":"sat","active":true,"keys":{"1":"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"}}]}`)
fmt.Fprint(w, `{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true,"keys":{"1":"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"}}]}`)
default:
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{}`)
Expand Down
2 changes: 1 addition & 1 deletion src/merchant/merchant_degraded_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func TestNewFullMerchant_WalletInitFail_FallsBackToDegraded(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/v1/keysets" {
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"keysets":[{"id":"00ad268c4d1f5826","unit":"sat","active":true}]}`)
fmt.Fprint(w, `{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true}]}`)
return
}
http.Error(w, "internal server error", http.StatusInternalServerError)
Expand Down
4 changes: 2 additions & 2 deletions src/merchant/merchant_health_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func reachableServer(t *testing.T) *httptest.Server {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
if r.URL.Path == "/v1/keysets" {
_, _ = w.Write([]byte(`{"keysets":[{"id":"00ad268c4d1f5826","unit":"sat","active":true}]}`))
_, _ = w.Write([]byte(`{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true}]}`))
}
}))
t.Cleanup(srv.Close)
Expand Down Expand Up @@ -225,7 +225,7 @@ func TestProbeMint_TrailingSlashTrimmed(t *testing.T) {
requestedPath = r.URL.Path
w.Header().Set("Content-Type", "application/json")
writeKeysetsOK(w)
_, _ = w.Write([]byte(`{"keysets":[{"id":"00ad268c4d1f5826","unit":"sat","active":true}]}`))
_, _ = w.Write([]byte(`{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true}]}`))
}))
defer srv.Close()

Expand Down
123 changes: 123 additions & 0 deletions src/merchant/merchant_keyset_rotation_init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package merchant

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"sync/atomic"
"testing"

"github.com/OpenTollGate/gonuts-tollgate/crypto"
"github.com/OpenTollGate/gonuts-tollgate/wallet"
"github.com/OpenTollGate/tollgate-module-basic-go/src/config_manager"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
)

// The #818 slice at the merchant level: booting against a REACHABLE mint
// that has rotated its keyset set since the wallet last synced must reach
// "=== Merchant ready ===", not the degraded mode the issue shipped
// ("Wallet initialization failed (… Unknown Keyset) — starting in degraded
// mode" with a first-reachable trigger that never fires because the mint
// was reachable all along).
//
// The fake mint is reachable on every keyset endpoint but no longer knows
// the stale keyset id the pre-rotation wallet.db still has as its active
// record — the exact desk-run shape from the issue.

// honestStaleKeysetID derives a second honest id (a keyset this mint never
// served, standing in for one it has retired and dropped).
func honestStaleKeysetID(t *testing.T) string {
t.Helper()
priv, err := secp256k1.GeneratePrivateKey()
if err != nil {
t.Fatalf("generate private key: %v", err)
}
return crypto.DeriveKeysetId(crypto.PublicKeys{1: priv.PubKey()})
}

func seedRetiredKeysetWallet(t *testing.T, dir, mintURL, staleID string) {
t.Helper()
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatalf("mkdir wallet dir: %v", err)
}
db, err := wallet.InitStorage(dir)
if err != nil {
t.Fatalf("seed InitStorage: %v", err)
}
rec := crypto.WalletKeyset{Id: staleID, MintURL: mintURL, Unit: "sat", Active: true}
if err := db.SaveKeyset(&rec); err != nil {
t.Fatalf("seed SaveKeyset: %v", err)
}
if err := db.Close(); err != nil {
t.Fatalf("seed close: %v", err)
}
}

// TestMerchantReadyUnderKeysetRotation_Issue818 boots the full startup
// path (probe → wallet → advertisement) with a rotated mint and a
// pre-rotation wallet: the merchant must come up READY.
func TestMerchantReadyUnderKeysetRotation_Issue818(t *testing.T) {
staleID := honestStaleKeysetID(t)
var keysetsProbes atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/keysets":
keysetsProbes.Add(1)
w.Header().Set("Content-Type", "application/json")
// 000f715baf5d4c2e is what DeriveKeysetId derives for this fake's
// amount-1 key (the generator point) — v0.14.0's #705 guard
// refuses ids the published keys do not derive to.
fmt.Fprint(w, `{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true,"input_fee_ppk":0}]}`)
case "/v1/keys":
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true,"keys":{"1":"0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798"}}]}`)
case "/v1/keys/" + staleID:
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprint(w, `{"detail":"Unknown Keyset","code":12001}`)
default:
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{}`)
}
}))
t.Cleanup(srv.Close)

cm, testDir := setupTestConfigManager(t)
// GetConfig returns a defensive copy — the mutation must be persisted
// and reloaded, or the merchant boots against the factory-default mint
// (the first version of this test "passed" by reaching coinos.io).
cfg := cm.GetConfig()
cfg.AcceptedMints = simpleMintConfig(srv.URL)
if err := config_manager.SaveConfig(cm.ConfigFilePath, cfg); err != nil {
t.Fatalf("SaveConfig: %v", err)
}
if err := cm.ReloadConfig(); err != nil {
t.Fatalf("ReloadConfig: %v", err)
}

// The pre-rotation wallet lives where the merchant will look for it:
// next to the config file (filepath.Dir of ConfigFilePath).
seedRetiredKeysetWallet(t, testDir, srv.URL, staleID)

m, err := New(cm)
if err != nil {
t.Fatalf("New: %v", err)
}
if s, ok := m.(interface{ Shutdown() error }); ok {
defer func() { _ = s.Shutdown() }()
}
if _, degraded := m.(*MerchantDegraded); degraded {
t.Fatal("merchant degraded at boot under keyset rotation with a reachable mint — the #818 brick")
}
full, ok := m.(*Merchant)
if !ok {
t.Fatalf("New returned %T, want *Merchant", m)
}
if got := full.GetBalance(); got != 0 {
t.Fatalf("balance: want 0 got %d", got)
}
if keysetsProbes.Load() == 0 {
t.Fatal("the merchant never probed the rotating mint — the test did not exercise it")
}
}
7 changes: 7 additions & 0 deletions src/merchant/merchant_swap_fee_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ func TestIsBelowSwapFeeError(t *testing.T) {
want bool
}{
{"could not swap proofs: token amount 1 is below the mint's swap fees (1): nothing to swap", true},
// gonuts-tollgate v0.13.1 (#752): the amount == fee case used to
// pass the wallet's below-fees guard and POST an empty outputs
// array — the mint's raw CU107 400 reached the payer. The wallet
// now refuses it with this message, and the classifier must map
// it to the same clean below-swap-fee notice.
{"could not swap proofs: token amount 1 is entirely consumed by the mint's swap fees (1): nothing to swap", true},
{"could not swap proofs: swap request for 1 sat (fees 1) produced no outputs: refusing to send an empty swap", true},
{"could not swap proofs: no outputs provided", true},
{"token already spent", false},
{"short keyset ID 0118 not found in mint keysets", false},
Expand Down
2 changes: 1 addition & 1 deletion src/merchant/mint_health_tracker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func newTestTracker(config *config_manager.Config, client *http.Client) *MintHea
func writeKeysetsOK(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"keysets":[{"id":"00ad268c4d1f5826","unit":"sat","active":true}]}`))
_, _ = w.Write([]byte(`{"keysets":[{"id":"000f715baf5d4c2e","unit":"sat","active":true}]}`))
}

// --- Unit Tests ---
Expand Down
Loading