Page MenuHomePhabricator

[Cashtab] Refactor Orderbook to handle more logic
ClosedPublic

Authored by bytesofman on Oct 26 2024, 23:08.

Details

Reviewers
emack
Group Reviewers
Restricted Project
Commits
rABCf65ac048151a: [Cashtab] Refactor Orderbook to handle more logic
Summary

The current way that Agora loads offers is not ideal. We load and prepare all active listings on the network before we render anything.

It would be better if each OrderBook component accepted a tokenId instead of activeOffers. That way each OrderBook component could load its own activeOffers. When an offer is accepted or canceled, only one OrderBook needs to update (instead of the entire page).

This makes the component more robust and portable.

During this refactor, other improvements for simplicity and robustness were introduced, as it would have been much harder to complete this diff while leaving those complications around. It would be quite difficult to split these changes into several diffs, as we would have to preserve and work around some bad practices.

The extensive integration tests on the component allow this kind of complex but performance-improving refactor.

Summary of benefits

  • Better conditional rendering of components that depend on calculated or API-fetched information
  • We do not reload ALL offers after a buy or a cancel, only the orderbook affected
  • Since each orderbook now handles the offer fetching and updating logic, it's much easier to stick an orderbook on another screen (for example a token landing page that only loads an orderbook for one token)
  • Faster load time as we render an orderbook as soon as we get the tokenId, and then each orderbook (in parallel) starts getting its own offers
Test Plan

npm test

This version is live at https://cashtab-local-dev.netlify.app/

Diff Detail

Repository
rABC Bitcoin ABC
Lint
Lint Not Applicable
Unit
Tests Not Applicable

Event Timeline

do not refresh all offers when you make a tx, instead lock the screen, improve token name spinner renders to preven unnecessary loader renders, update tests

Failed tests logs:

====== CashTab Unit Tests: <Agora /> We can see multiple offers, some we made, others we did not, and we can cancel an offer ======
Error: expect(element).toBeInTheDocument()

element could not be found in the document
    at Object.toBeInTheDocument (/work/cashtab/src/components/Agora/__tests__/index.test.js:472:59)

Each failure log is accessible here:
CashTab Unit Tests: <Agora /> We can see multiple offers, some we made, others we did not, and we can cancel an offer

Failed tests logs:

====== CashTab Unit Tests: <Agora /> We can see multiple offers, some we made, others we did not, and we can cancel an offer ======
Error: expect(element).toBeInTheDocument()

element could not be found in the document
    at Object.toBeInTheDocument (/work/cashtab/src/components/Agora/__tests__/index.test.js:472:59)

Each failure log is accessible here:
CashTab Unit Tests: <Agora /> We can see multiple offers, some we made, others we did not, and we can cancel an offer

hm haven't seen this before and not seeing it locally. I don't see an obvious flakiness improvement in the test, running again.

Failed tests logs:

====== CashTab Unit Tests: <Agora /> We can see multiple offers, some we made, others we did not, and we can cancel an offer ======
Error: expect(element).toBeInTheDocument()

element could not be found in the document
    at Object.toBeInTheDocument (/work/cashtab/src/components/Agora/__tests__/index.test.js:472:59)

Each failure log is accessible here:
CashTab Unit Tests: <Agora /> We can see multiple offers, some we made, others we did not, and we can cancel an offer

add another loading check to flaky integration test

emack requested changes to this revision.Oct 27 2024, 03:25
emack added a subscriber: emack.
emack added inline comments.
cashtab/src/components/Agora/OrderBook.js
432–442 ↗(On Diff #50469)

this is a great approach, I'm assuming this means you can make synchronous API calls, let it process in the background, with cached data already loaded?

Will be adopting this for ecc

490 ↗(On Diff #50469)

If the agora.activeOffersByTokenId() call above returns undefined then it will not throw an error and proceed directly to this sort call, resulting in a null pointer. Is it guaranteed that the API call above will return a non-undefined value?

522 ↗(On Diff #50469)

Is the assumption here that if there is no change to takeTokenSatoshis then there is no need to make any further API calls, hence no need to re-render?

677 ↗(On Diff #50469)

Prevent unintended white screens

cashtab/src/components/Agora/index.js
210 ↗(On Diff #50469)

I'd suggest to miminize technical jargon for user facing messages

235–236 ↗(On Diff #50469)
289–290 ↗(On Diff #50469)
This revision now requires changes to proceed.Oct 27 2024, 03:25
bytesofman marked 7 inline comments as done.
bytesofman added inline comments.
cashtab/src/components/Agora/OrderBook.js
432–442 ↗(On Diff #50469)

imo the biggest available improvement in this sort of case is using typescript

Chronik returns typed responses, so you can know exactly what is available when you have your response. Then you can make sure stuff renders according to what is available -- this means you can do things like render token icons immediately (only need a token id for this) -- then gradually render other features as the required data is loaded.

I'm assuming this means you can make synchronous API calls, let it process in the background, with cached data already loaded?

the API calls are async, but since each component owns its own API calls, they are all made in parallel, and each component loads as soon as it has the data it needs (vs before we were using promise.all, which did load all the data in parallel, but no orderbook could be rendered until we had the data for every orderbook).

490 ↗(On Diff #50469)

The API call and all of the associated calcs are wrapped in try...catch

In the event of an error, we setAgoraQueryError(true) -- which renders a notice that there was an API query error. \

For now, if this happens, the only option for the user is to reload the page. We could handle it dynamically, but at the moment there isn't much point. If chronik call succeeds and the server is properly indexed, we will always get data of the expected shape (another example of why typescript here would give us some more confidence).

If the chronik call fails or the server is not properly indexed, there's really nothing we can do except inform the user we can't get the data.

522 ↗(On Diff #50469)

There is never a need to make further API calls unless the user buys or cancels an offer, which we know will change the available offers in this orderbook. Going forward, it would be more robust and faster to use websockets to make changes to orderbooks based on market activity. Chronik already has the tech for this. But I think we should get it working with the API first.

websocket implementation would make agora as fast or faster than a CEX.

for this useEffect, we are saying "if we do not have the data we need to properly render the orderbook, then there is no point in calculating a validation error"

Because takeTokenSatoshis is constrained by the terms of each agora offer, the only validation we calculate is "will accepting this quantity result in an unspendable offer?"

No need to calculate this if we do not have the quantity data we need to show the user what he's buying.

We re-render whenever takeTokenSatoshis changes (depth bar fill changes, rendered prices and quantities change), but we do not make API calls unless the user buys or cancels an offer.

677 ↗(On Diff #50469)

This is already gated by canRenderOrderbook, which requires activeOffers.length > 0

activeOffers is initialized as [] and can only be updated if we do not run into any errors in the try...catch that fetches and prepares the agora data.

So, we are already checking for these two conditions.

Another example tho of why typescript would help a lot.

cashtab/src/components/Agora/index.js
210 ↗(On Diff #50469)

This note is more for the dev than the user. titles can appear as tooltips, but in this case would only be briefly. But we can use titles (esp titles that appear at different loading stages) to confirm expected screen behavior.

tbh I had forgotten what the loading wallet context variable meant until digging into what specifically we had to do to avoid reloading the whole page on any user action. I think keeping the title more specific is more important than scaring the user, who probably never sees this (and, if he does, for long periods of time ... would be helpful to know as a dev).

235–236 ↗(On Diff #50469)

more "we should use typescript"

similar to OrderBook, this is already gated.

activeOffersCashtab can only be null (if we are waiting on API calls) or an object with two arrays (if we avoid the try...catch errors in getListedTokens)

If it is null, then we are loading a spinner instead of this component

{activeOffersCashtab === null ? (
                        <Spinner title="Loading active offers" />
                    ) : (
                        <>
                            {loading && (

Possibly something is still going on and we get to a situation where Array.isArray(activeOffersCashtab.offeredFungibleTokenIdsThisWallet) is not true, and hence white screen of death. However if we are getting to this, need to understand why, and gate it.

I want to avoid adding unnecessary checks that may prevent whitescreen while concealing other unexpected behavior.

Again tho...the better answer here would be "use typescript"

289–290 ↗(On Diff #50469)

same as above

if chronik API calls are successful, then we do not expect this to ever not be an array.

if chronik API calls fail, then this activeOffersCashtab === null and we do not render this component.

This revision is now accepted and ready to land.Oct 28 2024, 11:50
This revision was automatically updated to reflect the committed changes.
bytesofman marked 7 inline comments as done.