Skip to content

feat: CashToken USD pricing and Cauldron DEX swaps - #3

Merged
joemarct merged 8 commits into
masterfrom
feat/token-pricing-cauldron-swap
Sep 3, 2026
Merged

joemarct merged 8 commits into
masterfrom
feat/token-pricing-cauldron-swap

Conversation

@joemarct

@joemarct joemarct commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

Adds USD pricing for CashTokens and a Cauldron DEX swap flow to the Paytaca CLI, plus CI/docs/test housekeeping.

Features

CashToken USD pricing

  • New src/utils/prices.ts — client for the Watchtower /api/asset-prices/ endpoint (batched, mirrors paytaca-app's market price handling).
  • paytaca token list and paytaca token info now show USD values per token and a portfolio total.
  • New paytaca token price <category> [amount] command to look up a token's USD price and value an amount (defaults to wallet balance).
  • Unit tests in src/utils/prices.test.ts.

Cauldron DEX swap

  • New paytaca swap <tokenId> <amount> command to trade CashTokens against BCH through Cauldron liquidity pools.

    Two directions:

    • sell (default) — sell tokens for BCH
    • buy — spend BCH to buy tokens

    Shows a quote (rate, amounts, transfer fee), asks for confirmation, then builds, signs, and broadcasts the trade transaction.

  • Internals under src/wallet/cauldron/:

    • api.ts — Cauldron / riften indexer REST client (global fetch, Node 20+).
    • pools.ts (+ tests) — active-pool discovery and pricing math.
    • swap.ts — swap estimation/execution orchestration.
    • transact.ts — trade transaction construction via @cashlab/cauldron ExchangeLab (libauth templates).
  • src/wallet/bch.ts — new getUtxos() helper for token/UTXO selection.

  • New dependency: @cashlab/cauldron.

Housekeeping

  • src/nostr/store.test.ts — reset rejected mocks after the "handles errors gracefully" test so receding failures don't leak stderr noise into other tests.
  • .github/workflows/opencode.yml — bump opencode GitHub workflow model to opencode/kimi-k2.6.
  • AGENTS.md — added, adapted from paytaca-app for this CLI (security review checklist, OS-keychain storage model, CLI conventions).

Docs

  • README updated for the new token price and swap commands, architecture summary, and dependencies.

Test

  • npm test — 88 tests pass across 5 files, clean stderr.
  • npm run build (tsc) — clean.

Add src/utils/prices.ts (fetchAssetPrices, getUsdPerToken, getBchUsdPrice,
tokenAmountToUsd, formatUsd) mirroring paytaca-app's market price handling.
Show USD value in `token list` and `token info`, and add a new
`token price <category> [amount]` command that prices a given amount
(defaults to wallet balance). Unpriced tokens fall back gracefully.
Generic token <-> BCH swap via the Cauldron DEX (mirrors paytaca-app):
- fetch active pools + token data from the riften indexer
- best-rate trade quoting with attemptTrade (ExchangeLab)
- signed trade tx built via createTradeTx and broadcast via watchtower
- new 'paytaca swap' command (sell token for BCH / buy token with BCH)
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Build and tests pass (88 tests, clean stderr). Here is my review:

Security & Correctness

🟡 Medium: swap command --chipnet is unsupported but accepted

File: src/commands/swap.ts (lines 62, 65), src/wallet/cauldron/api.ts (line 8)

The Cauldron indexer URL is hardcoded to https://indexer.riften.net (mainnet only). The swap command accepts --chipnet, but the quote and execution will query mainnet pools while using a chipnet BCH wallet. This can produce invalid transactions or unexpected behavior.

Fix: Reject --chipnet for swap until a chipnet Cauldron endpoint exists, or add a chipnet base URL and switch on isChipnet.

// In registerSwapCommand
if (opts.chipnet) {
  console.log(chalk.red('\nError: Cauldron swaps are not yet supported on chipnet.\n'))
  process.exit(1)
}

🟡 Low: createInputAndOutput assumes at least one spendable coin exists

File: src/wallet/cauldron/transact.ts (line 108)

const privateKey = (spendableCoins[0] as SpendableCoinP2PKH<Output>).key

If the wallet has no UTXOs, this throws a runtime error rather than a graceful "insufficient funds" message.

Fix: Add a guard:

if (spendableCoins.length === 0) throw new Error('No UTXOs available to fund the trade')

🟡 Low: attemptTrade assumes pools is non-empty

File: src/wallet/cauldron/transact.ts (line 65)

let supplyTokenId = pools[0]!.output.token.token_id

If called with an empty pools array, it crashes. swap.ts checks this upstream, but attemptTrade is exported and could be used elsewhere.

Fix: Add if (pools.length === 0) throw new Error('No pools provided') at the top of the function.

🟡 Low: Unescaped token category in getUtxos URL

File: src/wallet/bch.ts (line 293)

url += opts.category + '/'

While current callers validate category as 64-char hex, getUtxos is a public method. If called with an unvalidated string containing path characters, it could alter the request path.

Fix: Use encodeURIComponent(opts.category) or validate inside the method.

Style & Maintainability

🟢 Minor: Typo in variable name

File: src/wallet/cauldron/pools.ts (line 127)

multiplerDecimalsmultiplierDecimals

🟢 Minor: tokenData non-null assertion unnecessary

File: src/commands/swap.ts (line 101)

The null check on line 97 already returns early, so tokenData! on line 101 is redundant (and flagged by tsc). Remove the !.

Positive Findings

  • Quote freshness: executeSwap re-fetches the quote at execution time (not reusing the displayed quote), which prevents stale-price trades. Good.
  • Secret handling: Mnemonic and private keys are held only in memory, never logged, serialized, or persisted. watchtowerUtxosToSpendableCoins caches keys in a local Map scoped to the function — acceptable.
  • Input validation: Token IDs are consistently validated as 64-char hex across all commands.
  • Test hygiene: The store.test.ts mock reset after the error test is correct and prevents stderr leakage into subsequent tests.
  • Architecture: The separation of concerns (api.tspools.tsswap.tstransact.ts) mirrors the paytaca-app structure cleanly and is easy to follow.

Verdict

Approve with minor fixes. The functional code is solid and well-tested. The only must-fix before merge is the --chipnet acceptance in swap (disable it or add chipnet support). The other issues (empty UTXO/pool guards, URL encoding, typo) are quick defensive improvements worth making in the same PR.

New%20session%20-%202026-09-03T08%3A58%3A01.993Z
opencode session  |  github run

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Security & Correctness Review

Build & Test Verification

  • Build: tsc passes cleanly.
  • Tests: 88 tests pass across 5 files, clean stderr.

Fix Verification (from previous review)

All previously flagged medium/low issues have been addressed:

Issue Status File
--chipnet rejected in swap command ✅ Fixed src/commands/swap.ts lines 64-69
createInputAndOutput empty UTXO guard ✅ Fixed src/wallet/cauldron/transact.ts line 110
attemptTrade empty pool guard ✅ Fixed src/wallet/cauldron/transact.ts line 65
getUtxos URL encoding ✅ Fixed src/wallet/bch.ts line 294 (encodeURIComponent)
tokenData non-null assertion ✅ Fixed src/commands/swap.ts line 107 (no ! used)

New / Remaining Findings

🟢 Minor: Typo in variable name

File: src/wallet/cauldron/pools.ts (line 127)

multiplerDecimalsmultiplierDecimals. Purely cosmetic but worth fixing.

🟢 Minor: Missing "insufficient BCH" guard in createInputAndOutput

File: src/wallet/cauldron/transact.ts (lines 211-217)

After consuming BCH UTXOs to cover satoshisToSupply, the function doesn't verify that remainingSats <= 0n. If the wallet lacks enough BCH, the transaction proceeds to ExchangeLab.createTradeTx and fails with a less informative error. Adding an explicit guard improves UX:

if (remainingSats > 0n) {
  throw new Error('Insufficient BCH to fund the trade and transaction fees')
}

🟢 Minor: remainingSats variable type could be more explicit

Not an issue, but the code is sound.

Positive Findings

  • Quote freshness preserved: executeSwap re-fetches the quote at execution time rather than reusing the displayed quote, preventing stale-price trades.
  • Secret handling: Mnemonic and private keys are held only in memory, never logged or persisted. watchtowerUtxosToSpendableCoins caches keys in a local Map scoped to the function.
  • Input validation: Token IDs are consistently validated as 64-char hex across all commands.
  • Test hygiene: store.test.ts mock reset after the error test is correct and prevents stderr leakage.
  • Architecture: Clean separation of concerns (api.tspools.tsswap.tstransact.ts).

Verdict

Approve with the typo fix. The functional code is solid, well-tested, and the previous review's security issues have all been resolved. The only change worth making before merge is correcting multiplerDecimalsmultiplierDecimals in src/wallet/cauldron/pools.ts.

New%20session%20-%202026-09-03T09%3A08%3A02.700Z
opencode session  |  github run

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Security & Correctness Review

Build & Test Verification

  • Build: tsc passes cleanly.
  • Tests: 88 tests pass across 5 files, clean stderr.

Fix Verification (from previous reviews)

All previously flagged issues have been addressed:

Issue Status File
--chipnet rejected in swap command ✅ Fixed src/commands/swap.ts
createInputAndOutput empty UTXO guard ✅ Fixed src/wallet/cauldron/transact.ts
attemptTrade empty pool guard ✅ Fixed src/wallet/cauldron/transact.ts
getUtxos URL encoding ✅ Fixed src/wallet/bch.ts
tokenData non-null assertion ✅ Fixed src/commands/swap.ts
multiplerDecimals typo ✅ Fixed src/wallet/cauldron/pools.ts
Missing "insufficient BCH" guard ✅ Fixed src/wallet/cauldron/transact.ts

New Findings

🟡 Medium: Missing insufficient tokens guard in createInputAndOutput for sell trades

File: src/wallet/cauldron/transact.ts (lines 139–163)

When selling tokens, the function loops through tokenCoins to cover tokensToSupply, but if the wallet holds fewer tokens than required, the loop exits with remainingTokens still positive. The function then proceeds to ExchangeLab.createTradeTx with insufficient token inputs, producing an invalid transaction.

Fix: Add a guard after the token consumption loop:

if (remainingTokens > 0n) {
  throw new Error('Insufficient tokens to fund the trade')
}

🟢 Minor: buildSignedTradeTx omits network when constructing LibauthHDWallet

File: src/wallet/cauldron/swap.ts (line 216)

const hdWallet = new LibauthHDWallet(mnemonic, derivationPath)

The constructor defaults to mainnet, but bchWallet.isChipnet is available. Since swap currently rejects chipnet this is latent, but as an exported utility it should be correct:

const hdWallet = new LibauthHDWallet(mnemonic, derivationPath, bchWallet.isChipnet ? 'chipnet' : 'mainnet')

Positive Findings

  • Quote freshness: executeSwap re-fetches the quote at execution time rather than reusing the displayed quote, preventing stale-price trades.
  • Secret handling: Mnemonic and private keys are held only in memory, never logged or persisted. watchtowerUtxosToSpendableCoins caches keys in a local Map scoped to the function.
  • Input validation: Token IDs are consistently validated as 64-char hex across all commands.
  • Test hygiene: store.test.ts mock reset after the error test is correct and prevents stderr leakage.
  • Architecture: Clean separation of concerns (api.tspools.tsswap.tstransact.ts).

Verdict

Approve with the token sufficiency guard fix. The functional code is solid, well-tested, and the previous review's security issues have all been resolved. The only remaining change worth making before merge is adding the remainingTokens > 0n guard in src/wallet/cauldron/transact.ts for sell trades.

New%20session%20-%202026-09-03T09%3A27%3A58.856Z
opencode session  |  github run

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Security & Correctness Review

Build & Test Verification

  • Build: tsc passes cleanly.
  • Tests: 88 tests pass across 5 files, clean stderr.

Fix Verification (from previous reviews)

All previously flagged issues have been addressed and verified in the current code:

Issue Status File
--chipnet rejected in swap command ✅ Fixed src/commands/swap.ts lines 64–69
createInputAndOutput empty UTXO guard ✅ Fixed src/wallet/cauldron/transact.ts line 110
attemptTrade empty pool guard ✅ Fixed src/wallet/cauldron/transact.ts line 65
getUtxos URL encoding ✅ Fixed src/wallet/bch.ts line 294
tokenData non-null assertion ✅ Fixed src/commands/swap.ts line 107 (no !)
multiplerDecimals typo ✅ Fixed src/wallet/cauldron/pools.ts line 127
Missing "insufficient BCH" guard ✅ Fixed src/wallet/cauldron/transact.ts lines 223–225
Missing insufficient tokens guard ✅ Fixed src/wallet/cauldron/transact.ts lines 151–153
buildSignedTradeTx network argument ✅ Fixed src/wallet/cauldron/swap.ts lines 216–220

New / Remaining Findings

🟢 Minor: Defensive check against negative price responses

File: src/utils/prices.ts (line 107)

if (!isFinite(raw) || raw === 0) continue

If the API returns a negative price_value, this guard passes and would produce negative USD prices/values. Changing raw === 0 to raw <= 0 is a safer defensive check:

if (!isFinite(raw) || raw <= 0) continue

Positive Findings

  • Quote freshness preserved: executeSwap re-fetches the quote at execution time rather than reusing the displayed quote, preventing stale-price trades.
  • Secret handling: Mnemonic and private keys are held only in memory, never logged or persisted. watchtowerUtxosToSpendableCoins caches keys in a local Map scoped to the function.
  • Input validation: Token IDs are consistently validated as 64-char hex across all commands.
  • Test hygiene: store.test.ts mock reset after the error test is correct and prevents stderr leakage.
  • Architecture: Clean separation of concerns (api.tspools.tsswap.tstransact.ts).

Verdict

Approve. The functional code is solid, well-tested, and all prior security issues have been resolved. The only remaining suggestion is the trivial raw <= 0 guard in src/utils/prices.ts, which is safe to fix in a follow-up or as part of this merge.

New%20session%20-%202026-09-03T09%3A40%3A39.118Z
opencode session  |  github run

@joemarct
joemarct merged commit 8015db8 into master Sep 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant