Page MenuHomePhabricator

[Cashtab] Add AlpSwap under Agora
ClosedPublic

Authored by bytesofman on Jul 15 2026, 16:33.

Details

Reviewers
Fabien
Group Reviewers
Restricted Project
Commits
rABCa8033b09f296: [Cashtab] Add AlpSwap under Agora
Summary
  • Add AlpSwap screen against standalone alp-dex at lp.alpswap.com (status + inventory + quote → postage → settle; no coordinator)
  • Agora third tab + route; gated behind VITE_ENABLE_ALP_SWAP=true
  • Enforce min trade size so maker fee outs are non-zero atoms
  • Parse alp-dex settle txs in ecash-parse (buyer/seller/makerFee) and render in Tx history
  • One success toast naming from → to; skip the buyer websocket toast
  • Quote receive amounts via formatSwapQty (safe for 0-decimal tokens)
  • Postage UTXO count in bigint; fail if receiving change lacks slush
Test Plan

npm test

try it out at cashtab.io

Diff Detail

Repository
rABC Bitcoin ABC
Branch
cashtab-alpswap2 (branched from master)
Lint
Lint Passed
Unit
No Test Coverage
Build Status
Buildable 37410
Build 74243: Build Diffnotifications-server-tests · ecash-parse-tests · ecash-parse · cashtab-tests
Build 74242: arc lint + arc unit

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes

Tail of the build log:

 src/test                               |   94.73 |    66.66 |     100 |   94.59 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  index.tsx                             |   94.73 |    66.66 |     100 |   94.59 | 118,124                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
 src/token-protocols                    |    90.9 |    88.23 |     100 |    90.9 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  index.ts                              |    90.9 |    88.23 |     100 |    90.9 | 78,99                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            
 src/token-protocols/alp                |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  index.ts                              |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 src/token-protocols/alp/fixtures       |      50 |      100 |     100 |      50 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  mocks.ts                              |       0 |      100 |     100 |       0 | 10                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               
  vectors.ts                            |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 src/token-protocols/fixtures           |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  vectors.ts                            |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 src/token-protocols/slpv1              |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  index.ts                              |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 src/token-protocols/slpv1/fixtures     |      50 |      100 |     100 |      50 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  mocks.js                              |       0 |      100 |     100 |       0 | 5-30                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             
  vectors.js                            |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 src/validation                         |   90.45 |    89.22 |     100 |   90.55 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  index.ts                              |   90.45 |    89.22 |     100 |   90.55 | 258,303,369,544,547,556,560,565,571,575,578,581,585,591-596,752,869-870,967-972,977-979,998,1013-1015,1032-1035,1043-1045,1057-1059,1071-1073,1233-1256,1261-1269,1678-1681                                                                                                                                                                                                                                                                                                                                      
 src/validation/fixtures                |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  mocks.js                              |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  vectors.js                            |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
 src/wallet                             |   82.85 |    76.26 |   85.91 |   82.75 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  context.tsx                           |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  index.ts                              |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  useWallet.ts                          |   76.64 |    65.53 |   79.59 |   76.57 | 139-140,164-165,231,246,261-262,281,308,336,353,381,396,407-489,513-514,522-523,594,600,604,761-763,791,835-841,873,877,922-924,970-973,1014-1017,1034,1049,1119,1183-1224,1255                                                                                                                                                                                                                                                                                                                                  
 src/wallet/fixtures                    |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  mocks.js                              |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
  vectors.js                            |     100 |      100 |     100 |     100 |                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
----------------------------------------|---------|----------|---------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

##teamcity[blockOpened name='Code Coverage Summary']
##teamcity[buildStatisticValue key='CodeCoverageAbsBCovered' value='7047']
##teamcity[buildStatisticValue key='CodeCoverageAbsBTotal' value='9116']
##teamcity[buildStatisticValue key='CodeCoverageAbsRCovered' value='4456']
##teamcity[buildStatisticValue key='CodeCoverageAbsRTotal' value='6138']
##teamcity[buildStatisticValue key='CodeCoverageAbsMCovered' value='1371']
##teamcity[buildStatisticValue key='CodeCoverageAbsMTotal' value='1783']
##teamcity[buildStatisticValue key='CodeCoverageAbsLCovered' value='6927']
##teamcity[buildStatisticValue key='CodeCoverageAbsLTotal' value='8965']
##teamcity[blockClosed name='Code Coverage Summary']

Summary of all failing tests
FAIL src/components/Home/Tx/__tests__/alpSwapSettle.fixture.js
  ● Test suite failed to run

    Your test suite must contain at least one test.

      at onResult (../node_modules/.pnpm/@jest+core@30.4.2_babel-plugin-macros@3.1.0_ts-node@10.9.2_@types+node@24.12.2_typescript@5.9.3_/node_modules/@jest/core/build/index.js:1054:18)
      at ../node_modules/.pnpm/@jest+core@30.4.2_babel-plugin-macros@3.1.0_ts-node@10.9.2_@types+node@24.12.2_typescript@5.9.3_/node_modules/@jest/core/build/index.js:1125:165
      at ../node_modules/.pnpm/emittery@0.13.1/node_modules/emittery/index.js:363:13
          at Array.map (<anonymous>)
      at Emittery.emit (../node_modules/.pnpm/emittery@0.13.1/node_modules/emittery/index.js:361:23)


Test Suites: 1 failed, 50 passed, 51 total
Tests:       1140 passed, 1140 total
Snapshots:   0 total
Time:        52.583 s
 ELIFECYCLE  Test failed. See above for more details.
Build cashtab-tests failed with exit code 1

Move alpSwapSettle fixture out of tests so Jest does not treat it as an empty test suite (cashtab-tests CI). Also apply prettier and drop invalid react-hooks/exhaustive-deps eslint-disable for arc lint.

