Skip to content

Wallet.dispose() is missing an await, so cleanup can finish after it returns #801

Description

@gringokiwi

Plain English

wallet.dispose() is supposed to fully shut down before it returns, so callers can safely tear down their own resources (like a database connection) right after. It doesn't: it kicks off the real cleanup but never waits for it, so the promise resolves early. If an event arrives in that gap, the SDK tries to use resources (like the caller's database) that may already be gone.

Root cause

Wallet.dispose() in chunk-DSC3G6QZ.js (~line 479247):

async dispose() {
  const manager = this._contractManager ?? (this._contractManagerInitializing ? await this._contractManagerInitializing.catch(() => void 0) : void 0);
  manager?.dispose();          // <-- missing `await`
  this._contractManager = void 0;
  this._contractManagerInitializing = void 0;
}

ContractManager.dispose() itself is correct — it awaits contractEventsSubscriptionReady and then unsubscribes:

async dispose() {
  this.disposePromise ??= (async () => {
    this.disposed = true;
    ...
    const subscription = await this.contractEventsSubscriptionReady;
    subscription?.();
  })();
  return this.disposePromise;
}

But because Wallet.dispose() never awaits it, await wallet.dispose() resolves before the SSE subscription is actually torn down.

Repro

From demos/intents/swap-to-lightning/mainnet/typescript/index.ts:

console.log("Waiting for the swap to settle or refund...");
const outcome = await manager.waitForSwapCompletion(rfqId);
console.log("Swap finished:", outcome);

await manager.stop();
await transport.close();
await wallet.dispose();
closeDB(); // <-- app-owned SQLite connection, passed in via SQLExecutor

Observed error

The swap itself completes successfully (state: 'settled'), but shutdown throws:

Funded! https://arkade.space/tx/ff3848bac52ab964e334afafaab093b067775316f853cf145fac68af05e0b4eb
Waiting for the swap to settle or refund...
Swap finished: { state: 'settled', txid: undefined }
Error handling contract event: TypeError: The database connection is not open
    at Database.prepare (.../better-sqlite3/lib/methods/wrappers.js:5:21)
    at Object.all (.../index.ts:97:10)
    at SQLiteContractRepository.getContracts (@arkade-os/sdk/dist/repositories/sqlite/index.js:546:32)
    at process.processTicksAndRejections (node:internal/process/task_queues:104:5)
    at async _ContractManager.annotateVtxos (@arkade-os/sdk/dist/chunk-DSC3G6QZ.js:9740:23)
    at async _ContractManager.fetchContractVtxosBulk (@arkade-os/sdk/dist/chunk-DSC3G6QZ.js:10323:23)
    at async _ContractManager.fetchContractVxosFromIndexer (@arkade-os/sdk/dist/chunk-DSC3G6QZ.js:10261:41)
    at async _ContractManager.syncContracts (@arkade-os/sdk/dist/chunk-DSC3G6QZ.js:10168:20)
    at async _ContractManager.handleContractEvent (@arkade-os/sdk/dist/chunk-DSC3G6QZ.js:10115:11)

@arkade-os/sdk@0.4.66.

Suggested fix

Add the missing await in Wallet.dispose():

async dispose() {
  const manager = this._contractManager ?? ...;
  await manager?.dispose();   // was: manager?.dispose();
  this._contractManager = void 0;
  this._contractManagerInitializing = void 0;
}

Separately, ContractManager.handleContractEvent has no disposed guard at its top, so even a fully-awaited dispose() doesn't cover a handler that was already mid-flight (e.g. awaiting a network call) the instant dispose() was called. Worth an early-return check there too for full safety.

Workaround

The demo now awaits contractManager.dispose() directly instead of wallet.dispose(), which sidesteps the missing await:

const contractManager = await wallet.getContractManager();
...
await contractManager.dispose();
closeDB();

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions