Solana

Recovery and receipts

Observe a saved attempt with bounded reads; preserve uncertain outcomes, local evidence and business identity.

Alpha documentation

Save identity before effects

Persist the business order and its operation ID, network, sender, recipient, asset and exact amount before calling preparation. Edge retains reviewed message/transaction digests, approval, attempt identity, signature and observations. These records answer different questions; preserve their relationship rather than reconstructing an equivalent-looking new payment.

After a view reload, reopen the saved order and read the same operation. For Edge use transactionStatus({operationId}); for a direct Dispatch integration reconcile its recorded attempt. Neither needs a new request ID, wallet creation or another send. A missing key may prevent future signing, but it does not erase a transaction already submitted from that address.

An unknown result is not a failed transaction

A timeout can mean the reply was lost after acceptance. A rejected promise can also follow an incomplete local evidence write. Read both the saved state and any recoveryError; do not infer no effect merely from a spinner, exception or missing RPC history.

Recorded stateAppropriate next actionWhat stays protected
preparingResume preparation with exactly the saved terms; inspect provider/storage failure.Original request binding and reservation.
prepared / preflightedReview freshness and continue explicit preflight/approval.No implied wallet approval.
blockedShow policy reasons; resolve the intended request through a deliberate new review.Do not disable checks simply to obtain a green result.
authorisedSend the exact recorded bytes only on the explicit action.Signed bytes can be submitted by someone who obtains them.
submitting, submitted, unknownObserve/reconcile that operation and signature.No replacement payment or automatic re-signing.
confirmedInspect matching evidence and continue to stronger commitment when required.Confirmation is not fulfilment or necessarily terminal finality.
finalisedRetain matching evidence; repair local tracking if still incomplete.Do not demote retained finality because later RPC history is absent.
failedInspect the actual observed transaction error and retained attempt.A new business attempt, when appropriate, is a separate decision.
expired before any submissionUse explicit supported refresh and record the linked successor.Fresh bytes require fresh preflight and approval.

Circuit can remain submitted while Edge reports confirmed; the live coordinator closes the ledger on supported terminal evidence, not on every intermediate observation. That is different from an unresolved local tracking error. Circuit's staleness indicator does not release a reserved execution leg.

Bound observation instead of retrying submission

This complete helper observes one saved devnet operation with a bounded read count. Copy the client module beside it. Call it explicitly with an AbortController and render only its allowlisted summary. A stop request cancels local waiting/scheduling; the client exposes no cancellation of a provider read already in flight.

js
import {createSolanaClient, hasBoundDevnetReceipt} from './solana-client.mjs';
const client = createSolanaClient(() => window.capp?.solana);

function pause(ms, signal) {
  return new Promise(resolve => {
    const finish = () => { clearTimeout(timer); signal?.removeEventListener('abort', finish); resolve(); };
    const timer = setTimeout(finish, ms);
    signal?.addEventListener('abort', finish, {once: true});
    if (signal?.aborted) finish();
  });
}

export async function observeSavedPayment({operationId, onUpdate, signal, maxReads = 10, intervalMs = 3000}) {
  if (typeof operationId !== 'string' || !operationId.trim() || typeof onUpdate !== 'function' ||
      !Number.isInteger(maxReads) || maxReads < 1 || maxReads > 60 ||
      !Number.isInteger(intervalMs) || intervalMs < 1000 || intervalMs > 60000) {
    throw new Error('Provide a saved operation, update callback and bounded observation settings');
  }
  for (let reads = 1; reads <= maxReads; reads++) {
    if (signal?.aborted) return {reason: 'stopped', reads: reads - 1};
    let row;
    try { row = await client.call('edge', 'transactionStatus', {operationId}); }
    catch {
      return {reason: 'observation-unavailable', reads}; // Existing outcome remains unresolved.
    }
    if (signal?.aborted) return {reason: 'stopped', reads};
    if (row.operationId !== operationId || row.network !== 'devnet' || row.simulated !== false) {
      throw new Error('Observation belongs to another operation or mode');
    }
    const summary = {
      operationId, state: String(row.state),
      signature: typeof row.signature === 'string' ? row.signature : null,
      boundReceipt: hasBoundDevnetReceipt(row),
      trackingNeedsAttention: Boolean(row.recoveryError)
    };
    onUpdate(summary); // Render as text; do not export the full signed operation.
    if (!row.recoveryError && (row.state === 'finalised' && summary.boundReceipt ||
        row.state === 'failed' && row.observation?.error)) {
      return {reason: 'terminal-observation', reads};
    }
    if (!['submitting', 'submitted', 'confirmed', 'finalised', 'failed', 'unknown'].includes(row.state)) {
      return {reason: 'review-required', reads};
    }
    if (reads < maxReads) await pause(intervalMs, signal);
  }
  return {reason: 'read-budget-exhausted', reads: maxReads};
}

Call controller.abort() when removing the view and catch the returned promise. read-budget-exhausted and observation-unavailable are not transaction failure states. Offer a deliberate later Check action with the same identity. This helper neither prepares nor signs nor submits; it does not require a newly connected wallet merely to try observation.

The signature is public chain information but can correlate a customer's activity. Show it where necessary; omit it from a public support report unless relevant and knowingly shared. The summary deliberately excludes signed transaction bytes, private keys and raw error payloads.

Read receipts within their trust boundary

hasBoundDevnetReceipt(result) checks matching identity across a trusted Edge row, receipt and exact chain evidence. It requires live devnet flags, suitable observed states, met commitment and consistent operation/attempt/signature/digests. It is a structural binding helper, not a remote signature verifier for arbitrary uploaded JSON.

EvidenceEstablishes within its source boundaryDoes not establish
Signature observationA provider reported a state for the signature.Intended recipient/amount or successful delivery.
Bound Edge receipt plus chain evidenceObserved bytes match the recorded operation at the receipt's commitment.Authenticity of a caller's copied JSON.
Independent payment verificationTrusted RPC observations match merchant-owned request terms and recipient credit.Invoice claim uniqueness, delivery or refund authority.
Durable business claim/outboxYour service recorded the product decision and resumable work.A guarantee supplied automatically by a Sidecar.

Edge receipt() reads an already-collected receipt; transactionStatus() performs reconciliation and can write local tracking. Witness receipt() performs observation to assemble a receipt, while readReceipt() reads a retained snapshot. Name similarity is not identical behaviour.

Local persistence has a scope

Ledgers are scoped to the project/component/application/network and the browser origin or installed storage environment. They are not an online backup, cross-device database or distributed lock. The source uses serialized calls and Web Locks when available; that does not create atomic execution across independent devices, origins or external services.

Storage-unavailable, corrupt-store, legacy-state and capacity errors preserve existing records rather than silently starting empty. Use the supported recovery/export workflow and verified backups. Do not edit storage keys, clear the browser or uninstall while an external outcome is unresolved. Raising a configured capacity limit does not resolve existing payments.

Wallet backup, signed-operation records, receipt exports and the merchant ledger are different materials. Restoring a wallet does not restore missing orders; exporting receipts does not back up the wallet. Preserve the original damaged data before any explicitly supported recovery.

Application outcomes remain separate

A confirmed payment does not automatically fulfil an order, deliver a report, mint an asset or authorise a refund. Verify independently at the trusted business boundary, make a durable unique claim and record resumable fulfilment work.

A refund is a separately reviewed operation. A late payment or rejected verifier result does not reverse the original transfer. Escalate incomplete evidence with the saved identifiers rather than inventing certainty or issuing another payment to make the UI look complete.

↑ ↓ to navigate · Enter to open · Esc to close. Search stays in your browser.