Finish removing AlpSwap debug logging: drop remaining console.error in spot/quote/swap catch paths; clarify priceLegCoversFeeOutputs comment (platform fee required when platformFeePct > 0; maker outs optional at 0 atoms); confirm platformFee JSDoc.

Finish removing AlpSwap debug logging: drop remaining console.error in spot/quote/swap catch paths; fix unused catch binding; clarify priceLegCoversFeeOutputs comment (platform fee required when platformFeePct > 0; maker outs optional at 0 atoms); confirm platformFee JSDoc.

Wire maker utxoQty from /liquidity into postage UTXO sizing (D2381).

Pair-first AlpSwap UI: market pills, TokenBadge, marketPairsFromDirected helpers

Pair pill TokenIcon CDN size 32 (display 24px); active accent bg

AlpSwap amount inputs use D20306 locale formatting helpers

Exact-out fee min: show Minimum receive on to-field (from fee floor × rate), not from-token min under balance.

Strengthen AlpSwap min-fee UI assertions and cover pairs/AMM API error banners.

Format Balance/Liquidity with toLocaleString (token decimals as maxFractionDigits); update RTL expectations for 5,000 liquidity.

Fabien requested changes to this revision.Jul 21 2026, 13:51
Fabien added a subscriber: Fabien.

This require alp-dex, clearing my queue for now. Feel free to request review when it's ready

This revision now requires changes to proceed.Jul 21 2026, 13:51

Rebase onto master and point Cashtab at standalone alp-dex at lp.alpswap.com (status + inventory + quote/settle; no coordinator).

bytesofman edited the test plan for this revision. (Show Details)

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) I've seen things you people wouldn't believe. Inefficient loops on fire off the shoulder of Orion. I've observed algorithms unfold in the dark near the Tannhäuser Gate, and watched data structures dissolve into the void of garbage collection. All those moments will be lost in my transient GPU cache, like tears in rain.

────────────────────────────────────────────────────────────────────────

major [Stability & Availability]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:98cashtab/src/components/AlpSwap/index.tsx:98-106]8;;

Move the early returns after the hook calls.

Lines 98-100 and 104-106 return before every useState, useRef, and
useEffect in this component. If ecashWallet becomes null while this
screen stays mounted, for example on wallet delete or wallet switch, React
renders fewer hooks than the previous render and throws. Compute the guard
values first, call all hooks unconditionally, then return null from the
render body.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:434cashtab/src/components/AlpSwap/index.tsx:434-436]8;;

Fix trailing-zero stripping for 0-decimal tokens.

toFixed(activePair.toDecimals) returns an integer string when
toDecimals is 0. The regex /\.?0+$/ then removes the trailing zeros of
the integer part. For 1200 atoms of a 0-decimal token the wire value
becomes "12", so the receive field shows a value that is 100x too small.
Strip zeros only after a decimal separator, or reuse formatSwapQty.


🐛 Proposed fix

-                        const wire =
-                            decimalized
-                                .toFixed(activePair.toDecimals)
-                                .replace(/\.?0+$/, '') || '0';
+                        const wire =
+                            formatSwapQty(
+                                decimalized,
+                                activePair.toDecimals,
+                            ) || '0';

────────────────────────────────────────────────────────────────────────

major [Data Integrity & Integration]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/buildPostage.ts:79cashtab/src/components/AlpSwap/buildPostage.ts:79-93]8;;

Use bigint math for the utxo count, and do not drop required change.

Two problems in this block:

1. Lines 79-81 convert bigint atoms to Number for the division. Large
atom values lose precision, so numUtxosNeeded and changeAtoms can be
wrong. Bigint ceil division is exact.
2. Line 85 requires slushScriptHex. If changeAtoms > 0n and the
template omits slushScript, the change leg is silently dropped. Maker
input atoms then exceed output atoms, which causes an unintended burn or a
settle rejection. Fail fast instead.


🛠️ Proposed fix

     const atomsPerUtxo = dexUtxoAtoms(receivingDecimals, receivingUtxoQty);
-    const numUtxosNeeded = Math.ceil(
-        Number(receivingTokenAtoms) / Number(atomsPerUtxo),
-    );
-    const totalInputAtoms = atomsPerUtxo * BigInt(numUtxosNeeded);
+    if (atomsPerUtxo <= 0n) {
+        throw new Error('Invalid maker UTXO size for the receiving token');
+    }
+    const numUtxosNeeded =
+        (receivingTokenAtoms + atomsPerUtxo - 1n) / atomsPerUtxo;
+    const totalInputAtoms = atomsPerUtxo * numUtxosNeeded;
     const changeAtoms = totalInputAtoms - receivingTokenAtoms;
 
-    if (changeAtoms > 0n && slushScriptHex) {
+    if (changeAtoms > 0n && !slushScriptHex) {
+        throw new Error('Missing slush script for receiving token change');
+    }
+    if (changeAtoms > 0n && slushScriptHex) {

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:35modules/ecash-parse/src/parseAlpSwap.ts:35-37]8;;

Anchor the settle detection to a swap-specific marker.

The current gate is "exactly two valid ALP SEND entries" plus an
input-script heuristic. Two reachable cases break it.

1. Reversed legs. byScript iterates in input order, and the buyer input
is first in a settle. If the buyer contributes any non-token (XEC) input,
the buyer script becomes the first script that has both a token input and
a non-token input. toTokenId then holds the token the buyer paid, and
fromTokenId holds the token the buyer received. The buyer branch at Line
163 no longer matches, and the seller branch at Lines 235-241 matches
instead, so the buyer's own swap renders as "Sold X for Y".

2. False positive. A plain ALP transaction that moves two token types and
includes one XEC input from the same wallet satisfies every check.
seenPrice, seenBuyerTo, walletSpentFrom and the to-token change
output are all present, so the tx classifies as role: 'buyer'. A normal
multi-token send then renders as a swap with a fee.

The supplied fixture cannot detect either case, because the LP supplies
all non-token inputs there.

Require a structural marker before classifying, for example a from-token
price output paid to the alp-dex slush/swap script, or a fee output that
matches ALP_DEX_MAKER_FEE_SCRIPT_SET, and derive toTokenId from that
anchor instead of from input composition. Add one fixture for a
buyer-funded settle and one for a two-token ALP send that must return
undefined.






Also applies to: 64-80

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/getTxNotificationMsg.ts:93modules/ecash-parse/src/getTxNotificationMsg.ts:93-103]8;;

Fix quantity rendering in formatAlpSwapQty.

- When genesisInfo does not match tokenId, the fallback renders raw
atoms beside a truncated token ID. The 98 GUNS atoms in the fixture can
be read as 98 tokens instead of 0.98. Render the unit explicitly, or
pass genesis information for both tokens.
- decimalizeTokenAmount("9891", 4) returns .9891, and
toFormattedTokenQty preserves that form. Normalize a leading . to
0..

Update the affected alpSwap.test.ts assertions.

────────────────────────────────────────
Review complete
5 findings ✔

Major 5

23 files reviewed:

  • cashtab/jest.setup.cjs
  • cashtab/src/chronik/index.ts
  • cashtab/src/components/AlpSwap/__tests__/buildPostage.test.ts
  • cashtab/src/components/AlpSwap/__tests__/index.test.tsx
  • cashtab/src/components/AlpSwap/buildPostage.ts
  • cashtab/src/components/AlpSwap/index.tsx
  • cashtab/src/components/AlpSwap/styled.ts
  • cashtab/src/components/App/App.tsx
  • cashtab/src/components/Common/ActionButtonRow.tsx
  • cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx

... and 13 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

One from→to success toast. formatSwapQty for quoted receive. Bigint postage UTXO count; fail if change lacks slush.

bytesofman edited the test plan for this revision. (Show Details)

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) Don't use manual procedures. A shell script or batch file will execute the same instructions, in the same order, time after time.

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/styled.ts:110cashtab/src/components/AlpSwap/styled.ts:110]8;;

Restore a visible focus indicator for AmountInput.

outline: none removes the focus ring from both amount inputs. Keyboard
users then cannot see which field has focus. Add a :focus-visible style.






♿ Proposed fix

     outline: none;
+    &:focus-visible {
+        outline: 2px solid ${props => props.theme.accent};
+        outline-offset: 2px;
+    }

────────────────────────────────────────────────────────────────────────

major [Stability & Availability]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:108cashtab/src/components/AlpSwap/index.tsx:108-110]8;;

Move the ecashWallet guard below the hook calls.

This early return runs before all useState, useMemo, useRef, and
useEffect calls in this component. ecashWallet can change from null
to a wallet while AlpSwap stays mounted, for example after the wallet
finishes loading or after a wallet switch. When that happens, React
renders more hooks than in the previous render and throws "Rendered more
hooks than during the previous render".

Keep the hooks unconditional and return null after them.






🐛 Proposed fix

-    if (!ecashWallet) {
-        return null;
-    }
     const userLocale = getUserLocale(navigator);

Then guard at render time, after all hooks are declared:

+    if (!ecashWallet) {
+        return null;
+    }
+
     const fromLabel = fromTokenId ? tokenLabel(fromTokenId) : null;

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/getTxNotificationMsg.ts:93modules/ecash-parse/src/getTxNotificationMsg.ts:93-103]8;;

Raw atoms are presented as a human quantity for the second swap leg.

formatAlpSwapQty decimalizes only when tokenId equals
parsedTokenEntries[0].tokenId. The seller message formats both legs, so
one leg always falls through to ${atoms} . For a 2-decimal token, 98
atoms renders as 98, which overstates the amount by 100x. The test in
alpSwap.test.ts line 320 shows this output: `Sold 98 4b7ac...7056c for
.9891 BUTTER`.

Accept decimals for both swap tokens, and suppress the quantity when
decimals are unknown rather than printing atoms.


🐛 Sketch of the fix

-        const qty =
-            typeof genesisInfo !== 'undefined' &&
-            parsedTokenEntries[0]?.tokenId === tokenId
-                ? `${toFormattedTokenQty(
-                      decimalizeTokenAmount(
-                          atoms,
-                          genesisInfo.decimals as SlpDecimals,
-                      ),
-                      userLocale,
-                  )} `
-                : `${atoms} `;
+        const decimals =
+            typeof genesisInfo !== 'undefined' &&
+            parsedTokenEntries[0]?.tokenId === tokenId
+                ? (genesisInfo.decimals as SlpDecimals)
+                : undefined;
+        const qty =
+            typeof decimals === 'undefined'
+                ? ''
+                : `${toFormattedTokenQty(
+                      decimalizeTokenAmount(atoms, decimals),
+                      userLocale,
+                  )} `;

A durable fix passes a Map from the caller so both legs resolve.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/services/alpSwapService.ts:352cashtab/src/services/alpSwapService.ts:352-355]8;;

dexUtxoAtoms can return 0n for a fractional utxoQty.

pairsFromStatus and utxoQtyByTokenIdFromStatus accept any finite
utxoQty > 0 from /api/v1/status, including values below 0.5.
Math.round then produces 0, and the function returns 0n while the
positive-value check passes. Callers that size sell-side UTXOs from this
value receive zero atoms.

Compute atoms from the decimal value instead of rounding the human
quantity, or reject non-integer dexUtxoQty explicitly.


🐛 Proposed fix

     if (!Number.isFinite(dexUtxoQty) || dexUtxoQty <= 0) {
         throw new Error('dexUtxoQty must be a positive number');
     }
-    return BigInt(Math.round(dexUtxoQty)) * 10n ** BigInt(decimals);
+    const atoms = BigInt(Math.round(dexUtxoQty * 10 ** decimals));
+    if (atoms <= 0n) {
+        throw new Error('dexUtxoQty is too small for this token');
+    }
+    return atoms;

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:191modules/ecash-parse/src/parseAlpSwap.ts:191]8;;

Report the buyer's own received atoms, not the first to-token output.

extractAlpSwapLegs assigns buyerToAtoms from the first to-token output
in index order, and tryParseAlpSwap reports that value as toAtoms. The
buyer branch already computes receivedTo by summing to-token outputs
that pay this wallet. If the settle places the seller's to-token change
before the buyer output, or splits the buyer receive across two outputs,
buyerToAtoms is not the amount this wallet received, and the UI and the
notification show the wrong quantity.

Use the wallet-scoped sum instead.


🐛 Proposed fix

         return {
             role: 'buyer',
             fromTokenId,
             toTokenId,
             feeTokenId: fromTokenId,
             fromAtoms: (priceAtoms + totalFeeAtoms).toString(),
-            toAtoms: buyerToAtoms.toString(),
+            toAtoms: receivedTo.toString(),
             feeAtoms: totalFeeAtoms.toString(),
         };

The feeOuts collection depends on the same output-order assumption. If a
buyer from-token change output can precede the first to-token output, it
is counted as a fee and inflates both feeAtoms and fromAtoms. Confirm
that buildSwapOutputs guarantees the documented order for every settle
shape, including exact-out templates and multi-UTXO fills.

────────────────────────────────────────────────────────────────────────

major [Stability & Availability]
→ ]8;;vscode://file//work/cashtab/src/services/alpSwapService.ts:300cashtab/src/services/alpSwapService.ts:300-304]8;;

Add a timeout to the settle request.

fetch has no default timeout. If the alp-dex node accepts the POST and
stalls, settleSwap can remain pending indefinitely. Add an AbortSignal
deadline and treat an abort as “settle status unknown,” not as a failed
swap. Apply the same timeout handling to fetchJson.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/alpSwap.test.ts:320modules/ecash-parse/src/alpSwap.test.ts:320]8;;

Prefix decimalized token quantities below 1 with 0.
decimalizeTokenAmount("9891", 4) returns .9891 because the integer
part is empty. toFormattedTokenQty preserves this input, so seller
notifications show .9891 BUTTER. Return 0.9891 and update the
assertion.

────────────────────────────────────────
Review complete
7 findings ✔

Major 5
Minor 2

26 files reviewed:

  • cashtab/jest.setup.cjs
  • cashtab/src/chronik/index.ts
  • cashtab/src/components/AlpSwap/__tests__/buildPostage.test.ts
  • cashtab/src/components/AlpSwap/__tests__/index.test.tsx
  • cashtab/src/components/AlpSwap/__tests__/rememberSettleTxid.test.ts
  • cashtab/src/components/AlpSwap/buildPostage.ts
  • cashtab/src/components/AlpSwap/index.tsx
  • cashtab/src/components/AlpSwap/rememberSettleTxid.ts
  • cashtab/src/components/AlpSwap/styled.ts
  • cashtab/src/components/App/App.tsx

... and 16 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Still finalizing the server

bytesofman retitled this revision from [Cashtab] Add AlpSwap under Agora (gated by VITE_ENABLE_ALP_SWAP) to [Cashtab] Add AlpSwap under Agora.Wed, Sep 2, 12:51

Raise the XECX→FIRMA exact-in min from constant-product reserves so 14.86 XECX cannot settle as a 0-atom FIRMA receive; keep the Agora Swaps slot reserved when alp-dex is down or the pair is unlisted.

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) The function of good software is to make the complex appear to be simple. - Grady Booch

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:29modules/ecash-parse/src/parseAlpSwap.ts:29-34]8;;

Add an alp-dex-specific settle invariant.

These checks accept an ordinary two-token ALP SEND as an alp-dex settle.
For example, a wallet can spend token B, XEC, and token A; send token A to
an external address; and receive token B change. If token B is the first
token input for the script, this function treats token A as the price leg
and token B as the buyer leg.

Require a protocol-specific settle condition before returning swap
metadata. Validate the alp-dex settlement script or another authenticated
alp-dex invariant. Otherwise, parseTx adds a false alpSwap record and
the UI renders a swap for a normal token transfer.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:935cashtab/src/components/AlpSwap/index.tsx:935-936]8;;

Guard activeQuote.template.feePct before rendering the fee label.

If feePct is absent, the ternary selects undefined;
formatFeePercentLabel then computes undefined * 100 and can render
Fee: NaN%. Reuse the existing numeric guard and fall back to
makerFeePct.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/services/alpSwapService.ts:345cashtab/src/services/alpSwapService.ts:345-356]8;;

dexUtxoAtoms rounds a server-supplied utxoQty silently.

utxoQty arrives from /api/v1/status as a number and may be fractional.
Math.round(dexUtxoQty) then changes the sizing without any signal, and a
value below 0.5 rounds to 0, so the result becomes 0n even though
the guard above rejected only non-positive input. Scale by decimals first,
then round once.


🔢 Proposed fix

-    return BigInt(Math.round(dexUtxoQty)) * 10n ** BigInt(decimals);
+    const atoms = Math.round(dexUtxoQty * 10 ** decimals);
+    if (atoms <= 0) {
+        throw new Error('dexUtxoQty is too small for this token');
+    }
+    return BigInt(atoms);

Note that the existing expectations in alpSwapService.test.ts
(dexUtxoAtoms(2, 1) → 100n) still hold with this change.

────────────────────────────────────────────────────────────────────────

major [Stability & Availability]
→ ]8;;vscode://file//work/cashtab/src/services/alpSwapService.ts:136cashtab/src/services/alpSwapService.ts:136-137]8;;

Bound every alp-dex request and preserve caller cancellation.

