Skip to content

Latest commit

 

History

History
100 lines (93 loc) · 20.5 KB

error-contracts.md

File metadata and controls

100 lines (93 loc) · 20.5 KB

Public Error Contracts

This document is the audit surface for Swift SDK error behavior. It records which public runtime surfaces can fail, which structured OMSWalletError shape apps should see, what recovery decision the error supports, whether upstreamError should be present, and which tests own the contract.

Terms

  • code is the stable app-facing compatibility field. Branch on OMSWalletErrorCode raw values for durable app behavior.
  • operation identifies the public SDK operation that failed. Use operation.rawValue for logs and analytics.
  • retryable is nullable. When it is non-nil, it describes the failed SDK operation, not the whole user intent. For example, retryable transaction status polling means retry status lookup; it does not mean blindly resend the original transaction write.
  • upstreamError is normalized diagnostic detail from a remote OMS service response, malformed remote response, or transport failure. Use it for logging and service-specific troubleshooting, not primary app branching. upstreamError.service is .waas or .indexer (raw values "waas" and "indexer"). upstreamError.code is a string; numeric WebRPC codes are stringified (e.g. "7313"). upstreamError.cause is the reason the service gave for the error, when it gave one, such as why a request was invalid. It is set only from the service's response, never from the SDK's own transport failures.
  • underlyingError is Swift-local diagnostic context. It is present when the SDK wraps a lower-level Swift error such as an internal transaction-flow error, HTTP transport failures, URLError, generated service-client failures, or a decoding error. For wallet API calls, transport failures (a URLError, including the SDK's request timeout) reach the SDK wrapped in the generated client's WebRPCTransportError, which keeps only their description, so underlyingError is that transport error, not the URLError. It can be absent for deliberate local SDK errors such as missing session and stale wallet selection. Do not serialize or depend on underlyingError for cross-SDK behavior.
  • OMS_TRANSACTION_EXECUTION_UNCONFIRMED means transaction preparation succeeded and produced a txnId, but the execute request failed before the SDK could confirm whether the transaction was submitted. Do not blindly resend the write.
  • OMS_TRANSACTION_STATUS_LOOKUP_FAILED means the transaction was submitted, but post-submit status polling failed. Retry by checking transaction status with the returned txnId.

Maintenance Approach

  • Update this matrix, Tests/OMSWalletTests/PublicErrorContractsTests.swift, the generated API.md (scripts/generate-api.sh), and README.md together when a public SDK method gains, removes, or intentionally changes an error contract.
  • Keep backend and upstream mapping tests representative rather than exhaustive per method. Cover each transport or response family through real public calls instead of duplicating the same assertions for every method.
  • Public runtime methods should own runtime error contract coverage. Use internal test fixtures only when the normalized field shape itself is the unit under test.
  • Keychain, signer, and storage classes are internal platform boundaries in this SDK. Cover their failures in focused tests unless a failure is intentionally normalized through a documented public OMSWalletError.
  • Serialized contract changes are not automatically regressions. Decide whether the new error shape is the intended public contract: if correct, update the assertion and related docs; if accidental, fix the implementation.
  • Treat message changes as user-visible API changes, even when code and recovery behavior are unchanged.

SDK Matrix

Public surface Failure family User-facing error Recovery meaning upstreamError Covering test
omsWallet.wallet.startEmailAuth, representative WaaS methods WaaS transport failure OMSWalletError, .requestFailed, operation-specific, retryable == true for transport failures Retry the same read/auth request when appropriate Present PublicErrorContractsTests.swift
Requests signed with the session credential (wallet calls, Execute, the expiry refresh, wallet import) The request takes more than 30 seconds in all: waiting for its turn behind the SDK's other requests of the same credential, signing, and the response, resends included. The call returns at the deadline even when the signer is still running (signing runs off the Swift concurrency pool, so a blocking signer cannot delay it); a request whose signing finishes later is never sent. Cancelling the calling task answers it at once with CancellationError, and a request not yet sent is then never sent. URLSession's own timeout only limits the time between received bytes .requestFailed, retryable == true, upstreamError.name == "WebrpcRequestFailed"; underlyingError is the generated client's transport error describing URLError.timedOut (not the URLError itself); the request is cancelled. A request counts as possibly sent only once its HTTP request is about to start, after signing. For Execute, the rows below apply: one that timed out while waiting or being signed was never sent, one that was sent stays .transactionExecutionUnconfirmed Retry; for Execute, only when it was not sent Present (WebrpcRequestFailed) SignedRequestDeadlineTests.swift
omsWallet.wallet.completeEmailAuth WaaS domain error SDK-specific code such as .authCommitmentConsumed Follow the SDK code; for consumed commitments, restart auth Present PublicErrorContractsTests.swift
omsWallet.wallet.*, representative WaaS methods WaaS HTTP error OMSWalletError, .httpError, status, retryable == true for 5xx Use SDK code/status for branching; log upstream detail Present PublicErrorContractsTests.swift
omsWallet.wallet.completeEmailAuth and PendingWalletSelection actions Local auth/session/selection state .sessionMissing, .walletSelectionStale, .walletSelectionUnavailable, or .walletSelectionInFlight (OMS_WALLET_SELECTION_IN_FLIGHT) Fix local flow state or restart auth; no remote diagnostics are expected Absent PublicErrorContractsTests.swift
OIDC redirect and ID-token auth methods Local OIDC config or validated callback failure Thrown OMSWalletError with .sessionMissing or .validationError Fix redirect config or restart OIDC flow Absent PublicErrorContractsTests.swift
handleOIDCRedirectCallback URL or state does not match the pending redirect .notOIDCRedirectCallback result Ignore the unrelated callback; the pending redirect remains available Absent MockWalletTests.swift
omsWallet.wallet.startOIDCRedirectAuth, handleOIDCRedirectCallback Local OIDC redirect-state persistence or restore failure Thrown OMSWalletError with .storageError Retry OIDC auth after resolving the local storage issue Absent PublicErrorContractsTests.swift
Protected wallet methods: getIdToken, signing and transaction methods, wallet import, getTransactionStatus, access authorization/usage/listing/revocation Missing or expired local session .sessionMissing or .sessionExpired Authenticate again or recover local session. A locally expired session is first refreshed with GetCredential; no other request is made. A server that extended the session lets the method continue. A session whose keychain key another process removed or replaced ends without a request: .sessionExpired (and an .expired change) when its stored record is still its own or its expiry passed, .sessionMissing (and .signedOut) when another sign-in replaced it Absent PublicErrorContractsTests.swift, CredentialExpiryRefreshTests.swift, SharedWalletTests.swift
omsWallet.wallet.claimWallet, PendingWalletSelection.claimAndSelectWallet, and automatic wallet selection when it claims a claimable wallet WaaS rejects the claim with AccountNotFound (code 7301, HTTP 404): the wallet is not allocated to the caller's verified email, was already claimed, or the caller has no verified email .requestFailed, status == 404, retryable == false, operation .walletClaimWallet or .pendingWalletSelectionClaimAndSelectWallet (or the sign-in method's operation during automatic selection). Both keep the pending selection or current session and active wallet; claimAndSelectWallet rejects an ID outside the selection's claimableWallets locally with .walletSelectionUnavailable. Automatic selection fails sign-in and ends the session, as a failed wallet creation does, without creating a wallet instead Refresh the claimable wallets by signing in again, or use another wallet Present (name == "AccountNotFound", code == "7301") ClaimableWalletTests.swift
Protected wallet methods above, and useWallet, claimWallet, createWallet, listWallets with an active wallet WaaS answers a request signed with the active session's credential with not found (KeyNotFound, code 7202) or expired (KeyExpired, code 7203) .sessionExpired with the method's operation; the session ends and session change observers receive .expired once. This includes a rejected Execute: WaaS rejects the credential before the handler runs, so nothing was sent and the error is .sessionExpired without txnId, not .transactionExecutionUnconfirmed. Status polling after a submitted transaction keeps .transactionStatusLookupFailed with its txnId (and the session still ends). Sign-in flows and unsigned public calls (inspectRemoteCredential, isValid*) are not mapped: their 7202 refers to another credential and stays .requestFailed Authenticate again Absent PublicErrorContractsTests.swift
Protected wallet methods above WaaS answers a wallet call with Unauthorized (code 7207, HTTP 401), which is not specific to the credential (for example, no access to that wallet) .requestFailed with the method's operation, status == 401, retryable == false. The SDK then starts a background GetCredential (joining one in flight): a revoked credential ends the session as in the next row, otherwise the session is kept and its expiry refreshed Follow the upstream detail; do not treat it as session expiry Present (name == "Unauthorized", code == "7207") PublicErrorContractsTests.swift
Protected wallet methods above, the session expiry timer, the background expiry refresh, and session restore WaaS answers the SDK's expiry refresh (GetCredential) with Unauthorized (code 7207). GetCredential involves no wallet, so this means the credential was revoked. Only the WaaS code counts: the API gateway's Unauthorized with code 1000 keeps the session as any other refresh failure does As for 7202/7203 above: .sessionExpired with the method's operation when an operation triggered the refresh; the session ends, its signing key is removed, and session change observers receive .expired once Authenticate again Absent CredentialExpiryRefreshTests.swift
Every method that signs with the session credential WaaS rejects the request's nonce (PreconditionFailed, code 7206, HTTP 412). The SDK sends its own signed requests one at a time per signing credential, so they reach WaaS in nonce order; a rejection means another process (such as an app extension sharing the keychain) signed with the same credential in between The SDK signs the request again with a fresh nonce and resends it, at most 2 times. WaaS rejects a stale nonce in its credential middleware before any method runs, so a resent Execute cannot submit twice. A third rejection surfaces as .requestFailed, status == 412, retryable == false, also for Execute, because nothing was submitted Retry the call Present (name == "PreconditionFailed", code == "7206") PublicErrorContractsTests.swift, WaasRequestSigningTests.swift, TestConcurrentSignedRequestsGoOutOneAtATimeWithoutNonceRejections
Protected wallet methods above The expiry refresh for a locally expired session fails with a transport or other non-credential error The refresh's error, normally .requestFailed with retryable == true; the session is kept Retry when the wallet API is reachable Present CredentialExpiryRefreshTests.swift
isValidMessageSignature, isValidTypedDataSignature, isValidSolanaMessageSignature, isValidTronMessageSignature, isValidTronTypedDataSignature called without walletAddress No active wallet, or the active wallet belongs to another chain family .sessionMissing (or .sessionExpired), or .validationError Sign in with a wallet of the method's family, or pass walletAddress; with an address no session is required. A locally expired session is first refreshed with GetCredential; no other request is made Absent SignatureVerificationTests.swift
isValidMessageSignature, isValidTypedDataSignature, isValidSolanaMessageSignature, isValidTronMessageSignature, isValidTronTypedDataSignature called with an empty or whitespace-only walletAddress Blank address; it does not fall back to the active wallet .validationError ("walletAddress must not be empty") Pass a wallet address, or omit walletAddress to use the active wallet. No remote request was made Absent SignatureVerificationTests.swift
Wallet signing, transactions, import, and owner access methods SDK-local validation, wallet-family mismatch (checked against the active wallet's stored type), non-bare contract method name, or fee-selection failure .validationError Correct parameters, select the required wallet family, or fix local fee selection; do not retry as an upstream outage Absent PublicErrorContractsTests.swift, WalletImportTests.swift, MockWalletTests.swift, TronWalletTests.swift
Wallet auth, selection, creation, import, and listWallets methods Successful WaaS response with an Ethereum wallet address that is not 0x plus 40 hex digits (any of these methods), an unreadable credential expiresAt, or a wallet to activate with an unknown network family or key origin (auth, selection, creation, and import only) .invalidResponse Treat the payload as unusable; the wallet is not activated Absent ActiveWalletSessionTests.swift
omsWallet.wallet.importWallet, importEncryptedWallet Address already imported (WaaS AddressAlreadyImported, code 7313) .walletAddressAlreadyImported (OMS_WALLET_ADDRESS_ALREADY_IMPORTED), status == 409, retryable == false; the active wallet is unchanged Select the existing wallet or use a different key Present PublicErrorContractsTests.swift
omsWallet.wallet.getWalletImportRecipientKey, importWallet, importEncryptedWallet Recipient-key attestation is missing, stale, malformed, untrusted, or does not match the request/response .attestationVerificationFailed with the wallet-import operation Do not encrypt or submit key material; retry only after confirming the SDK's managed PCR0 trust policy and WaaS environment Absent WalletImportTests.swift
omsWallet.wallet.isValidMessageSignature, isValidSolanaMessageSignature, isValidTypedDataSignature, isValidTronMessageSignature, isValidTronTypedDataSignature WaaS validation backend failure .httpError, .requestFailed, or .invalidResponse with the validation operation Retry based on SDK code/status; log upstream detail Present PublicErrorContractsTests.swift
omsWallet.wallet.sendTransaction, sendSolanaTransfer, sendTronTransaction, callContract, callTronContract Execute request fails after prepare .transactionExecutionUnconfirmed, operation == .walletExecute, retryable == false, txnId Do not blindly resend the write; preserve txnId and upstream detail for diagnostics Present when execute crossed a transport/upstream boundary PublicErrorContractsTests.swift
omsWallet.wallet.sendTransaction, sendSolanaTransfer, sendTronTransaction, callContract, callTronContract The Execute request failed before it was sent: signing it failed (for example, the keychain could not read the key), or it timed out while waiting for its turn or being signed .requestFailed with the method's operation, retryable == true, no txnId Nothing reached the wallet API; retry or fix the cause Present (WebrpcRequestFailed) SignedRequestDeadlineTests.swift
omsWallet.wallet.sendTransaction, callContract (EVM only; WaaS never returns 7314 for Solana or Tron) WaaS answers Execute with TransactionSimulationFailed (code 7314): the relayer refused to dispatch a reverting transaction, so WaaS closed it as failed and never sent it .requestFailed with the method's operation, status == 400, retryable == false, no txnId Fix the transaction (it would revert); nothing was submitted Present (TransactionSimulationFailed, 7314) PublicErrorContractsTests.swift
omsWallet.wallet.sendTransaction, sendSolanaTransfer, sendTronTransaction, callContract, callTronContract WaaS refuses Execute before submitting anything: its credential middleware rejects the request with InvalidSignature (7201), UsageLimitExceeded (7205), or Unauthorized (7207, also raised by Execute's own access checks, which run before submission). For sendTronTransaction and callTronContract, also InvalidRequest (7200) and InvalidFeeOption (7310), which Tron's Execute raises while pricing the transaction again, before claiming or broadcasting it (TransactionExpired stays unconfirmed: it can mean an earlier Execute claimed it). PreconditionFailed (7206) is covered above; 7202/7203 end the session .requestFailed with the method's operation, the upstream status (400, 429, 401), retryable == false, no txnId. A 7207 also starts the background GetCredential described above Nothing was submitted; follow the upstream detail Present (7201, 7205, 7207, and for Tron 7200, 7310) PublicErrorContractsTests.swift, TronWalletTests.swift
omsWallet.wallet.sendTransaction, sendSolanaTransfer, sendTronTransaction, callContract, callTronContract Submitted transaction status polling fails .transactionStatusLookupFailed, operation == .walletTransactionStatus, retryable == true, txnId Retry status lookup, not the original write Present when polling crossed a transport/upstream boundary PublicErrorContractsTests.swift
omsWallet.wallet.getTransactionStatus Direct status lookup backend failure .httpError, .requestFailed, or .invalidResponse with operation == .walletGetTransactionStatus Retry status lookup or surface backend status to the user Present PublicErrorContractsTests.swift
omsWallet.wallet.authorizeRemoteAccess The wallet already has the maximum number of active remote sessions (WaaS SessionCapExceeded, code 7208, HTTP 409; 100 sessions per wallet by default) .requestFailed, operation == .walletAuthorizeRemoteAccess, status == 409, retryable == false; the session is kept Revoke an existing remote session with revokeAccess(credentialId:sessionId:), then authorize again Present (name == "SessionCapExceeded", code == "7208") PublicErrorContractsTests.swift
omsWallet.wallet.inspectRemoteCredential, authorizeRemoteAccess, getRemoteAccessSession, getRemoteAccessSessionUsage, access listing and revocation WaaS owner-access backend failure .httpError, .requestFailed, or .invalidResponse with access operation Retry based on SDK code/status; log upstream detail Present PublicErrorContractsTests.swift, MockWalletTests.swift
omsWallet.indexer.getBalances, getTransactionHistory, getSolanaBalances, getTronBalances IndexerGateway backend, transport, malformed JSON, or malformed payload .httpError, .requestFailed, or .invalidResponse with indexer operation Retry based on SDK code/status; log upstream detail Present for remote/transport response failures PublicErrorContractsTests.swift, IndexerTests.swift (including TestGetTronBalancesRejectsInvalidResponses and TestGetTronBalancesSurfacesGatewayRequestErrors)
omsWallet.indexer.getBalances, getTransactionHistory, getSolanaBalances, getTronBalances IndexerGateway non-JSON HTTP body .httpError with sanitized message Do not expose raw upstream HTML/text bodies; log normalized detail Present, sanitized PublicErrorContractsTests.swift
Normalized OMSWalletError values Error field contract Stable public fields on SDK-produced errors Branch on code; use the remaining fields for recovery and diagnostics As normalized by the SDK PublicErrorContractsTests.swift