Skip to content

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 04 Oct 08:35
· 16 commits to main since this release

Every request now goes to one server, NoirWire's own, and carries an anonymous session. Both apps must change how they boot.

Breaking

For both apps:

  • Platform has a new required port, sessionStore: get, set and remove of one small JSON value in plain app storage. Not the vault, and never inside the wallet record.

  • Env has a new required value, apiBaseUrl, and envFrom refuses settings without it. EnvSettings gains apiBaseUrl, platform, development and rpcUrl.

  • configureHttp, HttpConfig and its headers() are gone. There is nothing to call in their place: where requests go is env.apiBaseUrl, and the one header every request carries is added by the package. A server or a test that named an RPC provider with configureHttp({ rpcUrl }) now sets rpcUrl in the environment.

  • RPC_RELAY_PATH, JUPITER_RELAY_PATH and PRIVATE_PAYMENT_RELAY_PATH are gone. An address is built with apiUrl(route, rest).

  • The paths changed, so the web app's own relay routes are no longer called by this package:

    Was Is
    POST /api/rpc POST /v1/rpc
    /api/jupiter/* /v1/jupiter/*
    POST /api/private-payments/* POST /v1/private-payments/*
    GET and POST /api/relayer GET and POST /v1/relayer
    GET /api/prices GET /v1/prices
    GET /api/history/:symbol/:range GET /v1/history/:symbol/:range
    POST /api/event (each app's own) POST /v1/events, as apiUrl("events")
  • Every one of those requests carries Authorization: Bearer <token>. An app's own requests to the server go through authorizedFetch to carry it too.

  • A new failure code, notAvailableNow, in ChainErrorCode: a switch over the codes that lists them all needs the new case. chainErrorMessage already words it.

  • memoryPlatform() now includes a sessionStore, and testEnv() an apiBaseUrl (https://api.noirwire.test). A test that pinned a relative path such as /api/jupiter/swap/v2/order now sees https://api.noirwire.test/v1/jupiter/swap/v2/order. A test setup calls installTestPlatform() in place of installPlatform(memoryPlatform()) and configureHttp(...), or its first request tries to start a real session.

  • WAIT_LIMIT_MS replaces each app's own table, and where the two differed the longer stands: a check may now run 30 s on the phone (was 20 s) and an action 120 s on the web (was 90 s).

For the web app:

  • Set apiBaseUrl to /api with platform: "web", and have the host forward /api/:path* to the server; or name the server's origin and allow it in connect-src.
  • Install a sessionStore over localStorage, and keep the cross-tab locks: a session's renewal runs under the lock noirwire-session.
  • Delete src/components/wallet/passwordCheck.ts's copy of the rule and call assessPasswordWith(checker, password) with the bundled checker. Delete src/components/localCopy.ts and WAIT_LIMIT_MS in src/components/waiting/limits.ts; their words and numbers are here now (see Added).
  • The server-side platform passes rpcUrl to envFrom, where it passed it to configureHttp.

For the mobile app:

  • Set apiBaseUrl to the server's origin (https://api.noirwire.com), in place of the relay URL. Plain http is accepted only for localhost, 127.0.0.1 and 10.0.2.2, and only with development: true. A path is refused on the phone.
  • Delete the X-NoirWire-Client header and the HTTP configuration that carried it (src/platform/httpConfig.ts): nothing reads headers() any more.
  • Install a sessionStore over the app's plain key-value storage.
  • Delete src/features/phoneCopy.ts and WAITING_LIMIT_MS in src/ui/useWaiting.ts; their words and numbers are here now.

Added

  • apiUrl(route, rest?) in @noirwire/shared/infrastructure: the one function that builds a request's address, from env.apiBaseUrl and the server's paths (session, rpc, jupiter, privatePayments, relayer, prices, history, events, health). apiBaseUrl is an origin, or on the web a path on the page's own origin.
  • The anonymous session. createSessionKeeper in @noirwire/shared/application starts one (POST /v1/session, no token, no body), keeps it through sessionStore, renews it a minute before its token runs out (POST /v1/session/refresh) and whenever the server turns it down, shares one start or renewal among callers that arrive together and, under the platform lock, among tabs, starts a new one when a renewal is refused or the server answers session_expired, and replaces one older than SESSION_MAX_AGE_MS (24 hours; env.sessionMaxAgeMs sets another). Storage is the truth for every tab: what a tab holds is checked against it before each use, and a drop takes the same lock as a renewal, so a session dropped in one tab is not brought back or used on by another. A session request is given SESSION_REQUEST_TIMEOUT_MS (10 s), and after a failure nothing more is asked for a growing, jittered pause (SESSION_RETRY, a minute at most), during which callers are told at once. The session is a quota bucket, not an identity: it is not derived from the wallet, it rotates daily, and a wallet reset drops it.
  • authorizedFetch(input, init) in @noirwire/shared/infrastructure: fetch with the session's token. After a 401 a read or an unsigned build is made once more with a renewed session; a request that hands over a signed transaction, or asks the relayer to sign one, never is. dropSession(), keepSessionWith() and sessionRoutes beside it.
  • errorsCopy.chain.notAvailableNow, said the same on both platforms: "We can't do this right now. Nothing was sent, and your money has not moved. Try again." It is what a person reads when no session could be had, so a request was never made, or when a read was turned down twice for its session.
  • ApiError, API_ERRORS, ApiErrorCode and apiErrorIn in @noirwire/shared/domain, and apiErrorOf(response) in @noirwire/shared/infrastructure: every error the server writes itself, { code, error }, read by its code and never by its sentence. isTransient asks one again by its code. A provider's own error passes through as it came.
  • npm run test:api: every client held to the server's OpenAPI file, named by NOIRWIRE_OPENAPI; it is part of npm test and skipped there, saying so, when the variable is unset. npm run test:api:live: the read paths against a running server named by NOIRWIRE_API_URL.
  • In @noirwire/shared/testing: installTestPlatform(overrides?), fakeSession(), memorySessionStore(), fakeApi(routes) to answer the server by path, and TEST_API_URL.
  • assessPasswordWith(checker, password) in @noirwire/shared/wallet, with PasswordChecker and PasswordAssessment: the password rule as a synchronous function over a checker the app already holds, so a screen that bundles the checker runs the same rule with nothing loaded on demand. assessPassword is built on it.
  • WAIT_LIMIT_MS in @noirwire/shared/presentation: how long each kind of wait may run before a screen ends it (content 20 s, check 30 s, review 30 s, action 120 s).
  • The words both apps kept for themselves: waitingCopy.overdue, waitingCopy.actionHeld, waitingCopy.gettingReady and mobileWaitingCopy.overdue; onboardingCopy.import.notNow and onboardingCopy.phrase.discarded, .newPhrase, .acknowledgeNew; portfolioCopy.balances and portfolioCopy.archived; marketsCopy.pricesUnavailable; appCopy.offline; earnCopy.unread and earnCopy.notHere; commonCopy.tryAgain; mobileWalletCopy.newPassword.checkFailed; mobilePortfolioCopy.create.forExample and .nameNeeded.

Fixed

  • A relayer-paid send or Earn move that landed is no longer reported as "did not go through, safe to try again" when the app is closed after the broadcast. The signed transaction the relayer returns (now { transaction, signature }, signing only) is checked, its id is written into the reservation, and only then is it sent. A reservation with no recorded id is never released on its blockhash alone: the signer's recent transactions are searched for it first (signer and ownSignature on the pending action; unfindable when the chain cannot be searched, which only the person can then clear).
  • Cash no longer reads as it did before an action whose network cost it paid: the cost is taken off locally the moment a tracker send lands, and off an Earn move whose balances could not be read back.
  • "Max" on an Earn withdrawal takes the whole position back by its shares (the Lend program's redeem), where it asked for a USDC amount that could be a few units more than the chain would give and failed. Nothing is left behind. Any withdrawal that asks for what the position is worth or more is that redemption.
  • No raw chain or program text reaches a person. A failed simulation is worded in one place (simulationRefusal), and failureMessage replaces any account that carries JSON, an instruction error or a program log with the plain words for what the action was doing (saysRawChainError).
  • A fee payer that cannot pay the network reads as the relayer not being usable ("The network cost could not be covered in USDC right now. Nothing was sent."), not as "InsufficientFundsForFee".
  • A tracker sent from a portfolio with no cash says the portfolio needs cash for the network cost. It is decided before the relayer is asked, whose failed simulation used to read as "not available".
  • The funding form states its fees to the same decimal as its review: 0.025 and 25.225, not 0.03 and 25.23.

Changed

  • reviewSend reads the recipient from the network and answers recipient beside the cost (SendReview): null for a wallet, why it cannot receive, or "unreadable". sendRecipientRefusal(review) words it. SendChain needs checkRecipient. No screen can review a send to an address nobody checked.
  • Activity entries carry networkCost. A row and the detail show it, and a return from Earn reads as what arrived: 10.00 withdrawn at a cost of 0.02 is "+$9.98", with amount, arrived and networkCost on the detail. ActivityRowView.networkCost and ActivityDetailView.arrived and .networkCost are new.
  • A wallet imported from its phrase is marked imported, and activityListView answers importedNote on it: activity from before the import, made on another device, is not shown.
  • What is in Earn counts in a value on both platforms: homeView's total includes it, and portfolioView takes the portfolio's Earn as a fifth argument and answers inEarn. An app that added Earn to a total itself must stop.
  • earnReviewView takes failure with the action and amount it was about, and answers error only on a review of that same action and amount.
  • resetWallet() drops the session once the wallet is removed, and a tab that hears of a reset made elsewhere drops the one it holds.
  • An import hands the thread back between owners, before each key is derived and before each owner's accounts are worked out, so a slow phone keeps drawing and a tap on Cancel is heard promptly.
  • A signed submit that was dispatched is never reported as "nothing was sent". Any answer short of a clear success, the server's own 401 included, is an unknown outcome: the reservation is kept and the chain settles it. Only a submit that never left the device, for want of a session, fails as notAvailableNow. A private transfer handed over with no signature to settle by is unknown too, where a refusal used to be taken at its word.
  • The relayer's client goes by the server's codes: unavailable means the replica never had the request, insufficient_payment that the fee is asked for again, and anything it cannot vouch for is unknown.
  • Live prices and a chart read the server's answers as they are now: { prices } aged by the Age header, { points }, and an error body for everything else.