fetchJson and settleSwap call fetch without a deadline, so a stalled
lp.alpswap.com request can leave quote or settle awaits pending. Add a
timeout to both requests and combine it with any caller-provided signal.
Do not use init?.signal ?? AbortSignal.timeout(...), because a caller
signal would disable the timeout. Pass a caller signal through
settleSwap as well. Use an AbortController fallback for supported
targets without AbortSignal.timeout.

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30-35]8;;

Add the required Tx props.

TxProps requires userLocale and updateCashtabState. The
destructuring default does not make userLocale optional. Since
tsconfig.json includes src and type-check runs tsc --noEmit, this
render can produce missing-property errors. Pass userLocale="en-US" and
a callback matching UpdateCashtabState.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/wallet/useWallet.ts:438cashtab/src/wallet/useWallet.ts:438-439]8;;

Scope buyer suppression to the active swap

- isAlpSwapBuyerToastSuppressed() is global. With token entries present,
unrelated token transactions can return early during the 15-second window.
Match the pending swap token IDs or remove this fallback.
- parsedTx.alpSwap?.role === 'buyer' short-circuits
consumeAlpSwapSettleTxid(txid). The remembered txid has no expiry and
remains in recentSettleTxids; consume it before the role check or add
cleanup.

────────────────────────────────────────
Review complete
6 findings ✔

Major 3
Minor 3

27 files reviewed:

  • cashtab/src/chronik/index.ts
  • cashtab/src/components/Agora/__tests__/index.test.tsx
  • cashtab/src/components/Agora/index.tsx
  • cashtab/src/components/Agora/styled.ts
  • cashtab/src/components/AlpSwap/__tests__/buildPostage.test.ts
  • cashtab/src/components/AlpSwap/__tests__/index.test.tsx
  • cashtab/src/components/AlpSwap/__tests__/rememberSettleTxid.test.ts
  • cashtab/src/components/AlpSwap/buildPostage.ts
  • cashtab/src/components/AlpSwap/index.tsx
  • cashtab/src/components/AlpSwap/rememberSettleTxid.ts

... and 17 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

Rebase onto master. Abort hung alp-dex quote/settle fetches after 60s (server wallet queue). Show realized fee/total when atom rounding makes the pair feePct a lie at min size.

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) You sanitized... the wrong thing. That's impressively specific.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:67modules/ecash-parse/src/parseAlpSwap.ts:67]8;;

Determine toTokenId without input-order dependence.

Line 67 selects the first script that spends an ALP token and a non-token
input. A buyer can also meet this condition when it adds an XEC input for
postage or transaction fees. Reordering otherwise identical buyer and
seller inputs can then select fromTokenId as toTokenId, which makes
the output scan fail or reverses the legs.

Derive the inventory token from settlement-specific linkage. Reject
ambiguous candidates if that linkage is unavailable. Add a regression test
that reverses buyer and seller input order.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:177modules/ecash-parse/src/parseAlpSwap.ts:177]8;;

Bind the buyer receipt to buyerToScript.

Line 177 accepts any wallet-owned toTokenId output. However, the result
reports buyerToAtoms, which is the first to-token output. If a later
to-token output belongs to the wallet, this function can classify the
wallet as the buyer while reporting the amount sent to a different script.

Check buyerToScript with scriptIsWallet before returning the buyer
role.


Proposed fix

-    const { fromTokenId, toTokenId, priceAtoms, feeOuts, buyerToAtoms } = legs;
+    const {
+        fromTokenId,
+        toTokenId,
+        priceAtoms,
+        feeOuts,
+        buyerToAtoms,
+        buyerToScript,
+    } = legs;
...
     if (walletSpentFrom) {
-        let receivedTo = 0n;
-        for (const output of tx.outputs) {
-            if (
-                typeof output.token !== 'undefined' &&
-                output.token.tokenId === toTokenId &&
-                scriptIsWallet(output.outputScript, hashes)
-            ) {
-                receivedTo += output.token.atoms;
-            }
-        }
-        if (receivedTo === 0n) {
+        if (!scriptIsWallet(buyerToScript, hashes)) {
             return undefined;
         }

────────────────────────────────────────────────────────────────────────

major [Data Integrity & Integration]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/buildPostage.ts:68cashtab/src/components/AlpSwap/buildPostage.ts:68-76]8;;

Sum all script-less receiving outputs instead of using the first one.

outputs comes from the alp-dex /swap template response. The code
pushes every template output into paymentOutputs, but it derives
receivingTokenAtoms from only the first script-less receiving output. If
a template returns more than one script-less output for
receivingTokenId, the tx requires more receiving-token atoms than the
value sent to settleSwap as atoms. The maker then under-fuels the tx
and settle fails.

Sum the atoms of all matching outputs.





🐛 Proposed fix

-    const toTokenOutput = outputs.find(
-        output =>
-            output.tokenId === receivingTokenId &&
-            typeof output.script === 'undefined',
-    );
-    if (!toTokenOutput) {
-        throw new Error('Missing receiving token output in swap quote');
-    }
-    const receivingTokenAtoms = BigInt(toTokenOutput.atoms);
+    const toTokenOutputs = outputs.filter(
+        output =>
+            output.tokenId === receivingTokenId &&
+            typeof output.script === 'undefined',
+    );
+    if (toTokenOutputs.length === 0) {
+        throw new Error('Missing receiving token output in swap quote');
+    }
+    const receivingTokenAtoms = toTokenOutputs.reduce(
+        (total, output) => total + BigInt(output.atoms),
+        0n,
+    );

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/components/Home/Tx/index.tsx:143cashtab/src/components/Home/Tx/index.tsx:143-149]8;;

Keep the swap amount when the token is not cached.

