mirror of
https://github.com/Brotocol-xyz/bro-sdk.git
synced 2026-01-12 22:25:00 +08:00
docs: add cross-chain swap example
This commit is contained in:
@@ -54,6 +54,7 @@
|
||||
|
||||
### Other Changes
|
||||
|
||||
- Added cross-chain swap example code for developer reference
|
||||
- Multiple Stacks contract upgrades
|
||||
- Support for new fee charge model
|
||||
- Upgraded viem dependency
|
||||
|
||||
24
examples/cross-chain-swap/.gitignore
vendored
Normal file
24
examples/cross-chain-swap/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
29
examples/cross-chain-swap/README.md
Normal file
29
examples/cross-chain-swap/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# XLink SDK Cross-Chain Swap Demo
|
||||
|
||||
This demo project showcases how to implement cross-chain asset exchange functionality using XLink SDK and ALEX SDK.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This demo application demonstrates how to integrate XLink SDK and ALEX SDK to create a complete cross-chain swap experience. Users can seamlessly transfer and exchange digital assets between different blockchains.
|
||||
|
||||
## Usage
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Ensure you have the following tools installed:
|
||||
- Node.js
|
||||
- pnpm (recommended) or npm
|
||||
|
||||
### Install Dependencies
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### Run Development Server
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
The application will run at http://localhost:5173
|
||||
13
examples/cross-chain-swap/index.html
Normal file
13
examples/cross-chain-swap/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
37
examples/cross-chain-swap/package.json
Normal file
37
examples/cross-chain-swap/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "xlink-sdk-cross-chain-swap-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"simulation": "tsx simulation/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@stacks/common": "^7.0.2",
|
||||
"@stacks/connect": "^8.1.7",
|
||||
"@stacks/network": "^7.0.2",
|
||||
"@stacks/stacks-blockchain-api-types": "^7.14.1",
|
||||
"@stacks/transactions": "^7.0.5",
|
||||
"@xlink-network/xlink-sdk": "file:../..",
|
||||
"alex-sdk": "github:alexgo-io/alex-sdk#feat/detailed-swap-route-info",
|
||||
"c32check": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-query": "^3.39.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"prettier": "^3.5.3",
|
||||
"stxer": "^0.4.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2",
|
||||
"vite": "^6.2.2"
|
||||
}
|
||||
}
|
||||
2141
examples/cross-chain-swap/pnpm-lock.yaml
generated
Normal file
2141
examples/cross-chain-swap/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
0
examples/cross-chain-swap/public/.gitkeep
Normal file
0
examples/cross-chain-swap/public/.gitkeep
Normal file
397
examples/cross-chain-swap/src/App.css
Normal file
397
examples/cross-chain-swap/src/App.css
Normal file
@@ -0,0 +1,397 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 2rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.3rem;
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.8rem;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input, select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
margin: 0.3rem 0;
|
||||
background-color: #f3f4f6;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.routes-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.route-item {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
background-color: #f5f5f5;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.selected-route {
|
||||
border: 2px solid #646cff;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
background-color: #efefff;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
color: #213547;
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.routes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.route-card {
|
||||
padding: 15px;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background-color: #f3f4f6;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.route-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.route-card.selected {
|
||||
border-color: #646cff;
|
||||
background-color: #f5f6ff;
|
||||
}
|
||||
|
||||
.route-card p {
|
||||
margin: 5px 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.routes-section {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.routes-section h3 {
|
||||
color: #1a1a1a;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.routes-section p {
|
||||
color: #6b7280;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.bridge-info {
|
||||
background-color: #ffffff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.bridge-info pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background-color: #ffffff;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
padding: 2rem 0;
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
background-color: #ffffff;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.app-footer p {
|
||||
margin: 0;
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.swap-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.route-select {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.amount-input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.route-dropdown {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background-color: #f3f4f6;
|
||||
font-size: 1rem;
|
||||
color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.route-dropdown:hover {
|
||||
border-color: #646cff;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.route-dropdown:focus {
|
||||
outline: none;
|
||||
border-color: #646cff;
|
||||
box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.amount-field {
|
||||
width: 100%;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background-color: #f3f4f6;
|
||||
font-size: 1rem;
|
||||
color: #1a1a1a;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.amount-field:hover {
|
||||
border-color: #646cff;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.amount-field:focus {
|
||||
outline: none;
|
||||
border-color: #646cff;
|
||||
box-shadow: 0 0 0 2px rgba(100, 108, 255, 0.2);
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.no-routes {
|
||||
color: #6b7280;
|
||||
font-size: 0.9rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 2rem;
|
||||
background-color: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 3px solid #f3f4f6;
|
||||
border-top: 3px solid #646cff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
29
examples/cross-chain-swap/src/App.tsx
Normal file
29
examples/cross-chain-swap/src/App.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { XLinkSDK } from "@xlink-network/xlink-sdk"
|
||||
import { AlexSDK } from "alex-sdk"
|
||||
import { FC } from "react"
|
||||
import "./App.css"
|
||||
import { SwapRouteSelector } from "./components/SwapRouteSelector"
|
||||
import { QueryClient, QueryClientProvider } from "react-query"
|
||||
|
||||
const alex = new AlexSDK()
|
||||
const xlink = new XLinkSDK()
|
||||
const queryClient = new QueryClient()
|
||||
|
||||
const App: FC = () => {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="app-container">
|
||||
<header className="app-header">
|
||||
<h1>XLink Cross-Chain Swap Demo</h1>
|
||||
</header>
|
||||
<main className="app-main">
|
||||
<div className="content-wrapper">
|
||||
<SwapRouteSelector alexSDK={alex} xlinkSDK={xlink} />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
319
examples/cross-chain-swap/src/components/SwapRouteSelector.tsx
Normal file
319
examples/cross-chain-swap/src/components/SwapRouteSelector.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import {
|
||||
KnownChainId,
|
||||
KnownRoute,
|
||||
StacksContractAddress,
|
||||
SwapRoute_WithExchangeRate,
|
||||
toSDKNumberOrUndefined,
|
||||
XLinkSDK,
|
||||
} from "@xlink-network/xlink-sdk"
|
||||
import { AlexSDK } from "alex-sdk"
|
||||
import { FC, Fragment, useEffect, useState } from "react"
|
||||
import { useQuery } from "react-query"
|
||||
import { getAvailableRoutes } from "../utils/getAvailableRoutes"
|
||||
import { getSwapRoutesViaALEX } from "../utils/getSwapRoutesViaALEX"
|
||||
import { getSwapRoutesViaEVMDEX } from "../utils/getSwapRoutesViaEVMDEX"
|
||||
import { formatXLinkSDKChainName } from "../utils/formatXLinkSDKChainName"
|
||||
import { useDebouncedValue } from "../hooks/useDebouncedValue"
|
||||
|
||||
const STORAGE_KEY = "xlink_matcha_api_key"
|
||||
|
||||
export const SwapRouteSelector: FC<{
|
||||
alexSDK: AlexSDK
|
||||
xlinkSDK: XLinkSDK
|
||||
}> = ({ alexSDK, xlinkSDK }) => {
|
||||
const [matchaAPIKey, setMatchaAPIKey] = useState(() => {
|
||||
return localStorage.getItem(STORAGE_KEY) || ""
|
||||
})
|
||||
const [swapAmount, setSwapAmount] = useState("")
|
||||
const [selectedRoute, setSelectedRoute] = useState<null | KnownRoute>(null)
|
||||
const [selectedSwapRoute, setSelectedSwapRoute] =
|
||||
useState<null | SwapRoute_WithExchangeRate>(null)
|
||||
|
||||
const debouncedSwapAmount = useDebouncedValue(swapAmount, 500)
|
||||
const debouncedRoute = useDebouncedValue(selectedRoute, 500)
|
||||
const debouncedAPIKey = useDebouncedValue(matchaAPIKey, 500)
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(STORAGE_KEY, matchaAPIKey)
|
||||
}, [matchaAPIKey])
|
||||
|
||||
const availableRoutes = useQuery({
|
||||
queryKey: ["availableRoutes"],
|
||||
queryFn: () => getAvailableRoutes(xlinkSDK),
|
||||
})
|
||||
|
||||
const alexRoutes = useQuery({
|
||||
enabled: !!debouncedRoute && !!debouncedSwapAmount,
|
||||
queryKey: [
|
||||
"alexRoutes",
|
||||
JSON.stringify(debouncedRoute),
|
||||
debouncedSwapAmount,
|
||||
],
|
||||
queryFn: () => {
|
||||
if (debouncedRoute == null) {
|
||||
throw new Error("No route selected")
|
||||
}
|
||||
if (!isNumber(debouncedSwapAmount)) {
|
||||
throw new Error("No swap amount")
|
||||
}
|
||||
|
||||
return getSwapRoutesViaALEX(
|
||||
{
|
||||
alexSDK: alexSDK,
|
||||
xlinkSDK: xlinkSDK,
|
||||
},
|
||||
{
|
||||
...debouncedRoute,
|
||||
amount: toSDKNumberOrUndefined(Number(debouncedSwapAmount)),
|
||||
slippage: toSDKNumberOrUndefined(0.01),
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const evmDexRoutes = useQuery({
|
||||
enabled: !!debouncedRoute && !!debouncedSwapAmount && !!debouncedAPIKey,
|
||||
queryKey: [
|
||||
"evmDexRoutes",
|
||||
JSON.stringify(debouncedRoute),
|
||||
debouncedSwapAmount,
|
||||
],
|
||||
queryFn: () => {
|
||||
if (debouncedRoute == null) {
|
||||
throw new Error("No route selected")
|
||||
}
|
||||
if (!debouncedAPIKey) {
|
||||
throw new Error("No matcha API key")
|
||||
}
|
||||
if (!isNumber(debouncedSwapAmount)) {
|
||||
throw new Error("No swap amount")
|
||||
}
|
||||
|
||||
return getSwapRoutesViaEVMDEX(
|
||||
{
|
||||
xlinkSDK: xlinkSDK,
|
||||
matchaAPIKey: debouncedAPIKey,
|
||||
},
|
||||
{
|
||||
...debouncedRoute,
|
||||
amount: toSDKNumberOrUndefined(Number(debouncedSwapAmount)),
|
||||
slippage: toSDKNumberOrUndefined(0.01),
|
||||
},
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const bridgeInfo = useQuery({
|
||||
enabled: !!selectedRoute && !!selectedSwapRoute,
|
||||
queryKey: [
|
||||
"bridgeInfo",
|
||||
JSON.stringify(selectedRoute),
|
||||
JSON.stringify(selectedSwapRoute),
|
||||
],
|
||||
queryFn: () => {
|
||||
if (selectedRoute == null) {
|
||||
throw new Error("No route selected")
|
||||
}
|
||||
if (selectedSwapRoute == null) {
|
||||
throw new Error("No swap route selected")
|
||||
}
|
||||
|
||||
if (KnownChainId.isBitcoinChain(selectedRoute.fromChain)) {
|
||||
return xlinkSDK.bridgeInfoFromBitcoin({
|
||||
...selectedRoute,
|
||||
swapRoute: selectedSwapRoute,
|
||||
amount: toSDKNumberOrUndefined(Number(swapAmount)),
|
||||
})
|
||||
}
|
||||
|
||||
if (KnownChainId.isBRC20Chain(selectedRoute.fromChain)) {
|
||||
return xlinkSDK.bridgeInfoFromBRC20({
|
||||
...selectedRoute,
|
||||
swapRoute: selectedSwapRoute,
|
||||
amount: toSDKNumberOrUndefined(Number(swapAmount)),
|
||||
})
|
||||
}
|
||||
|
||||
if (KnownChainId.isRunesChain(selectedRoute.fromChain)) {
|
||||
return xlinkSDK.bridgeInfoFromRunes({
|
||||
...selectedRoute,
|
||||
swapRoute: selectedSwapRoute,
|
||||
amount: toSDKNumberOrUndefined(Number(swapAmount)),
|
||||
})
|
||||
}
|
||||
|
||||
if (KnownChainId.isEVMChain(selectedRoute.fromChain)) {
|
||||
throw new Error("EVM chain not support cross-chain swap yet")
|
||||
}
|
||||
|
||||
if (KnownChainId.isStacksChain(selectedRoute.fromChain)) {
|
||||
throw new Error("Stacks chain not support cross-chain swap yet")
|
||||
}
|
||||
|
||||
throw new Error("Unsupported chain: " + selectedRoute.fromChain)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
{availableRoutes.isLoading && (
|
||||
<div className="loading-overlay">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading routes...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="section">
|
||||
<h2>Basic Information</h2>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter 0x API Key"
|
||||
value={matchaAPIKey}
|
||||
onChange={e => setMatchaAPIKey(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>Swap Settings</h2>
|
||||
<div className="swap-group">
|
||||
<div className="route-select">
|
||||
<select
|
||||
value={selectedRoute ? JSON.stringify(selectedRoute) : ""}
|
||||
onChange={e =>
|
||||
setSelectedRoute(
|
||||
e.target.value ? JSON.parse(e.target.value) : null,
|
||||
)
|
||||
}
|
||||
className="route-dropdown"
|
||||
disabled={availableRoutes.isLoading}
|
||||
>
|
||||
<option value="">Select Route</option>
|
||||
{availableRoutes.data?.map((route, index) => (
|
||||
<option key={index} value={JSON.stringify(route)}>
|
||||
{route.fromTokenName} (
|
||||
{formatXLinkSDKChainName(route.fromChain)}) →{" "}
|
||||
{route.toTokenName} ({formatXLinkSDKChainName(route.toChain)})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="amount-input">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Enter swap amount"
|
||||
value={swapAmount}
|
||||
onChange={e => setSwapAmount(e.target.value)}
|
||||
className="amount-field"
|
||||
disabled={availableRoutes.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedRoute && (
|
||||
<div className="section">
|
||||
<h2>Swap Routes</h2>
|
||||
|
||||
<div className="routes-section">
|
||||
<h3>via ALEX</h3>
|
||||
{alexRoutes.isLoading ? (
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading ALEX routes...</p>
|
||||
</div>
|
||||
) : alexRoutes.data?.type === "success" ? (
|
||||
<div className="routes-grid">
|
||||
{alexRoutes.data.swapRoutes.map((route, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`route-card ${selectedSwapRoute === route ? "selected" : ""}`}
|
||||
onClick={() => setSelectedSwapRoute(route)}
|
||||
>
|
||||
<p>
|
||||
<StacksTokenName address={route.fromTokenAddress} />
|
||||
{route.swapPools.map((p, index) => (
|
||||
<Fragment key={index}>
|
||||
→
|
||||
<StacksTokenName address={p.toTokenAddress} />
|
||||
</Fragment>
|
||||
))}
|
||||
</p>
|
||||
<p>Exchange Rate: {route.composedExchangeRate}</p>
|
||||
<p>Minimum Receive: {route.minimumAmountsToReceive}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="no-routes">No ALEX routes available</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="routes-section">
|
||||
<h3>via EVM DEX</h3>
|
||||
{evmDexRoutes.isLoading ? (
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading EVM DEX routes...</p>
|
||||
</div>
|
||||
) : evmDexRoutes.data?.type === "success" ? (
|
||||
<div className="routes-grid">
|
||||
{evmDexRoutes.data.swapRoutes.map((route, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`route-card ${selectedSwapRoute === route ? "selected" : ""}`}
|
||||
onClick={() => setSelectedSwapRoute(route)}
|
||||
>
|
||||
<p>Route {index + 1}</p>
|
||||
<p>Chain: {route.evmChain}</p>
|
||||
<p>Exchange Rate: {route.composedExchangeRate}</p>
|
||||
<p>Minimum Receive: {route.minimumAmountsToReceive}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="no-routes">No EVM DEX routes available</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="section">
|
||||
<h2>Bridge Information</h2>
|
||||
{selectedSwapRoute && (
|
||||
<>
|
||||
{bridgeInfo.isLoading ? (
|
||||
<div className="loading-container">
|
||||
<div className="loading-spinner"></div>
|
||||
<p>Loading bridge information...</p>
|
||||
</div>
|
||||
) : (
|
||||
bridgeInfo.data && (
|
||||
<div className="bridge-info">
|
||||
<pre>{JSON.stringify(bridgeInfo.data, null, 2)}</pre>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const StacksTokenName: FC<{
|
||||
address: StacksContractAddress
|
||||
}> = ({ address }) => {
|
||||
return (
|
||||
<abbr title={`${address.deployerAddress}.${address.contractName}`}>
|
||||
{address.contractName}
|
||||
</abbr>
|
||||
)
|
||||
}
|
||||
|
||||
const isNumber = (value: string): value is `${number}` => {
|
||||
if (value === "") return false
|
||||
const num = Number(value)
|
||||
return !isNaN(num) && isFinite(num) && num > 0
|
||||
}
|
||||
16
examples/cross-chain-swap/src/hooks/useDebouncedValue.ts
Normal file
16
examples/cross-chain-swap/src/hooks/useDebouncedValue.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
export function useDebouncedValue<T>(value: T, delay: number): T {
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value)
|
||||
|
||||
useEffect(() => {
|
||||
const timeoutId = setTimeout(() => {
|
||||
setDebouncedValue(value)
|
||||
}, delay)
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
}
|
||||
}, [value, delay])
|
||||
|
||||
return debouncedValue
|
||||
}
|
||||
62
examples/cross-chain-swap/src/index.css
Normal file
62
examples/cross-chain-swap/src/index.css
Normal file
@@ -0,0 +1,62 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
10
examples/cross-chain-swap/src/main.tsx
Normal file
10
examples/cross-chain-swap/src/main.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import App from "./App"
|
||||
import "./index.css"
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
import { KnownChainId } from "../../node_modules/@xlink-network/xlink-sdk/lib/utils/types/knownIds"
|
||||
|
||||
export const formatXLinkSDKChainName = (
|
||||
chain: KnownChainId.KnownChain,
|
||||
): string => {
|
||||
if (KnownChainId.isBitcoinChain(chain)) {
|
||||
return "Bitcoin"
|
||||
}
|
||||
|
||||
if (KnownChainId.isBRC20Chain(chain)) {
|
||||
return "BRC-20"
|
||||
}
|
||||
|
||||
if (KnownChainId.isRunesChain(chain)) {
|
||||
return "Runes"
|
||||
}
|
||||
|
||||
if (KnownChainId.isStacksChain(chain)) {
|
||||
return "Stacks"
|
||||
}
|
||||
|
||||
if (KnownChainId.isEVMChain(chain)) {
|
||||
switch (chain) {
|
||||
case KnownChainId.EVM.Ethereum:
|
||||
return "Ethereum"
|
||||
case KnownChainId.EVM.Sepolia:
|
||||
return "Sepolia"
|
||||
case KnownChainId.EVM.BSC:
|
||||
return "BSC"
|
||||
case KnownChainId.EVM.BSCTestnet:
|
||||
return "BSC Testnet"
|
||||
case KnownChainId.EVM.CoreDAO:
|
||||
return "CoreDAO"
|
||||
case KnownChainId.EVM.CoreDAOTestnet:
|
||||
return "CoreDAO Testnet"
|
||||
case KnownChainId.EVM.Bsquared:
|
||||
return "B²"
|
||||
case KnownChainId.EVM.BOB:
|
||||
return "BOB"
|
||||
case KnownChainId.EVM.Bitlayer:
|
||||
return "Bitlayer"
|
||||
case KnownChainId.EVM.Lorenzo:
|
||||
return "Lorenzo"
|
||||
case KnownChainId.EVM.Merlin:
|
||||
return "Merlin"
|
||||
case KnownChainId.EVM.AILayer:
|
||||
return "AILayer"
|
||||
case KnownChainId.EVM.Mode:
|
||||
return "Mode"
|
||||
case KnownChainId.EVM.XLayer:
|
||||
return "XLayer"
|
||||
case KnownChainId.EVM.Arbitrum:
|
||||
return "Arbitrum"
|
||||
case KnownChainId.EVM.Aurora:
|
||||
return "Aurora"
|
||||
case KnownChainId.EVM.Manta:
|
||||
return "Manta"
|
||||
case KnownChainId.EVM.Linea:
|
||||
return "Linea"
|
||||
case KnownChainId.EVM.Base:
|
||||
return "Base"
|
||||
case KnownChainId.EVM.BlifeTestnet:
|
||||
return "Blife Testnet"
|
||||
case KnownChainId.EVM.BeraTestnet:
|
||||
return "Bera Testnet"
|
||||
default:
|
||||
return "Unknown EVM Chain"
|
||||
}
|
||||
}
|
||||
|
||||
return chain
|
||||
}
|
||||
68
examples/cross-chain-swap/src/utils/getAvailableRoutes.ts
Normal file
68
examples/cross-chain-swap/src/utils/getAvailableRoutes.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import {
|
||||
KnownChainId,
|
||||
KnownRoute,
|
||||
KnownTokenId,
|
||||
XLinkSDK,
|
||||
} from "@xlink-network/xlink-sdk"
|
||||
|
||||
export const getAvailableRoutes = async (
|
||||
xlinkSDK: XLinkSDK,
|
||||
): Promise<(KnownRoute & { fromTokenName: string; toTokenName: string })[]> => {
|
||||
const routes = await _getAvailableRoutes(xlinkSDK)
|
||||
return routes.map(
|
||||
(route): KnownRoute & { fromTokenName: string; toTokenName: string } =>
|
||||
({
|
||||
fromChain: route[0][0],
|
||||
fromToken: route[0][1],
|
||||
fromTokenName: route[0][2],
|
||||
toChain: route[1][0],
|
||||
toToken: route[1][1],
|
||||
toTokenName: route[1][2],
|
||||
}) as any,
|
||||
)
|
||||
}
|
||||
|
||||
type ChainTokenPair = readonly [
|
||||
chain: KnownChainId.KnownChain,
|
||||
token: KnownTokenId.KnownToken,
|
||||
tokenName: string,
|
||||
]
|
||||
|
||||
type AvailableRoute = readonly [from: ChainTokenPair, to: ChainTokenPair]
|
||||
|
||||
const _getAvailableRoutes = async (
|
||||
xlinkSDK: XLinkSDK,
|
||||
): Promise<AvailableRoute[]> => {
|
||||
const alexBrc20 = await xlinkSDK.brc20TickToBRC20Token(
|
||||
KnownChainId.BRC20.Mainnet,
|
||||
"alex$",
|
||||
)
|
||||
|
||||
const ausdBrc20 = await xlinkSDK.brc20TickToBRC20Token(
|
||||
KnownChainId.BRC20.Mainnet,
|
||||
"ausd$",
|
||||
)
|
||||
|
||||
const result: [from: ChainTokenPair, to: ChainTokenPair][] = []
|
||||
if (alexBrc20 != null) {
|
||||
result.push([
|
||||
[KnownChainId.Bitcoin.Mainnet, KnownTokenId.Bitcoin.BTC, "BTC"],
|
||||
[KnownChainId.BRC20.Mainnet, alexBrc20, "alex$"],
|
||||
])
|
||||
result.push([
|
||||
[KnownChainId.BRC20.Mainnet, alexBrc20, "alex$"],
|
||||
[KnownChainId.Bitcoin.Mainnet, KnownTokenId.Bitcoin.BTC, "BTC"],
|
||||
])
|
||||
}
|
||||
if (ausdBrc20 != null) {
|
||||
result.push([
|
||||
[KnownChainId.Bitcoin.Mainnet, KnownTokenId.Bitcoin.BTC, "BTC"],
|
||||
[KnownChainId.BRC20.Mainnet, ausdBrc20, "ausd$"],
|
||||
])
|
||||
result.push([
|
||||
[KnownChainId.BRC20.Mainnet, ausdBrc20, "ausd$"],
|
||||
[KnownChainId.Bitcoin.Mainnet, KnownTokenId.Bitcoin.BTC, "BTC"],
|
||||
])
|
||||
}
|
||||
return result
|
||||
}
|
||||
107
examples/cross-chain-swap/src/utils/getSwapRoutesViaALEX.ts
Normal file
107
examples/cross-chain-swap/src/utils/getSwapRoutesViaALEX.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
KnownChainId,
|
||||
KnownRoute,
|
||||
SDKNumber,
|
||||
SwapRouteViaALEX_WithExchangeRate,
|
||||
SwapRouteViaALEX_WithMinimumAmountsOut,
|
||||
toSDKNumberOrUndefined,
|
||||
XLinkSDK,
|
||||
} from "@xlink-network/xlink-sdk"
|
||||
import { getALEXSwapParameters } from "@xlink-network/xlink-sdk/swapHelpers"
|
||||
import { AlexSDK } from "alex-sdk"
|
||||
import { sortBy, uniqBy } from "lodash-es"
|
||||
|
||||
export async function getSwapRoutesViaALEX(
|
||||
context: {
|
||||
alexSDK: AlexSDK
|
||||
xlinkSDK: XLinkSDK
|
||||
},
|
||||
swapRequest: KnownRoute & {
|
||||
amount: SDKNumber
|
||||
slippage: SDKNumber
|
||||
},
|
||||
): Promise<
|
||||
| { type: "failed"; reason: "unsupported-route" }
|
||||
| {
|
||||
type: "success"
|
||||
swapRoutes: (SwapRouteViaALEX_WithExchangeRate &
|
||||
SwapRouteViaALEX_WithMinimumAmountsOut)[]
|
||||
}
|
||||
> {
|
||||
const { alexSDK, xlinkSDK } = context
|
||||
|
||||
const swapParameters = await getALEXSwapParameters(xlinkSDK, swapRequest)
|
||||
if (swapParameters == null) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
if (
|
||||
// ALEX SDK does not support testnet
|
||||
swapParameters.stacksChain === KnownChainId.Stacks.Testnet
|
||||
) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
const [fromTokenAddress, toTokenAddress] = await Promise.all([
|
||||
xlinkSDK.stacksAddressFromStacksToken(
|
||||
swapParameters.stacksChain,
|
||||
swapParameters.fromToken,
|
||||
),
|
||||
xlinkSDK.stacksAddressFromStacksToken(
|
||||
swapParameters.stacksChain,
|
||||
swapParameters.toToken,
|
||||
),
|
||||
])
|
||||
if (fromTokenAddress == null || toTokenAddress == null) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
const [fromCurrency, toCurrency] = await Promise.all([
|
||||
alexSDK.fetchTokenInfo(
|
||||
`${fromTokenAddress.deployerAddress}.${fromTokenAddress.contractName}`,
|
||||
),
|
||||
alexSDK.fetchTokenInfo(
|
||||
`${toTokenAddress.deployerAddress}.${toTokenAddress.contractName}`,
|
||||
),
|
||||
])
|
||||
if (fromCurrency == null || toCurrency == null) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
const routes = await alexSDK.getAllPossibleRoutesWithDetails(
|
||||
fromCurrency.id,
|
||||
toCurrency.id,
|
||||
toBigInt(Number(swapRequest.amount), fromCurrency.wrapTokenDecimals),
|
||||
)
|
||||
if (routes.length === 0) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
return {
|
||||
type: "success",
|
||||
swapRoutes: uniqBy(
|
||||
sortBy(routes, r => r.toAmount),
|
||||
r => r.toAmount,
|
||||
)
|
||||
.slice(0, 5)
|
||||
.map(r => ({
|
||||
...r,
|
||||
via: "ALEX",
|
||||
minimumAmountsToReceive: toSDKNumberOrUndefined(
|
||||
toNumber(r.toAmount, toCurrency.wrapTokenDecimals) *
|
||||
(1 - Number(swapRequest.slippage)),
|
||||
),
|
||||
composedExchangeRate: toSDKNumberOrUndefined(
|
||||
toNumber(r.toAmount, toCurrency.wrapTokenDecimals) /
|
||||
toNumber(r.fromAmount, fromCurrency.wrapTokenDecimals),
|
||||
),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
function toNumber(value: bigint, moveDecimalPlaces: number): number {
|
||||
return Number(value) / 10 ** moveDecimalPlaces
|
||||
}
|
||||
function toBigInt(value: number, moveDecimalPlaces: number): bigint {
|
||||
return BigInt(Math.floor(value * 10 ** moveDecimalPlaces))
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
KnownRoute,
|
||||
SDKNumber,
|
||||
SwapRouteViaEVMDexAggregator_WithExchangeRate,
|
||||
SwapRouteViaEVMDexAggregator_WithMinimumAmountsOut,
|
||||
toSDKNumberOrUndefined,
|
||||
XLinkSDK,
|
||||
} from "@xlink-network/xlink-sdk"
|
||||
import {
|
||||
getDexAggregatorRoutes,
|
||||
getPossibleEVMDexAggregatorSwapParameters,
|
||||
fetchMatchaPossibleRoutesFactory,
|
||||
} from "@xlink-network/xlink-sdk/swapHelpers"
|
||||
|
||||
export async function getSwapRoutesViaEVMDEX(
|
||||
context: {
|
||||
xlinkSDK: XLinkSDK
|
||||
matchaAPIKey: string
|
||||
},
|
||||
swapRequest: KnownRoute & {
|
||||
amount: SDKNumber
|
||||
slippage: SDKNumber
|
||||
},
|
||||
): Promise<
|
||||
| { type: "failed"; reason: "unsupported-route" }
|
||||
| {
|
||||
type: "success"
|
||||
swapRoutes: (SwapRouteViaEVMDexAggregator_WithExchangeRate &
|
||||
SwapRouteViaEVMDexAggregator_WithMinimumAmountsOut)[]
|
||||
}
|
||||
> {
|
||||
const { xlinkSDK } = context
|
||||
|
||||
const possibleSwapParameters =
|
||||
await getPossibleEVMDexAggregatorSwapParameters(xlinkSDK, swapRequest)
|
||||
if (possibleSwapParameters.length === 0) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
const routes = await getDexAggregatorRoutes(xlinkSDK, {
|
||||
routeFetcher: fetchMatchaPossibleRoutesFactory({
|
||||
baseUrl: "/api/matcha",
|
||||
apiKey: context.matchaAPIKey,
|
||||
}),
|
||||
routes: possibleSwapParameters.map(p => ({
|
||||
evmChain: p.evmChain,
|
||||
fromToken: p.fromToken,
|
||||
toToken: p.toToken,
|
||||
amount: p.fromAmount,
|
||||
slippage: swapRequest.slippage,
|
||||
})),
|
||||
})
|
||||
if (routes == null) {
|
||||
return { type: "failed", reason: "unsupported-route" }
|
||||
}
|
||||
|
||||
return {
|
||||
type: "success",
|
||||
swapRoutes: routes.map(r => ({
|
||||
via: "evmDexAggregator",
|
||||
evmChain: r.evmChain,
|
||||
fromEVMToken: r.fromToken,
|
||||
toEVMToken: r.toToken,
|
||||
composedExchangeRate: toSDKNumberOrUndefined(
|
||||
Number(r.toAmount) / Number(r.fromAmount),
|
||||
),
|
||||
minimumAmountsToReceive: toSDKNumberOrUndefined(
|
||||
Number(r.toAmount) * (1 - Number(swapRequest.slippage)),
|
||||
),
|
||||
})),
|
||||
}
|
||||
}
|
||||
1
examples/cross-chain-swap/src/vite-env.d.ts
vendored
Normal file
1
examples/cross-chain-swap/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
21
examples/cross-chain-swap/tsconfig.json
Normal file
21
examples/cross-chain-swap/tsconfig.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
"allowJs": false,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
9
examples/cross-chain-swap/tsconfig.node.json
Normal file
9
examples/cross-chain-swap/tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
32
examples/cross-chain-swap/vite.config.ts
Normal file
32
examples/cross-chain-swap/vite.config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineConfig } from "vite"
|
||||
import react from "@vitejs/plugin-react"
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: "/xlink-sdk-example/cross-chain-swap/",
|
||||
server: {
|
||||
proxy: {
|
||||
"/api/matcha": {
|
||||
target: "https://api.0x.org",
|
||||
changeOrigin: true,
|
||||
rewrite: path => path.replace(/^\/api\/matcha/, ""),
|
||||
configure: (proxy, _options) => {
|
||||
proxy.on("error", (err, _req, _res) => {
|
||||
console.log("proxy error", err)
|
||||
})
|
||||
proxy.on("proxyReq", (proxyReq, req, _res) => {
|
||||
console.log("Sending Request to the Target:", req.method, req.url)
|
||||
})
|
||||
proxy.on("proxyRes", (proxyRes, req, _res) => {
|
||||
console.log(
|
||||
"Received Response from the Target:",
|
||||
proxyRes.statusCode,
|
||||
req.url,
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user