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
3 changes: 3 additions & 0 deletions db/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/db.<Symb
3s cap).
- SQLite `busy_timeout = 30 000 ms` under WAL mode tolerates
multi-actor contention bursts.
- Postgres test fixture slots bound Docker startup and migrations only. Helpers
release the slot before returning the initialized store; retaining it until
test cleanup can deadlock Go's parallel-test barrier.
- `ledger_entries.entry_id` and `wallet_utxo_log.entry_id` use
`INTEGER PRIMARY KEY AUTOINCREMENT` to prevent rowid reuse after
deletion, preserving append-only ordering.
Expand Down
3 changes: 3 additions & 0 deletions db/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ For field-level detail, use `go doc github.com/lightninglabs/wavelength/db.<Symb
3s cap).
- SQLite `busy_timeout = 30 000 ms` under WAL mode tolerates
multi-actor contention bursts.
- Postgres test fixture slots bound Docker startup and migrations only. Helpers
release the slot before returning the initialized store; retaining it until
test cleanup can deadlock Go's parallel-test barrier.
- `ledger_entries.entry_id` and `wallet_utxo_log.entry_id` use
`INTEGER PRIMARY KEY AUTOINCREMENT` to prevent rowid reuse after
deletion, preserving append-only ordering.
Expand Down
23 changes: 16 additions & 7 deletions db/postgres_fixture.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,12 @@ func NewTestPgFixture(t testing.TB, expiry time.Duration,
return fixture
}

// acquireTestPgFixtureSlot bounds active Postgres containers in parallel test
// runs. The db package marks most tests parallel, and unbounded docker startup
// can starve CI runners enough that stores observe partially initialized
// schemas.
// acquireTestPgFixtureSlot bounds concurrent Postgres fixture initialization.

@lightninglabs-gateway lightninglabs-gateway Bot Aug 26, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 Dismissed by @bhandras

🟡 F8 (Minor) — Live Postgres containers are no longer bounded · db/postgres_fixture.go:127

The semaphore previously bounded live fixtures at four, because the slot was held until TearDown; it now bounds only the initialization window, so the number of simultaneously running Postgres containers is capped by -parallel (GOMAXPROCS by default) rather than by testPgFixtureParallelism. On an 8- or 16-CPU runner that is 2–4× the containers PR #772 was sized for, and the starvation it was added to prevent — "stores observe partially initialized schemas" — is memory/IO pressure from concurrently running containers as much as from concurrent startup. The rewritten comment and the db/AGENTS.md invariant both describe the new, narrower guarantee accurately, so this is a resource-envelope decision rather than a mismatch between code and docs; the local timings in the PR description show the dev host absorbs it, not that a constrained CI runner does. If you want the old ceiling back without reintroducing hold-and-wait, a second, larger semaphore acquired for the container's lifetime (sized independently of the init bound) keeps the two concerns separate. Accepting the change as-is is also defensible — the CI signal on this head is what settles it.

// The db package marks most tests parallel, and unbounded Docker startup and
// migrations can starve CI runners enough that stores observe partially
// initialized schemas. Callers release the slot once store initialization
// returns; holding it for the full test lifetime can deadlock Go's parallel
// test barrier.
func acquireTestPgFixtureSlot() func() {
testPgFixtureSem <- struct{}{}

Expand All @@ -140,6 +142,15 @@ func acquireTestPgFixtureSlot() func() {
}
}

// releaseFixtureInitSlot releases the fixture's initialization slot. The
// closure is idempotent so teardown remains a safe fallback for callers that
// fail before store initialization returns.
func (f *TestPgFixture) releaseFixtureInitSlot() {
if f.releaseSlot != nil {
f.releaseSlot()
}
}

// GetDSN returns the DSN (Data Source Name) for the started Postgres node.
func (f *TestPgFixture) GetDSN() string {
return f.GetConfig().DSN(false)
Expand All @@ -160,9 +171,7 @@ func (f *TestPgFixture) GetConfig() *PostgresConfig {
// TearDown stops the underlying docker container.
func (f *TestPgFixture) TearDown(t testing.TB) {
err := f.pool.Purge(f.resource)
if f.releaseSlot != nil {
f.releaseSlot()
}
f.releaseFixtureInitSlot()
require.NoError(t, err, "Could not purge resource")
}

Expand Down
2 changes: 2 additions & 0 deletions db/postgres_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func NewTestPostgresDB(t testing.TB) *PostgresStore {
log := btclog.Disabled

sqlFixture := NewTestPgFixture(t, DefaultPostgresFixtureLifetime, true)
defer sqlFixture.releaseFixtureInitSlot()

// Cleanups run in reverse order. Register the fixture first so the
// store pool registered below is closed before its container is
Expand Down Expand Up @@ -62,6 +63,7 @@ func NewTestPostgresDBWithVersion(t testing.TB, version uint) *PostgresStore {
log := btclog.Disabled

sqlFixture := NewTestPgFixture(t, DefaultPostgresFixtureLifetime, true)
defer sqlFixture.releaseFixtureInitSlot()

// Cleanups run in reverse order. Register the fixture first so the
// store pool registered below is closed before its container is
Expand Down
8 changes: 5 additions & 3 deletions db/postgres_test_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ import (
"github.com/stretchr/testify/require"
)

// TestNewTestPostgresDBClosesStore verifies that the common fixture helper
// closes its connection pool before tearing down the Postgres container.
func TestNewTestPostgresDBClosesStore(t *testing.T) {
// TestNewTestPostgresDBLifecycle verifies that the common fixture helper
// releases its initialization slot before returning and closes its connection
// pool before tearing down the Postgres container.
func TestNewTestPostgresDBLifecycle(t *testing.T) {
var store *PostgresStore

t.Run("fixture lifetime", func(t *testing.T) {
store = NewTestPostgresDB(t)
require.NoError(t, store.DB.Ping())
require.Empty(t, testPgFixtureSem)
})

require.NotNil(t, store)
Expand Down
1 change: 1 addition & 0 deletions db/test_postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func NewTestDBHandleFromPath(t testing.TB, dbPath string) *PostgresStore {
sqlFixture = NewTestPgFixture(
t, DefaultPostgresFixtureLifetime, true,
)
defer sqlFixture.releaseFixtureInitSlot()

store, err := NewPostgresStore(sqlFixture.GetConfig(), log)
if err != nil {
Expand Down
19 changes: 18 additions & 1 deletion docs/wavewalletdk_mobile.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,17 @@ not snake_case. Host models must map those exact names (e.g. `@SerialName`
(`data_dir`, `wallet_esplora_url`, …), and `OpenWalletFromPasskey`, whose
request is a small camelCase-tagged struct (`prfOutput`). Every other request
decodes into the matching `wavewalletdk.*Request` DTO, so it follows the
PascalCase rule.
PascalCase rule. `Receive` additionally accepts `TimeoutSeconds`: zero or an
omitted field selects a 20-second default, and values above five minutes are
rejected. A binding-owned deadline or lifecycle cancellation returns an error
beginning `receive outcome uncertain; reconcile Activity before retrying`. It
does not prove that the receive session was not created. The host must reconcile
the authoritative Activity view and recover any matching invoice before
deliberately asking to create another one. Repeatable reads (`GetInfo`,
`Balance`, `List`, `ExitStatus`, `ExitSummary`, `GetExitPlan`, `Status`, and the
scalar balance/readiness helpers) use a 10-second binding-owned deadline because
gomobile cannot carry a caller `context.Context`; these reads are safe for the
host to request again after timeout.

`StartExternalSeedWallet` is another explicit exception in the private binding
ABI. Its startup envelope is snake_case (`config`, `seed_entropy`,
Expand Down Expand Up @@ -95,6 +105,13 @@ func Stop() error
and in-flight RPCs / subscriptions unwind on shutdown.
- The startup deadline bounds daemon readiness only. External-seed opening and
recovery use the lifecycle context cancelled by `Stop`.
- The portable Go binding cannot observe an iOS or Android application
lifecycle. A host that leaves the embedded daemon alive while its process is
suspended also leaves external gRPC connections frozen on their old network
path. After a real background/resume transition, call `Stop` and `Start`
(then unlock the same durable wallet) so external transports are re-dialled.
The lifecycle state machine prevents overlapping daemon instances; it does
not infer foreground state for the host.

### External seed wallets

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/btcsuite/btcd v0.26.0
github.com/btcsuite/btcd/btcec/v2 v2.5.0
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b
github.com/btcsuite/btcwallet v0.18.0
github.com/btcsuite/btcwallet v0.18.1-0.20260826052527-33c252f3b4d6
github.com/btcsuite/btcwallet/walletdb v1.6.0
github.com/btcsuite/btcwallet/wtxmgr v1.6.0
github.com/golang-migrate/migrate/v4 v4.19.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -658,8 +658,8 @@ github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns
github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ=
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM=
github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE=
github.com/btcsuite/btcwallet v0.18.0 h1:VSRClNLT7NX0wmJEGALz3jOZRRjWPpUdp7VI1Akie1o=
github.com/btcsuite/btcwallet v0.18.0/go.mod h1:1ZMc1EEskov+AKKv4kCMZqN8BwVh9rpXwEyxbeWy2A4=
github.com/btcsuite/btcwallet v0.18.1-0.20260826052527-33c252f3b4d6 h1:A49O49JWTm2xaI3lWWtlYAQ8dpM3eMCu6JV52AWXOhQ=
github.com/btcsuite/btcwallet v0.18.1-0.20260826052527-33c252f3b4d6/go.mod h1:1ZMc1EEskov+AKKv4kCMZqN8BwVh9rpXwEyxbeWy2A4=
github.com/btcsuite/btcwallet/wallet/txauthor v1.4.0 h1:oIkGj32YK1CvWaJGlVwZA1f+y/KVHkfrd2PoST0ZpQs=
github.com/btcsuite/btcwallet/wallet/txauthor v1.4.0/go.mod h1:sGrBjcqQ8UPexuRajFs72+o544CJn3Pavv/5H0VAWVk=
github.com/btcsuite/btcwallet/wallet/txrules v1.3.0 h1:D5aGMwWIxdqek3xEJs4eOdMoh6iga2EI2xSlaXCdnNo=
Expand Down
19 changes: 19 additions & 0 deletions sdk/wavewalletdk/mobile/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ application-facing wallet API.
`applyMobileConfig` into a `wavewalletdk.Config`; validated (non-negative
durations/counts, `uint32`-safe recovery window) before merging onto
`wavewalletdk.DefaultConfig()`.
- `mobileReceiveRequest` — unexported wire DTO carrying the SDK's current
`AmountSat`/`Memo` fields plus the mobile-only `TimeoutSeconds`. It is
projected onto `wavewalletdk.ReceiveRequest` after deadline validation.
- RPC verbs (`GetInfo`, `CreateWallet`, `UnlockWallet`, `Balance`, `Deposit`,
`Receive`, `PrepareSend`, `SendPrepared`, `List`, `Exit`, `ExitStatus`,
`ExitSummary`, `GetExitPlan`, `SweepWallet`, `Status`, `Subscribe`,
`OpenWalletFromPasskey`) — each dereferences the singleton `wavewalletdk.Client`
via `activeClient()`, decodes a JSON request into the matching
`wavewalletdk.*Request`, and marshals the `wavewalletdk.*Result` response.
`Receive` is the exception: it decodes `mobileReceiveRequest` first so the
binding can own its request deadline without changing the SDK DTO.
- Scalar conveniences (`ConfirmedBalanceSat`, `PendingInboundSat`,
`WalletReady`, `IsRunning`) — avoid a JSON round trip for hot-path reads;
`IsRunning` never blocks on an RPC.
Expand All @@ -57,6 +62,18 @@ application-facing wallet API.
can succeed (e.g. after OS suspend/resume).
- `startEmbedded` separates its daemon-readiness deadline from the lifecycle
context used to open or recover the wallet. `Stop` cancels both.
- gomobile cannot carry a caller `context.Context`, so this package owns
per-call deadlines. Repeatable reads (`GetInfo`, `Balance`, `List`,
`ExitStatus`, `ExitSummary`, `GetExitPlan`, `Status`, and the scalar
balance/readiness helpers) use `readContext`'s 10-second deadline. `Receive`
uses `TimeoutSeconds`, defaults to 20 seconds when zero or omitted, and
rejects negative values or values above five minutes.
- A `Receive` canceled by its binding-owned deadline or the parent lifecycle is
returned with the stable `receiveUncertainErrorPrefix`. The result is
uncertain: the host must reconcile Activity before deliberately requesting
another invoice.
- Every bounded call context derives from the daemon-lifetime parent returned
by `activeClient`, so `Stop` still cancels in-flight calls immediately.
- Startup through `startEmbedded` and `Subscription.Next` recover panics into
a returned `error`; those are the entry points documented to survive a panic
without crossing the gomobile boundary and killing the host process.
Expand All @@ -70,6 +87,8 @@ application-facing wallet API.

## Deep Docs

- [docs/wavewalletdk_mobile.md](../../../docs/wavewalletdk_mobile.md) — Binding
ABI, JSON field casing, per-call deadlines, and host lifecycle rules.
- [sdk/wavewalletdk/CLAUDE.md](../CLAUDE.md) — Wrapped SDK; see for full DTO and
RPC method detail.
- [ARCHITECTURE.md](../../../ARCHITECTURE.md) — System-wide package map.
19 changes: 19 additions & 0 deletions sdk/wavewalletdk/mobile/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ application-facing wallet API.
`applyMobileConfig` into a `wavewalletdk.Config`; validated (non-negative
durations/counts, `uint32`-safe recovery window) before merging onto
`wavewalletdk.DefaultConfig()`.
- `mobileReceiveRequest` — unexported wire DTO carrying the SDK's current
`AmountSat`/`Memo` fields plus the mobile-only `TimeoutSeconds`. It is
projected onto `wavewalletdk.ReceiveRequest` after deadline validation.
- RPC verbs (`GetInfo`, `CreateWallet`, `UnlockWallet`, `Balance`, `Deposit`,
`Receive`, `PrepareSend`, `SendPrepared`, `List`, `Exit`, `ExitStatus`,
`ExitSummary`, `GetExitPlan`, `SweepWallet`, `Status`, `Subscribe`,
`OpenWalletFromPasskey`) — each dereferences the singleton `wavewalletdk.Client`
via `activeClient()`, decodes a JSON request into the matching
`wavewalletdk.*Request`, and marshals the `wavewalletdk.*Result` response.
`Receive` is the exception: it decodes `mobileReceiveRequest` first so the
binding can own its request deadline without changing the SDK DTO.
- Scalar conveniences (`ConfirmedBalanceSat`, `PendingInboundSat`,
`WalletReady`, `IsRunning`) — avoid a JSON round trip for hot-path reads;
`IsRunning` never blocks on an RPC.
Expand All @@ -57,6 +62,18 @@ application-facing wallet API.
can succeed (e.g. after OS suspend/resume).
- `startEmbedded` separates its daemon-readiness deadline from the lifecycle
context used to open or recover the wallet. `Stop` cancels both.
- gomobile cannot carry a caller `context.Context`, so this package owns
per-call deadlines. Repeatable reads (`GetInfo`, `Balance`, `List`,
`ExitStatus`, `ExitSummary`, `GetExitPlan`, `Status`, and the scalar
balance/readiness helpers) use `readContext`'s 10-second deadline. `Receive`
uses `TimeoutSeconds`, defaults to 20 seconds when zero or omitted, and
rejects negative values or values above five minutes.
- A `Receive` canceled by its binding-owned deadline or the parent lifecycle is
returned with the stable `receiveUncertainErrorPrefix`. The result is
uncertain: the host must reconcile Activity before deliberately requesting
another invoice.
- Every bounded call context derives from the daemon-lifetime parent returned
by `activeClient`, so `Stop` still cancels in-flight calls immediately.
- Startup through `startEmbedded` and `Subscription.Next` recover panics into
a returned `error`; those are the entry points documented to survive a panic
without crossing the gomobile boundary and killing the host process.
Expand All @@ -70,6 +87,8 @@ application-facing wallet API.

## Deep Docs

- [docs/wavewalletdk_mobile.md](../../../docs/wavewalletdk_mobile.md) — Binding
ABI, JSON field casing, per-call deadlines, and host lifecycle rules.
- [sdk/wavewalletdk/CLAUDE.md](../CLAUDE.md) — Wrapped SDK; see for full DTO and
RPC method detail.
- [ARCHITECTURE.md](../../../ARCHITECTURE.md) — System-wide package map.
12 changes: 9 additions & 3 deletions sdk/wavewalletdk/mobile/convenience.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ package mobile

// ConfirmedBalanceSat returns the confirmed wallet balance in satoshis.
func ConfirmedBalanceSat() (int64, error) {
client, ctx, err := activeClient()
client, parentCtx, err := activeClient()
if err != nil {
return 0, err
}
ctx, cancel := readContext(parentCtx)
defer cancel()

bal, err := client.Balance(ctx)
if err != nil {
Expand All @@ -23,10 +25,12 @@ func ConfirmedBalanceSat() (int64, error) {

// PendingInboundSat returns the pending inbound balance in satoshis.
func PendingInboundSat() (int64, error) {
client, ctx, err := activeClient()
client, parentCtx, err := activeClient()
if err != nil {
return 0, err
}
ctx, cancel := readContext(parentCtx)
defer cancel()

bal, err := client.Balance(ctx)
if err != nil {
Expand All @@ -39,10 +43,12 @@ func PendingInboundSat() (int64, error) {
// WalletReady reports whether the daemon wallet is fully unlocked and ready to
// sign. It is the scalar form of GetInfo().WalletState == ready.
func WalletReady() (bool, error) {
client, ctx, err := activeClient()
client, parentCtx, err := activeClient()
if err != nil {
return false, err
}
ctx, cancel := readContext(parentCtx)
defer cancel()

info, err := client.GetInfo(ctx)
if err != nil {
Expand Down
Loading
Loading