Repository navigation
Releases: Noirwire/shared-noirwire
Release list
v0.9.0
A private move runs between any two of the person's own places: main wallet to portfolio as before, and now portfolio to main wallet and portfolio to portfolio. The portfolio signs for itself and needs no SOL, the same as the main wallet.
Breaking
For both apps:
- Move:
fundPrivately(deps, { from, to, amount, symbol })replacesportfolioIdwith the two ends, each a portfolio's id orFUNDING. The move as it was is{ from: FUNDING, to: portfolioId }. ItsrefreshneedsportfolioAssetbesidefunding. A move to where it left, or to or from an archived portfolio, is refused. - Move, the arrival:
awaitPrivateArrival(deps, { from, to, symbol, balanceBefore, amount })takes the same two ends. An arrival in the main wallet is recorded there. - Move, the screens:
fundingAmountView,fundingReviewViewandfundingOutcomeViewtake an optionalsourceLabel, the portfolio the money leaves; absent means the main wallet.portfolioLabelis where it arrives: passfundingCopy.wallet.inSentencewhen that is the main wallet.fundingDraft'sfundingBalanceis what the place the money leaves holds. - Pending words:
PendingWords.privateTransfergets a null label for a move into the main wallet.pendingWordsalready answers it.
Added
fundingAmountViewanswersmax, the most that can move once both fees are paid on top, andnothingToMovefor a portfolio that holds none of the asset.fundingDraftanswers the samemax.fundingCopy.from,toandswapfor the two ends of a move.- A move out of a portfolio is listed there as "Moved to Main wallet" or "Moved to" the portfolio's name, once its transaction has landed.
Changed
- The move's title is "Move money", and its wording no longer assumes the main wallet is where the money leaves.
v0.8.0
A move to a portfolio is called arrived only once all of it is there. The settlement service delivers a private transfer in three parts, seconds apart, and the arrival was called on the first rise in the balance: a move of 10 USDC was reported, and recorded in Activity, as 3.33 USDC, while the other two parts landed a few seconds later.
Breaking
For both apps:
- Move to portfolio:
awaitPrivateArrival(deps, { portfolioId, symbol, balanceBefore, amount })takes theamountthat was moved, the same number passed tofundPrivately. It resolves once the balance has risen by all of it. When the window ends with only part of it there it resolves to the balance as it stands, and null still means nothing arrived. - Move to portfolio, the wait:
fundingOutcomeViewtakes a new outcome,"onItsWay". Show it as soon asfundPrivatelyanswerssubmitted, in place of the progress stages: it names the amount that was moved and can be closed. Keep awaiting the arrival behind it, and change to"done"when it resolves, or"pending"on null. - Routes:
fundingSendParamsandreadSendSourceare gone: neither app called them. A send from the main wallet passesFUNDINGas its source.
Changed
- The private move's footer on both platforms ends "Private moves are powered by MagicBlock."
Fixed
- A read of the main wallet's balances that was on its way while an action stored a newer balance no longer writes the older number back over it.
v0.7.0
The funding wallet is now called the main wallet, and it is a wallet in its own right. It is the person's own public wallet: money arrives there, it has a page of its own with one balance, its USDC and SOL, its activity and four actions, and it can send. Money for one of the person's own portfolios still only goes through Move to portfolio, which keeps the two apart in public. In code it is still funding everywhere: no type, field, function, route parameter or stored key was renamed.
Breaking
For both apps:
- The name: every string that said "funding wallet" says "main wallet". Update any app test that asserts the old wording, and any string the app wrote itself. The table under Changed lists the lines that changed beyond the name.
- Activity kinds:
ActivityKindhas a new"deposit"(money that arrived in the main wallet). Add it to any record or switch keyed on the kind. An entry'sportfolioIdcan beFUNDING:activityRowandactivityDetailViewalready name it "Main wallet" (portfolioFallbackon the detail, withportfolionull), so draw the fallback where there is no portfolio to open. - The main wallet page:
fundingWalletView(reads, wallet, updatedAt, freshness)replacesfundingWalletView({ balance, readFailed }). Drawtotal(null: draw nothing),assetsunder it,emptywhen not null, the four actionsmove,send,receive,addMoney(each{ label, target, disabled, reason }), andactivity.loading,unavailableandstaleare drawn as on Home. Gone:waiting,balance,balanceLabel,lead,readFailed,move.quiet, andmobileFundingCopy.page. - Reading it: open the page, and the Move sheet, with
money().refresh.fundingBalances(), notrefresh.funding(address, "USDC"). Only the full read writes "Money arrived" into Activity; the single-asset read is the one an action makes after it lands and never does. - Home: draw
view.fundingWallet(label,value,target: { to: "fundingWallet" }) as a row that opens the page.HomeTargethas the new member. The "is in your main wallet" notice and its action are as they were. - Send: open the sheet for the main wallet with
fundingSendParams()and read its source withreadSendSource(readPortfolioParamstill answers null for it). TakesendSourceView(reads, wallet, sourceId, platform)for the title, the name, the own address, the assets with what is held, andempty. Pass itsfundingtosendFormView,fromFunding: truetosendReviewView, itsnametosendResultView, and the same id asportfolioIdtoreviewSendandsend. Reserve and read pending underFUNDING. - Send, refusals:
reviewSend(...).recipientcan be"ownPortfolio": showsendRecipientRefusal(review)on the form, as for a recipient that cannot receive. When the review'sneedsCashis shown for the main wallet its action is "Add money": open the add-money sheet, not Move. - Refusal reasons:
RefusalReasonhas"moreThanFunding"and"ownPortfolioFromFunding";refusalMessagewords both.
For the web app:
-
SendDialogandwalletActions.send/reviewSendare the call sites above; build the main wallet's page and its Home row.
For the mobile app:
-
FundingWalletScreen(the new view, andfundingBalances()on opening),useFundFlow(its opening read),useSendFlowandSendScreen(the source),usePendingBlock(FUNDING)andHomeScreen(the row) are the call sites above. -
useSendFlowkeepsunsendableasUnsendable | null: handle"ownPortfolio"before setting it.
Added
- Money that arrives in the main wallet from outside is written into Activity as "Money arrived", by the difference a balance refresh finds, valued at the current price. Nothing is written while any action is unsettled, from an action's own read, or from the first read of a restored wallet.
sendandreviewSendtakeFUNDINGas the source: the main wallet sends its USDC and its SOL with its own key, pays the network from its own SOL when it holds enough, and is priced with the relayer when it does not.fundingWalletView,fundingWalletValue,fundingActivity,sendSourceView,fundingSendParams,readSendSource,HomeView.fundingWallet, and the typesFundingWalletView,FundingWalletAction,FundingWalletTarget,SendSourceView.fundingCopy.wallet,sendCopy.mainWallet,networkCostCopy.mainWallet,errorsCopy.send.moreThanMainWallet,errorsCopy.send.useMove,activityCopy.movedToPortfolio.
Fixed
- An action reserved for the main wallet that landed without this device seeing it wrote no activity entry. A send from the main wallet that lands unseen is now recorded once the chain shows it.
Changed
| Where | Was | Is |
|---|---|---|
| every string | "funding wallet", "Funding wallet" | "main wallet", "Main wallet" |
portfolioCopy.addMoney.arrived |
"{amount} USDC has arrived in your funding wallet. Move it to a portfolio before buying." | "{amount} USDC is in your main wallet. Move it to a portfolio to invest." |
settingsCopy.funding.lead |
"Money sent here must be moved into a portfolio before you can invest." | "Your money arrives here. Move it to a portfolio to invest." |
fundingCopy.emptyBefore |
"Your {asset} funding balance is empty. " | "Your main wallet holds no {asset}. " |
pendingActionCopy.subject.funding, .balance.funding |
"... from the funding wallet", "the funding wallet's balance" | "... from your main wallet", "your main wallet's balance" |
onboarding reunitedIntro, importedIntro |
"Opened the funding wallet at ...", "The funding wallet is at ..." | "Opened your main wallet at ...", "Your main wallet is at ..." |
classifyRecipient own label |
"Funding wallet" | "Main wallet" |
mobileFundingCopy.page |
the old page's words | removed; the page reads fundingCopy.wallet |
A record written by this version holds entries an earlier version does not know (deposit, and an entry of the main wallet), and an earlier version refuses to open it. Do not run an older build against a wallet this one has written to.
v0.6.1
Home's total value is everything the person has in the wallet: every active portfolio, what is in Earn, and now what waits in the funding wallet too (its USDC, its SOL and any other token, each at its own price). Money that has arrived and not yet been moved into a portfolio no longer leaves the total at zero. "Ready to invest" is unchanged: it counts cash inside portfolios only. A total that counts SOL or a tracker in the funding wallet needs a live price for it, by the same rule as a portfolio's holdings.
Nothing for the apps to change: homeView and portfolioOverview keep their shapes.
v0.6.0
An unknown balance is never shown as a zero. A wallet unlocked while NoirWire could not be reached showed "$0.00" for its total, its cash and every portfolio, beside a notice that the figures "may be out of date": a person reads that as a real zero balance. Until balances have loaded once, every figure worked out from them is now null, the types say so, and the screen carries one line with a retry. Also here: Costs states prices only, the chart hint follows the pointer and not the platform, Home says when Earn does not run, and a testing note for the header, Welcome and About.
Breaking
The types changed on purpose, so an app cannot print a zero by accident: a figure from balances is Figure (string | null), and a component draws nothing for null. Each app keeps one ReadFreshness for its balance read already (0.5.3, for homeView); pass that same value everywhere below.
For both apps:
- Home:
view.total.valueandview.cash.valuearestring | null, and so arevalueandlineon each row ofview.portfoliosandview.archived.rows. Draw nothing for null (the waiting placeholder whileview.loading), never "$0.00". Newview.unavailable: { text, retry } | null: when it is not null, showtextonce, with a button labelledretrythat runs the balance refresh, in place of the stale notice (view.staleis null then).view.emptyis false andview.waitingis null until balances load, so the empty wallet's "Add money" lead and the "has arrived" notice are not drawn over an unread wallet. - Home, Earn:
view.earningis renamedview.earn. It is null where Earn does not run (the app passesundefinedas the Earn total off the main network, as now). Whenview.earnis null, hide the Home Earn tile and the "could be earning" card (portfolioCopy.earnPromo); do not decide that from the network in the app. - Portfolio list:
portfolioRowView(reads, portfolio, updatedAt, balances)takes the balance read as a fourth argument.valueandlinearestring | null. - Portfolio and pie pages:
portfolioView(reads, wallet, id, updatedAt, balances, inEarn?):balancesis the fifth argument andinEarnmoves to sixth.valueandcashLinearestring | null; a pie'smix.slices[].trailingisstring | null. Newview.loadingandview.unavailable(draw as on Home). Until balances load,primary,rebalance,holdingsandemptyare null, and every button inquietbut Receive hasdisabled: trueand areason: show the reason where the screen showssendReason. - Tracker page:
TrackerState.freshnesstakesbalancesas well (a visitor passesNEVER_READ).holding.quantityand.valuewere already nullable; until balances load they are null,holding.rowsis empty andholding.noneis null, so draw neither a quantity nor "You do not own this tracker yet". Newview.unavailable. Buy is disabled and Sell is not offered until balances load. - Send:
SendFormStatetakesbalances.amountLineandavailable.valuearestring | null.balanceUnavailablenow also carries the reason while balances have never loaded: keep drawing it where it is drawn. - Buy and sell:
TradeFormStatetakesbalances;availableisstring | null, andbalanceUnavailablecarries the reason.portfolioChoices(reads, wallet, side, symbol, balances)andnoMoneyView(reads, wallet, portfolioId, balances)take it last; a choice'scaptionisstring | null, andnoMoneyViewanswers null until balances load. - Invest in a pie:
pieInvestViewtakesbalances;availableisstring | null, and newbalanceUnavailableis the reason to show under the amount. - Move to portfolio:
FundingAmountStatetakes a requiredreadFailed: boolean(whether the funding wallet's read on opening failed) besidefundingBalance. Newview.unavailable: show its line and retry when not null. The web'sleadno longer says "Available 0.00 USDC" before the read: it names no amount.choosePortfolioViewtakesbalances, and each row'scashisstring | null. - Earn:
earnScreenView,earnPortfolioViewandearnSheetViewtakebalances. A row'scash(andcash,cashAvailableonearnPortfolioView) isstring | null; the sheet'savailableisstring | nulland it has a newbalanceUnavailable. The screen has newloadingandunavailable; until balances load,depositandwithdraware disabled,deposit.reasonis the unavailable line and no rowopensanything. The Earn total was already "Unavailable", never a zero, until every position is read, and is unchanged.earnChoiceViewandearnAmountVieware unchanged: they are reached only through the actions the screen now holds back. - Chart hint:
marketsCopy.detail.chartHintis now{ touch, mouse }andmobileMarketsCopy.detail.chartHintis gone. DrawchartHint(pointer)from@noirwire/shared/presentation. - Testing note: draw
betaView().tag("Beta") as a small label beside the NoirWire mark in the app header and on Welcome. DrawaboutView().beta("NoirWire is in testing. Start with small amounts.") as a line on Settings' About page. Both are shown on every network, the main network included: they are about the product, not the network.networkLabel()and the test-network banner keep their rule.
For the web app:
-
app/portfolio/page.tsx(homeView,portfolioView),PortfolioDetail,TradeDialog(tradeFormView,noMoneyView),SendDialog,FundPrivatelyFlow,PieOrderDialog,app/earn/page.tsxandapp/markets/[symbol]/page.tsxare the call sites above. -
DesktopHomedraws the "could be earning" card fromportfolioCopy.earnPromo: draw it only whenhome.earnis not null. -
app/markets/[symbol]/page.tsxdrawscopy.chartHint: drawchartHint(pointer), wherepointeris "touch" when the device reports a coarse pointer (matchMedia("(pointer: coarse)")) and "mouse" otherwise. - The Risks row in Settings reads
settingsCopy.risks.description, which changed: nothing to do.
For the mobile app:
-
HomeScreen(view.earningbecomesview.earn),PortfolioScreen,TrackerScreen,TradeSheet(portfolioChoices,tradeFormView,noMoneyView),SendScreen,FundScreen(fundingAmountView,choosePortfolioView),PieOrderSheet(pieInvestView,noMoneyView) andEarnScreenare the call sites above. -
TrackerScreendrawsmobileMarketsCopy.detail.chartHint: drawchartHint("touch"). - The Settings row for Risks and its sublabel are drawn from the app's own strings if they are not read from
settingsCopy.risks.description: make the sublabel "What can go wrong".
Added
hasLoaded(read)in@noirwire/shared/domain: whether a read has ever come back.balancesView(read),balancesUnavailable(),BalancesView,UnavailableViewandFigurein@noirwire/shared/presentation: the one rule for balances that have never loaded.chartHint(pointer)andChartPointerin@noirwire/shared/presentation.betaView(),BetaViewandAboutView.beta;appCopy.beta.portfolioCopy.balances.unavailable.
Fixed
- A wallet whose balances had never loaded showed a total of "$0.00", "Ready to invest $0.00" and "$0.00" on each portfolio, and Home led with "Add money" as if the wallet were empty. Every figure is now null until the first read succeeds, and the wallet is not called empty.
- The notice over those zeros said "What you see may be out of date." That wording is now only for balances that did load once; where they never loaded the line is "We couldn't load your balances. Check your connection and try again.", with a retry.
- Send, Move to portfolio, Buy, Sell, a pie's Invest and Earn could be started against a balance that had never been read. They are held back until it has, with the same reason.
- The web's Move to portfolio sheet said "Available 0.00 USDC" before the funding wallet had been read.
- The chart hint told a phone's browser or a tablet to hover.
Changed
| Where | Was | Is |
|---|---|---|
settingsCopy.costs.move |
"Moving money into a portfolio privately: 0.1% + $0.20. It usually arrives within a minute and can ..." | "Moving money into a portfolio privately: 0.1% + $0.20." |
settingsCopy.risks.description |
"What can go wrong, stated once" | "What can go wrong" |
marketsCopy.detail.chartHint |
"Hover to see the price and date." (and the phone's "Press and hold ...") | { touch: "Press and hold to see the price and date.", mouse: "Hover to see the price and date." } |
portfolioCopy.balances.unavailable (new) |
"We couldn't load your balances. Check your connection and try again." | |
fundingCopy.leadPrivate, .leadPublic |
"... Available {amount}." | the same, and without the "Available" sentence when available is null |
appCopy.beta.tag, appCopy.beta.line (new) |
... |
v0.5.3
The fixes from a second first-time-user walkthrough. Costs states the trading fee or says there is none; the add-money sheet opens its costs in place, so the address stays on screen; Home, Markets and a tracker's page say their data may be out of date the moment a refresh fails; a missing price is said one way; bringing a wallet back is "Restore" everywhere; the everyday screens use plain words; Earn off the main network is one line; and a cold open with no connection is answered within eight seconds. The test suite lost the cases that only pinned prose and gained rule checks in their place.
Breaking
For both apps:
- Freshness:
homeViewtakes a sixth, required argument,freshness: { now, balances, prices }.MarketsStateandTrackerStatetake a requiredfreshness({ now, prices }, and{ now, prices, chart }) and no longer takeloading. PasslivePricesFreshness()from@noirwire/shared/infrastructureasprices. For balances and for a chart, keep aReadFreshness(start fromNEVER_READ) and move it on withrecordRead(previous, ok, Date.now())each time the read ends, whereokis the result the refresh already answers with (the web'sbalanceRefreshalready has it aswhole).nowis the screen's own clock: draw again when a refresh ends and at least everyREFRESH_INTERVAL_MS. Drawview.staleas the quiet notice and the waiting state whileview.loading; delete each app's own stale and first-load logic (BalanceStatus's own flag,refreshFailedon Home,firstLoadon Markets). Refresh balances everyREFRESH_INTERVAL_MSwhile Home is in view and when the app comes back into view: a read older thanSTALE_AFTER_MSshows the notice. - Cold open: run the network gate through
checkNetwork(() => connection.getGenesisHash(), expectedGenesisHash())from@noirwire/shared/application. It answers"ok" | "wrongNetwork" | "unreachable"withinNETWORK_CHECK_LIMIT_MS(8 s). The phone's owncheckNetworkhad no limit, and the web's waited 15 s (CHAIN_CHECK_LIMIT_MS): delete both. - Add money:
addMoneyView(wallet, { tradeFeeBps })takes the app's trading fee (noirwireFeeBps()).view.footeris gone. Renderview.costs.label("What does it cost?") as a disclosure that is closed at first (expandedByDefault: false) and showsview.costs.linesunder it when tapped. It must not navigate: the sheet and its address stay on screen. The Settings "Costs" page is unchanged. - Costs:
costsViewaccepts{ tradeFeeBps?: number | null }.settingsCopy.costs.tradeAtReviewis gone (tradeNoFeereplaces it). - A missing price:
TrackerRowView.noLivePriceis gone, andpriceis "Price unavailable right now." when there is no live price: draw the one string, with nothing beside it.TrackerView.pricewithout a live price is{ live: false, figure }(nonote).marketsCopy.atReview,marketsCopy.noLivePrice,marketsCopy.detail.priceUnavailableandportfolioCopy.home.atRevieware gone: usecommonCopy.priceUnavailable.marketsCopy.rowLabeltakes the price as a string. - Tracker page:
aboutis{ title, lines, retired }and its title is "About".about.notOfferedandabout.issuerDetailsare gone: the certificate line, the issuer's powers and where it is not offered are nowrisks.lines, and the issuer link's label isrisks.details. Draw all of them behindrisks.title("Read the risks").marketsCopy.detail.aboutAndRiskis gone. - Trade review:
tradeReviewView(...).risksis{ title, lines }. The web's "What you are buying" block (tradeCopy.tracker.what,.notOffered) moves behind "Read the risks": drawrisks.linesthere. - Earn screen:
EarnScreenView.venueand.mainnetOnlyare gone. Whenview.notHereis not null, show that one line at the top and nothing else of the screen's body:rate,deposit,withdrawandtotalare null androwsis empty.depositandtotalare now nullable. Drawview.risks.linesbehindview.risks.titleon the screen itself; the venue is named there and nowhere above it. The title isview.title("Earn") with no line under it. - Earn sheets:
earnSheetViewandearnReviewViewno longer takevenue.earnCopy.lead,.beforeDepositand.depositRiskare strings, not functions.earnCopy.earning(amount)takes one argument.earnCopy.notHereis gone (useearnScreenView(...).notHere, orearnCopy.mainnetOnly). - Network names:
networkLabel()answers "Solana" on the main network and "Solana test network" otherwise. Show a network label (the web's nav and banner) only whenisMainnet()is false.commonCopy.tradingUnavailableOnandpieCopy.order.tradingUnavailableare strings, not functions of the network. - Discard prompt: render
discardPromptView():title,body("What you typed will be lost."),keep,discard. - About: render
aboutView().help.action.urlis amailto:link andwebsite.action.urlanhttps:one: open them on a tap.
For the web app:
-
earnCopy.notHere(networkLabel())on the Earn page,copy.lead(venue), the venue lines under the title and in the summary, andearnCopy.earning(amount, venue)inPortfolioDetailare the call sites of the Earn changes above. -
MarketRow,DesktopMarkets,app/markets/page.tsxandDesktopHomereadatReviewandnoLivePricedirectly: drawcommonCopy.priceUnavailableonce in their place.
For the mobile app:
-
TrackerRowdrawsrow.noLivePriceandTrackerScreendrawsview.price.note,view.about.notOfferedandview.about.issuerDetails: see the tracker changes above.
Added
ReadFreshness,NEVER_READ,recordRead,freshnessOf,Freshness,REFRESH_INTERVAL_MS(30 s) andSTALE_AFTER_MS(60 s, two refresh intervals) in@noirwire/shared/domain: the one rule for how current a read is. A read may be out of date the moment its latest attempt fails, or once its last success is older thanSTALE_AFTER_MS; it is current again on the next success; one that never came back, with no failure, is waiting.livePricesFreshness()in@noirwire/shared/infrastructure: when prices last arrived and whether the latest poll failed.POLL_MSis nowREFRESH_INTERVAL_MS.freshnessView,FreshnessView,ScreenFreshness,HomeFreshnessandTrackerFreshnessin@noirwire/shared/presentation.HomeView.staleand.loading;MarketsView.loading;TrackerView.loading. A chart is read once for a range, so only its own failure makes it stale.checkNetwork,NetworkCheckandNETWORK_CHECK_LIMIT_MS(8 s) in@noirwire/shared/application.aboutView(),discardPromptView()andCostsStatein@noirwire/shared/presentation.AddMoneyView.costs;EarnScreenView.notHereand.risks;TrackerView.risks.details; the trade review'srisks.- Copy:
commonCopy.discardBody,settingsCopy.costs.tradeNoFee,earnCopy.venueLine.
Fixed
- Costs no longer says "the fee is shown in the review" where no trading fee is set. It says there is no NoirWire fee, which is what is true.
- Markets and a tracker's page showed no notice for up to two minutes after the connection went mid-session: the price feed kept the last prices as live and told nobody its poll had failed. The failed poll is now recorded and the notice shows at once, over prices that are still drawn.
- A cold open with no connection could show nothing for up to 40 seconds on the phone, while the first read was asked for again and again. The check now gives up waiting after 8 seconds and the app says it cannot reach NoirWire.
- Earn off the main network showed a disabled button and an "Unavailable" for the rate, the total and every row. It now says "Earn runs on the main network." once.
- A discard confirmation had a title and no body.
Changed
Every string that changed (mobile marks the phone's variant):
| Where | Was | Is |
|---|---|---|
settingsCopy.costs.tradeAtReview |
"Buying or selling a tracker: the fee is shown in the review." | tradeNoFee: "Buying or selling a tracker: no NoirWire fee." |
portfolioCopy.addMoney.steps.get.detail |
"USDC is a digital dollar: 1 USDC = $1. Send it from any app or wallet that supports USDC on the {network} network. You do not need an account with us." | "USDC is a digital dollar: 1 USDC = $1. Many apps sell it. Buy it there, then send it on the {network} network... |
v0.5.2
The product talks about what a person can do, never about what it lacks or cannot do in the main path: a copy-wide audit rewrote the sentences that led with a limitation, and a test now refuses their return. Alongside it, a round of small pieces both apps had quietly written twice (a step's status word, the chart's price-and-date join, a dialog's close label, a phrase-copy warning) now have one shared home, and Home's emptiness check no longer leads with "Add money" over a wallet that still has money in an archived portfolio or in Earn, unread.
Breaking
For both apps:
- Home's emptiness rule:
homeViewtakes a fifth, required argument,archivedHeld: ArchivedHeldState({ holds: boolean; earnUnknown: boolean }). Passholds: truewhile any archived portfolio holds a tracker or money moved into it, andearnUnknown: truewhile any archived portfolio's Earn position has not been confirmed as zero (still loading, or its read failed). Either one now keeps Home from leading with "Add money" over a wallet that is not really empty. The web app can delete its ownhomeOverHeldMoneywrapper andwalletIsEmptyinsrc/components/portfolio/homeState.tsand callhomeViewdirectly with the two booleans it already computes for them. Separately,homeViewno longer treats an unread Earn total (earn === null) as confirmed zero: a wallet whose active-portfolio Earn figure has not come back yet now keeps both buttons instead of assuming there is nothing to show. -
chartReadout(...)answerstext, the price and date already joined ("$100.00 · 14 Jul 2026, 12:00"). The web app'sPriceChartbuilt this join itself (`${readout.price} · ${readout.date}`); read.textand delete the local join. - Removed from the add-money explainer: the card-payments sentence is gone from
portfolioCopy.addMoney.steps.get.detail(see Changed).
Added
Pieces reported by both apps as written twice, now with one shared home (a phone variant only where the wording truly differs):
commonCopy.showLabel,.hideLabel(a reveal control's accessible name),.increaseLabel,.decreaseLabel(a stepper's accessible name),.percentSpoken(a stepper's spoken value),.closeLabel(a close control's accessible name where plain "Close" is ambiguous),.nothingHereYet,.discardThis,.keepEditing,.discard(a sheet's unsaved-changes prompt),.stepStatus(a progress step's status word: waiting, in progress, done, failed, not done),.andList(joins a list the way a sentence does: "SOL", "SOL and USDC", "SOL, USDC and SPYx").commonCopy.backand.closealready covered the plain labels; apps reading a local "Back" or "Close" string should read those instead.marketsCopy.detail.chartHint("Hover to see the price and date.") andmobileMarketsCopy.detail.chartHint("Press and hold to see the price and date."): the one-line hint under a tracker's chart.mobileOnboardingCopy.phrase.wordLabel(position, word), the accessible name for one word of the recovery-phrase grid, and.phrase.copy, the phrase-copy warning (confirmTitle,confirmBody,copyAnyway,copied,copiedNote,failed) a phone warns with before copying a recovery phrase to the clipboard.onboardingCopy.phrase.revealand.hiddenalready covered the reveal button and the concealed word; the mobile app's ownPhraseGrid.tsxduplicates both verbatim today and should read them instead.mobileWalletCopy.protection.refused: what stands in place of a secret (an address, a recovery phrase) the phone could not confirm is kept out of screenshots and recordings.mobileSendCopy.camera: the camera-permission screen that stands in for the scanner (purpose,allow,off,offDetail,openSettings).mobileAppCopy.runtimeFailure: what the phone says when its own security checks fail on startup, before anything else opens (title,detail,configFailure,closing).
Changed
The owner's rule on copy: the product states the working route as steps, and never volunteers a limitation in the main path (onboarding, Welcome, Home, add money, funding, markets, trade, Earn, send). A network a feature does not reach now says where it does run, the way errorsCopy.trade.mainnetOnly already did for Live trading's own refusal message. Every string that changed:
| Where | Was | Is |
|---|---|---|
portfolioCopy.addMoney.steps.get.detail |
"USDC is a digital dollar: 1 USDC = $1. NoirWire cannot take card payments yet. Buy USDC in any app or service that can send it on the {network} network. No account with us is needed." | "USDC is a digital dollar: 1 USDC = $1. Send it from any app or wallet that supports USDC on the {network} network. You do not need an account with us." |
commonCopy.tradingUnavailableOn |
"Live trading is unavailable on {network}. You can explore trackers, but cannot place an order here." | "Live trading runs on Solana mainnet. You can explore trackers on {network}." |
errorsCopy.trade.mainnetOnly |
"Live trading is only available on mainnet." | "Live trading runs on mainnet." |
errorsCopy.earn.mainnetOnly |
"Earning is only available on Solana mainnet." | "Earning runs on Solana mainnet." |
earnCopy.mainnetOnly |
"Earn is available on Solana mainnet only." | "Earn runs on Solana mainnet." |
earnCopy.notHere |
"Earn is available on Solana mainnet only. Nothing can be lent or withdrawn on {network}." | "Earn runs on Solana mainnet. Switch from {network} to lend or withdraw." |
pieCopy.order.tradingUnavailable |
"Live tracker trading is unavailable on {network}. The pie is saved, and can be invested on mainnet." | "The pie is saved. Live tracker trading runs on mainnet; switch from {network} to invest it." |
tests/unit/copyVariants.test.ts gained PRODUCT_LIMITATION_PHRASES and productLimitationsIn, alongside the existing REPLACED_NAMES: a test refuses the card-payments sentence and the "unavailable on" / "mainnet only" / "only available on" / "not supported" phrasings wherever they reappear in the everyday copy (the Privacy and Risks pages are excluded, same as the existing name check).
v0.5.1
The wording a first-time user meets, and the view models that carry it, so both apps draw the same Welcome, empty Home, add-money sheet, Costs, tracker page and import. One name for each thing on both platforms. Both apps have rendering work to do: see Breaking.
Breaking
For both apps:
-
Welcome: render
welcomeView(platform).onboardingCopy.welcome.lead,.import,.lookAround,.trustMainnet,.trustTestNetworkandmobileOnboardingCopy.welcomeare gone;.lines,.restore,.exploreand.trustreplace them. Onlycreateis a filled button. The server explanation is no longer on this screen (it stays in Settings, Privacy). A development-only link such as "Open the UI kit" is the app's own: keep it out of Welcome's main column and out of every build that is not a development build. -
Home:
HomeView.secondarymay be null (it is while the wallet is empty: the one button is "Add money").HomeView.howTois gone: open the add-money sheet and renderaddMoneyView(wallet)there.HomeTargetno longer has{ to: "receive"; reveal }; it has{ to: "addMoney" }. Drawtotal.explainer("Only you see this total") as the tappable line,explanationunder the button when it is not null, and no header arc whenshowArcis false. -
Add money: render
addMoneyView(wallet): its title, three steps, the address block inside step 2 (shown at once, no reveal tap, with Copy andnetwork), and the footer link, which opens Costs.address.captureAllowedis true: allow a screenshot of this sheet, and keep capture protection on the recovery phrase, the password, import and portfolio addresses. -
Costs: add a Settings row
settingsCopy.costs.titleand a page or sheet that renderscostsView({ tradeFeeBps }), passingnoirwireFeeBps()from@noirwire/shared/infrastructure. Open the same view from "What does it cost?". -
Tracker page:
TrackerStatehas a new requiredsmallestOrderUsd(passgaslessFromUsd(), the figure the trade sheet already uses).TrackerView.captionis now "NVIDIA tracker · NVDAx"; drawfollowsunder it,minimumnear Buy,staleas the quiet notice, the chart'shighandlow, andrisks.linesbehindrisks.title("Read the risks").about.readRisksmoved torisks.title, and the issuer's powers moved fromabout.linestorisks.lines. On press and hold, showchartReadout(points, x, { range, readAt }). -
Markets: draw
MarketsView.staleas the same quiet notice. Passloading: truewhile prices are read for the first time, on Markets and on a tracker's page, so the notice does not show over a first load. -
Cold open with NoirWire out of reach: render
unreachableView({ hasWallet, locked }), and whenunlockOfferedis true show the unlock screen with the message as a notice instead of replacing it.unlock()andunlockWithKeyBits()read only the device's own storage and never waited on the network; what held unlocking back is each app's network gate, which must let the unlock screen through. After unlock, with NoirWire still out of reach, the view answers the balances wording, and every signature is still refused by the network check that runs before it. -
Unlock: render
unlockProblemView(problem, platform). Never clear the password field on a failed unlock; whentypedis"keepSelected"(a wrong password) select the text. -
Password: render
newPasswordView(platform).ruleabove the field, before anything is typed.onboardingCopy.password.introandmobileOnboardingCopy.password.introare now functions of the minimum length; the view model fills it in. -
Import: show
importWaitingView(...).noteunder the button for as long as an import runs. WhilelookFurtherView(...).continuePausedis not null, disable Continue and show that line. WhenimportSourceView(resolution).skippedis not null, do not show the choice of addresses: go on withskipped.schemeand sayskipped.line. -
Buying with no money: call
noMoneyView(reads, wallet, portfolioId)when Buy is tapped (portfolioIdnull from a tracker's page with no portfolio chosen yet). When it is not null show its title, detail and button in place of the trade's first step. -
Receive:
receiveViewno longer takesplatform(both platforms say the same). Remove it from the call. -
Funding:
fundingReviewView(...).confirm.labelis "Move privately".fundingWalletView(...).showAddressis nowaddMoney, andfundingAmountView(...).emptyNotice.actionreads "Add money": both open the add-money sheet. -
Import pacing:
IMPORT_REQUESTS_PER_SECONDis 3 (was 8).Pacerhas two new required methods,slowDown(holdMs)andhold(holdMs). A pacer fromcreatePacerhas both; one written by hand forpaceImportWithneeds them. A further look can now run past a minute while it waits for the session's allowance: keep showinglookFurtherView(...).continuePausedand a way to cancel for as long as it runs. -
Removed copy keys (each app reads some of these directly):
portfolioCopy.home:findStocks(usefindTrackers),addUsdc,showFundingAddress,howToAddMoney,cashAvailableandcashToInvest(usereadyToInvest).portfolioCopy.detail:addMoney(usemoveToPortfolio).portfolioCopy.addMoney:stepGet,stepGetDetail,stepSend,stepSendDetail,stepMove,stepMoveDetail,depositAddress,footnote(useaddMoneyView).mobilePortfolioCopy.receive(useportfolioCopy.receive.publicNote).marketsCopy.detail:liveNote,quoteNote(usefinalPrice),issuerControl(usepublicTradesandissuerPowers).mobileMarketsCopy.detail:liveNote,dividends.mobileFundingCopy:title(usefundingCopy.titlePrivate),cash(usecommonCopy.readyToInvest).
-
Renamed copy keys:
Was Is onboardingCopy.welcome.import.restoreonboardingCopy.welcome.lookAround.exploreonboardingCopy.welcome.lead.lines(two lines)commonCopy.cashAvailablecommonCopy.readyToInvestportfolioCopy.home.dayIndicative,.heldDayIndicative.dayApproximate,.heldDayApproximateportfolioCopy.home.togetherOnlyHere.onlyYouSeeportfolioCopy.home.shelves.stocks.shelves.companiesportfolioCopy.earnPromo.waitingCash.waitingMoneyportfolioCopy.detail.cashToInvest.readyToInvestportfolioCopy.detail.moveMoneyHere.moveToPortfolioportfolioCopy.detail.addMoneyFirst.moveMoneyFirstnetworkCostCopy.moveMoneyHere.moveToPortfoliofundingCopy.noPrivateRoute.noPrivateMovefundingCopy.depositLink.addressLinkmobileFundingCopy.showAddress,.page.showAddress.addMoney,.page.addMoneymarketsCopy.liveIndicative.approximatePricemarketsCopy.detail.indicative.detail.approximatemarketsCopy.detail.noChart(range).detail.noChart(a string)tradeCopy.moreThanCash,.priceAboveCash.moreThanReady,.priceAboveReadytradeCopy.noCash,.noCashDetail.noMoney,.noMoneyDetailpieCopy.ordermoreThanCash,keepAsCash,noCash,noCashDetailmoreThanReady,keepAsUsdc,noMoney,noMoneyDetailearnCopy.noCash,.cash,.terms.leavesCash.noMoney,.ready,.terms.leaves
For the web app:
-
welcomeView("web").exampleholds the example portfolios, the three steps and the footnote the page shows beside the column. -
fundingTitle(asset, false)reads "Move SOL publicly".
For the mobile app:
-
welcomeView("mobile").exampleis null. - About: show
mobileSettingsCopy.about.helpwith.helpContact, and.websitewith.websiteValue. - Polish that is the app's own to draw: "Add at least one tracker." once; "Type a name first." cleared when the tab changes;
sendReason("Nothing to send yet.") under Send and not under Receive; inner padding on a pie's percentage; "Move to portfolio" the same size on a portfolio and on a pie.
Added
welcomeView(platform),addMoneyView(wallet),costsView({ tradeFeeBps }),privateMoveCostText(), `unreachableV...
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 t...
v0.4.1
Added
-
The waiting standard.
waitingView(elapsedMs, kind, steps?)in@noirwire/shared/presentationsays what to show as time passes: nothing beforeWAITING_DELAY_MS(300 ms), then a quiet signal, then a calm "still working" line afterSTILL_WORKING_AFTER_MS(4 s for content, a check or a review; 8 s for an action), and which step of multi-step work is current. A pure function of elapsed time: each platform owns its clock.importWaitingViewandimportFailedTextdo the same for an import, withIMPORT_LAST_STEP_AFTER_MS(6 s). -
waitingCopy, and the import's progress words (onboardingCopy.import.progress, with the phone's lead and failure inmobileOnboardingCopy.import.progress). -
Quiet retries for reads:
withRetries,readWithRetries,READ_RETRY,isTransientin@noirwire/shared/application. Three tries, 400 ms and then 800 ms apart, on a request that never got through, ran out of time or came back 429 or 5xx. Wrapped: the balance refresh, live prices, price history, tracker multipliers, the lending rate and position, import lookups,checkRecipient, and preparing a review (a trade's price, a send's cost, whether a holding is open, the relayer's price and keys). Nothing that signs or submits is retried, and a refusal never is. -
mobileSettingsCopyandmobileWalletCopy: the phone's Settings, Unlock and Reset words, as variants.unlockProblemTextandisWrongPasswordin@noirwire/shared/presentation. -
walletCopy.reset.notRemoved,walletCopy.crossTab.noticeandwalletCopy.crossTab.refused, which the web app kept for itself. -
decimalAmountin@noirwire/shared/domain: a period or a comma as the decimal separator, and null for text that could be read two ways. -
signAsClientandunsignedTransactionin@noirwire/shared/testing, for a test's stand-in chain client to sign through the real signing guard. The testing layer may now importinfrastructure/for that. -
The phone's layout tokens in
@noirwire/shared/design:mobileRadius,layout,size,MIN_TARGET,opacity,overlayColor,fonts,motion, andspace[8]. -
failureAccountin@noirwire/shared/presentation: a failure in its own account, which is what it is counted under. -
lookFurtherForPortfolios(mnemonic, scheme, activity)in@noirwire/shared/infrastructure, andlookFurtherViewwithonboardingCopy.import.lookFurther("Missing a portfolio? Look further"): a second scan a person can ask for from an import's result. It carries on from where the first stopped (SchemeActivity.scannedThrough) and gives up only afterEXTENDED_DISCOVERY_GAP(100) unused addresses in a row. -
canCreatePortfolioandunusedPortfoliosInARowin@noirwire/shared/application, and the constantsDISCOVERY_GAP(20),EXTENDED_DISCOVERY_GAPandMAX_UNUSED_PORTFOLIOS_IN_A_ROW(10) in@noirwire/shared/domain. -
createPacerin@noirwire/shared/application, a rate pacer with an injectable clock, andpaceImportWithandIMPORT_REQUESTS_PER_SECOND(8) in@noirwire/shared/infrastructure.withRetriestakesjitterandrandom. -
MAX_ACTIVITY_ENTRIES(500) in@noirwire/shared/domain,olderNotKepton the Activity list view, and its words for both platforms. -
focusRingand thefocustoken in@noirwire/shared/design;findStockandphraseWordsin@noirwire/shared/infrastructure;canonicalSymbolon the catalog;tooPreciseandsmallestAmountin@noirwire/shared/domain;portfolioNameTakenin@noirwire/shared/application;privacySectionHelpin@noirwire/shared/presentation;LOCK_SIGNAL_KEYin@noirwire/shared/wallet.
Changed
lock()locks the wallet everywhere it is open: it announces the lock through the vault, undernoirwire.wallet.lock(a counter, nothing about the wallet), and every other tab or running copy locks on hearing it. A reset does the same when the record would not be removed. An idle lock stays with the tab that went idle.changePasswordrefuses the password it already has: "That is already your password."- Every button variant, the icon button, the input and the chip carry a keyboard focus ring in a colour named outright (
focusRing). - Every counted noun goes through
plural: "1 asset", "1 result", "Order placed". sendCopy.invalidAmountandmobileSendCopy.invalidAmount: "Enter a finite amount greater than zero." and "Enter an amount greater than zero." are now "Enter an amount, like 12.50."- An amount typed with more decimals than its asset has, which includes one below its smallest unit, is refused in a send, when moving money in, in Earn and in a trade: "That amount has too many decimals. The smallest amount is 0.000001 USDC."
parseRecoveryPhrasereads a phrase as people paste one (numbered lists, commas, line breaks, tabs, capitals) and says exactly what is wrong: how many words there were, which word is not a recovery phrase word and where it stands, or that the words do not form a phrase. It answersproblembesideerror.stockBySymboland the catalog'sassetfind a tracker whatever the capitals:nvdaxis NVDAx.createPortfoliorefuses a name another portfolio already has, archived ones included, withduplicateName.fundingAmountViewtakestouchedand shows no error under an amount field nobody has typed in.createPortfoliorefuses withunusedPortfolios("You have several portfolios that were never used. Use one of those first. An archived one can be restored.") once the wallet ends in ten portfolios in a row that never held or did anything, archived ones included.- An import asks about each candidate address in one request instead of two, paces all its requests to eight a second across both sets of addresses, and adds jitter to its pauses after a refusal.
- The wallet's record keeps its 500 most recent activity entries. The oldest are dropped as new ones are written.
Failure and waiting messages say what happened, what it means for the money and what to do next, and no longer say how the app asked. This changes what the web says too.
| String | Was | Is |
|---|---|---|
onboardingCopy.import.networkFailed |
Could not reach the network to check balances. Try again. | We couldn't finish importing your wallet. Nothing was saved in this browser. Try again. |
onboardingCopy.import.checking |
Checking balances and recovering portfolios onchain... | Finding your portfolios... |
onboardingCopy.password.encryptFailed |
Could not encrypt the wallet. Try again. | We couldn't encrypt your wallet, so nothing was saved. Try again. |
mobileOnboardingCopy.import.networkFailed |
Could not reach the network to check this phrase. Try again. | We couldn't finish importing your wallet. Nothing was saved on this phone. Try again. |
mobileOnboardingCopy.import.offline |
You're offline. Importing needs the network to find what this phrase holds. | You're offline. Nothing was saved on this phone. Go back online to import your wallet. |
appCopy.networkGate.wrongNetwork(n) |
This app is built for n, but its network connection serves a different chain. Nothing can be sent until that is fixed. | NoirWire is not connected to n as it should be. Your money has not moved, and nothing can be sent until this is fixed. Try again later. |
appCopy.networkGate.unreachable |
The network could not be reached, so balances cannot be shown safely. | We can't show your balances right now. Your money has not moved. Try again. |
mobileAppCopy.network.checking |
Checking the network... | Getting things ready... |
errorsCopy.chain.noQuote |
Jupiter has no price for this order right now. | There is no price for this order right now. Nothing was traded. Try again in a moment. |
errorsCopy.chain.wrongNetwork |
The network connection serves a different chain than this app is built for. Nothing was signed or sent. | NoirWire is not connected to Solana as it should be, so this was stopped. Nothing was signed or sent.... |