@goplausible/openclaw-algorand-plugin 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/x402-fetch.ts +3 -10
- package/openclaw.plugin.json +4 -2
- package/package.json +1 -1
- package/skills/algorand-interaction/SKILL.md +12 -6
- package/skills/algorand-interaction/references/algorand-mcp.md +81 -7
- package/skills/algorand-interaction/references/examples-algorand-mcp.md +77 -2
- package/skills/haystack-router-development/SKILL.md +85 -0
- package/skills/haystack-router-development/references/api-reference.md +381 -0
- package/skills/haystack-router-development/references/configuration.md +184 -0
- package/skills/haystack-router-development/references/fees-and-referrals.md +91 -0
- package/skills/haystack-router-development/references/getting-started.md +93 -0
- package/skills/haystack-router-development/references/migration.md +53 -0
- package/skills/haystack-router-development/references/node-automation.md +113 -0
- package/skills/haystack-router-development/references/quotes.md +155 -0
- package/skills/haystack-router-development/references/react-integration.md +260 -0
- package/skills/haystack-router-development/references/swaps.md +161 -0
- package/skills/haystack-router-interaction/SKILL.md +146 -0
- package/skills/haystack-router-interaction/references/configuration.md +53 -0
- package/skills/haystack-router-interaction/references/getting-started.md +48 -0
- package/skills/haystack-router-interaction/references/node-automation.md +51 -0
- package/skills/haystack-router-interaction/references/quotes.md +80 -0
- package/skills/haystack-router-interaction/references/swaps.md +84 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
- **Node.js** >= 20
|
|
6
|
+
- **algosdk** 3.x (peer dependency)
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @txnlab/haystack-router algosdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## RouterClient Initialization
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { RouterClient } from '@txnlab/haystack-router'
|
|
18
|
+
|
|
19
|
+
const router = new RouterClient({
|
|
20
|
+
apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
|
|
21
|
+
})
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### With Options
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const router = new RouterClient({
|
|
28
|
+
apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
|
|
29
|
+
autoOptIn: true, // Auto-detect asset opt-in needs
|
|
30
|
+
referrerAddress: 'ABC...', // Earn 25% of swap fees
|
|
31
|
+
feeBps: 15, // Fee in basis points (default: 10)
|
|
32
|
+
debugLevel: 'info', // 'none' | 'info' | 'debug' | 'trace'
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### TestNet
|
|
37
|
+
|
|
38
|
+
Override the algod connection and API base URL for TestNet:
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const router = new RouterClient({
|
|
42
|
+
apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
|
|
43
|
+
algodUri: 'https://testnet-api.4160.nodely.dev/',
|
|
44
|
+
// Set apiBaseUrl if using a TestNet-specific API endpoint
|
|
45
|
+
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick Example
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { RouterClient } from '@txnlab/haystack-router'
|
|
52
|
+
|
|
53
|
+
const router = new RouterClient({
|
|
54
|
+
apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Get a quote: swap 1 ALGO → USDC
|
|
58
|
+
const quote = await router.newQuote({
|
|
59
|
+
fromASAID: 0, // ALGO
|
|
60
|
+
toASAID: 31566704, // USDC
|
|
61
|
+
amount: 1_000_000, // 1 ALGO in microAlgos
|
|
62
|
+
address: activeAddress,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
console.log(`Expected output: ${quote.quote} microUSDC`)
|
|
66
|
+
console.log(`USD value: $${quote.usdOut}`)
|
|
67
|
+
|
|
68
|
+
// Execute the swap (use-wallet signer for browser, custom signer for Node.js)
|
|
69
|
+
const swap = await router.newSwap({
|
|
70
|
+
quote,
|
|
71
|
+
address: activeAddress,
|
|
72
|
+
signer: transactionSigner,
|
|
73
|
+
slippage: 1, // 1% slippage tolerance
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const result = await swap.execute()
|
|
77
|
+
console.log(`Confirmed in round ${result.confirmedRound}`)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Amounts and Units
|
|
81
|
+
|
|
82
|
+
All amounts are in **base units** (smallest denomination):
|
|
83
|
+
|
|
84
|
+
| Asset | Decimals | 1 unit in base | Example |
|
|
85
|
+
| ------------------- | -------- | -------------- | -------------------- |
|
|
86
|
+
| ALGO (ASA 0) | 6 | 1,000,000 | `1_000_000` = 1 ALGO |
|
|
87
|
+
| USDC (ASA 31566704) | 6 | 1,000,000 | `5_000_000` = 5 USDC |
|
|
88
|
+
|
|
89
|
+
Convert human-readable amounts:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const amount = BigInt(Math.floor(parseFloat(userInput) * 10 ** decimals))
|
|
93
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Migration from @txnlab/deflex
|
|
2
|
+
|
|
3
|
+
## Package Rename
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm uninstall @txnlab/deflex
|
|
7
|
+
npm install @txnlab/haystack-router
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Import Changes
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// Before
|
|
14
|
+
import { DeflexClient } from '@txnlab/deflex'
|
|
15
|
+
|
|
16
|
+
// After
|
|
17
|
+
import { RouterClient } from '@txnlab/haystack-router'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Class and Type Renames
|
|
21
|
+
|
|
22
|
+
| Before (@txnlab/deflex) | After (@txnlab/haystack-router) |
|
|
23
|
+
| ----------------------- | ------------------------------- |
|
|
24
|
+
| `DeflexClient` | `RouterClient` |
|
|
25
|
+
| `DeflexQuote` | `SwapQuote` |
|
|
26
|
+
| `DeflexTransaction` | `SwapTransaction` |
|
|
27
|
+
| `DeflexConfig` | `Config` |
|
|
28
|
+
| `DeflexConfigParams` | `ConfigParams` |
|
|
29
|
+
| `DeflexSignature` | `Signature` |
|
|
30
|
+
|
|
31
|
+
## API Endpoint
|
|
32
|
+
|
|
33
|
+
The SDK automatically uses the updated API endpoint. No manual URL changes needed.
|
|
34
|
+
|
|
35
|
+
## Functionality
|
|
36
|
+
|
|
37
|
+
All functionality is identical — only naming changed. A find-and-replace of the class and type names is sufficient.
|
|
38
|
+
|
|
39
|
+
### Quick Migration
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# Find all files that import from @txnlab/deflex
|
|
43
|
+
grep -r "@txnlab/deflex" --include="*.ts" --include="*.tsx" -l
|
|
44
|
+
|
|
45
|
+
# Replace imports
|
|
46
|
+
# @txnlab/deflex → @txnlab/haystack-router
|
|
47
|
+
# DeflexClient → RouterClient
|
|
48
|
+
# DeflexQuote → SwapQuote
|
|
49
|
+
# DeflexTransaction → SwapTransaction
|
|
50
|
+
# DeflexConfig → Config
|
|
51
|
+
# DeflexConfigParams → ConfigParams
|
|
52
|
+
# DeflexSignature → Signature
|
|
53
|
+
```
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Node.js Automation (SDK)
|
|
2
|
+
|
|
3
|
+
When building Node.js automation scripts that integrate Haystack Router directly in application code:
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @txnlab/haystack-router algosdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Environment Variables
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# .env
|
|
15
|
+
HAYSTACK_API_KEY=1b72df7e-1131-4449-8ce1-29b79dd3f51e # Free tier (60 requests/min)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Complete Example
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import algosdk from 'algosdk'
|
|
22
|
+
import { RouterClient } from '@txnlab/haystack-router'
|
|
23
|
+
|
|
24
|
+
async function main() {
|
|
25
|
+
const apiKey = process.env.HAYSTACK_API_KEY!
|
|
26
|
+
|
|
27
|
+
// For application code, use use-wallet or AlgorandClient for signing
|
|
28
|
+
// For Node.js scripts, create a custom signer:
|
|
29
|
+
const account = algosdk.mnemonicToSecretKey(process.env.MNEMONIC!)
|
|
30
|
+
const address = account.addr.toString()
|
|
31
|
+
|
|
32
|
+
const signer = async (
|
|
33
|
+
txnGroup: algosdk.Transaction[],
|
|
34
|
+
indexesToSign: number[],
|
|
35
|
+
): Promise<Uint8Array[]> => {
|
|
36
|
+
return indexesToSign.map(
|
|
37
|
+
(index) => algosdk.signTransaction(txnGroup[index], account.sk).blob,
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Initialize router
|
|
42
|
+
const router = new RouterClient({
|
|
43
|
+
apiKey,
|
|
44
|
+
autoOptIn: true,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// Get quote: 1 ALGO → USDC
|
|
48
|
+
const quote = await router.newQuote({
|
|
49
|
+
fromASAID: 0,
|
|
50
|
+
toASAID: 31566704,
|
|
51
|
+
amount: 1_000_000,
|
|
52
|
+
address,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
console.log(`Expected output: ${Number(quote.quote) / 1e6} USDC`)
|
|
56
|
+
console.log(`USD value: $${quote.usdOut.toFixed(2)}`)
|
|
57
|
+
|
|
58
|
+
// Execute swap
|
|
59
|
+
const swap = await router.newSwap({
|
|
60
|
+
quote,
|
|
61
|
+
address,
|
|
62
|
+
signer,
|
|
63
|
+
slippage: 1,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const result = await swap.execute()
|
|
67
|
+
console.log(`Confirmed in round ${result.confirmedRound}`)
|
|
68
|
+
console.log(`Transaction IDs: ${result.txIds.join(', ')}`)
|
|
69
|
+
|
|
70
|
+
const summary = swap.getSummary()
|
|
71
|
+
if (summary) {
|
|
72
|
+
console.log(`Input: ${summary.inputAmount} microunits`)
|
|
73
|
+
console.log(`Output: ${summary.outputAmount} microunits`)
|
|
74
|
+
console.log(`Fees: ${summary.totalFees} microAlgos`)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main().catch(console.error)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tracking with Notes
|
|
82
|
+
|
|
83
|
+
Attach identifiers to transactions for backend tracking:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
const swap = await router.newSwap({
|
|
87
|
+
quote,
|
|
88
|
+
address,
|
|
89
|
+
signer,
|
|
90
|
+
slippage: 1,
|
|
91
|
+
note: new TextEncoder().encode(
|
|
92
|
+
JSON.stringify({
|
|
93
|
+
orderId: 'order-123',
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
await swap.execute()
|
|
100
|
+
const txId = swap.getInputTransactionId()
|
|
101
|
+
console.log(`Tracked: order-123 → ${txId}`)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Debug Logging
|
|
105
|
+
|
|
106
|
+
Enable verbose logging for troubleshooting:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
const router = new RouterClient({
|
|
110
|
+
apiKey,
|
|
111
|
+
debugLevel: 'debug', // 'none' | 'info' | 'debug' | 'trace'
|
|
112
|
+
})
|
|
113
|
+
```
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Quotes
|
|
2
|
+
|
|
3
|
+
## Getting a Quote
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import { RouterClient } from '@txnlab/haystack-router'
|
|
7
|
+
|
|
8
|
+
const router = new RouterClient({
|
|
9
|
+
apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
const quote = await router.newQuote({
|
|
13
|
+
fromASAID: 0, // ALGO
|
|
14
|
+
toASAID: 31566704, // USDC
|
|
15
|
+
amount: 1_000_000, // 1 ALGO in base units
|
|
16
|
+
address: activeAddress,
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Parameters
|
|
21
|
+
|
|
22
|
+
| Parameter | Type | Required | Description |
|
|
23
|
+
| ------------------- | ------------------ | -------- | ----------------------------------------------- |
|
|
24
|
+
| `fromASAID` | `number \| bigint` | Yes | Input asset ID (0 = ALGO) |
|
|
25
|
+
| `toASAID` | `number \| bigint` | Yes | Output asset ID |
|
|
26
|
+
| `amount` | `number \| bigint` | Yes | Amount in base units |
|
|
27
|
+
| `type` | `string` | No | `'fixed-input'` (default) or `'fixed-output'` |
|
|
28
|
+
| `address` | `string` | No | User address (needed for auto opt-in detection) |
|
|
29
|
+
| `maxGroupSize` | `number` | No | Max transactions in group (default: 16) |
|
|
30
|
+
| `maxDepth` | `number` | No | Max routing hops (default: 4) |
|
|
31
|
+
| `optIn` | `boolean` | No | Include opt-in transaction for output asset |
|
|
32
|
+
| `disabledProtocols` | `Protocol[]` | No | Protocols to exclude from routing |
|
|
33
|
+
|
|
34
|
+
## Quote Response (SwapQuote)
|
|
35
|
+
|
|
36
|
+
`newQuote()` returns a `SwapQuote` (extends `FetchQuoteResponse`):
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
quote.quote // bigint — expected output amount in base units
|
|
40
|
+
quote.amount // bigint — original input amount
|
|
41
|
+
quote.usdIn // number — USD value of input
|
|
42
|
+
quote.usdOut // number — USD value of output
|
|
43
|
+
quote.userPriceImpact // number | undefined — price impact %
|
|
44
|
+
quote.marketPriceImpact // number | undefined — market price impact %
|
|
45
|
+
quote.route // Route[] — routing path details
|
|
46
|
+
quote.flattenedRoute // Record<string, number> — protocol split percentages
|
|
47
|
+
quote.quotes // DexQuote[] — individual DEX quotes
|
|
48
|
+
quote.requiredAppOptIns // number[] — app IDs needing opt-in
|
|
49
|
+
quote.createdAt // number — timestamp (ms)
|
|
50
|
+
quote.address // string | undefined — user address if provided
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Quote Types
|
|
54
|
+
|
|
55
|
+
**Fixed-input** (default): Specify exact input amount, receive variable output.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const quote = await router.newQuote({
|
|
59
|
+
fromASAID: 0,
|
|
60
|
+
toASAID: 31566704,
|
|
61
|
+
amount: 1_000_000, // Exact: send 1 ALGO
|
|
62
|
+
type: 'fixed-input',
|
|
63
|
+
})
|
|
64
|
+
// quote.quote = expected USDC received
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Fixed-output**: Specify desired output, send variable input.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const quote = await router.newQuote({
|
|
71
|
+
fromASAID: 0,
|
|
72
|
+
toASAID: 31566704,
|
|
73
|
+
amount: 1_000_000, // Exact: receive 1 USDC
|
|
74
|
+
type: 'fixed-output',
|
|
75
|
+
})
|
|
76
|
+
// quote.quote = ALGO required to send
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Displaying Quote Data
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const fromDecimals = 6 // ALGO
|
|
83
|
+
const toDecimals = 6 // USDC
|
|
84
|
+
|
|
85
|
+
const outputHuman = Number(quote.quote) / 10 ** toDecimals
|
|
86
|
+
const inputHuman = Number(quote.amount) / 10 ** fromDecimals
|
|
87
|
+
const rate = outputHuman / inputHuman
|
|
88
|
+
|
|
89
|
+
console.log(`${inputHuman} ALGO → ${outputHuman} USDC`)
|
|
90
|
+
console.log(`Rate: 1 ALGO = ${rate.toFixed(4)} USDC`)
|
|
91
|
+
console.log(`USD in: $${quote.usdIn.toFixed(2)}`)
|
|
92
|
+
console.log(`USD out: $${quote.usdOut.toFixed(2)}`)
|
|
93
|
+
|
|
94
|
+
if (quote.userPriceImpact !== undefined) {
|
|
95
|
+
console.log(`Price impact: ${quote.userPriceImpact.toFixed(2)}%`)
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Route Details
|
|
100
|
+
|
|
101
|
+
Each quote includes routing information showing how the swap is split:
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
// Flattened view: protocol → percentage
|
|
105
|
+
for (const [protocol, pct] of Object.entries(quote.flattenedRoute)) {
|
|
106
|
+
console.log(`${protocol}: ${pct}%`)
|
|
107
|
+
}
|
|
108
|
+
// e.g., "TinymanV2: 60%", "Pact: 40%"
|
|
109
|
+
|
|
110
|
+
// Detailed route with hops
|
|
111
|
+
for (const route of quote.route) {
|
|
112
|
+
console.log(`${route.percentage}% of swap:`)
|
|
113
|
+
for (const hop of route.path) {
|
|
114
|
+
console.log(` ${hop.in.unit_name} → ${hop.out.unit_name} via ${hop.name}`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Asset Opt-In Detection
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Option 1: Set autoOptIn on the client
|
|
123
|
+
const router = new RouterClient({
|
|
124
|
+
apiKey: '1b72df7e-1131-4449-8ce1-29b79dd3f51e', // Free tier (60 requests/min)
|
|
125
|
+
autoOptIn: true,
|
|
126
|
+
})
|
|
127
|
+
const quote = await router.newQuote({
|
|
128
|
+
fromASAID: 0,
|
|
129
|
+
toASAID: 31566704,
|
|
130
|
+
amount: 1_000_000,
|
|
131
|
+
address: activeAddress, // Required for auto opt-in
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Option 2: Check manually and pass optIn flag
|
|
135
|
+
const needsOptIn = await router.needsAssetOptIn(activeAddress, 31566704)
|
|
136
|
+
const quote = await router.newQuote({
|
|
137
|
+
fromASAID: 0,
|
|
138
|
+
toASAID: 31566704,
|
|
139
|
+
amount: 1_000_000,
|
|
140
|
+
optIn: needsOptIn,
|
|
141
|
+
})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Lower-Level: fetchQuote()
|
|
145
|
+
|
|
146
|
+
`fetchQuote()` returns the raw `FetchQuoteResponse` without `SwapQuote` enhancements (no bigint coercion, no `createdAt`). Use `newQuote()` unless you need the raw response.
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const raw = await router.fetchQuote({
|
|
150
|
+
fromASAID: 0,
|
|
151
|
+
toASAID: 31566704,
|
|
152
|
+
amount: 1_000_000,
|
|
153
|
+
})
|
|
154
|
+
// raw.quote is string | number (not bigint)
|
|
155
|
+
```
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
# React Integration
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Install dependencies:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @txnlab/haystack-router @txnlab/use-wallet-react algosdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Wrap your app with `WalletProvider`:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { WalletProvider, WalletId } from '@txnlab/use-wallet-react'
|
|
15
|
+
|
|
16
|
+
const walletProviders = [
|
|
17
|
+
{ id: WalletId.PERA },
|
|
18
|
+
{ id: WalletId.DEFLY },
|
|
19
|
+
{ id: WalletId.LUTE },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
function App() {
|
|
23
|
+
return (
|
|
24
|
+
<WalletProvider wallets={walletProviders}>
|
|
25
|
+
<SwapInterface />
|
|
26
|
+
</WalletProvider>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Basic Swap Component
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useState } from 'react'
|
|
35
|
+
import { useWallet } from '@txnlab/use-wallet-react'
|
|
36
|
+
import { RouterClient, type SwapQuote } from '@txnlab/haystack-router'
|
|
37
|
+
|
|
38
|
+
function SwapInterface() {
|
|
39
|
+
const { activeAddress, transactionSigner } = useWallet()
|
|
40
|
+
const [amount, setAmount] = useState('')
|
|
41
|
+
const [fromAsset, setFromAsset] = useState(0) // ALGO
|
|
42
|
+
const [toAsset, setToAsset] = useState(31566704) // USDC
|
|
43
|
+
const [slippage, setSlippage] = useState('1')
|
|
44
|
+
const [quote, setQuote] = useState<SwapQuote | null>(null)
|
|
45
|
+
|
|
46
|
+
const getQuote = async () => {
|
|
47
|
+
const router = new RouterClient({
|
|
48
|
+
apiKey: import.meta.env.VITE_HAYSTACK_API_KEY,
|
|
49
|
+
autoOptIn: true,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const amountInBaseUnits = BigInt(Math.floor(parseFloat(amount) * 1_000_000))
|
|
53
|
+
|
|
54
|
+
const result = await router.newQuote({
|
|
55
|
+
fromASAID: fromAsset,
|
|
56
|
+
toASAID: toAsset,
|
|
57
|
+
amount: amountInBaseUnits,
|
|
58
|
+
address: activeAddress!,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
setQuote(result)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const executeSwap = async () => {
|
|
65
|
+
if (!quote || !activeAddress) return
|
|
66
|
+
|
|
67
|
+
const router = new RouterClient({
|
|
68
|
+
apiKey: import.meta.env.VITE_HAYSTACK_API_KEY,
|
|
69
|
+
autoOptIn: true,
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const swap = await router.newSwap({
|
|
73
|
+
quote,
|
|
74
|
+
address: activeAddress,
|
|
75
|
+
signer: transactionSigner,
|
|
76
|
+
slippage: parseFloat(slippage),
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const result = await swap.execute()
|
|
80
|
+
console.log(`Confirmed in round ${result.confirmedRound}`)
|
|
81
|
+
|
|
82
|
+
setQuote(null) // Clear stale quote
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div>
|
|
87
|
+
<input
|
|
88
|
+
type="number"
|
|
89
|
+
value={amount}
|
|
90
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
91
|
+
placeholder="Amount"
|
|
92
|
+
/>
|
|
93
|
+
<button onClick={getQuote} disabled={!activeAddress || !amount}>
|
|
94
|
+
Get Quote
|
|
95
|
+
</button>
|
|
96
|
+
|
|
97
|
+
{quote && (
|
|
98
|
+
<div>
|
|
99
|
+
<p>Output: {(Number(quote.quote) / 1e6).toFixed(4)} USDC</p>
|
|
100
|
+
<p>USD value: ${quote.usdOut.toFixed(2)}</p>
|
|
101
|
+
<p>
|
|
102
|
+
Route:{' '}
|
|
103
|
+
{Object.entries(quote.flattenedRoute)
|
|
104
|
+
.map(([protocol, pct]) => `${protocol}: ${pct}%`)
|
|
105
|
+
.join(', ')}
|
|
106
|
+
</p>
|
|
107
|
+
<button onClick={executeSwap}>Execute Swap</button>
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## TanStack Query Integration
|
|
116
|
+
|
|
117
|
+
For production UIs, use TanStack Query for auto-refreshing quotes and better state management.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm install @tanstack/react-query
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
125
|
+
import {
|
|
126
|
+
useQuery,
|
|
127
|
+
useMutation,
|
|
128
|
+
QueryClient,
|
|
129
|
+
QueryClientProvider,
|
|
130
|
+
} from '@tanstack/react-query'
|
|
131
|
+
import { useWallet } from '@txnlab/use-wallet-react'
|
|
132
|
+
import { RouterClient, type SwapQuote } from '@txnlab/haystack-router'
|
|
133
|
+
|
|
134
|
+
const queryClient = new QueryClient()
|
|
135
|
+
|
|
136
|
+
function SwapWithAutoRefresh() {
|
|
137
|
+
const { activeAddress, transactionSigner } = useWallet()
|
|
138
|
+
const [amount, setAmount] = useState('')
|
|
139
|
+
const [debouncedAmount, setDebouncedAmount] = useState('')
|
|
140
|
+
const [fromAsset] = useState(0)
|
|
141
|
+
const [toAsset] = useState(31566704)
|
|
142
|
+
const [slippage] = useState(1)
|
|
143
|
+
|
|
144
|
+
// Debounce amount input (500ms)
|
|
145
|
+
useEffect(() => {
|
|
146
|
+
const timer = setTimeout(() => setDebouncedAmount(amount), 500)
|
|
147
|
+
return () => clearTimeout(timer)
|
|
148
|
+
}, [amount])
|
|
149
|
+
|
|
150
|
+
const router = useMemo(
|
|
151
|
+
() =>
|
|
152
|
+
new RouterClient({
|
|
153
|
+
apiKey: import.meta.env.VITE_HAYSTACK_API_KEY,
|
|
154
|
+
autoOptIn: true,
|
|
155
|
+
}),
|
|
156
|
+
[],
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
const isValidRequest =
|
|
160
|
+
activeAddress && debouncedAmount && parseFloat(debouncedAmount) > 0
|
|
161
|
+
|
|
162
|
+
// Auto-fetch and refresh quotes
|
|
163
|
+
const {
|
|
164
|
+
data: quote,
|
|
165
|
+
error,
|
|
166
|
+
isLoading,
|
|
167
|
+
} = useQuery({
|
|
168
|
+
queryKey: ['quote', fromAsset, toAsset, debouncedAmount, activeAddress],
|
|
169
|
+
queryFn: () =>
|
|
170
|
+
router.newQuote({
|
|
171
|
+
fromASAID: fromAsset,
|
|
172
|
+
toASAID: toAsset,
|
|
173
|
+
amount: BigInt(Math.floor(parseFloat(debouncedAmount) * 1_000_000)),
|
|
174
|
+
address: activeAddress!,
|
|
175
|
+
}),
|
|
176
|
+
enabled: !!isValidRequest,
|
|
177
|
+
refetchInterval: 15_000, // Refresh every 15 seconds
|
|
178
|
+
retry: 1,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Swap mutation
|
|
182
|
+
const swapMutation = useMutation({
|
|
183
|
+
mutationFn: async () => {
|
|
184
|
+
if (!quote || !activeAddress) throw new Error('Missing quote or address')
|
|
185
|
+
|
|
186
|
+
const swap = await router.newSwap({
|
|
187
|
+
quote,
|
|
188
|
+
address: activeAddress,
|
|
189
|
+
signer: transactionSigner,
|
|
190
|
+
slippage,
|
|
191
|
+
})
|
|
192
|
+
return swap.execute()
|
|
193
|
+
},
|
|
194
|
+
onSuccess: (result) => {
|
|
195
|
+
console.log(`Confirmed in round ${result.confirmedRound}`)
|
|
196
|
+
queryClient.invalidateQueries({ queryKey: ['quote'] })
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
return (
|
|
201
|
+
<div>
|
|
202
|
+
<input
|
|
203
|
+
type="number"
|
|
204
|
+
value={amount}
|
|
205
|
+
onChange={(e) => setAmount(e.target.value)}
|
|
206
|
+
placeholder="Amount"
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
{isLoading && <p>Fetching quote...</p>}
|
|
210
|
+
{error && <p>Error: {(error as Error).message}</p>}
|
|
211
|
+
|
|
212
|
+
{quote && (
|
|
213
|
+
<div>
|
|
214
|
+
<p>Output: {(Number(quote.quote) / 1e6).toFixed(4)} USDC</p>
|
|
215
|
+
<p>USD: ${quote.usdOut.toFixed(2)}</p>
|
|
216
|
+
<button
|
|
217
|
+
onClick={() => swapMutation.mutate()}
|
|
218
|
+
disabled={swapMutation.isPending}
|
|
219
|
+
>
|
|
220
|
+
{swapMutation.isPending ? 'Swapping...' : 'Swap'}
|
|
221
|
+
</button>
|
|
222
|
+
</div>
|
|
223
|
+
)}
|
|
224
|
+
</div>
|
|
225
|
+
)
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Key patterns:
|
|
230
|
+
|
|
231
|
+
- **Debounced input**: Prevent quote requests on every keystroke
|
|
232
|
+
- **Auto-refresh**: `refetchInterval: 15_000` keeps quotes current
|
|
233
|
+
- **Invalidation**: Clear stale quotes after a successful swap
|
|
234
|
+
- **Loading/error states**: TanStack Query manages all async state
|
|
235
|
+
|
|
236
|
+
## Displaying Route Details
|
|
237
|
+
|
|
238
|
+
```tsx
|
|
239
|
+
function RouteDisplay({ quote }: { quote: SwapQuote }) {
|
|
240
|
+
return (
|
|
241
|
+
<div>
|
|
242
|
+
<h4>Route</h4>
|
|
243
|
+
{quote.route.map((route, i) => (
|
|
244
|
+
<div key={i}>
|
|
245
|
+
<strong>{route.percentage}%</strong>
|
|
246
|
+
{route.path.map((hop, j) => (
|
|
247
|
+
<span key={j}>
|
|
248
|
+
{j > 0 && ' → '}
|
|
249
|
+
{hop.in.unit_name} → {hop.out.unit_name} ({hop.name})
|
|
250
|
+
</span>
|
|
251
|
+
))}
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
254
|
+
{quote.userPriceImpact !== undefined && (
|
|
255
|
+
<p>Price impact: {quote.userPriceImpact.toFixed(2)}%</p>
|
|
256
|
+
)}
|
|
257
|
+
</div>
|
|
258
|
+
)
|
|
259
|
+
}
|
|
260
|
+
```
|