diff --git a/src/lib/swap/vdlSwapConfig.ts b/src/lib/swap/vdlSwapConfig.ts new file mode 100644 index 0000000..78a71bb --- /dev/null +++ b/src/lib/swap/vdlSwapConfig.ts @@ -0,0 +1,78 @@ +export const VDL_CHAIN_ID = 'beezee-1'; +export const VDL_DENOM = 'factory/bze13gzq40che93tgfm9kzmkpjamah5nj0j73pyhqk/uvdl'; +export const VDL_SWAP_TITLE = 'Buy / Sell VDL'; + +/** + * @skip-go/client appends `/v2/...` to this origin. + * Skip allows browser CORS, so the wallet calls it directly. + * Do not add `/v2` here, and do not point this at a hostname with no DNS record. + */ +export const SKIP_GO_API_URL = 'https://api.skip.build'; + +export type VdlSwapMode = 'buy' | 'sell'; + +export interface SkipDefaultRoute { + srcChainId?: string; + srcAssetDenom?: string; + destChainId?: string; + destAssetDenom?: string; + srcLocked?: boolean; + destLocked?: boolean; +} + +export interface SkipAssetFilter { + source?: Record; + destination?: Record; +} + +export interface VdlSwapConfig { + defaultRoute: SkipDefaultRoute; + filter: SkipAssetFilter; + filterOut: SkipAssetFilter; +} + +const vdlOnly: Record = { + [VDL_CHAIN_ID]: [VDL_DENOM], +}; + +export function vdlSwapCopy(mode: VdlSwapMode): string { + switch (mode) { + case 'buy': + return 'Pay with any asset. Receive VDL.'; + case 'sell': + return 'Pay with VDL. Receive any asset.'; + default: { + const unreachable: never = mode; + return unreachable; + } + } +} + +export function vdlSwapConfig(mode: VdlSwapMode): VdlSwapConfig { + switch (mode) { + case 'buy': + return { + defaultRoute: { + destChainId: VDL_CHAIN_ID, + destAssetDenom: VDL_DENOM, + destLocked: true, + }, + filter: { destination: vdlOnly }, + filterOut: { source: vdlOnly }, + }; + case 'sell': + return { + defaultRoute: { + srcChainId: VDL_CHAIN_ID, + srcAssetDenom: VDL_DENOM, + srcLocked: true, + }, + filter: { source: vdlOnly }, + filterOut: { destination: vdlOnly }, + }; + default: { + const unreachable: never = mode; + return unreachable; + } + } +} diff --git a/src/popup/pages/Dashboard.tsx b/src/popup/pages/Dashboard.tsx index d423110..36e4334 100644 --- a/src/popup/pages/Dashboard.tsx +++ b/src/popup/pages/Dashboard.tsx @@ -56,6 +56,7 @@ import NetworkManagerModal from '../components/NetworkManagerModal'; import IBCTransferModal from '../components/IBCTransferModal'; import { useNetworkStore } from '@/store/networkStore'; import { fetchIBCConnections, IBCChannel } from '@/lib/cosmos/ibc-connections'; +import { VDL_SWAP_TITLE } from '@/lib/swap/vdlSwapConfig'; interface DashboardProps { onNavigateToStaking?: () => void; @@ -803,7 +804,7 @@ const Dashboard: React.FC = ({ {/* Scrollable Content */} - {/* Cross Chain Swaps Button */} + {/* Buy / Sell VDL */} diff --git a/src/popup/pages/Swap.tsx b/src/popup/pages/Swap.tsx index 43ba9fd..862067b 100644 --- a/src/popup/pages/Swap.tsx +++ b/src/popup/pages/Swap.tsx @@ -1,5 +1,5 @@ -import React, { useMemo, useCallback, useRef } from 'react'; -import { Box, HStack, Text, useToast, IconButton } from '@chakra-ui/react'; +import React, { useMemo, useCallback, useRef, useState } from 'react'; +import { Box, HStack, Text, useToast, IconButton, Button } from '@chakra-ui/react'; import { ArrowBackIcon } from '@chakra-ui/icons'; import { Widget } from '@skip-go/widget'; import { useWalletStore } from '@/store/walletStore'; @@ -7,8 +7,6 @@ import { useNetworkStore } from '@/store/networkStore'; import { networkRegistry, isCosmosNetwork, - isEvmNetwork, - isSvmNetwork, getHealthyEndpoint, } from '@/lib/networks'; import type { CosmosNetworkConfig, SvmNetworkConfig } from '@/lib/networks'; @@ -17,6 +15,13 @@ import { fromBech32 } from '@cosmjs/encoding'; import { createWalletClient, defineChain, http, type Hex } from 'viem'; import { privateKeyToAccount } from 'viem/accounts'; import { Keypair, PublicKey } from '@solana/web3.js'; +import { + SKIP_GO_API_URL, + vdlSwapConfig, + vdlSwapCopy, + VDL_SWAP_TITLE, + type VdlSwapMode, +} from '@/lib/swap/vdlSwapConfig'; /** * Vidulum affiliate fee configuration. @@ -25,7 +30,6 @@ import { Keypair, PublicKey } from '@solana/web3.js'; */ const VIDULUM_FEE_BPS = import.meta.env.VIDULUM_FEE_BPS || ''; const VIDULUM_FEE_BASE_ADDRESS = import.meta.env.VIDULUM_FEE_BASE_ADDRESS || ''; -const SKIP_API_URL = 'https://api.vidulum.app/skip/api/skip/v2'; const skipSvmChainIdByNetworkId: Record = { 'solana-mainnet': 'solana', @@ -51,6 +55,8 @@ const SwapPage: React.FC = ({ onBack }) => { const { getEnabledNetworks } = useNetworkStore(); const toast = useToast(); const lastErrorToastRef = useRef<{ message: string; timestamp: number } | null>(null); + const [mode, setMode] = useState('buy'); + const swapConfig = useMemo(() => vdlSwapConfig(mode), [mode]); /** * Build connected addresses map for ALL Cosmos chains in the registry. @@ -283,35 +289,6 @@ const SwapPage: React.FC = ({ onBack }) => { return adapterLike as any; }, []); - /** - * Build the source filter so the widget only shows chains/assets - * the user has enabled in their network preferences. - */ - const sourceFilter = useMemo>(() => { - const filter: Record = {}; - const enabledNetworks = getEnabledNetworks(); - - for (const network of enabledNetworks) { - if (isCosmosNetwork(network)) { - if (connectedAddresses[network.id]) { - filter[network.id] = undefined; - } - } else if (isEvmNetwork(network)) { - const skipChainId = String(network.chainId); - if (connectedAddresses[skipChainId]) { - filter[skipChainId] = undefined; - } - } else if (isSvmNetwork(network)) { - const skipChainId = getSkipSvmChainId(network); - if (connectedAddresses[skipChainId]) { - filter[skipChainId] = undefined; - } - } - } - - return filter; - }, [getEnabledNetworks, connectedAddresses]); - /** * Build chainIdsToAffiliates for Vidulum's fee collection. * Derives the fee recipient address on each enabled Cosmos chain from the @@ -550,7 +527,7 @@ const SwapPage: React.FC = ({ onBack }) => { {/* Header */} - + } @@ -560,38 +537,56 @@ const SwapPage: React.FC = ({ onBack }) => { onClick={onBack} /> - Cross-Chain Swap + {VDL_SWAP_TITLE} + + + + + + {vdlSwapCopy(mode)} + {/* Skip Go Widget */} { + it('buys VDL on BeeZee and leaves the pay-with side open', () => { + const config = vdlSwapConfig('buy'); + + expect(config.defaultRoute).toEqual({ + destChainId: VDL_CHAIN_ID, + destAssetDenom: VDL_DENOM, + destLocked: true, + }); + expect(config.defaultRoute.srcLocked).toBeUndefined(); + expect(config.defaultRoute.srcChainId).toBeUndefined(); + expect(config.defaultRoute.srcAssetDenom).toBeUndefined(); + expect(config.defaultRoute).not.toHaveProperty('amountIn'); + expect(config.defaultRoute).not.toHaveProperty('amountOut'); + + expect(config.filter.destination).toEqual({ + [VDL_CHAIN_ID]: [VDL_DENOM], + }); + expect(config.filter.source).toBeUndefined(); + + expect(config.filterOut.source).toEqual({ + [VDL_CHAIN_ID]: [VDL_DENOM], + }); + expect(config.filterOut.destination).toBeUndefined(); + }); + + it('sells BeeZee VDL and leaves the receive side open', () => { + const config = vdlSwapConfig('sell'); + + expect(config.defaultRoute).toEqual({ + srcChainId: VDL_CHAIN_ID, + srcAssetDenom: VDL_DENOM, + srcLocked: true, + }); + expect(config.defaultRoute.destLocked).toBeUndefined(); + expect(config.defaultRoute.destChainId).toBeUndefined(); + expect(config.defaultRoute.destAssetDenom).toBeUndefined(); + expect(config.defaultRoute).not.toHaveProperty('amountIn'); + expect(config.defaultRoute).not.toHaveProperty('amountOut'); + + expect(config.filter.source).toEqual({ + [VDL_CHAIN_ID]: [VDL_DENOM], + }); + expect(config.filter.destination).toBeUndefined(); + + expect(config.filterOut.destination).toEqual({ + [VDL_CHAIN_ID]: [VDL_DENOM], + }); + expect(config.filterOut.source).toBeUndefined(); + }); + + it('does not lock the same side for buy and sell', () => { + const buy = vdlSwapConfig('buy'); + const sell = vdlSwapConfig('sell'); + + expect(buy.filter.destination).toBeDefined(); + expect(buy.filter.source).toBeUndefined(); + expect(sell.filter.source).toBeDefined(); + expect(sell.filter.destination).toBeUndefined(); + }); + + it('uses the BeeZee factory denom', () => { + expect(VDL_CHAIN_ID).toBe('beezee-1'); + expect(VDL_DENOM).toBe('factory/bze13gzq40che93tgfm9kzmkpjamah5nj0j73pyhqk/uvdl'); + }); + + it('returns the page copy', () => { + expect(VDL_SWAP_TITLE).toBe('Buy / Sell VDL'); + expect(vdlSwapCopy('buy')).toBe('Pay with any asset. Receive VDL.'); + expect(vdlSwapCopy('sell')).toBe('Pay with VDL. Receive any asset.'); + }); + + it('calls Skip directly so the client can append /v2', () => { + expect(SKIP_GO_API_URL).toBe('https://api.skip.build'); + expect(SKIP_GO_API_URL.endsWith('/v2')).toBe(false); + expect(SKIP_GO_API_URL.includes('api.vidulum.app')).toBe(false); + }); +});