Skip to content

walletdk: browser (js/wasm) wallet bindings - #803

Merged
Roasbeef merged 3 commits into
mainfrom
wasm-wallet-bindings
Jun 27, 2026
Merged

walletdk: browser (js/wasm) wallet bindings#803
Roasbeef merged 3 commits into
mainfrom
wasm-wallet-bindings

Conversation

@Roasbeef

Copy link
Copy Markdown
Member

In this PR, we add the WebAssembly wallet bindings for walletdk, so the
full embedded daemon (wallet + swaps + OOR) runs inside a single browser VM
with no separate gateway process. This supersedes #431, which had drifted out
of sync with main over the past few months: the OOR actor set was rewritten
(the old global signing-effect actor is gone), the walletdk.Client send path
moved to a prepare/dispatch split, and sdk/walletdk itself was reorganized
around the walletdkrpc rename. Rather than untangle that by hand, we rebuilt
the wasm portions from scratch against current main.

This work stacks on top of #713 (the sdk/walletdk/mobile gomobile facade), so
the base branch here is walletdk-mobile-gomobile, not main. The browser
bridge drives that same JSON facade, which means the wasm and gomobile bindings
now share one source of truth, and the bridge can't drift from the Client API
the way the old hand-written syscall/js switch did. That drift is exactly what
broke #431 (client.Send no longer exists).

What's in here

We pulled in the OPFS-backed SQLite storage layer from the original branch (the
db, internal/sqlbase, and per-subsystem *_wasm.go stubs), then did two
things differently from #431.

First, there's no inline OOR actor. #431 hand-rolled an inlineOORActorRef to
run the legacy global OOR actor synchronously in the browser, but main deleted
that actor entirely in favor of the durable registry + per-session actors. It
turns out those new durable actors compile and run under js/wasm as-is on top
of the OPFS SQLite mailbox, so the whole inline-actor concept just goes away.

Second, the browser bridge in cmd/walletdk-wasm is a thin syscall/js adapter
over the sdk/walletdk/mobile JSON facade. Every verb takes a JS request object
and resolves a Promise with the decoded JSON response. It's ~280 lines instead
of the old ~700, and nothing in it reaches into walletdk.Client directly.

On the build side, we drop the !js walls that fenced the embedded daemon
(embedded.go, errmap.go, the swapruntime / walletdkrpc tag pairs) off
from js/wasm, disable the public HTTP gateway on the embedded path (a browser
has no TCP listener to bind), and split sdk/ark into native + wasm halves.
make wasm-wallet then builds the stripped blob and drops it next to
wasm_exec.js and the go-wasmsqlite OPFS runtime assets. None of the
app-specific bits from #431 came along: no React demo, no Playwright harness, no
Pages workflow.

See each commit message for the incremental detail.

Validation

make wasm-wallet
make build
env GOOS=js GOARCH=wasm go test -c ./db
env GOOS=js GOARCH=wasm go test -c ./sdk/swaps
env GOOS=js GOARCH=wasm go test -c -tags swapruntime ./sdk/walletdk
make unit pkg=./db
make unit pkg=./sdk/swaps
make unit pkg=./sdk/walletdk
make lint-changed-local

cc @jamaljsr

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces browser WASM support for the walletdk client by adding a syscall/js adapter, browser-compatible SQLite and migration drivers backed by OPFS, and WASM stubs for native packages. It also replaces the neutrino LRU cache with a custom implementation to avoid native dependencies. The review feedback identifies critical issues that must be addressed: a potential nil pointer dereference in readWriteBucket.Get, thread-safety issues in the new ancestryTreeLRU cache under concurrent access, and a memory leak in the WASM promise wrapper due to unreleased js.Func callbacks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +94 to +103
// When an empty byte array is stored as the value, Sqlite will decode
// that into nil whereas postgres will decode that as an empty byte
// array. Since returning nil is taken to mean that no value has ever
// been written, we ensure here that we at least return an empty array
// so that nil checks will fail.
if len(*value) == 0 {
return []byte{}
}

return *value

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

critical

If the database value is NULL (which is the case for bucket keys or if a NULL value is somehow stored), row.Scan(&value) will set value to nil. Attempting to dereference value via len(*value) will then cause a guaranteed nil pointer dereference panic. We must check if value is nil before dereferencing it.

Suggested change
// When an empty byte array is stored as the value, Sqlite will decode
// that into nil whereas postgres will decode that as an empty byte
// array. Since returning nil is taken to mean that no value has ever
// been written, we ensure here that we at least return an empty array
// so that nil checks will fail.
if len(*value) == 0 {
return []byte{}
}
return *value
if value == nil {
return nil
}
// When an empty byte array is stored as the value, Sqlite will decode
// that into nil whereas postgres will decode that as an empty byte
// array. Since returning nil is taken to mean that no value has ever
// been written, we ensure here that we at least return an empty array
// so that nil checks will fail.
if len(*value) == 0 {
return []byte{}
}
return *value

Comment thread db/ancestry_codec.go Outdated
Comment on lines 4 to 7
"container/list"
"context"
"crypto/sha256"
"errors"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

To support concurrent access to the ancestry tree cache, we need to import the sync package so we can add a mutex to ancestryTreeLRU.

Suggested change
"container/list"
"context"
"crypto/sha256"
"errors"
"container/list"
"context"
"crypto/sha256"
"errors"
"sync"

