Skip to content

Commit 425cb96

Browse files
feat: near intents swapper (#11016)
1 parent 0d41bc9 commit 425cb96

File tree

30 files changed

+2647
-11
lines changed

30 files changed

+2647
-11
lines changed

.claude/skills/swapper-integration/NEAR_INTENTS_RESEARCH.md

Lines changed: 1567 additions & 0 deletions
Large diffs are not rendered by default.

.claude/skills/swapper-integration/SKILL.md

Lines changed: 159 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,133 @@ Create `packages/swapper/src/swappers/[SwapperName]Swapper/`
221221

222222
**Refer to** `@examples.md` for code templates. **Copy patterns** from similar existing swappers.
223223

224-
#### Step 3: Register the swapper
224+
#### Step 3: Add Swapper-Specific Metadata (ONLY if needed)
225+
226+
**When is metadata needed?**
227+
- Deposit-to-address swappers (Chainflip, NEAR Intents) - need deposit address, swap ID for status polling
228+
- Order-based swappers (CowSwap) - need order ID for status tracking
229+
- Any swapper that requires tracking state between quote → execution → status polling
230+
231+
**When is metadata NOT needed?**
232+
- Direct transaction swappers (Bebop, 0x, Portals) - transaction is built from quote, no async tracking needed
233+
- Same-chain aggregators where transaction hash is sufficient for status tracking
234+
- Most EVM-only swappers that return transaction data directly
235+
236+
**If your swapper doesn't need async status polling or deposit addresses, skip this step!**
237+
238+
**Three places to add metadata:**
239+
240+
**a. Define types** (`packages/swapper/src/types.ts`):
241+
242+
Add to `TradeQuoteStep` type:
243+
```typescript
244+
export type TradeQuoteStep = {
245+
// ... existing fields
246+
[swapperName]Specific?: {
247+
depositAddress: string
248+
swapId: number
249+
// ... other swapper-specific fields
250+
}
251+
}
252+
```
253+
254+
Add to `SwapperSpecificMetadata` type (for swap storage):
255+
```typescript
256+
export type SwapperSpecificMetadata = {
257+
chainflipSwapId: number | undefined
258+
nearIntentsSpecific?: {
259+
depositAddress: string
260+
depositMemo?: string
261+
timeEstimate: number
262+
deadline: string
263+
}
264+
// Add your swapper's metadata here
265+
[swapperName]Specific?: {
266+
// ... fields needed for status polling
267+
}
268+
// ... other fields
269+
}
270+
```
271+
272+
**b. Populate in quote** (`packages/swapper/src/swappers/[Swapper]/swapperApi/getTradeQuote.ts`):
273+
274+
Store metadata in the TradeQuoteStep:
275+
```typescript
276+
const tradeQuote: TradeQuote = {
277+
// ... other fields
278+
steps: [{
279+
// ... step fields
280+
[swapperName]Specific: {
281+
depositAddress: response.depositAddress,
282+
swapId: response.id,
283+
// ... other data needed later
284+
}
285+
}]
286+
}
287+
```
288+
289+
**c. Extract into swap** (TWO places required!):
290+
291+
**Place 1**: `src/components/MultiHopTrade/components/TradeConfirm/hooks/useTradeButtonProps.tsx`
292+
293+
Add to metadata object around line 114-126:
294+
```typescript
295+
metadata: {
296+
chainflipSwapId: firstStep?.chainflipSpecific?.chainflipSwapId,
297+
nearIntentsSpecific: firstStep?.nearIntentsSpecific,
298+
// Add your swapper's metadata extraction here:
299+
[swapperName]Specific: firstStep?.[swapperName]Specific,
300+
relayTransactionMetadata: firstStep?.relayTransactionMetadata,
301+
stepIndex: currentHopIndex,
302+
quoteId: activeQuote.id,
303+
streamingSwapMetadata: { ... }
304+
}
305+
```
306+
307+
**Place 2**: `src/lib/tradeExecution.ts` (CRITICAL - often forgotten!)
308+
309+
Add to metadata object around line 156-161:
310+
```typescript
311+
metadata: {
312+
...swap.metadata,
313+
chainflipSwapId: tradeQuote.steps[0]?.chainflipSpecific?.chainflipSwapId,
314+
nearIntentsSpecific: tradeQuote.steps[0]?.nearIntentsSpecific,
315+
// Add your swapper's metadata extraction here:
316+
[swapperName]Specific: tradeQuote.steps[0]?.[swapperName]Specific,
317+
relayTransactionMetadata: tradeQuote.steps[0]?.relayTransactionMetadata,
318+
stepIndex,
319+
}
320+
```
321+
322+
**Why both places?**
323+
- `useTradeButtonProps` creates the initial swap (before wallet signature)
324+
- `tradeExecution` updates the swap during execution (after wallet signature, with actual tradeQuote)
325+
- If you only add to one place, metadata will be missing!
326+
327+
**d. Access in status check** (`packages/swapper/src/swappers/[Swapper]/endpoints.ts`):
328+
329+
```typescript
330+
checkTradeStatus: async ({ config, swap }) => {
331+
const { [swapperName]Specific } = swap?.metadata ?? {}
332+
333+
if (![swapperName]Specific?.swapId) {
334+
throw new Error('swapId is required for status check')
335+
}
336+
337+
// Use metadata to poll API
338+
const status = await api.getStatus([swapperName]Specific.swapId)
339+
// ...
340+
}
341+
```
342+
343+
**Example: NEAR Intents metadata flow**
344+
```
345+
1. Quote: Store in step.nearIntentsSpecific.depositAddress
346+
2. Swap creation: Extract to swap.metadata.nearIntentsSpecific
347+
3. Status check: Read from swap.metadata.nearIntentsSpecific.depositAddress
348+
```
349+
350+
#### Step 4: Register the swapper
225351

226352
Update these files to register your new swapper:
227353

@@ -234,12 +360,26 @@ Update these files to register your new swapper:
234360
- Export new swapper
235361

236362
3. **`packages/swapper/src/types.ts`**
237-
- Add transaction metadata type if needed
238-
- Add API config fields
363+
- Add API config fields (if not already done in metadata step)
239364

240365
4. **CSP Headers** (if swapper calls external API):
241-
- Add API domain to `headers/csps/index.ts`
242-
- Create `headers/csps/defi/swappers/[SwapperName].ts` with CSP rules
366+
- Create `headers/csps/defi/swappers/[SwapperName].ts`:
367+
```typescript
368+
import type { Csp } from '../../../types'
369+
370+
export const csp: Csp = {
371+
'connect-src': ['https://api.[swapper].com'],
372+
}
373+
```
374+
- Register in `headers/csps/index.ts`:
375+
```typescript
376+
import { csp as [swapperName] } from './defi/swappers/[SwapperName]'
377+
378+
export const csps = [
379+
// ... other csps
380+
[swapperName],
381+
]
382+
```
243383

244384
5. **UI Integration** (`src/`):
245385

@@ -277,11 +417,14 @@ Update these files to register your new swapper:
277417

278418
**d. Wire up feature flag:**
279419
- File: `src/state/helpers.ts`
280-
- Add to `isCrossAccountTradeSupported` (if applicable)
281-
- Add to `getEnabledSwappers`:
420+
- Add to `isCrossAccountTradeSupported` function parameter and switch statement (if swapper supports cross-account)
421+
- Add to `getEnabledSwappers` function:
282422
```typescript
283423
export const getEnabledSwappers = (
284-
{ [SwapperName]Swap, ...otherFlags }: FeatureFlags,
424+
{
425+
[SwapperName]Swap, // Add to destructured parameters
426+
...otherFlags
427+
}: FeatureFlags,
285428
...
286429
): Record<SwapperName, boolean> => {
287430
return {
@@ -292,9 +435,15 @@ Update these files to register your new swapper:
292435
}
293436
```
294437

295-
**e. Update test mocks (if applicable):**
438+
**e. Update test mocks (REQUIRED):**
296439
- File: `src/test/mocks/store.ts`
297-
- Add feature flag to mock state
440+
- Add feature flag to mock featureFlags object:
441+
```typescript
442+
featureFlags: {
443+
// ... other flags
444+
[SwapperName]Swap: false,
445+
}
446+
```
298447

299448
6. **Configuration**:
300449

.env

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ VITE_FEATURE_ARBITRUM_BRIDGE=true
3535
VITE_FEATURE_CUSTOM_TOKEN_IMPORT=true
3636
VITE_FEATURE_USDT_APPROVAL_RESET=true
3737
VITE_FEATURE_PORTALS_SWAPPER=true
38+
VITE_FEATURE_NEAR_INTENTS_SWAP=false
3839
VITE_FEATURE_RUNEPOOL=true
3940
VITE_FEATURE_RUNEPOOL_DEPOSIT=true
4041
VITE_FEATURE_RUNEPOOL_WITHDRAW=true
@@ -188,6 +189,9 @@ VITE_FEATURE_BEBOP_SWAP=false
188189
VITE_CHAINFLIP_API_KEY=09bc0796ff40435482c0a54fa6ae2784
189190
VITE_CHAINFLIP_API_URL=https://chainflip-broker.io
190191

192+
# NEAR Intents
193+
VITE_NEAR_INTENTS_API_KEY=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjIwMjUtMDQtMjMtdjEifQ.eyJ2IjoxLCJrZXlfdHlwZSI6ImludGVncmF0aW9ucyIsInBhcnRuZXJfaWQiOiJnb21lcy1zaGFwZS1zaGlmdCIsImlhdCI6MTc2MjgxNjQ3MCwiZXhwIjoxNzk0MzUyNDcwfQ.BHFPJ1y-UnVBN3Y_PtMfP9MRng-hKPowYDLOeLj4Cnsvs9lNgikgaC_e41PO4LduMKiRRrtwfRhzUfV5Usdsf8IS9U7mF1UrUwDqyEEOF10weJWYU36Gg6NyNuIFgJhvV8sFzwPShbenLHIR3gIZ97pyBmpJ8jTDZu7ncw6kVqY6hcsu6H9Pyl9jYNdSwhdWUgZ9UswPPTeecrF1wgQPpE3i3tNT-fTbDtj-DswmEIT3f8qgfgZBi7cde68gsGiVy7v0cSE2r8y9UwFWejuUoltUDrTEmF6lCJHGuCaKYqGqZs2MBiIr5xnYpzlsKFTYlUNa8cTTcXng_pzWd5LrsA
194+
191195
# relay
192196
VITE_FEATURE_SWAPPER_RELAY=true
193197
VITE_RELAY_API_URL=https://api.relay.link

.env.development

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# Swapper feature flags
66
VITE_FEATURE_BEBOP_SWAP=true
7+
VITE_FEATURE_NEAR_INTENTS_SWAP=true
78

89
# feature flags
910
VITE_FEATURE_THORCHAIN_TCY_ACTIVITY=true
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import type { Csp } from '../../../types'
2+
3+
export const csp: Csp = {
4+
'connect-src': ['https://1click.chaindefuser.com'],
5+
}

headers/csps/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { csp as zeroX } from './defi/swappers/0x'
3030
import { csp as bebop } from './defi/swappers/Bebop'
3131
import { csp as butterSwap } from './defi/swappers/ButterSwap'
3232
import { csp as cowSwap } from './defi/swappers/CowSwap'
33+
import { csp as nearIntents } from './defi/swappers/NearIntents'
3334
import { csp as oneInch } from './defi/swappers/OneInch'
3435
import { csp as portals } from './defi/swappers/Portals'
3536
import { csp as thor } from './defi/swappers/Thor'
@@ -107,6 +108,7 @@ export const csps = [
107108
zeroX,
108109
bebop,
109110
cowSwap,
111+
nearIntents,
110112
oneInch,
111113
portals,
112114
thor,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@chakra-ui/system": "^2.6.2",
7777
"@chakra-ui/tag": "^3.1.1",
7878
"@cowprotocol/app-data": "^2.3.0",
79+
"@defuse-protocol/one-click-sdk-typescript": "^0.1.1-0.2",
7980
"@emotion/react": "^11.13.0",
8081
"@emotion/styled": "^11.13.0",
8182
"@formatjs/intl-getcanonicallocales": "^2.3.0",

packages/swapper/src/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { jupiterApi } from './swappers/JupiterSwapper/endpoints'
1414
import { jupiterSwapper } from './swappers/JupiterSwapper/JupiterSwapper'
1515
import { mayachainApi } from './swappers/MayachainSwapper/endpoints'
1616
import { mayachainSwapper } from './swappers/MayachainSwapper/MayachainSwapper'
17+
import { nearIntentsApi } from './swappers/NearIntentsSwapper/endpoints'
18+
import { nearIntentsSwapper } from './swappers/NearIntentsSwapper/NearIntentsSwapper'
1719
import { portalsApi } from './swappers/PortalsSwapper/endpoints'
1820
import { portalsSwapper } from './swappers/PortalsSwapper/PortalsSwapper'
1921
import { relaySwapper } from './swappers/RelaySwapper'
@@ -80,12 +82,17 @@ export const swappers: Record<SwapperName, (SwapperApi & Swapper) | undefined> =
8082
...bebopSwapper,
8183
...bebopApi,
8284
},
85+
[SwapperName.NearIntents]: {
86+
...nearIntentsSwapper,
87+
...nearIntentsApi,
88+
},
8389
[SwapperName.Test]: undefined,
8490
}
8591

8692
// Slippage defaults. Don't export these to ensure the getDefaultSlippageDecimalPercentageForSwapper helper function is used.
8793
const DEFAULT_SLIPPAGE_DECIMAL_PERCENTAGE = '0.002' // .2%
8894
const DEFAULT_COWSWAP_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' // .5%
95+
const DEFAULT_NEAR_INTENTS_SLIPPAGE_DECIMAL_PERCENTAGE = '0.005' // .5%
8996
const DEFAULT_PORTALS_SLIPPAGE_DECIMAL_PERCENTAGE = '0.025' // 2.5%
9097
const DEFAULT_THOR_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' // 1%
9198
const DEFAULT_MAYA_SLIPPAGE_DECIMAL_PERCENTAGE = '0.01' // 1%
@@ -120,6 +127,8 @@ export const getDefaultSlippageDecimalPercentageForSwapper = (
120127
throw new Error('Default slippage not supported by Relay')
121128
case SwapperName.ButterSwap:
122129
return DEFAULT_BUTTERSWAP_SLIPPAGE_DECIMAL_PERCENTAGE
130+
case SwapperName.NearIntents:
131+
return DEFAULT_NEAR_INTENTS_SLIPPAGE_DECIMAL_PERCENTAGE
123132
default:
124133
return assertUnreachable(swapperName)
125134
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { Swapper } from '../../types'
2+
import { executeEvmTransaction, executeSolanaTransaction } from '../../utils'
3+
4+
export const nearIntentsSwapper: Swapper = {
5+
executeEvmTransaction,
6+
executeSolanaTransaction,
7+
executeUtxoTransaction: (txToSign, { signAndBroadcastTransaction }) => {
8+
return signAndBroadcastTransaction(txToSign)
9+
},
10+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const ONE_CLICK_BASE_URL = 'https://1click.chaindefuser.com'
2+
3+
export const DEFAULT_SLIPPAGE_BPS = 50
4+
5+
export const DEFAULT_QUOTE_DEADLINE_MS = 30 * 60 * 1000

0 commit comments

Comments
 (0)