Skip to content

feat: fee network-switch reset, muxed accounts, gas resource breakdown, IPFS failover (#956 #957 #958 #959) - #986

Merged
k-deejah merged 1 commit into
Sorokit:mainfrom
Abidoyesimze:fix/956-959-fees-muxed-gas-ipfs
Oct 15, 2026
Merged

k-deejah merged 1 commit into
Sorokit:mainfrom
Abidoyesimze:fix/956-959-fees-muxed-gas-ipfs

Conversation

@Abidoyesimze

Copy link
Copy Markdown
Contributor

Closes #956
Closes #957
Closes #958
Closes #959

This PR covers four related issues. Each section below says what changed, why, and how it is tested.

#958: FeeEstimator: network switch, surge alert, speed multipliers

Problem: after a network switch, the previous network's fees stayed on screen until the next poll (up to 30s). Surge pricing wasn't explained. Users had no way to pay more for faster inclusion.

Changes

  • useFeeData now reads network from useSorokit() and builds a key from the name and passphrase. When the key changes, the shared fee cache is cleared and a new estimate is fetched right away. The skeleton shows instead of stale values.
  • A generation counter discards any request still in flight from the old network, so a late response can't overwrite the new network's fees. Several mounted estimators share the cache, and only the first to see the new key performs the reset.
  • If the base fee is above SURGE_FEE_THRESHOLD (300 stroops, 3× the minimum), a role="alert" banner appears. It shows the ratio and explains that ledgers are full and transactions are competing on fee. The compact variant shows a "Surge pricing" badge.
  • New 1.2x / 1.5x / 2.0x toggle buttons (aria-pressed) show a priority fee based on the recommended fee. Clicking the active button clears it. A new optional onMultiplierChange(multiplier, fee) callback reports the choice.
  • The fee is computed in tenths, so 500 × 1.2 gives exactly 600. With plain float math it comes out as 600.0000000000001 and rounds up to 601.

Tests (FeeEstimator.test.tsx): a network switch clears fees and refetches; a stale old-network response is ignored; no refetch when the network values are unchanged; surge banner shown above 300 and not at exactly 300; compact surge badge; multiplier select, switch, clear and callback; multiplier rounding.

#956: utils: muxed accounts and lossless stroop formatting

Problem: validateStellarAddress rejected valid 69-character muxed (M…) accounts, and converting stroops with Number(stroops) / 1e7 loses precision above 2^53.

Changes

  • validateStellarAddress accepts both G… (56 chars) and M… (69 chars, SEP-23) addresses. Both go through full StrKey decoding: base32, version byte, payload length and CRC16-XMODEM checksum. I refactored the decoding into shared decodeStrKey / encodeStrKey helpers.
    • I kept the in-house decoder, matching the existing comment that this package deliberately doesn't depend on the Stellar SDK. The logic mirrors StrKey.isValidMed25519PublicKey in js-stellar-base.
  • New decodeMuxedAccount(address) returns { baseAddress, id }. The ID is a decimal string, because 64-bit IDs don't fit in a JS number. There is also an isMuxedAccount helper.
  • New formatTokenAmount(amount, decimals = 7) does the conversion with BigInt and string formatting. It always gives exactly 7 decimals, handles negatives and i128-size amounts, and returns null for non-integer input.
  • toXLM now uses it, while keeping its parseInt-style leading-digit parsing and its "0.0000000" fallback for invalid input.

Tests (utils.test.ts): the SEP-23 test vectors (ID 0 and ID 2^63); a corrupted checksum, mutated payload, wrong length or case, and a G payload with an M prefix are all rejected; the decoded base address round-trips; stroop edge cases (0, 1, just above 2^53, max int64, i128, negatives); invalid input; formatting round-trips back to the original stroops.

#959: GasOptimizer: resource breakdown and panic diagnostics

Problem: cost was shown as one lump sum, contract panics appeared as raw diagnostic output, and the zero-limit and zero-fee cases weren't tested.

Changes

  • New collapsible Resource Breakdown (aria-expanded). It lists CPU instructions, memory, ledger read bytes and ledger write bytes, each with its own fee, plus a total in stroops and XLM.
    • It uses estimate.resources (a new optional SorobanResourceUsage field on GasEstimate) when the client provides simulation data. Otherwise it uses the configured CPU and memory limits and labels them as such.
    • Rates live in the exported RESOURCE_FEE_RATES: 25 stroops per 10k instructions, 1,786 per KB read, 11,800 per KB written. These are approximate Mainnet values and validators can change them. Memory has no separate fee.
  • New src/lib/diagnostics.ts with parseContractPanic():
    • It finds the Rust panic message (panicked at '…') and the host error (Error(WasmVm, InvalidAction), Error(Contract, #3), …) and turns them into plain English.
    • It also reads base64 XDR diagnostic events by pulling the readable SCVal strings out of the decoded bytes.
    • Errors that aren't contract failures, such as network errors, return null and are shown as before.
  • When simulation fails with a panic, the UI shows a role="alert" explanation. The raw diagnostics are kept in a collapsed <details>.
  • Fee totals treat empty or invalid fee strings as 0, so zero-fee transactions never show NaN.

Tests (GasOptimizer.test.tsx): the breakdown with simulated resources (per-resource amounts and costs checked); the fallback to configured limits; empty resource limits give zeros, not NaN; computeResourceCosts with missing, zero, negative and non-finite values; rounding; zero-fee and empty-fee transactions; panic and custom contract error codes rendered in plain English; XDR panic extraction; known host error mapping; non-contract errors left unchanged.

#957: ipfs: gateway failover, CIDv1 sub-paths, 8s timeout

Problem: one gateway failing (429/504) broke NFT media, ipfs://bafy…/metadata.json wasn't parsed properly, and stalled requests could hang forever.

Changes

  • IPFS_GATEWAYS is now ipfs.io → Cloudflare → Pinata → dweb.link. ipfs.io stays first because Cloudflare retired its public gateway in 2024 and now redirects.
  • New parseIpfsUri / isValidCid:
    • Handles CIDv0 (Qm…) and CIDv1 (base32 b…, base36 k…) with nested file paths.
    • Also handles the legacy ipfs://ipfs/ prefix and existing gateway URLs.
    • Strips query strings and fragments, and lowercases upper-case CIDv1.
    • Strings that aren't valid CIDs still pass through, so behaviour stays compatible.
  • New fetchIpfsResource(url, { timeoutMs, signal }): each gateway attempt gets its own AbortController with an 8s (IPFS_REQUEST_TIMEOUT_MS) timeout. A non-2xx response, network error or timeout moves on to the next gateway, and an abort from the caller stops the whole chain.
  • NFTGallery images move to the next gateway if they haven't loaded after 8s, and changing the image URL cancels the stalled browser request.
    • The timer only runs while the image is on screen (useIsVisible), because loading="lazy" images don't start loading until scrolled to.
    • An image that has loaded is never timed out.

Tests (new ipfs.test.ts, plus NFTGallery.test.tsx): CID validation; CIDv1 sub-path parsing and resolution; gateway order and clamping; failover on 429/504 and on network errors; abort after exactly 8s (fake timers) then fallback; every gateway stalling; a caller abort stopping the chain. In the gallery: an 8s stall moves to the next gateway, and a loaded image isn't timed out. I updated the existing gallery gateway test for the added Pinata step.

Testing notes

I couldn't run the full suite locally because my machine ran out of disk space during npm install. The pure logic (StrKey and muxed decoding against the SEP-23 vectors, BigInt formatting, CID parsing, gateway failover and timeout, panic parsing) was run directly in Node and matches the test expectations. Lint, typecheck, build and the full vitest suite are left to CI on this PR.

…n, IPFS failover

- FeeEstimator (Sorokit#958): clear cached fees and refetch immediately on network
  switch, drop stale in-flight responses, surge pricing alert (base fee >
  300 stroops), 1.2x/1.5x/2.0x speed multiplier toggles.
- utils (Sorokit#956): validate 69-char muxed M-addresses via StrKey decoding,
  add decodeMuxedAccount, lossless BigInt formatTokenAmount; toXLM uses it.
- GasOptimizer (Sorokit#959): collapsible Resource Breakdown (CPU, memory, ledger
  read/write with per-resource fees), plain-English contract panic
  diagnostics, NaN-safe zero-fee/zero-limit handling.
- ipfs (Sorokit#957): Cloudflare/ipfs.io/Pinata/dweb.link failover, CIDv0/CIDv1
  parsing with sub-paths, 8s AbortController timeout per gateway; NFT
  images advance to the next gateway after an 8s stall.

Closes Sorokit#956
Closes Sorokit#957
Closes Sorokit#958
Closes Sorokit#959
@drips-wave

drips-wave Bot commented Oct 15, 2026

Copy link
Copy Markdown

@Abidoyesimze Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Comment thread src/lib/ipfs.ts
}

// Drop a query string or fragment; neither is part of the content path.
rest = rest.replace(/[?#].*$/, "").replace(/^\/+/, "");
Comment thread src/lib/ipfs.ts
rest = rest.replace(/[?#].*$/, "").replace(/^\/+/, "");
const slash = rest.indexOf("/");
const rawCid = slash === -1 ? rest : rest.slice(0, slash);
const path = slash === -1 ? "" : rest.slice(slash + 1).replace(/\/+$/, "");
@k-deejah
k-deejah merged commit 689b6e0 into Sorokit:main Oct 15, 2026
1 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants