Skip to content

[Bug]: Failed contract reads are silently replaced with fallback values, unlike writes #227

Description

@re-gius

Component

Other

Priority

P1

What happened?

packages/ui handles read and write failures under opposite policies. The write helper fails loudly:

// lib/contractWrite.ts
async function submitWrite(tx, action): Promise<Hash> {
  try { result = await tx; }
  catch (error) { throw new Error(describeContractError(error, action)); }
  if (!result.ok) throw new Error(failure(action, describeDispatchError(result.dispatchError)));
  return result.txHash as Hash;
}

Both failure shapes are covered and the revert bytes are decoded against the contract ABIs before any fallback. The helper is correct; individual call sites can still discard what it throws, so "writes fail loudly" holds for the layer and not for every path through it. Reads do the reverse:

// composables/useContracts.ts:173
export function safeRead<T>(label: string, fallback: T, fn: () => Promise<T>): Promise<T> {
  return withContractRecovery(fn).catch((error) => {
    console.warn(label, error);
    return fallback;
  });
}

An RPC failure, a stale contract address, an ABI mismatch or a revert all become a console.warn and a substituted value. The caller cannot tell a real answer from a swallowed error, and the user sees nothing.

The same commonly happens for other reads and two instances are worth naming.

Availability is decided from the wrong source, which gates the registration funnel. store/useUserStoreManager.ts:141:

const owner = resolvedAddress ?? nameOwner ?? ZERO;   // available = owner === ZERO

This prefers the resolver's addressOf record over the registrar's owner, and getOwnerOfDomain returns null on read failure (store/useResolverStore.ts:114,120), indistinguishable from "no owner". So an owned name reads "available" whenever that read fails, sending the user into a paid register that reverts; a free name with a leftover addr record reads "taken" and blocks a legitimate registration. This store backs the green "Available" in the search box.

The fallback is sometimes structural rather than a value. readAllPages (store/useUserStoreManager.ts:30-36) does if (!page) break;, treating a failed page read as end-of-list and returning a short list as complete. Names or uploads silently disappear, and components/profile/EscrowTab.vue:363 then omits the matching escrow positions, so deposits look missing.

Expected behavior

A failed contract read reaches the user the same way a failed write already does. Where a fallback is a genuine answer, such as "this name has no text record", it is kept. Where absence has no valid representation, such as a price, a tier or a minimum age, the read rejects and the caller surfaces the error.

Reproduction

Point the app at a stale contract address, which is the live situation described in #221, and register a name. The query returns unsuccessful, priceWithoutCheck yields price: 0n, the transaction is submitted with value: 0, and the user sees a contract revert about insufficient value rather than a message about the failed price lookup. The real error appears only in the browser console.

The same is reproducible by blocking the RPC endpoint mid-session: prices display as zero rather than as an error.

Additional context

safeRead's fallback semantics hold where a fallback is a real answer, such as "this name has no text record", and do not hold where no value can represent a failed lookup, such as a price, a tier or a minimum age. Reads in the first group can keep their current behaviour.

That gives a narrow fix: split safeRead into a fallback-preserving variant and a rejecting variant, and move the reads whose absence is unrepresentable onto the latter. The write layer already turns a thrown error into a readable message via describeContractError, so no new error-presentation work is needed.

This composes with two known bugs, which is why it matters for launch. #221 leaves stale contract addresses in place, and #223 means no mainnet configuration exists yet; both produce failing reads, and both currently surface as plausible wrong values rather than as errors.

Scoped to packages/ui deliberately. The CLI has smaller instances of the same class, notably listEscrowPositions swallowing per-name errors and normalizeFlags defaulting to "no revert", and sync-abis failing open is already tracked in #222.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Secondary Prioritydotns-sdkRelated to the DotNS SDKtype: bugBug report

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions