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
94 changes: 38 additions & 56 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,81 +4,63 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Project Overview

Haystack Router SDK is a TypeScript/JavaScript SDK for the Haystack Order Router - smart order routing and DEX aggregation on Algorand. This is a pnpm workspace monorepo containing the SDK package and example implementations.
TypeScript SDK (`@txnlab/haystack-router`) for the Haystack Order Router, which does smart order routing and DEX aggregation on Algorand. This is a pnpm workspace with the SDK in `packages/haystack/` and example apps in `examples/` (react, react-query, node-cli), which consume the SDK via `workspace:*`. Peer dependency: `algosdk ^3`.

> **Note:** This SDK was migrated from `@txnlab/deflex` (TxnLab/deflex-js) to `@txnlab/haystack-router` (TxnLab/haystack-js) as part of a rebranding effort. See [MIGRATION.md](../MIGRATION.md) for details on migrating from the old package.
> Migrated from `@txnlab/deflex` (TxnLab/deflex-js). See [MIGRATION.md](../MIGRATION.md).

## Commands

Run from the repo root. `build`/`test`/`typecheck` are filtered to the SDK package. `lint`/`format` cover the whole repo.

```bash
# Install dependencies
pnpm install

# Build the SDK (runs tsdown and publint)
pnpm build

# Run tests
pnpm test
pnpm test:watch # Watch mode
pnpm test:coverage # With coverage

# Run a single test file
pnpm --filter @txnlab/haystack-router vitest run tests/client.test.ts

# Linting and formatting
pnpm lint # ESLint
pnpm format # Check formatting
pnpm format:fix # Fix formatting

# Type checking
pnpm build # tsdown + publint --strict
pnpm test # vitest run (also: test:watch, test:coverage)
pnpm lint # eslint (import-x/order enforced, alphabetized)
pnpm format # prettier --check (format:fix to write)
pnpm typecheck
pnpm run ci # build, lint, format, typecheck, test

# Run all CI checks
pnpm run ci
# Single test file / single test
pnpm --filter @txnlab/haystack-router exec vitest run tests/client.test.ts
pnpm --filter @txnlab/haystack-router exec vitest run -t "test name"

# CI also builds the examples (not part of `pnpm run ci`). Requires the SDK to be built first.
pnpm --filter "./examples/**" run build
```

## Architecture

### SDK Package (`packages/haystack/`)

The SDK exports from `src/index.ts`:
The public API is whatever `src/index.ts` re-exports: client, composer, constants, middleware, types, utils. `logger.ts`, `debug.ts` and `launch.ts` are internal.

- **client.ts** - `RouterClient` class: Main entry point for interacting with the Haystack Router API. Handles quote fetching, swap transaction creation, and automatic asset opt-in detection.
### Swap flow

- **composer.ts** - `SwapComposer` class: Builder pattern for constructing atomic transaction groups. Manages transaction lifecycle (BUILDING → BUILT → SIGNED → SUBMITTED → COMMITTED). Supports adding custom transactions and ABI method calls before/after swaps.

- **middleware.ts** - `SwapMiddleware` interface: Plugin system for extending swap functionality. Middleware can modify quote parameters, add transactions before/after swaps. Includes built-in `AutoOptOutMiddleware`.

- **types.ts** - TypeScript types for API requests/responses, quotes, transactions.

- **constants.ts** - Default configuration values and protocol constants.

### Workspace Structure

```
packages/haystack/ # Main SDK package (@txnlab/haystack-router)
examples/
react/ # React + Vite example
react-query/ # React + TanStack Query example
node-cli/ # Node.js CLI example
```
1. `RouterClient.newQuote()` runs each middleware's `shouldApply`, then `adjustQuoteParams`, then calls `fetchQuote()` (GET `/fetchQuote`) and normalizes the result to a `SwapQuote` with bigint fields. Calling `fetchQuote()` directly **skips** `adjustQuoteParams`.
2. `RouterClient.newSwap()` POSTs the quote's encrypted `txnPayload` to `/fetchExecuteSwapTxns` and returns a `SwapComposer` built on algosdk's `AtomicTransactionComposer`.
3. `SwapComposer.addSwapTransactions()` builds the group in this order: user-added txns, then middleware `beforeSwap`, then app opt-ins for `quote.requiredAppOptIns` the account lacks, then API swap txns, then middleware `afterSwap`. It checks the 16-txn group limit. `sign()`/`submit()`/`execute()` call it automatically if it hasn't run yet.
4. `execute()` records the input and output txn IDs, reads the actual output amount from the last swap app call's inner txns (exposed by `getSummary()`), and on failure logs context through `debug.ts`.

