Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/opencode.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/kimi-k2.5
model: opencode/kimi-k2.6
use_github_token: true
60 changes: 60 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# AI Agent Instructions

## General Rules

- Do NOT auto-commit changes — ask before committing.
- Do NOT write new files unless explicitly asked; prefer editing existing code.
- Do NOT add comments to code unless requested.
- Keep explanations and responses concise.
- Follow existing code style and patterns in the codebase (TypeScript, ESM with `.js` import suffixes, 2-space indent).
- Prefer the dedicated tools (Read, Grep, Glob, Edit, Write) over bash for file operations.

## Environment

- This is a **command-line** tool for **desktop** (macOS, Linux, Windows) — there is no UI, no frontend code, no mobile code.
- The binary is `paytaca` (Commander.js), invoked as `node bin/paytaca.js` or the global `paytaca` command.
- `--chipnet` selects the testnet network; everything defaults to mainnet.

## Tech Stack

- **Language:** TypeScript (ES modules, compiled with `tsc` to `dist/`; imports use `.js` suffix).
- **CLI framework:** Commander.js
- **Key derivation:** `@bitauth/libauth` (pinned to 2.0.0-alpha.8) + `bip39` (BIP44: `m/44'/145'/0'` for BCH; `m/44'/1237'/0'/0/0` for Nostr).
- **Transactions:** `watchtower-cash-js` (UTXO fetching, tx building/signing/broadcasting).
- **Secret storage:** OS-native keychain via `@napi-rs/keyring` (prebuilt Rust binaries, no node-gyp).
- **Testing:** Vitest (`npm test` → `vitest run`). There is **no lint script** — run `npm run build` (tsc) to type-check.
- **Dev scripts:** `npm run build` (tsc), `npm run dev` (tsc --watch), `npm test` (vitest run).

## Conventions

- Run `npm run build` before signaling completion — it type-checks the whole project.
- Run `npm test` when changes affect tested code (tests live alongside sources as `*.test.ts`).
- Keep `console.error` out of test output — spy/mock expected error paths in tests or they leak noise into other tests.

## Key Storage (differs from paytaca-app)

- Secrets are stored in the **OS keychain**, NOT in app storage or capacitor plugins:
- macOS — Keychain; Linux — GNOME Keyring / KWallet; Windows — Credential Manager.
(Backed by `@napi-rs/keyring`, service name `paytaca-cli`.)
- Key names mirror paytaca-app for continuity:
- Mnemonic: `mn_{walletHash}`; Active wallet: `active_wallet`.
(`src/storage/keychain.ts`, `src/wallet/index.ts`)
- Nostr chat keys are **not stored** — they are re-derived from the wallet mnemonic at runtime via HD path `m/44'/1237'/0'/0/0` and held only in memory during a chat session (`src/nostr/keys.ts`).
- Chat state (contacts, rooms, messages) is persisted as JSON at `~/.paytaca/chat-state.json` (mode 0600) — this is non-secret metadata only;never write keys to it.

## Automated Code Review (Security)

Every pull request undergoes an AI-assisted security review. Pay particular attention to changes that could affect:

- Entropy and recovery-phrase generation (`bip39`, `src/wallet/index.ts`)
- Private-key derivation (`src/wallet/keys.ts`, `src/nostr/keys.ts`) — HD paths, WIF encoding, in-memory key material
- Recovery-phrase storage and retrieval (`src/storage/keychain.ts`, `src/wallet/index.ts`) — keychain-only, no plaintext files, no logging of phrases, no persistence beyond the keychain aside from `~/.paytaca/chat-state.json` metadata
- Encryption and signing (transactions via `watchtower-cash-js`/libauth, x402 payment signing, Nostr event signing in memory)
- Transaction construction (UTXO selection, change addresses, token-aware z-prefix addresses, `src/wallet/cauldron/*`)
- Smart contracts / token pools (Cauldron DEX swaps in `src/wallet/cauldron/*`)
- Secret material in memory — derived keys and mnemonic must not be logged, printed, or serialized into persisted state or CLI output except where explicitly requested (e.g. `wallet export` shows the seed phrase with a warning)
- Migration of sensitive data — no legacy key schemes exist in the CLI; do not introduce any
- External interfaces that could influence signing behavior — relay URLs (`kind 10050`), x402 `PaymentRequired` payloads, UTXO data from watchtower, cauldron pool data: never sign data without validating it is well-formed and intended
- CLI output handling — avoid echoing secrets into logs, `--json` output, or error messages (sanitize addresses/hex in errors).

For findings, reference the specific file and line, describe the risk and severity, and suggest a concrete fix.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ paytaca address list --token # List token-aware z-prefix addresses
### CashTokens

