Page MenuHomePhabricator

[marlin] Add initial Firma support
ClosedPublic

Authored by Fabien on Apr 3 2026, 21:22.

Details

Reviewers
bytesofman
Group Reviewers
Restricted Project
Commits
rABC55591bca506d: [marlin] Add initial Firma support
Summary

A switch at the top of the app let the user switch between Firma and XEC.

There are some limitations for now:

  • There is no support for bip21 firma payment requests
  • There are a lot of legacy functions and variables that should be renamed now that tokens are supported

The design allow for easily adding any desired token, making marlin a good template for token wallets. For now only Firma is added, more might come later if I feel like it doesn't degrade the experience. It's still only send and receive at this point, but I think a swap feature might make sense. All of this will be other diffs, this one is already large enough.

Note that there is a special case for the Firma price: it's pegged to USD, so when the user selects USD as a fiat currency the conversion is forced to exactly 1/1. For all other currencies, USDT is used as a USD proxy.

Test Plan
pnpm web

cd web && pnpm test

Diff Detail

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

Event Timeline

Rebase, fix price for tokens

Fabien retitled this revision from [marlin] WIP: Add Firma support to [marlin] Add initial Firma support.Apr 7 2026, 15:23
Fabien edited the summary of this revision. (Show Details)
Fabien edited the summary of this revision. (Show Details)
Fabien published this revision for review.Apr 7 2026, 15:25
Fabien edited the summary of this revision. (Show Details)
Fabien edited the test plan for this revision. (Show Details)
bytesofman requested changes to this revision.Apr 7 2026, 18:27
bytesofman added a subscriber: bytesofman.

A couple of general issues

(1) it may not be the right approach to treat XEC and tokens both as generalized assets, with "atoms" meaning sats for XEC and atoms for tokens.

this works, but I can see it being difficult to extend to more complex txs, e.g. those that send both XEC and a token, or XEC and multiple tokens.

Even now, a typical tx is an Agora buy or sell, where parsing involves showing both correct token amounts and correct XEC amounts. So keeping sats and atoms distinct could prove more extensible.

(2) More than enough features now to justify some integration tests...not necessarily with regtest, but something like Cypress, or perhaps there is a similar approach to Cashtab's react-testing-library "workflow" tests that could be implemented in this codebase. Would make it easy to review larger changes like this. As the app becomes more complex, reviews are forced to accept more on faith. From the dev perspective, the app is also getting to the point where adding these kinds of tests from nothing to full app coverage becomes daunting enough to dissuade entirely. So tests would become both more important and less likely to be initialized.

apps/marlin-wallet/web/src/amount.ts
12 ↗(On Diff #58907)

This (probably) is fine for FIRMA and XECX, with FIRMA having 4 decimal places and a modest-ish supply.

However it should probably still be tested to show that no accuracy is lost for very small or very large numbers.

This approach would not work for tokens with 9 decimal places and a large supply.

Cashtab uses string methods; it's also possible to generalize with bignumber.js

16 ↗(On Diff #58907)

same issue

107–120 ↗(On Diff #58907)

ecash-wallet exposes a fee() method on InspectAction, the class of inspectAction

apps/marlin-wallet/web/src/supported-assets.ts
28 ↗(On Diff #58907)

alt approach, could add an optional key here, say something like referencePrice, and use that instead of the "use 1.0" bailout currently set on Firma.

Firma is unlikely to be the only RWA token on eCash. As it stands now, if there is a EUR or CHF stable that should also be set to "1" in these currencies, then the same checks and return 1 must be added for them as well.

That said, I'm not sure it would improve anything to make this generalized. Could be more complex.

apps/marlin-wallet/web/src/transaction-manager.ts
37 ↗(On Diff #58907)

A transaction is not necessarily limited to a single tokenId. ALP to ALP swaps could be a common exception here.

This revision now requires changes to proceed.Apr 7 2026, 18:27

A couple of general issues

(1) it may not be the right approach to treat XEC and tokens both as generalized assets, with "atoms" meaning sats for XEC and atoms for tokens.

this works, but I can see it being difficult to extend to more complex txs, e.g. those that send both XEC and a token, or XEC and multiple tokens.

Even now, a typical tx is an Agora buy or sell, where parsing involves showing both correct token amounts and correct XEC amounts. So keeping sats and atoms distinct could prove more extensible.

That was my initial design, but it happens that having XEC being treated just like a token let me reuse a lot of code that would otherwise either be duplicated or "or typed" which makes it much harder to reason about. If it's about naming I can go with bits instead of atoms if you think it's less confusing.
Marlin is not trying to be fully featured, quite the opposite. In this context I would favor simplicity over extensibility, even if this require some refactors later on.

(2) More than enough features now to justify some integration tests...not necessarily with regtest, but something like Cypress, or perhaps there is a similar approach to Cashtab's react-testing-library "workflow" tests that could be implemented in this codebase. Would make it easy to review larger changes like this. As the app becomes more complex, reviews are forced to accept more on faith. From the dev perspective, the app is also getting to the point where adding these kinds of tests from nothing to full app coverage becomes daunting enough to dissuade entirely. So tests would become both more important and less likely to be initialized.

This one I agree 100%. Because there few features I can't have a ton of unit tests but I definitely need to add some functional tests. That's the next item on my list for Marlin.

That was my initial design, but it happens that having XEC being treated just like a token let me reuse a lot of code that would otherwise either be duplicated or "or typed" which makes it much harder to reason about. If it's about naming I can go with bits instead of atoms if you think it's less confusing.
Marlin is not trying to be fully featured, quite the opposite. In this context I would favor simplicity over extensibility, even if this require some refactors later on.

the decision is good provided it lines up with the intended direction of the app

apps/marlin-wallet/web/src/amount.ts
107–120 ↗(On Diff #58907)

yeah I wrote this function and even used it for XEC fee estimation in the function directly above... I just forgot about it

apps/marlin-wallet/web/src/supported-assets.ts
28 ↗(On Diff #58907)

I thought adding a Fiat typed "stablecoin" optional key but the issue is that I still need a crypto reference for the other fiat destinations.
For firma I use USDT as a source for non USD destination (e.g. USDT/EUR) so it's good enough. But that would be hard to do for CHF because I don't have such a crypto as a source.

The real solution would be to gather fiat exchange rates from some provider and translate USD/other fiat from this. I didn't get down that rabbit hole for now, which I think is fine for what I'm trying to achieve.

apps/marlin-wallet/web/src/transaction-manager.ts
37 ↗(On Diff #58907)

Here the tokenId refers to the active asset selected by the user, not the tokenId associated with a single transaction. If the user selects Firma the tokenId is the one from Firma, if he selects XEC then it's null.
It's used e.g. to compute the balance for this wallet for this tokenId

Fabien marked an inline comment as done.Apr 13 2026, 20:07
Fabien added inline comments.
apps/marlin-wallet/web/src/amount.ts
12 ↗(On Diff #58907)

I took a shortcut and added an assertion that it fits a safe number range, same below. This makes it work to up to 900 billion Firma which should buy me enough time to update if needed.

Feedback (missing the e2e tests)

Fabien planned changes to this revision.Apr 13 2026, 20:08

Tail of the build log:

     sitional balance -- shows spend transitional then available balance after mempoo               
     l and finalization WS (failed).png                                                             
  -  /work/apps/marlin-wallet/web/cypress/screenshots/transitional-balance.cy.ts/Tran     (1280x720)
     sitional balance -- shows spend transitional amount in fiat then finalizes to up               
     dated fiat primary (settings before tx) (failed).png                                           


================================================================================

  (Run Finished)


       Spec                                              Tests  Passing  Failing  Pending  Skipped  
  ┌────────────────────────────────────────────────────────────────────────────────────────────────┐
  │ ✔  history.cy.ts                            00:12        6        6        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  main.cy.ts                               00:07        5        5        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✔  mnemonic.cy.ts                           00:17        7        7        -        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✖  send.cy.ts                               189ms        1        -        1        -        - │
  ├────────────────────────────────────────────────────────────────────────────────────────────────┤
  │ ✖  transitional-balance.cy.ts               01:14        8        6        2        -        - │
  └────────────────────────────────────────────────────────────────────────────────────────────────┘
    ✖  2 of 5 failed (40%)                      01:51       27       24        3        -        -  

--------------------------------------------------------------------------------

  Debug faster with full visibility.

  Record to Cypress Cloud and get instant access to full test details and replays.
  Inspect the DOM, network events, and console logs exactly as they ran in CI.

  >> https://on.cypress.io/cloud-get-started

--------------------------------------------------------------------------------
 ELIFECYCLE  Command failed with exit code 3.
<i> [webpack-dev-server] Gracefully shutting down. To force exit, press ^C again. Please wait...
Error: Command failed with exit code 3: pnpm run cypress:run
    at makeError (/work/node_modules/.pnpm/execa@5.1.1/node_modules/execa/lib/error.js:60:11)
    at handlePromise (/work/node_modules/.pnpm/execa@5.1.1/node_modules/execa/index.js:118:26)
    at process.processTicksAndRejections (node:internal/process/task_queues:103:5) {
  shortMessage: 'Command failed with exit code 3: pnpm run cypress:run',
  command: 'pnpm run cypress:run',
  escapedCommand: '"pnpm run cypress:run"',
  exitCode: 3,
  signal: undefined,
  signalDescription: undefined,
  stdout: undefined,
  stderr: undefined,
  failed: true,
  timedOut: false,
  isCanceled: false,
  killed: false
}
/work/apps/marlin-wallet/web:
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  marlin-wallet-web@1.8.2 test:e2e: `start-server-and-test 'pnpm run dev' http://localhost:3000 'pnpm run cypress:run'`
Exit status 1
 ELIFECYCLE  Command failed.
Build marlin-wallet-e2e-tests failed with exit code 1

Fix existing e2e tests and add main screen tests

Fabien planned changes to this revision.Apr 13 2026, 20:46
apps/marlin-wallet/web/src/amount.ts
107–120 ↗(On Diff #58907)

When writing the tests I understood: it's done that way on purpose because I want to show gas + fees and not only the fees. I keep the UI showing "fee" only as I think it's simply easier to understand that way

Add e2e tests, rename estimateTokenSendFee to estimateTokenSendFeeAndGas for clarity

  • it's still difficult for me to think about XEC and tokens in the same bucket, with atoms sometimes meaning sats and sometimes meaning atoms. but it does work and it makes sense with the architecture of the app. potentially an opportunity for ecash-wallet to better support some of this stuff. Seems like marlin is doing heavier lifting here than it "should" need to do
  • At this point Marlin is no longer a simple app, at least in terms of codebase. I would normally be super wary of doing it all with vanilla JS. The main reason is that testing is much harder than with say react and react-testing-library. However with cypress, this is no longer an issue at all, and the tests are actually much better (and representative of cross-device behavior) than react-testing-library. The tests in Cashtab are comprehensive but also incredibly complex. Cashtab tests have been exceptionally difficult to maintain through dep upgrades, typescript upgrades. The required mocks often require so much babysitting that it's like the app or screen is rebuilt for a test. Improved testing tools and AI will probably lead to more cases like this, emphasis on no-framework code and in-environment testing; both "better" but previously either impossible or not worth the learning curve.
  • it's not necessarily intuitive that the user needs to click on the eCash logo in order to toggle between firma and ecash. I don't know if adding anything would really help though. Perhaps a dropdown-like carat to the left? But maybe it's better without. Once you know what to do, it's quite simple.

Some unrelated improvements I noticed in testing

  • Mnemonic entry inputs could have autocapitalization disabled
  • it would be nice if, when editing the "Amount" input, the user could still see the "Hold to send" button. right now you must manually dismiss the keyboard and then send
  • I'm able to run the cypress tests locally but only if I run cypress open from web/; I get a "not installed" error if i try pnpm run cypress:open ... I dunno this might be a local issue for me, not sure if it's a code issue at all
  • A good goal (mb) for both Cashtab and Marlin would be regtest integration cypress tests. In this way, we could avoid some of the mocking complexity, actually test websocket interactions, chained txs, wallet handling of external incoming txs, maybe even how multiple wallets interact with each other.
apps/marlin-wallet/web/src/amount.ts
37 ↗(On Diff #59082)

architecturally it often makes sense to remove chronik calls from functions like this, i.e. make the returned Tx object the input param

in this way you can isolate chronik calls and error handling, and it is easier to test the parsing logic of the function itself.

In this case ... the function is largely just a wrapper anyway, so no blocker

106 ↗(On Diff #59082)

not sure if we really want to start calling token dust sats "gas"

gas is already an established crypto term in ETH with a distinct meaning

if the info is worth presenting to users (total XEC cost impact of the tx), then it should also be added in ecash-wallet

apps/marlin-wallet/web/src/supported-assets.ts
36 ↗(On Diff #59082)

the general approach of limiting supported tokens to those specified in a codebase, and simply including their icons and decimal info, is much more robust and simple than the mess Cashtab has to deal with (querying and caching all of this info by tokenId, conditionally handling the failure of this cache absolutely everywhere, always fetching icons).

This revision is now accepted and ready to land.Apr 17 2026, 13:43
  • it's still difficult for me to think about XEC and tokens in the same bucket, with atoms sometimes meaning sats and sometimes meaning atoms. but it does work and it makes sense with the architecture of the app. potentially an opportunity for ecash-wallet to better support some of this stuff. Seems like marlin is doing heavier lifting here than it "should" need to do

I couldn't find a better name than atoms, which is used through the codebase for tokens but really isn't a specific L2 keyword (could be atomicBase or something more verbose like that, but in the end the meaning is the same). We could have some toolbox somewhere in ecash-wallet. I looked at how it's done in Firma and Cashtab: the first one uses a 3rd party lib and the latter string operations. So the first is much cleaner but adds an additional security maintenance, and the latter has to deal with complicated code and bad performance (even if this is not really impactful in a wallet). Not sure what would be the best option for ecash-wallet.

  • At this point Marlin is no longer a simple app, at least in terms of codebase. I would normally be super wary of doing it all with vanilla JS. The main reason is that testing is much harder than with say react and react-testing-library. However with cypress, this is no longer an issue at all, and the tests are actually much better (and representative of cross-device behavior) than react-testing-library. The tests in Cashtab are comprehensive but also incredibly complex. Cashtab tests have been exceptionally difficult to maintain through dep upgrades, typescript upgrades. The required mocks often require so much babysitting that it's like the app or screen is rebuilt for a test. Improved testing tools and AI will probably lead to more cases like this, emphasis on no-framework code and in-environment testing; both "better" but previously either impossible or not worth the learning curve.

Cypress is an incredible tool, thanks for the suggestion! It's amazing how the tests are especially easy to read. I also like the idea of running against a chronik node like we do for other integration tests. This would require the apps to support regtest (but that should be easy enough) and to find a way to communicate between the test setup and the frontend automation, which might be more difficult. I need to dig into cypress' internals to see if that's possible. This would make it possible to remove the chronik mocks used for the tests entirely (they would be constructed dynamically by a script instead).

  • it's not necessarily intuitive that the user needs to click on the eCash logo in order to toggle between firma and ecash. I don't know if adding anything would really help though. Perhaps a dropdown-like carat to the left? But maybe it's better without. Once you know what to do, it's quite simple.

OK I will explore the options.

Some unrelated improvements I noticed in testing

  • Mnemonic entry inputs could have autocapitalization disabled

It's disabled already, let's discuss offline to debug this.

  • it would be nice if, when editing the "Amount" input, the user could still see the "Hold to send" button. right now you must manually dismiss the keyboard and then send

Good idea, I'll explore the options here.

  • I'm able to run the cypress tests locally but only if I run cypress open from web/; I get a "not installed" error if i try pnpm run cypress:open ... I dunno this might be a local issue for me, not sure if it's a code issue at all

It's expected to run in web/, I did not add a script to run from the app root. I can do that tho

  • A good goal (mb) for both Cashtab and Marlin would be regtest integration cypress tests. In this way, we could avoid some of the mocking complexity, actually test websocket interactions, chained txs, wallet handling of external incoming txs, maybe even how multiple wallets interact with each other.

Agreed it would be nice, see above

apps/marlin-wallet/web/src/amount.ts
106 ↗(On Diff #59082)

I can revert to the previous name if you prefer, I don't mind.
What I want to show is the whole XEC cost associated with the token transaction. It's not only the fees but also the dust amount, which can be more than the fee. What matters is that I want a user with 10 XEC and 1 Firma to understand immediately that he can't send a tx with 4 XEC worth of fees + 5.46 for the the dust because he doesn't have enough XEC to cover it. IMO just calling it all fees is the best UX because people don't care about the difference, and they shouldn't.

I can refactor in a follow-up to include this to the InspectTx.

apps/marlin-wallet/web/src/supported-assets.ts
36 ↗(On Diff #59082)

I also solves another issue: there can't be scams

apps/marlin-wallet/web/src/amount.ts
106 ↗(On Diff #59082)

imo the UX is fine, since afaict there is no mention of "gas" in the app itself. It's not really a big deal to have it in the codebase, but mb comment that it's not the same thing as "gas" on ETH or other cryptos; it's a total L1 cost that is unique to XEC.

This revision was automatically updated to reflect the committed changes.