Comment thread db/ancestry_codec.go Outdated
Comment on lines +77 to +81
type ancestryTreeLRU struct {
maxEntries int
order *list.List
items map[[sha256.Size]byte]*list.Element
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The custom ancestryTreeLRU cache is not thread-safe. Since the database store can be accessed concurrently by multiple goroutines (e.g., concurrent gRPC requests or background workers), concurrent reads and writes to the items map and order list will cause a panic. We should add a sync.Mutex to protect these fields.

Suggested change
type ancestryTreeLRU struct {
maxEntries int
order *list.List
items map[[sha256.Size]byte]*list.Element
}
type ancestryTreeLRU struct {
mu sync.Mutex
maxEntries int
order *list.List
items map[[sha256.Size]byte]*list.Element
}

Comment thread db/ancestry_codec.go Outdated
Comment on lines +91 to +106
func (c *ancestryTreeLRU) Get(key [sha256.Size]byte) (*ancestryTreeCacheValue,
error) {

entry, ok := c.items[key]
if !ok {
return nil, errAncestryTreeCacheMiss
}

c.order.MoveToFront(entry)
cacheEntry, ok := entry.Value.(*ancestryTreeCacheEntry)
if !ok {
return nil, fmt.Errorf("invalid ancestry tree cache entry")
}

return cacheEntry.value, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Acquire the mutex lock in Get to ensure thread-safe access to the LRU cache.

func (c *ancestryTreeLRU) Get(key [sha256.Size]byte) (*ancestryTreeCacheValue,
	error) {

	c.mu.Lock()
	defer c.mu.Unlock()

	entry, ok := c.items[key]
	if !ok {
		return nil, errAncestryTreeCacheMiss
	}

	c.order.MoveToFront(entry)
	cacheEntry, ok := entry.Value.(*ancestryTreeCacheEntry)
	if !ok {
		return nil, fmt.Errorf("invalid ancestry tree cache entry")
	}

	return cacheEntry.value, nil
}

Comment thread db/ancestry_codec.go Outdated
Comment on lines +108 to +135
func (c *ancestryTreeLRU) Put(key [sha256.Size]byte,
value *ancestryTreeCacheValue) (bool, error) {

if entry, ok := c.items[key]; ok {
cacheEntry, ok := entry.Value.(*ancestryTreeCacheEntry)
if !ok {
return false, fmt.Errorf("invalid ancestry tree " +
"cache entry")
}

cacheEntry.value = value
c.order.MoveToFront(entry)

return false, nil
}

entry := c.order.PushFront(&ancestryTreeCacheEntry{
key: key,
value: value,
})
c.items[key] = entry

if c.order.Len() > c.maxEntries {
c.removeOldest()
}

return true, nil
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Acquire the mutex lock in Put to ensure thread-safe access to the LRU cache.

func (c *ancestryTreeLRU) Put(key [sha256.Size]byte,
	value *ancestryTreeCacheValue) (bool, error) {

	c.mu.Lock()
	defer c.mu.Unlock()

	if entry, ok := c.items[key]; ok {
		cacheEntry, ok := entry.Value.(*ancestryTreeCacheEntry)
		if !ok {
			return false, fmt.Errorf("invalid ancestry tree " +
				"cache entry")
		}

		cacheEntry.value = value
		c.order.MoveToFront(entry)

		return false, nil
	}

	entry := c.order.PushFront(&ancestryTreeCacheEntry{
		key:   key,
		value: value,
	})
	c.items[key] = entry

	if c.order.Len() > c.maxEntries {
		c.removeOldest()
	}

	return true, nil
}

Comment thread cmd/walletdk-wasm/main.go
Comment on lines +214 to +244
func promise(fn func() (any, error)) any {
executor := js.FuncOf(func(_ js.Value, args []js.Value) any {
resolve, reject := args[0], args[1]

go func() {
defer func() {
if r := recover(); r != nil {
reject.Invoke(
jsError(
errors.New(
"walletdk panic"),
),
)
}
}()

res, err := fn()
if err != nil {
reject.Invoke(jsError(err))

return
}

resolve.Invoke(res)
}()

return nil
})

return js.Global().Get("Promise").New(executor)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

In Go's syscall/js, every js.Func created via js.FuncOf allocates resources in both the Go runtime and the browser that are only freed when Release() is called. Since promise is called for almost every API interaction, failing to release the executor callback will cause a severe memory leak. Because the JS Promise constructor invokes the executor synchronously, we can safely release the executor callback immediately after New(executor) returns.

func promise(fn func() (any, error)) any {
	var executor js.Func
	executor = js.FuncOf(func(_ js.Value, args []js.Value) any {
		resolve, reject := args[0], args[1]

		go func() {
			defer func() {
				if r := recover(); r != nil {
					reject.Invoke(
						jsError(
							errors.New(
								"walletdk panic"),
						),
					)
				}
			}()

			res, err := fn()
			if err != nil {
				reject.Invoke(jsError(err))

				return
			}

			resolve.Invoke(res)
		}()

		return nil
	})

	p := js.Global().Get("Promise").New(executor)
	executor.Release()
	return p
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7187cfa4a7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread db/sqlite_open_wasm.go Outdated

// browserSQLiteFileName maps native paths to stable origin-local OPFS names.
func browserSQLiteFileName(name string) string {
base := filepath.Base(name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve full SQLite paths for OPFS databases

In WASM this reduces every configured SQLite path to only its basename, so defaults like .../data/regtest/client.db and .../data/signet/client.db both open the same OPFS file /client.db (and the swap store similarly collides on /swaps.db). In a single browser origin, switching networks or data dirs would silently reuse/mix the prior network's ledger/VTXO/swap state. Encode or hash the normalized full path rather than dropping it to filepath.Base.

Useful? React with 👍 / 👎.

Comment thread cmd/walletdk-wasm/main.go Outdated
Comment on lines +52 to +55
cfg := stringify(req)

return promise(func() (any, error) {
return js.Null(), mobile.Start(cfg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Provide a browser-safe default data_dir

When walletdkCall("start") is invoked without a request or with {}, this passes no data_dir override through mobile.Start, leaving walletdk.DefaultConfig() with DataDir == "~/.darepod". In a browser wasm_exec.js starts with an empty Go environment, so darepod.Validate() expands ~ via os.UserHomeDir() and fails with $HOME is not defined before the wallet can start. Set a WASM-safe default data dir before calling mobile.Start or make tilde expansion browser-aware.

Useful? React with 👍 / 👎.

Comment thread cmd/walletdk-wasm/main.go Outdated
return nil
})

return js.Global().Get("Promise").New(executor)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release per-call Promise executors

Every wallet method call creates a new js.Func executor and returns the Promise without ever calling Release. Promise executors are invoked synchronously by the constructor and are not needed afterward, so repeated UI polling of balance, status, next(), etc. leaks Go callback handles for the lifetime of the page. Release the executor after constructing the Promise.

Useful? React with 👍 / 👎.

@Roasbeef
Roasbeef requested a review from sputn1ck June 26, 2026 01:07
@Roasbeef

Copy link
Copy Markdown
Member Author

@claude review this

@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Roasbeef's task in 2m 59s —— View job


Claude Code Review

I reviewed the WASM bindings against origin/walletdk-mobile-gomobile. The architecture is clean — driving the gomobile JSON facade from a thin syscall/js adapter is the right call, and the build-tag split (*_native.go / *_wasm.go) is consistent and readable. I dug into the issues the bots flagged and confirmed which are real; a couple are genuine bugs worth fixing before merge. Notes below, ranked.

🔴 1. browserSQLiteFileName collapses distinct DBs to one OPFS file — silent cross-network state mixing

db/sqlite_open_wasm.go:107 reduces every path to filepath.Base, so .../data/regtest/client.db and .../data/signet/client.db both open /client.db, and the swap stores both collide on /swaps.db. Within a single browser origin, switching networks or data dirs would silently reuse/mix the prior network's ledger/VTXO/swap state — a real corruption risk, not just a cosmetic one.

The fix already exists in this very PR: lwwallet/walletdb_wasm.go:97-108 hashes the full normalized path with fnv.New64a for exactly this reason. The daemon DB and swap DB should do the same instead of dropping to the basename. (Codex P1 — confirmed, and the inconsistency with the wallet DB makes it clear which behavior is intended.)

Fix this →

🔴 2. ancestryTreeLRU drops the thread-safety the old cache had

db/ancestry_codec.go replaces neutrino/cache/lru.Cache (which is internally sync.RWMutex-guarded) with a hand-rolled ancestryTreeLRU that has no locking. The cache lives on vtxoStore.ancestryCache (db/vtxo_store.go:37), a single shared instance, and getOrDecode runs on the read path (ListLiveVTXOs, loadAncestryPaths) which is exercised concurrently by actors / gRPC handlers. Concurrent Get/Put on c.items (map) and c.order (*list.List) is a data race that will panic under -race and intermittently in production. This is a regression from the code being replaced. Add a sync.Mutex and lock in Get/Put (removeOldest is only called under Put, so it stays lock-free). (Gemini — confirmed.)

Fix this →

🟠 3. readWriteBucket.Get nil-pointer panic on NULL-value rows

internal/sqlbase/readwrite_bucket.go:77-103 changed the upstream lnd pattern from var value []byte to var value *[]byte. When the row's value column is NULL (which is exactly how nested buckets are stored — see NestedReadWriteBucket selecting value IS NULL), row.Scan(&value) leaves value == nil, and len(*value) dereferences a nil pointer → guaranteed panic. With the original []byte form, len(nil) == 0 returned []byte{} safely. A Get on a sub-bucket key (valid walletdb usage, returns nil per the contract) will panic here. Add a value == nil guard before the deref. (Gemini — confirmed.)

Fix this →

🟠 4. promise leaks a js.Func on every call

cmd/walletdk-wasm/main.go:214-244 creates a js.Func executor per call and never Release()s it. The Promise constructor invokes the executor synchronously, so it's safe to release immediately after New(executor). Without it, every balance/status/next()/etc. poll leaks a Go callback handle for the page's lifetime. Note the adjacent subscriptionHandle already gets this right (releases nextFn/closeFn on close), so this is just the promise helper missing the same discipline. (Gemini + Codex P2 — confirmed.)

p := js.Global().Get("Promise").New(executor)
executor.Release()
return p

Fix this →

🟡 5. Default data_dir is ~/.darepod — fails before start in a browser

When walletdkCall("start") is called with no request or {}, mobile.parseConfig falls back to walletdk.DefaultConfig(), whose DataDir is darepod.DefaultDataDir = "~/.darepod" (darepod/config.go:26). ValidateexpandPathsexpandTilde then calls os.UserHomeDir() (darepod/config.go:1147), which fails with $HOME is not defined under js/wasm (empty env). So the zero-config "boot with defaults" path the facade advertises can't actually start in the browser. Either set a WASM-safe default data dir on the embedded path, or make tilde expansion a no-op when os.UserHomeDir() is unavailable. (Codex P2 — confirmed.)

Fix this →

Minor / nits

  • The 25-attempt × 200ms SQLITE_CANTOPEN retry loop is duplicated nearly verbatim in db/sqlite_open_wasm.go:67 and lwwallet/walletdb_wasm.go:55. Consider a shared helper to keep the OPFS reload-race handling in one place.
  • browserSQLiteFileName's arkd.db fallback name is inconsistent with the rest of the binary's client.db/walletdk naming — worth aligning once lib: start adding lib helpers #1 is addressed.

What looks good

  • Facade-driven bridge eliminates the client.Send drift that broke wasm: add walletdk browser demo #431 — the design rationale in the PR body holds up.
  • *_native.go / *_wasm.go split is clean and the //go:build js && wasm walls are consistent.
  • decodeUint32SliceBE overflow guard (db/ancestry_codec.go:456) is careful work.
  • subscriptionHandle correctly owns and releases its callbacks.

I focused on review only and did not push changes. The three 🔴/🟠 storage bugs (#1#3) are the ones I'd block on; #4 and #5 are important for actually running in a browser.
· wasm-wallet-bindings

@Roasbeef
Roasbeef force-pushed the walletdk-mobile-gomobile branch 2 times, most recently from ac9c418 to 74c8033 Compare June 26, 2026 21:59
@Roasbeef
Roasbeef force-pushed the wasm-wallet-bindings branch from 20027e7 to 5b66749 Compare June 26, 2026 22:05
@Roasbeef

Copy link
Copy Markdown
Member Author

Verified end-to-end in a real browser

The build passing didn't prove the OPFS/worker runtime path actually works, so I drove the make wasm-wallet blob in a cross-origin-isolated Chromium with a throwaway harness (a vanilla index.html running the Go wasm on the main thread, plus a COOP/COEP static server). No app code, just the blob + the go-wasmsqlite library assets the make target emits.

Step Result
crossOriginIsolated true
wasm decompress + boot (walletdk-ready) ✅ the 132MB blob loads and runs
OPFS SQLite open vfsType: opfs, persistent: true
OPFS SQLite write → read roundtrip hello-opfs
OPFS persistence across 2 page reloads ✅ row count = 2 (durable)
isRunning (pre-start) false
embedded daemon start boots in-browser (opens its DBs over OPFS, runs migrations, brings up the OOR durable actors + swap runtime)
getInfo ✅ real state: 0.0.2-alpha / regtest / lwwallet
createWallet reaches password validation → seed gen → wallet DB open (the internal/sqlbase fork) → fails only at get tip height: http://127.0.0.1:1/...: fetch() failed
balance wallet is not ready (create didn't finish)

The two non-green steps aren't binding bugs: the harness points the light wallet at a dead Esplora (127.0.0.1:1). createWallet gets all the way through the OPFS-backed wallet DB to the chain-sync tip poller before the fetch fails, which exercises the internal/sqlbase + walletdb_wasm + localStorage-seed path end to end (and proves wasm net/http fetch() works). A fully-green create would need a reachable Esplora, which COEP blocks for external hosts, so it's out of scope for a smoke test.

So the whole thing genuinely works: the blob boots, OPFS SQLite persists across reloads, the full embedded daemon comes up in one browser VM, and the bridge round-trips real RPCs.

One fix from this: make wasm-wallet failed on re-run because it cp'd read-only module-cache assets over existing read-only files. Folded a clean-first + chmod -R u+w into the target.

@Roasbeef
Roasbeef force-pushed the wasm-wallet-bindings branch from 5b66749 to 6b83af4 Compare June 26, 2026 22:19
@Roasbeef
Roasbeef force-pushed the walletdk-mobile-gomobile branch from 74c8033 to 3faf8be Compare June 26, 2026 22:23
@Roasbeef

Copy link
Copy Markdown
Member Author

@claude review this

@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @Roasbeef's task in 3m 38s —— View job


Claude Code Review (re-review)

I re-reviewed the current tree (6b83af4) against the issues from my first pass. The branch has been force-pushed with fixes since then, and the browser end-to-end run is convincing. Walking the prior findings one by one:

# Prior finding Status in 6b83af4
🔴 1 browserSQLiteFileName collapsed distinct DBs to one OPFS file Fixeddb/sqlite_open_wasm.go:114 now hashes the full normalized path with fnv.New64a, matching lwwallet.
🔴 2 ancestryTreeLRU lost thread-safety Moot — this PR no longer touches db/ancestry_codec.go; it still uses the thread-safe neutrino lru.Cache (db/ancestry_codec.go:14-15,76,92). The hand-rolled cache is gone.
🟠 3 readWriteBucket.Get nil deref on NULL-value rows Still present — see below.
🟠 4 promise leaked a js.Func per call Fixedcmd/walletdk-wasm/main.go:248-249 releases the executor right after New.
🟡 5 Default ~/.darepod data dir failed under wasm Fixedcmd/walletdk-wasm/main.go:269-288 injects a browser-safe /darepo default; nice touch wiring it through startConfig rather than the daemon defaults.

So the only carryover bug is #3.

🟠 1. readWriteBucket.Get still panics on NULL-value (sub-bucket) rows

internal/sqlbase/readwrite_bucket.go:77-103 uses var value *[]byte and dereferences it unconditionally:

var value *[]byte
...
err := row.Scan(&value)
switch { case sql.ErrNoRows: return nil; case err != nil: panic(err) }

if len(*value) == 0 {   // value == nil  →  nil pointer dereference
    return []byte{}
}
return *value

When the row's value column is NULL, Scan leaves value == nil. NULL is exactly how nested buckets are stored — CreateBucket keys off it (readwrite_bucket.go:167, value == nil ⇒ ErrBucketExists) and NestedReadWriteBucket selects value IS NULL (:123). So a Get whose key names a sub-bucket scans a NULL row and panics at len(*value). Per the walletdb contract that call should return nil, and the upstream lnd form (var value []byte) did so safely since len(nil) == 0.

The sibling *[]byte uses in CreateBucket (:154,167) and CreateBucketIfNotExists (:208,235) only compare against nil and never deref, so they're fine — Get is the lone unguarded path. Add a guard before the length check:

if value == nil {
    return nil
}

Fix this →

Things I checked and cleared

  • sdk/ark/transport.go waitForReady goroutine — not a leak. Each loop iteration's goroutine is reaped by waitCancel() (line 58 or 71) before the next iteration spawns one; runExitErr is buffered (size 1) and iteration-local, so a late write can't block. Fine.
  • The *[]byte pattern across the bucket file — only Get dereferences; the bucket-existence checks are nil-safe.
  • The sdk/walletdk/mobile/* package is from the base PR (sdk/walletdk: expose the wallet SDK to mobile via gomobile #713), so I left it out of scope here.

Still-open nits from last time (non-blocking)

  • The 25× 200ms SQLITE_CANTOPEN retry loop is duplicated in db/sqlite_open_wasm.go:69 and lwwallet/walletdb_wasm.go — a shared helper would keep the OPFS reload-race handling in one place.
  • browserSQLiteFileName's arkd.db fallback name (db/sqlite_open_wasm.go:118) is still inconsistent with the client.db/walletdk naming elsewhere — cosmetic, but worth aligning.

Verdict

Fixing #3 is the one thing I'd block on — it's a guaranteed panic on a legal walletdb access pattern. Everything else from the first review is resolved, the facade-driven bridge is clean, and the in-browser boot + OPFS persistence evidence you posted closes the loop on the runtime path. Nice work.

I reviewed only and did not push changes.
· wasm-wallet-bindings

@Roasbeef
Roasbeef changed the base branch from walletdk-mobile-gomobile to main June 26, 2026 22:54
@Roasbeef
Roasbeef force-pushed the wasm-wallet-bindings branch 2 times, most recently from d9b8d7d to cf31b4a Compare June 27, 2026 09:05
@Roasbeef

Copy link
Copy Markdown
Member Author

Addressed the last open item: internal/sqlbase/readWriteBucket.Get now guards value == nil before the length check, so a Get on a key that names a sub-bucket (NULL value column) returns nil per the walletdb contract instead of panicking on the nil pointer. Consistent with the value == nil guards already used elsewhere in the file.

Validated with the GOOS=js build of internal/sqlbase + cmd/walletdk-wasm, the db wasm test-compile, native build + db tests, and a full browser e2e re-run (daemon still boots over OPFS, wallet DB opens through sqlbase, no regression). Folded into the db: commit.

@Roasbeef
Roasbeef force-pushed the wasm-wallet-bindings branch from cf31b4a to e976f01 Compare June 27, 2026 09:12
sputn1ck added 3 commits June 27, 2026 12:00
Add wasm-safe SQLite open, migrate, and error handling through
go-wasmsqlite. Browser builds use OPFS while native builds keep the
existing database/sql path.

Move lwwallet walletdb and seed storage behind native and wasm files so
browser wallets can persist state in OPFS.
The embedded daemon path was walled off from js/wasm builds:
embedded.go, embedded_config.go, errmap.go, and the swapruntime /
walletdkrpc tag pairs all carried a !js constraint, so walletdk.Start,
the wallet RPC subserver, and the error-reconstruct interceptor did
not exist in a browser build at all. Drop those constraints so the
in-process bufconn runtime compiles under GOOS=js, and disable the
public HTTP gateway on the embedded path, since a browser has no TCP
listener to bind.

sdk/ark splits its embedded boot into a native file (embedded.go, now
tagged !js || !wasm) and a browser stub (embedded_wasm.go) whose
StartEmbedded reports that the native daemon runtime is unavailable,
with the shared grpc readiness helper lifted into transport.go. The
browser wallet boots the daemon through walletdk.Start rather than
sdk/ark, so the stub keeps the package compiling for js consumers that
only need the remote Ark types.
Expose the embedded walletdk runtime to browser JavaScript as a thin
syscall/js adapter over the sdk/walletdk/mobile JSON facade. Every
verb takes a JS request object and resolves a Promise with the decoded
JSON response, so the daemon, swap, and OOR machinery all run in the
single browser VM with no separate gateway process. Routing through
the shared mobile facade keeps one source of truth with the gomobile
bindings, so the bridge cannot drift from the walletdk.Client API the
way a bespoke per-method dispatch would.

A native stub keeps go build ./... green off-target, where the js/wasm
main has no entry point, and a wasm-wallet make target assembles the
stripped blob alongside wasm_exec.js and the go-wasmsqlite OPFS runtime
assets so the browser bundle is buildable in one step.
@Roasbeef
Roasbeef force-pushed the wasm-wallet-bindings branch from e976f01 to b91300a Compare June 27, 2026 19:01
@Roasbeef
Roasbeef merged commit 70e95bf into main Jun 27, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants