Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: network requests #42

Merged
merged 14 commits into from
Jan 29, 2025
5 changes: 5 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@ VITE_FOO=bar
# chat woot
VITE_CHATWOOT_TOKEN=jmoXp9BPMSPEYHeJX5YKT15Q
VITE_CHATWOOT_URL=https://app.chatwoot.com

# Chainflip

VITE_CHAINFLIP_API_KEY=09bc0796ff40435482c0a54fa6ae2784
VITE_CHAINFLIP_API_URL=https://chainflip-broker.io
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@emotion/styled": "^11.11.0",
"@shapeshiftoss/caip": "^8.15.0",
"@shapeshiftoss/types": "^8.6.0",
"@tanstack/react-query": "^5.65.1",
"axios": "^1.6.5",
"framer-motion": "^10.17.9",
"match-sorter": "^6.3.1",
Expand All @@ -30,6 +31,7 @@
"viem": "^2.0.3"
},
"devDependencies": {
"@testing-library/react": "^16.2.0",
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
"@types/inquirer": "^9.0.7",
"@types/mixpanel-browser": "^2.48.1",
"@types/node": "^20.10.7",
Expand Down
13 changes: 9 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import './App.css'

import { Center } from '@chakra-ui/react'
import { QueryClientProvider } from '@tanstack/react-query'
import { createBrowserRouter, RouterProvider } from 'react-router-dom'
import { ChatwootButton } from 'components/Chatwoot'
import { SelectPair } from 'components/SelectPair'
import { Status } from 'components/Status/Status'
import { TradeInput } from 'components/TradeInput'

import { queryClient } from './config/react-query'

const router = createBrowserRouter([
{
path: '/',
Expand All @@ -26,10 +29,12 @@ function App() {
console.log(import.meta.env.VITE_FOO)

return (
<Center width='full' height='100vh' flexDir='column'>
<RouterProvider router={router} />
<ChatwootButton />
</Center>
<QueryClientProvider client={queryClient}>
<Center width='full' height='100vh' flexDir='column'>
<RouterProvider router={router} />
<ChatwootButton />
</Center>
</QueryClientProvider>
)
}

Expand Down
11 changes: 11 additions & 0 deletions src/config/react-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { QueryClient } from '@tanstack/react-query'

export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 60 * 24, // 24 hours
retry: 3,
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
},
},
})
60 changes: 60 additions & 0 deletions src/constants/caip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Subset of web caip constants with Chainfli-supported assets only
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
// We need to redeclare things here as @shapeshiftoss/caip is actually a monorepo project and not published on npm
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any way to make it exported from our monorepo so we can consume it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not for the time being unfortunately, unless we start publishing packages again, which is a story for another day but would indeed make things a lot easier!


import type { AssetId, ChainId } from '@shapeshiftoss/caip'

export const btcAssetId: AssetId = 'bip122:000000000019d6689c085ae165831e93/slip44:0'

export const ethAssetId: AssetId = 'eip155:1/slip44:60'
export const arbitrumAssetId: AssetId = 'eip155:42161/slip44:60'
export const solAssetId: AssetId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/slip44:501'
export const wrappedSolAssetId: AssetId =
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:So11111111111111111111111111111111111111112'
export const usdtAssetId: AssetId = 'eip155:1/erc20:0xdac17f958d2ee523a2206206994597c13d831ec7'
export const usdcAssetId: AssetId = 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'
export const usdcOnArbitrumOneAssetId: AssetId =
'eip155:42161/erc20:0xaf88d065e77c8cc2239327c5edb3a432268e5831'
export const usdcOnSolanaAssetId: AssetId =
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp/token:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
export const flipAssetId: AssetId = 'eip155:1/erc20:0x826180541412d574cf1336d22c0c0a287822678a'

export const btcChainId: ChainId = 'bip122:000000000019d6689c085ae165831e93'

export const ethChainId: ChainId = 'eip155:1'
export const arbitrumChainId: ChainId = 'eip155:42161'
export const baseChainId: ChainId = 'eip155:8453'

export const solanaChainId: ChainId = 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'

export const CHAIN_NAMESPACE = {
Evm: 'eip155',
Utxo: 'bip122',
CosmosSdk: 'cosmos',
Solana: 'solana',
} as const

export const CHAIN_REFERENCE = {
EthereumMainnet: '1',
BitcoinMainnet: '000000000019d6689c085ae165831e93',
ArbitrumMainnet: '42161', // https://chainlist.org/chain/42161
ArbitrumNovaMainnet: '42170', // https://chainlist.org/chain/42170
BaseMainnet: '8453', // https://chainlist.org/chain/8453
SolanaMainnet: '5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // https://namespaces.chainagnostic.org/solana/caip2
} as const

export const ASSET_NAMESPACE = {
erc20: 'erc20',
erc721: 'erc721',
erc1155: 'erc1155',
slip44: 'slip44',
splToken: 'token',
} as const

export const ASSET_REFERENCE = {
Bitcoin: '0',
Ethereum: '60',
Arbitrum: '60', // evm chain which uses ethereum derivation path as common practice
Solana: '501',
} as const

export const FEE_ASSET_IDS = [ethAssetId, btcAssetId, arbitrumAssetId, solAssetId]
2 changes: 2 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ interface ImportMetaEnv {
readonly VITE_CHATWOOT_URL: string
readonly VITE_CHATWOOT_TOKEN: string
readonly VITE_FEATURE_CHATWOOT: boolean
readonly VITE_CHAINFLIP_API_KEY: string
readonly VITE_CHAINFLIP_API_URL: string
}

interface ImportMeta {
Expand Down
91 changes: 91 additions & 0 deletions src/queries/chainflip/assets.test.ts
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { btcAssetId, ethAssetId, usdcOnSolanaAssetId } from 'constants/caip'
import { describe, expect, it } from 'vitest'

import { transformChainflipAssets } from './assets'
import type { ChainflipAssetsResponse } from './types'

describe('transformChainflipAssets', () => {
it('should transform multiple chainflip assets to assetIds', () => {
const mockResponse: ChainflipAssetsResponse = {
assets: [
{
id: 'btc.btc',
direction: 'both',
ticker: 'BTC',
name: 'Bitcoin',
network: 'Bitcoin',
networkLogo: '/networks/btc/logo.svg',
assetLogo: '/assets/btc.btc/logo.svg',
decimals: 8,
minimalAmount: 0.0007,
minimalAmountNative: '70000',
usdPrice: 102680.1024594243,
usdPriceNative: '102680102459',
},
{
id: 'eth.eth',
direction: 'both',
ticker: 'ETH',
name: 'Ethereum',
network: 'Ethereum',
networkLogo: '/networks/eth/logo.svg',
assetLogo: '/assets/eth.eth/logo.svg',
decimals: 18,
minimalAmount: 0.01,
minimalAmountNative: '10000000000000000',
usdPrice: 3175.43595522135,
usdPriceNative: '3175435955',
},
],
}

expect(transformChainflipAssets(mockResponse)).toEqual([btcAssetId, ethAssetId])
})

it('should transform single chainflip asset to assetId', () => {
const mockResponse: ChainflipAssetsResponse = {
assets: [
{
id: 'usdc.sol',
direction: 'both',
ticker: 'USDC',
name: 'solUSDC',
network: 'Solana',
contractAddress: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
networkLogo: '/networks/sol/logo.svg',
assetLogo: '/assets/usdc.sol/logo.svg',
decimals: 6,
minimalAmount: 10,
minimalAmountNative: '10000000',
usdPrice: 1.0014781977,
usdPriceNative: '1001478',
},
],
}

expect(transformChainflipAssets(mockResponse)).toEqual([usdcOnSolanaAssetId])
})

it('should return empty array for unsupported assets', () => {
const mockResponse: ChainflipAssetsResponse = {
assets: [
{
id: 'unsupported.asset',
direction: 'both',
ticker: 'UNSUPPORTED',
name: 'Unsupported Asset',
network: 'Unknown',
networkLogo: '/networks/unknown/logo.svg',
assetLogo: '/assets/unknown.asset/logo.svg',
decimals: 18,
minimalAmount: 1,
minimalAmountNative: '1000000000000000000',
usdPrice: 1,
usdPriceNative: '1000000000000000000',
},
],
}

expect(transformChainflipAssets(mockResponse)).toEqual([])
})
})

Check failure on line 91 in src/queries/chainflip/assets.test.ts

View workflow job for this annotation

GitHub Actions / Install Lint Type-check

Replace `·` with `⏎`
48 changes: 48 additions & 0 deletions src/queries/chainflip/assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useQuery } from '@tanstack/react-query'

Check failure on line 1 in src/queries/chainflip/assets.ts

View workflow job for this annotation

GitHub Actions / Install Lint Type-check

Run autofix to sort these imports!
import axios from 'axios'
import {
arbitrumAssetId,
btcAssetId,
ethAssetId,
flipAssetId,
solAssetId,
usdcAssetId,
usdcOnArbitrumOneAssetId,
usdcOnSolanaAssetId,
usdtAssetId,
} from 'constants/caip'
import type { ChainflipAssetsResponse } from './types'

const CHAINFLIP_API_URL = import.meta.env.VITE_CHAINFLIP_API_URL
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved

// Map Chainflip internal asset IDs to CAIPs
const chainflipToAssetId: Record<string, string> = {
'btc.btc': btcAssetId,
'eth.arb': arbitrumAssetId,
'eth.eth': ethAssetId,
'flip.eth': flipAssetId,
'sol.sol': solAssetId,
'usdc.arb': usdcOnArbitrumOneAssetId,
'usdc.eth': usdcAssetId,
'usdc.sol': usdcOnSolanaAssetId,
'usdt.eth': usdtAssetId,
}

export const transformChainflipAssets = (data: ChainflipAssetsResponse) => {
return data.assets
.map(asset => chainflipToAssetId[asset.id])
.filter((assetId): assetId is string => Boolean(assetId))
}

export const useChainflipAssetsQuery = () => {
return useQuery({
queryKey: ['chainflip', 'assets'],
queryFn: async () => {
const { data } = await axios.get<ChainflipAssetsResponse>(

Check failure on line 41 in src/queries/chainflip/assets.ts

View workflow job for this annotation

GitHub Actions / Install Lint Type-check

Replace `⏎········`${CHAINFLIP_API_URL}/assets`,⏎······` with ``${CHAINFLIP_API_URL}/assets``
`${CHAINFLIP_API_URL}/assets`,
)
return data
},
select: transformChainflipAssets,
})
}

Check failure on line 48 in src/queries/chainflip/assets.ts

View workflow job for this annotation

GitHub Actions / Install Lint Type-check

Replace `·` with `⏎`
30 changes: 30 additions & 0 deletions src/queries/chainflip/quote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useQuery } from '@tanstack/react-query'
import axios from 'axios'

import type { ChainflipQuote, ChainflipQuoteParams } from './types'

const CHAINFLIP_API_URL = import.meta.env.VITE_CHAINFLIP_API_URL
const CHAINFLIP_API_KEY = import.meta.env.VITE_CHAINFLIP_API_KEY

export const useChainflipQuoteQuery = (params: ChainflipQuoteParams) => {
const { sourceAsset, destinationAsset, amount, commissionBps } = params

return useQuery({
queryKey: ['chainflip', 'quote', params],
gomesalexandre marked this conversation as resolved.
Show resolved Hide resolved
queryFn: async () => {
const { data } = await axios.get<ChainflipQuote[]>(`${CHAINFLIP_API_URL}/quotes-native`, {
params: {
apiKey: CHAINFLIP_API_KEY,
sourceAsset,
destinationAsset,
amount,
...(commissionBps && { commissionBps }),
},
})

// TODO(gomes): select best quote based on output amount, since o.g doesn't support multiple quotes
return data[0]
},
})
}

Check failure on line 30 in src/queries/chainflip/quote.ts

View workflow job for this annotation

GitHub Actions / Install Lint Type-check

Delete `⏎`
70 changes: 70 additions & 0 deletions src/queries/chainflip/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
export type ChainflipAsset = {
id: string // e.g. "flip.eth"
direction: 'both' | 'ingress' | 'egress'
ticker: string
name: string
network: string
contractAddress?: string
networkLogo: string
assetLogo: string
decimals: number
minimalAmount: number
minimalAmountNative: string
usdPrice: number
usdPriceNative: string
}

export type ChainflipAssetsResponse = {
assets: ChainflipAsset[]
}

export type ChainflipQuoteFee = {
type: 'ingress' | 'network' | 'broker' | 'egress' | 'liquidity'
asset: string
amount: number
amountNative: string
}

export type ChainflipPoolInfo = {
baseAsset: string
quoteAsset: string
fee: {
asset: string
amount: number
amountNative: string
}
}

export type ChainflipQuote = {
type: 'regular' | 'dca'
ingressAsset: string
ingressAmount: number
ingressAmountNative: string
intermediateAsset: string | null
intermediateAmount: number | null
intermediateAmountNative: string | null
egressAsset: string
egressAmount: number
egressAmountNative: string
includedFees: ChainflipQuoteFee[]
recommendedSlippageTolerancePercent: number
lowLiquidityWarning: boolean
poolInfo: ChainflipPoolInfo[]
estimatedDurationSeconds: number
estimatedDurationsSeconds: {
deposit: number
swap: number
egress: number
}
estimatedPrice: number
chunkIntervalBlocks?: number | null
numberOfChunks?: number | null
boostQuote?: ChainflipQuote
}

export type ChainflipQuoteParams = {
sourceAsset: string
destinationAsset: string
amount: string
commissionBps?: number
}

Check failure on line 70 in src/queries/chainflip/types.ts

View workflow job for this annotation

GitHub Actions / Install Lint Type-check

Replace `·` with `⏎`
Loading
Loading