Examples use `"@txnlab/haystack-router": "workspace:*"` to reference the local SDK.
### Signing

## Key Patterns
- API swap txns with `signature: false` get the configured signer. The swap's input is the one that pays or transfers to the router app's address, found from the router's pre-signed app calls (falling back to the last user-signed txn). Only that txn gets the optional `note`, and its group index is tracked for `getInputTransactionId()`. Launch swaps have several user-signed txns, and their two `gas()` calls differ only by note.
- Launch-token swaps (`quote.launch`): the user-signed HayLaunch curve call is found by ABI selector (`launchCurveSide()` in `composer.ts`). A buy's output is the curve call's ARC-4 uint64 return, not an inner transfer; a sell's input is the `tokensToSell` app arg.
- Every other API txn carries a logic-sig or secret-key `signature` and is re-signed locally by `createSwapSigner()`. The API's group ID is stripped and reassigned by the ATC.
- The user signer can be an `algosdk.TransactionSigner` or an ARC-1 `SignerFunction`. `defaultSigner` filters out the `null`s that ARC-1 wallets return.

### Transaction Signing
### Middleware (`middleware.ts`, docs in `packages/haystack/MIDDLEWARE.md`)

The SDK accepts both `algosdk.TransactionSigner` and ARC-1 compliant signer functions. The `SignerFunction` type in composer.ts handles both patterns.
`SwapMiddleware` has `name`, `version`, `shouldApply(QuoteContext)`, and optional `adjustQuoteParams`, `beforeSwap(SwapContext)`, `afterSwap(SwapContext)`. `shouldApply` is called in both the quote and swap phases. Middleware that adds txns must lower `maxGroupSize` in `adjustQuoteParams`. Built-in: `AutoOptOutMiddleware`, which appends an asset close-out when a fixed-input swap spends the full balance.

### Middleware System
### Invariants and gotchas

Middleware implements `SwapMiddleware` interface with optional hooks:
- `shouldApply(context)` - Determine if middleware applies to a swap
- `modifyQuoteParams(params)` - Adjust quote parameters (e.g., reduce maxGroupSize)
- `getPreSwapTransactions(context)` - Add transactions before swap
- `getPostSwapTransactions(context)` - Add transactions after swap
- **Tinyman v1 is non-composable.** `DEPRECATED_PROTOCOLS` (`Tinyman`, `Humble`) are always added to `disabledProtocols`. `_allowNonComposableSwaps` re-enables Tinyman v1 only through `fetchQuote()`, can't be combined with opt-in, and is rejected by `newQuote()`. `addTransaction`/`addMethodCall` throw if `quote.flattenedRoute.Tinyman > 0`.
- `SwapComposerStatus` is cast directly from the ATC status, so its member order must match algosdk's `AtomicTransactionComposerStatus`.
- `Logger` is a static singleton. Each `RouterClient`/`SwapComposer` constructor overwrites the global level. Its `sanitize()` redacts `apiKey`, `algodToken`, signature values and `txnPayload`. Keep that redaction when you add log calls.
- Asset and app IDs, and amounts, are `bigint` in `QuoteContext`/`SwapContext`/`SwapQuote`. The raw API response uses numbers and strings.
- All HTTP goes through `utils.request()`, which throws `HTTPError`. Tests mock `../src/utils` or stub `algodClient.<method>().do()`. No network calls in tests.

### Peer Dependencies
## Commits and releases

The SDK requires `algosdk ^3.0.0` as a peer dependency.
- Conventional commits with a **lowercase** subject after the colon, e.g. `fix(client): handle timeout`. Renovate's capitalized `chore(deps): Update…` commits are the exception. Scopes: `client`, `composer`, `middleware`, `types`, `deps`, `examples`, `docs`.
- semantic-release runs on push to `main`. It derives the version from commit types (`feat`, `fix`, `refactor`, `perf` appear in release notes) and writes `packages/haystack/CHANGELOG.md` and the package version. Never edit either by hand.
- Mark a breaking change with a `BREAKING CHANGE:` footer only, never `feat!:`. The `angular` preset doesn't parse `!`, so the commit drops out of its release-notes section. PRs are squash-merged and semantic-release reads only the squash commit, so the footer must be in the squash message; trim GitHub's default squash body so footers from branch commits aren't repeated.
33 changes: 0 additions & 33 deletions .claude/skills/commit-messages.md

This file was deleted.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ This is a pnpm workspace monorepo. All packages and examples share dependencies

- **Haystack Router API Key** - An API key is included for free tier usage (60 requests/min). For production rate limits, request a key from [support@txnlab.dev](mailto:support@txnlab.dev).
- algosdk 3.0.0 or later (peer dependency)
- Node.js >= 20
- Node.js `^22.22.2 || >=24.15`
- pnpm 10.20.0 or later

### Installation
Expand Down
4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ A simple React application demonstrating basic Haystack Router SDK integration.

**Key Features:**

- React 18 with TypeScript and Vite
- React 19 with TypeScript and Vite
- Multiple wallet support (Pera, Defly, Lute)
- Complete swap interface with quote display

Expand Down Expand Up @@ -58,7 +58,7 @@ All examples use the local SDK package via pnpm workspaces. This means changes t

- **Haystack Router API Key** - A free tier key is included in the `.env.example` files (60 requests/min). For production rate limits, request a key from [support@txnlab.dev](mailto:support@txnlab.dev).
- algosdk 3.0.0 or later (peer dependency)
- Node.js >= 20
- Node.js >= 22
- pnpm 10.20.0 or later

### Installation
Expand Down
2 changes: 1 addition & 1 deletion examples/node-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ This example demonstrates how to use the Haystack Router SDK in a Node.js comman

- **Haystack Router API Key** - A free tier key is included in `.env.example` (60 requests/min). For production rate limits, request a key from [support@txnlab.dev](mailto:support@txnlab.dev).
- algosdk 3.0.0 or later (peer dependency)
- Node.js >= 20
- Node.js >= 22
- pnpm 10.20.0 or later

## Setup
Expand Down
6 changes: 3 additions & 3 deletions examples/node-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
"dependencies": {
"@txnlab/haystack-router": "workspace:*",
"algosdk": "^3.9.0",
"dotenv": "^16.4.7"
"dotenv": "^18.0.6"
},
"devDependencies": {
"@types/node": "^22",
"typescript": "^5"
"@types/node": "^22.20.5",
"typescript": "^6.0.3"
}
}
2 changes: 1 addition & 1 deletion examples/node-cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import algosdk from 'algosdk'
import { RouterClient } from '@txnlab/haystack-router'

// Load environment variables
config()
config({ quiet: true })

interface SwapConfig {
apiKey: string
Expand Down
15 changes: 8 additions & 7 deletions examples/react-query/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ A React application demonstrating advanced Haystack Router SDK integration with
- **Fresh quotes** - Quotes automatically refetch every 15 seconds
- **React Query integration** - Uses `useQuery` for quotes and `useMutation` for swaps
- **Multiple wallet support** (Pera, Defly, Lute)
- **Modern wallet UI** with `@txnlab/use-wallet-react` v4
- **Modern wallet UI** with `@txnlab/use-wallet-react` v5
- **Real-time updates** - No "Get Quote" button needed
- **Execute swaps** with slippage protection
- **Display quote details** and routing information

## Prerequisites

- **Haystack Router API Key** - A free tier key is included in `.env.example` (60 requests/min). For production rate limits, request a key from [support@txnlab.dev](mailto:support@txnlab.dev).
- Node.js >= 20
- Node.js >= 22
- pnpm 10.20.0 or later

## Setup
Expand Down Expand Up @@ -127,16 +127,17 @@ const swapMutation = useMutation({
The example is configured with multiple wallet providers in [src/main.tsx](./src/main.tsx):

```typescript
import { defly } from '@txnlab/use-wallet-defly'
import { pera } from '@txnlab/use-wallet-pera'
import { lute } from '@galaxypay/use-wallet-lute'

const walletManager = new WalletManager({
wallets: [WalletId.DEFLY, WalletId.PERA, WalletId.LUTE],
wallets: [defly(), pera(), lute()],
defaultNetwork: 'mainnet',
options: {
resetNetwork: true,
},
});
```

You can customize which wallets are available by modifying this configuration.
Each wallet comes from its own adapter package. To change which wallets are available, add or remove adapter packages and update this list.

## Notes

Expand Down
26 changes: 13 additions & 13 deletions examples/react-query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,22 @@
"preview": "vite preview"
},
"dependencies": {
"@blockshake/defly-connect": "^1.2.1",
"@perawallet/connect": "^1.4.2",
"@tanstack/react-query": "^5.62.14",
"@galaxypay/use-wallet-lute": "^1.0.0",
"@tanstack/react-query": "^5.104.1",
"@txnlab/haystack-router": "workspace:*",
"@txnlab/use-wallet-react": "^4.3.1",
"@txnlab/use-wallet-defly": "^5.0.1",
"@txnlab/use-wallet-pera": "^5.0.1",
"@txnlab/use-wallet-react": "^5.0.1",
"algosdk": "^3.9.0",
"lute-connect": "^1.6.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vite-plugin-node-polyfills": "^0.24.0"
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"typescript": "~6.0.3",
"vite": "^8.3.3",
"vite-plugin-node-polyfills": "^0.28.0"
}
}
14 changes: 5 additions & 9 deletions examples/react-query/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import {
WalletId,
WalletManager,
WalletProvider,
} from '@txnlab/use-wallet-react'
import { WalletManager, WalletProvider } from '@txnlab/use-wallet-react'
import { defly } from '@txnlab/use-wallet-defly'
import { pera } from '@txnlab/use-wallet-pera'
import { lute } from '@galaxypay/use-wallet-lute'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import App from './App'
import './index.css'

const walletManager = new WalletManager({
wallets: [WalletId.DEFLY, WalletId.PERA, WalletId.LUTE],
wallets: [defly(), pera(), lute()],
defaultNetwork: 'mainnet',
options: {
resetNetwork: true,
},
})

const queryClient = new QueryClient()
Expand Down
15 changes: 8 additions & 7 deletions examples/react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ A simple React application demonstrating basic Haystack Router SDK integration w
## Features

- Multiple wallet support (Pera, Defly, Lute)
- Modern wallet UI with `@txnlab/use-wallet-react` v4
- Modern wallet UI with `@txnlab/use-wallet-react` v5
- Get swap quotes from Haystack Router API
- Execute swaps with slippage protection
- Display quote details and routing information

## Prerequisites

- **Haystack Router API Key** - A free tier key is included in `.env.example` (60 requests/min). For production rate limits, request a key from [support@txnlab.dev](mailto:support@txnlab.dev).
- Node.js >= 20
- Node.js >= 22
- pnpm 10.20.0 or later

## Setup
Expand Down Expand Up @@ -72,16 +72,17 @@ pnpm preview
The example is configured with multiple wallet providers in [src/main.tsx](./src/main.tsx):

```typescript
import { defly } from '@txnlab/use-wallet-defly'
import { pera } from '@txnlab/use-wallet-pera'
import { lute } from '@galaxypay/use-wallet-lute'

const walletManager = new WalletManager({
wallets: [WalletId.DEFLY, WalletId.PERA, WalletId.LUTE],
wallets: [defly(), pera(), lute()],
defaultNetwork: 'mainnet',
options: {
resetNetwork: true,
},
});
```

You can customize which wallets are available by modifying this configuration.
Each wallet comes from its own adapter package. To change which wallets are available, add or remove adapter packages and update this list.

## Notes

Expand Down
24 changes: 12 additions & 12 deletions examples/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,21 @@
"preview": "vite preview"
},
"dependencies": {
"@blockshake/defly-connect": "^1.2.1",
"@perawallet/connect": "^1.4.2",
"@galaxypay/use-wallet-lute": "^1.0.0",
"@txnlab/haystack-router": "workspace:*",
"@txnlab/use-wallet-react": "^4.3.1",
"@txnlab/use-wallet-defly": "^5.0.1",
"@txnlab/use-wallet-pera": "^5.0.1",
"@txnlab/use-wallet-react": "^5.0.1",
"algosdk": "^3.9.0",
"lute-connect": "^1.6.3",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "~5.6.2",
"vite": "^6.0.5",
"vite-plugin-node-polyfills": "^0.24.0"
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^6.1.2",
"typescript": "~6.0.3",
"vite": "^8.3.3",
"vite-plugin-node-polyfills": "^0.28.0"
}
}
Loading
Loading