```bash
paytaca token list # List fungible tokens with balances
paytaca token info <category> # Token metadata, balance, and NFTs
paytaca token list # List fungible tokens with balances and USD values
paytaca token info <category> # Token metadata, balance, USD value, and NFTs
paytaca token price <category> [amount] # USD price of a token and value of an amount (default: balance)
paytaca token send <address> <amount> --token <cat> # Send fungible tokens
paytaca token send-nft <address> --token <cat> --commitment <hex> # Send an NFT
```
Expand Down Expand Up @@ -180,7 +181,7 @@ src/
send.ts BCH sending
history.ts transaction history (BCH and CashTokens)
address.ts HD address derivation (standard and z-prefix)
token.ts CashToken commands (list, info, send, send-nft)
token.ts CashToken commands (list, info, price, send, send-nft)
pay.ts x402 BCH payment handler for HTTP requests
check.ts Check if URL requires x402 payment
wallet/
Expand All @@ -193,6 +194,7 @@ src/
utils/
crypto.ts pubkey -> CashAddress pipeline
network.ts Watchtower URLs, derivation paths
prices.ts Watchtower asset-prices client (USD per token/BCH)
x402.ts x402 header parsing, payment requirement selection
types/
x402.ts x402 payment types (PaymentRequired, PaymentPayload, etc.)
Expand Down
32 changes: 30 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@bitauth/libauth": "2.0.0-alpha.8",
"@cashlab/cauldron": "^1.0.3",
"@napi-rs/keyring": "^1.2.0",
"bip39": "^3.1.0",
"chalk": "^5.4.1",
"commander": "^12.1.0",
"nostr-tools": "^2.23.3",
"js-sha256": "^0.9.0",
"nostr-tools": "^2.23.3",
"qrcode-terminal": "^0.12.0",
"watchtower-cash-js": "^0.2.4"
},
Expand Down
161 changes: 161 additions & 0 deletions src/commands/swap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* CLI command: swap <tokenId> <amount>
*
* Swap a CashToken against BCH through Cauldron pools.
*
* Two directions:
* - sell (default): sell tokens for BCH
* - buy: spend BCH to buy tokens
*
* Shows a quote (rate, amounts, trade fee), asks for confirmation, then
* builds + signs the trade transaction and broadcasts it.
*
* Cauldron is a liquidity protocol on Bitcoin Cash. Swaps execute against
* the active pools advertised by the riften indexer; the trade transaction
* is built with @cashlab/cauldron's ExchangeLab (libauth templates).
*/

import { Command } from 'commander'
import readline from 'readline'
import chalk from 'chalk'
import { loadWallet, loadMnemonic } from '../wallet/index.js'
import { BCH_DERIVATION_PATH } from '../utils/network.js'
import {
executeSwap,
estimateSwap,
formatQuote,
type SwapDirection,
} from '../wallet/cauldron/swap.js'
import { fetchTokenData } from '../wallet/cauldron/api.js'

function promptConfirmation(message: string): Promise<boolean> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})

return new Promise((resolve) => {
rl.question(chalk.bold(`\n ${message} (y/N): `), (answer) => {
rl.close()
const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'
resolve(confirmed)
})
})
}

export function registerSwapCommand(program: Command): void {
program
.command('swap')
.description('Swap a CashToken for BCH (sell) or BCH for a CashToken (buy)')
.argument('<tokenId>', '64-char hex token category to swap')
.argument('<amount>', 'Token amount (in the token\'s units)')
.option(
'--direction <direction>',
'Swap direction: sell (token→BCH) or buy (BCH→token) (default: sell)',
'sell'
)
.option(
'--raw',
'Interpret <amount> as raw base units instead of decimal token units'
)
.option('--yes', 'Skip the confirmation prompt')
.option('--chipnet', 'Use chipnet (testnet) instead of mainnet')
.action(async (tokenId: string, amountStr: string, opts) => {
if (opts.chipnet) {
console.log(
chalk.red('\nError: Cauldron swaps are not yet supported on chipnet.\n')
)
process.exit(1)
}
const isChipnet = false
const network = 'mainnet'
const direction: SwapDirection =
opts.direction === 'buy' ? 'buy' : 'sell'

const data = loadMnemonic()
if (!data) {
console.log(
chalk.red(
'\nNo wallet found. Run `paytaca wallet create` or `paytaca wallet import` first.\n'
)
)
process.exit(1)
}

if (!/^[a-fA-F0-9]{64}$/.test(tokenId)) {
console.log(chalk.red('\nError: tokenId must be a 64-char hex token category.\n'))
process.exit(1)
}

const parsedAmount = parseFloat(amountStr)
if (isNaN(parsedAmount) || parsedAmount <= 0) {
console.log(chalk.red('\nError: Amount must be a positive number.\n'))
process.exit(1)
}

console.log(chalk.bold(`\n ${direction === 'buy' ? 'Buying' : 'Selling'} token on ${network}`))
console.log(chalk.dim(` Token: ${tokenId}`))
console.log()

try {
// Resolve token metadata for decimals + symbol
const tokenData = await fetchTokenData(tokenId)
if (!tokenData) {
console.log(chalk.red('\nError: No cauldron token data found for this token.\n'))
process.exit(1)
}
const decimals = tokenData.bcmr.token.decimals
const symbol = tokenData.bcmr.token.symbol || tokenData.display_symbol

// Convert decimal amount to base units
const amount = opts.raw
? BigInt(parsedAmount)
: BigInt(Math.round(parsedAmount * 10 ** decimals))

// Show quote first
const quote = await estimateSwap({ tokenId, direction, amount })
console.log(chalk.cyan(' Quote:'))
for (const line of formatQuote(quote).split('\n')) {
console.log(` ${line}`)
}
console.log()

if (!opts.yes) {
const confirmed = await promptConfirmation('Confirm swap?')
if (!confirmed) {
console.log(chalk.yellow(' Swap cancelled.\n'))
process.exit(0)
}
}

const w = loadWallet()!
const bchWallet = w.forNetwork(isChipnet)

const result = await executeSwap({
tokenId,
direction,
amount,
bchWallet,
mnemonic: data.mnemonic,
derivationPath: BCH_DERIVATION_PATH,
})

if (result.success) {
console.log(chalk.green('\n Swap successful!\n'))
if (result.txid) {
console.log(` txid: ${result.txid}`)
const explorer = isChipnet
? 'https://chipnet.chaingraph.cash/tx/'
: 'https://bchexplorer.info/tx/'
console.log(chalk.dim(` ${explorer}${result.txid}`))
}
} else {
console.log(chalk.red(`\n Swap failed: ${result.error || 'Unknown error'}\n`))
process.exit(1)
}
} catch (err: any) {
console.log(chalk.red(`\n Error: ${err.message || err}\n`))
process.exit(1)
}
})
}
Loading
Loading