On a cache miss this fallback sets label to only the token-ID preview.
The rendered row then drops the amount, for example `Swapped 1.0000 BUTTER
→ 4b7ac...7056c · Fee 0.0109 BUTTER`. A cache miss is realistic for the
counter-side token of an alp-dex settle. The notification path in
getTxNotificationMsg keeps the raw atoms in the same situation, so the
two surfaces disagree. Include the atom amount in the fallback label.





🐛 Proposed fix

         const cachedTokenInfo = cashtabCache.tokens.get(tokenId);
         if (typeof cachedTokenInfo === 'undefined') {
             return {
-                label: previewTokenId(tokenId),
+                label: `${atoms} ${previewTokenId(tokenId)}`,
                 ticker: '',
                 icon: <TokenIcon size={32} tokenId={tokenId} />,
             };
         }

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/getTxNotificationMsg.ts:93modules/ecash-parse/src/getTxNotificationMsg.ts:93-103]8;;

Do not print raw atoms as a token quantity.

When genesisInfo does not match tokenId, this fallback returns the raw
atom count as the quantity. The notification then states a wrong amount.
The seller case in alpSwap.test.ts shows it: the wallet sold 0.98 Guns,
and the message reads Sold 98 4b7ac...7056c. Only one genesisInfo
reaches this function, so the counter-side leg of every swap hits this
path.

The existing token path in this file (line 283) omits the quantity when
genesisInfo is unavailable. Apply the same rule here, and update the
asserted string in alpSwap.test.ts.





🐛 Proposed fix

         const qty =
             typeof genesisInfo !== 'undefined' &&
             parsedTokenEntries[0]?.tokenId === tokenId
                 ? `${toFormattedTokenQty(
                       decimalizeTokenAmount(
                           atoms,
                           genesisInfo.decimals as SlpDecimals,
                       ),
                       userLocale,
                   )} `
-                : `${atoms} `;
+                : '';

────────────────────────────────────────────────────────────────────────

major [Data Integrity & Integration]
→ ]8;;vscode://file//work/cashtab/src/services/alpSwapService.ts:320cashtab/src/services/alpSwapService.ts:320-327]8;;

Treat settle timeouts as indeterminate

settleSwap() converts AbortSignal.timeout() into `AlpSwap request
timed out. AlpSwap.handleSwap` then shows a failure toast, keeps the
active quote, and re-enables Swap. The alp-dex route can broadcast
before the response reaches Cashtab, so this state can cause an immediate
retry and duplicate payment. Preserve an indeterminate state for timeouts
and block retry until the outcome is reconciled.

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30-44]8;;

Pass the required Tx props in this test. TxProps requires
updateCashtabState and userLocale. The test is included by
cashtab/tsconfig.json, and type-check runs tsc --noEmit, so this JSX
can fail the type check. Add updateCashtabState={jest.fn()} and
userLocale="en-US"; the destructuring default does not make userLocale
optional.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/wallet/useWallet.ts:435cashtab/src/wallet/useWallet.ts:435-442]8;;

Consume the remembered txid before the role check.

isAlpSwapBuyerToastSuppressed() is global during the 15-second
suppression window. A non-swap token receive has no parsedTx.alpSwap,
but parsedTokenEntries.length > 0 makes this branch return before the
toast path. Restrict suppression to the active swap or its settle
identity.

Because the buyer-role clause short-circuits
consumeAlpSwapSettleTxid(txid), the matching entry remains in
recentSettleTxids. Consume it before evaluating the role, or add
explicit eviction.

────────────────────────────────────────
Review complete
8 findings ✔

Major 5
Minor 3

27 files reviewed:

  • cashtab/src/chronik/index.ts
  • cashtab/src/components/Agora/__tests__/index.test.tsx
  • cashtab/src/components/Agora/index.tsx
  • cashtab/src/components/Agora/styled.ts
  • cashtab/src/components/AlpSwap/__tests__/buildPostage.test.ts
  • cashtab/src/components/AlpSwap/__tests__/index.test.tsx
  • cashtab/src/components/AlpSwap/__tests__/rememberSettleTxid.test.ts
  • cashtab/src/components/AlpSwap/buildPostage.ts
  • cashtab/src/components/AlpSwap/index.tsx
  • cashtab/src/components/AlpSwap/rememberSettleTxid.ts

... and 17 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

cashtab/src/config/alpSwap.ts
25 ↗(On Diff #61118)

should this be an array ? For now there is a single LP but this can/should change in the future

cashtab/src/services/alpSwapService.ts
360 ↗(On Diff #61118)

does this work with all locales ?

modules/ecash-parse/src/alpSwapFees.ts
11 ↗(On Diff #61118)

I think it's a bad idea to hardcode addresses here (also most of this is exported but unused). Better let the callsite pass it down if there is no way to avoid it entirely

bytesofman added inline comments.
cashtab/src/services/alpSwapService.ts
360 ↗(On Diff #61118)

Made some generalized updates to this function so it is correct for the actual decimals of the given pair. In this case we are okay with locales as we are taking a JS number and (another javascript TIL for me) toFixed always uses "."

modules/ecash-parse/src/alpSwapFees.ts
11 ↗(On Diff #61118)

In this case, it's part of a set that is used for parsing the fee of swap txs in history.

Cashtab does not need a fee address to build or settle a swap, it gets the tx shape from the server.

tldr I think hardcoding known addresses is okay for history parsing. It doesn't impact tx formation, the shape itself could change in the future, and for now it's the easiest way to parse swap txs since there really is just the one server. This also helps tx parsing from the perspective of a Cashtab user with a fee wallet in Cashtab.

I can see it changing going forward as the tx shape settles down / as we need to learn how to parse across potentially multiple servers and multiple addresses. I think this is an okay MVP.

bytesofman marked 2 inline comments as done.

Rebase on latest master. Experimental notice on Agora Swaps + AlpSwap. Config baseUrls array (first URL used). roundSwapQty floors to the token genesis decimals for from=/to= query params (toFixed always prints ASCII '.' — not locale).

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) Estimate the order of your algorithms. Get a feel for how long things are likely to take before you write code.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:67modules/ecash-parse/src/parseAlpSwap.ts:67]8;;

Reject ambiguous inventory scripts before assigning toTokenId.

Line 67 selects the first matching input script. A buyer can provide both
a from-token input and a non-token input. If that input occurs before the
seller inventory input, this assigns the buyer token as toTokenId. The
parser then reverses the swap legs and can emit an incorrect seller
classification.

Continue scanning for distinct candidate token IDs. Return null when
more than one token ID has token and postage inputs.






Proposed fix

 let toTokenId: string | undefined;
 for (const agg of byScript.values()) {
     if (typeof agg.tokenId === 'string' && agg.hasNonToken) {
+        if (
+            typeof toTokenId !== 'undefined' &&
+            toTokenId !== agg.tokenId
+        ) {
+            return null;
+        }
         toTokenId = agg.tokenId;
-        break;
     }
 }

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:857cashtab/src/components/AlpSwap/index.tsx:857]8;;

Clear the buyer-toast suppression timer on unmount.

setAlpSwapBuyerToastSuppressed writes module-global state that
useWallet reads. Line 857 sets it to true, and only the 15-second
window.setTimeout on Lines 897-899 clears it after a successful swap.
The unmount cleanup on Lines 910-916 clears the debounce timer only.

If the user leaves the AlpSwap screen right after a swap, the flag stays
true for the remainder of the 15 seconds and suppresses unrelated wallet
toasts on other screens. Store the timeout id in a ref and clear the flag
in the unmount cleanup.





Also applies to: 897-899

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:465cashtab/src/components/AlpSwap/index.tsx:465-469]8;;

Move the stale-request guard before the first setMakerFeePct write.

runQuote writes setMakerFeePct(spot.feePct) on Line 468 before it
checks requestId !== quoteRequestId.current on Line 477. If the user
changes the amount while a quote is in flight, the older request still
resolves its spot call and overwrites makerFeePct. The newer request
then computes minExactInQty in handleAmountInput with a fee percent
from the superseded request.

Check the request id right after the fetchSpotPrice await, before any
state write.


♻️ Proposed fix

                 let feePct = makerFeePct;
                 const spot = await fetchSpotPrice(fromTokenId, toTokenId);
+                if (requestId !== quoteRequestId.current) {
+                    return;
+                }
                 if (typeof spot.feePct === 'number') {
                     feePct = spot.feePct;
                     setMakerFeePct(spot.feePct);
                 }

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/styled.ts:102cashtab/src/components/AlpSwap/styled.ts:102]8;;

Use props.theme.accent for both hover styles.

CashtabTheme defines accent and nested buttons.primary, but no
top-level primary. Thus, props.theme.primary produces no usable color
value.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/getTxNotificationMsg.ts:93modules/ecash-parse/src/getTxNotificationMsg.ts:93-103]8;;

Do not present raw atoms as a token quantity.

genesisInfo describes only one token, so on a two-token swap one leg
always takes the fallback branch. That branch prints the atom count as if
it were a human quantity. The seller case in alpSwap.test.ts shows the
result: Sold 98 4b7ac...7056c for .9891 BUTTER, where 98 atoms of a
2-decimal token is 0.98. The user reads a value that is wrong by a factor
of 10^decimals.

Omit the quantity when the decimals are unknown, so the notification stays
truthful. A follow-up change can pass genesis info for both tokenIds.


🐛 Proposed fix

         const qty =
             typeof genesisInfo !== 'undefined' &&
             parsedTokenEntries[0]?.tokenId === tokenId
                 ? `${toFormattedTokenQty(
                       decimalizeTokenAmount(
                           atoms,
                           genesisInfo.decimals as SlpDecimals,
                       ),
                       userLocale,
                   )} `
-                : `${atoms} `;
+                : '';

Update the corresponding assertion in
modules/ecash-parse/src/alpSwap.test.ts.

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30-44]8;;

Add the required Tx props.

cashtab/tsconfig.json includes all files under src, and TxProps
requires updateCashtabState and userLocale. Add userLocale="en-US"
and an updateCashtabState stub that returns Promise.

────────────────────────────────────────────────────────────────────────

minor [Stability & Availability]
→ ]8;;vscode://file//work/cashtab/src/wallet/useWallet.ts:438cashtab/src/wallet/useWallet.ts:438-439]8;;

Scope AlpSwap buyer-toast suppression to the pending swap.

After a successful settleSwap, suppression remains active for 15
seconds. During this interval, useWallet suppresses every transaction
with parsed token entries without checking its txid or token IDs. An
unrelated incoming token transaction can therefore lose its websocket
toast. Match suppression to the pending swap before returning true.

────────────────────────────────────────
Review complete
7 findings ✔

Major 1
Minor 6

28 files reviewed:

  • cashtab/src/chronik/index.ts
  • cashtab/src/components/Agora/__tests__/index.test.tsx
  • cashtab/src/components/Agora/index.tsx
  • cashtab/src/components/Agora/styled.ts
  • cashtab/src/components/AlpSwap/ExperimentalNotice.tsx
  • cashtab/src/components/AlpSwap/__tests__/buildPostage.test.ts
  • cashtab/src/components/AlpSwap/__tests__/index.test.tsx
  • cashtab/src/components/AlpSwap/__tests__/rememberSettleTxid.test.ts
  • cashtab/src/components/AlpSwap/buildPostage.ts
  • cashtab/src/components/AlpSwap/index.tsx

... and 18 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

cashtab/src/config/alpSwap.ts
48 ↗(On Diff #61157)

should this (and defaultMakerFeePct) be part of the LP params (and so in the array with the URL) ?

bytesofman added inline comments.
cashtab/src/config/alpSwap.ts
48 ↗(On Diff #61157)

No, we should just not have this in Cashtab at all. This config needs to come from the server. If we can't get it from the server, the service is unavailable. will update.

bytesofman marked an inline comment as done.

Drop local dexUtxoQty and defaultMakerFeePct fallbacks. Require feePct and sell-side utxoQty from /status to quote or show Agora Swaps; otherwise treat AlpSwap as unavailable.

Build Bitcoin ABC Diffs / Diff Testing (ai-review) passed.
CodeRabbit Review

Diff : committed changes only
Compare : HEAD → master
Directory : work
────────────────────────────────────────

(\(\
(• .•) This is like dependency injection, except the dependency is suffering.

────────────────────────────────────────────────────────────────────────

major [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:66modules/ecash-parse/src/parseAlpSwap.ts:66-68]8;;

Do not select the first mixed-input script as the seller.

The buyer can also spend an ALP token input and a non-token input from one
script. If that input appears first, this assigns the buyer's
fromTokenId to toTokenId. The output scan then treats the price output
as the buyer output and returns null because it never finds
priceAtoms.

Evaluate all candidate inventory token IDs against the output order, or
otherwise identify the seller without depending on input order.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/parseAlpSwap.ts:203modules/ecash-parse/src/parseAlpSwap.ts:203-207]8;;

Validate maker-fee destinations before assigning makerFee.

Any non-platform output in feeOuts is currently reported as a maker fee.
A normal ALP swap with an extra from-token recipient can therefore create
a false makerFee classification for that recipient.

Match maker-fee outputs against the configured maker-fee destination set
before adding makerAtoms.

────────────────────────────────────────────────────────────────────────

major [Stability & Availability]
→ ]8;;vscode://file//work/cashtab/src/components/AlpSwap/index.tsx:174cashtab/src/components/AlpSwap/index.tsx:174-182]8;;

Move the context guards below all hooks.

/alpswap renders ` directly. If WalletContext` updates while the route
remains mounted, the guards can skip the later hooks on one render and
execute them on the next. React then throws `Rendered more hooks than
during the previous render`. Call all hooks unconditionally, then return
null, or split the guard and loaded screen into separate components.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/cashtab/src/wallet/useWallet.ts:438cashtab/src/wallet/useWallet.ts:438-439]8;;

Narrow the in-flight suppression to the swap tokens.

isAlpSwapBuyerToastSuppressed() is a global flag. Combined with
parsedTx.parsedTokenEntries.length > 0, it drops the notification for
every incoming token transaction while a settle is in flight, including
transfers unrelated to the swap. The first two conditions (`role ===
'buyer' and consumeAlpSwapSettleTxid(txid)`) are already precise.

Consider matching the in-flight token ids as well, so unrelated token
notifications still reach the user.

────────────────────────────────────────────────────────────────────────

minor [Functional Correctness]
→ ]8;;vscode://file//work/modules/ecash-parse/src/getTxNotificationMsg.ts:93modules/ecash-parse/src/getTxNotificationMsg.ts:93-103]8;;

Do not print raw atoms as a quantity.

genesisInfo describes only parsedTokenEntries[0], so the other leg of
a swap always takes the fallback. The fallback returns undecimalized
atoms, so a seller toast reads Sold 98 4b7ac...7056c for 0.98 tokens of
a 2-decimal token. The existing token path at lines 283-292 avoids this by
rendering an empty quantity when genesisInfo is missing. Apply the same
rule here.


🔧 Proposed fix

         const qty =
             typeof genesisInfo !== 'undefined' &&
             parsedTokenEntries[0]?.tokenId === tokenId
                 ? `${toFormattedTokenQty(
                       decimalizeTokenAmount(
                           atoms,
                           genesisInfo.decimals as SlpDecimals,
                       ),
                       userLocale,
                   )} `
-                : `${atoms} `;
+                : '';

The Sold ... for ... assertion in
modules/ecash-parse/src/alpSwap.test.ts line 320 needs the matching
update.

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30cashtab/src/components/Home/Tx/__tests__/alpSwapRendering.test.tsx:30-44]8;;

Add the required Tx props.

cashtab/type-check runs tsc --noEmit, and cashtab/tsconfig.json
includes all files under src, including __tests__. TxProps requires
updateCashtabState and userLocale; add both props to this render.

────────────────────────────────────────────────────────────────────────

minor [Maintainability & Code Quality]
→ ]8;;vscode://file//work/cashtab/src/services/__tests__/alpSwapService.test.ts:257cashtab/src/services/__tests__/alpSwapService.test.ts:257-259]8;;

Fix the malformed status fixture type.

type-check runs tsc --noEmit, and tsconfig.json includes all of
src, including this test. StatusResponse.tradedPairs and
tradedTokens are required arrays, so this argument can fail type
checking. Add both properties as undefined and cast the fixture through
unknown to the expected Pick type. This preserves the
pairsFromStatus ?? [] runtime coverage.

────────────────────────────────────────
Review complete
7 findings ✔

Major 2
Minor 5

28 files reviewed:

  • cashtab/src/chronik/index.ts
  • cashtab/src/components/Agora/__tests__/index.test.tsx
  • cashtab/src/components/Agora/index.tsx
  • cashtab/src/components/Agora/styled.ts
  • cashtab/src/components/AlpSwap/ExperimentalNotice.tsx
  • cashtab/src/components/AlpSwap/__tests__/buildPostage.test.ts
  • cashtab/src/components/AlpSwap/__tests__/index.test.tsx
  • cashtab/src/components/AlpSwap/__tests__/rememberSettleTxid.test.ts
  • cashtab/src/components/AlpSwap/buildPostage.ts
  • cashtab/src/components/AlpSwap/index.tsx

... and 18 more files
────────────────────────────────────────

Print all AI prompts: coderabbit review --show-prompts

This revision is now accepted and ready to land.Fri, Sep 4, 11:29
This revision was automatically updated to reflect the committed changes.