Repository navigation
v0.5.0
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:
-
Platformhas a new required port,sessionStore:get,setandremoveof one small JSON value in plain app storage. Not the vault, and never inside the wallet record. -
Envhas a new required value,apiBaseUrl, andenvFromrefuses settings without it.EnvSettingsgainsapiBaseUrl,platform,developmentandrpcUrl. -
configureHttp,HttpConfigand itsheaders()are gone. There is nothing to call in their place: where requests go isenv.apiBaseUrl, and the one header every request carries is added by the package. A server or a test that named an RPC provider withconfigureHttp({ rpcUrl })now setsrpcUrlin the environment. -
RPC_RELAY_PATH,JUPITER_RELAY_PATHandPRIVATE_PAYMENT_RELAY_PATHare gone. An address is built withapiUrl(route, rest). -
The paths changed, so the web app's own relay routes are no longer called by this package:
Was Is POST /api/rpcPOST /v1/rpc/api/jupiter/*/v1/jupiter/*POST /api/private-payments/*POST /v1/private-payments/*GETandPOST /api/relayerGETandPOST /v1/relayerGET /api/pricesGET /v1/pricesGET /api/history/:symbol/:rangeGET /v1/history/:symbol/:rangePOST /api/event(each app's own)POST /v1/events, asapiUrl("events") -
Every one of those requests carries
Authorization: Bearer <token>. An app's own requests to the server go throughauthorizedFetchto carry it too. -
A new failure code,
notAvailableNow, inChainErrorCode: aswitchover the codes that lists them all needs the new case.chainErrorMessagealready words it. -
memoryPlatform()now includes asessionStore, andtestEnv()anapiBaseUrl(https://api.noirwire.test). A test that pinned a relative path such as/api/jupiter/swap/v2/ordernow seeshttps://api.noirwire.test/v1/jupiter/swap/v2/order. A test setup callsinstallTestPlatform()in place ofinstallPlatform(memoryPlatform())andconfigureHttp(...), or its first request tries to start a real session. -
WAIT_LIMIT_MSreplaces 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
apiBaseUrlto/apiwithplatform: "web", and have the host forward/api/:path*to the server; or name the server's origin and allow it inconnect-src. - Install a
sessionStoreoverlocalStorage, and keep the cross-tablocks: a session's renewal runs under the locknoirwire-session. - Delete
src/components/wallet/passwordCheck.ts's copy of the rule and callassessPasswordWith(checker, password)with the bundled checker. Deletesrc/components/localCopy.tsandWAIT_LIMIT_MSinsrc/components/waiting/limits.ts; their words and numbers are here now (see Added). - The server-side platform passes
rpcUrltoenvFrom, where it passed it toconfigureHttp.
For the mobile app:
- Set
apiBaseUrlto the server's origin (https://api.noirwire.com), in place of the relay URL. Plain http is accepted only forlocalhost,127.0.0.1and10.0.2.2, and only withdevelopment: true. A path is refused on the phone. - Delete the
X-NoirWire-Clientheader and the HTTP configuration that carried it (src/platform/httpConfig.ts): nothing readsheaders()any more. - Install a
sessionStoreover the app's plain key-value storage. - Delete
src/features/phoneCopy.tsandWAITING_LIMIT_MSinsrc/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, fromenv.apiBaseUrland the server's paths (session,rpc,jupiter,privatePayments,relayer,prices,history,events,health).apiBaseUrlis an origin, or on the web a path on the page's own origin.- The anonymous session.
createSessionKeeperin@noirwire/shared/applicationstarts one (POST /v1/session, no token, no body), keeps it throughsessionStore, 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 answerssession_expired, and replaces one older thanSESSION_MAX_AGE_MS(24 hours;env.sessionMaxAgeMssets 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 givenSESSION_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:fetchwith 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()andsessionRoutesbeside 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,ApiErrorCodeandapiErrorInin@noirwire/shared/domain, andapiErrorOf(response)in@noirwire/shared/infrastructure: every error the server writes itself,{ code, error }, read by its code and never by its sentence.isTransientasks 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 byNOIRWIRE_OPENAPI; it is part ofnpm testand skipped there, saying so, when the variable is unset.npm run test:api:live: the read paths against a running server named byNOIRWIRE_API_URL.- In
@noirwire/shared/testing:installTestPlatform(overrides?),fakeSession(),memorySessionStore(),fakeApi(routes)to answer the server by path, andTEST_API_URL. assessPasswordWith(checker, password)in@noirwire/shared/wallet, withPasswordCheckerandPasswordAssessment: 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.assessPasswordis built on it.WAIT_LIMIT_MSin@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.gettingReadyandmobileWaitingCopy.overdue;onboardingCopy.import.notNowandonboardingCopy.phrase.discarded,.newPhrase,.acknowledgeNew;portfolioCopy.balancesandportfolioCopy.archived;marketsCopy.pricesUnavailable;appCopy.offline;earnCopy.unreadandearnCopy.notHere;commonCopy.tryAgain;mobileWalletCopy.newPassword.checkFailed;mobilePortfolioCopy.create.forExampleand.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 (signerandownSignatureon the pending action;unfindablewhen 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), andfailureMessagereplaces 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
reviewSendreads the recipient from the network and answersrecipientbeside the cost (SendReview): null for a wallet, why it cannot receive, or"unreadable".sendRecipientRefusal(review)words it.SendChainneedscheckRecipient. 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", withamount,arrivedandnetworkCoston the detail.ActivityRowView.networkCostandActivityDetailView.arrivedand.networkCostare new. - A wallet imported from its phrase is marked
imported, andactivityListViewanswersimportedNoteon 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, andportfolioViewtakes the portfolio's Earn as a fifth argument and answersinEarn. An app that added Earn to a total itself must stop. earnReviewViewtakesfailurewith the action and amount it was about, and answerserroronly 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:
unavailablemeans the replica never had the request,insufficient_paymentthat 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 theAgeheader,{ points }, and an error body for everything else.