import { getCsrfToken } from '@shared/api/csrf' import { tokenStore, refreshAccessToken } from '@shared/api/tokenStore' import { getOrganizationWallets, getOrganizationBalances, type OrgAmountRaw, type OrgWalletBalance, } from '@features/b2b' const WALLET_API_URL = 'https://app.cryptowallet.elcsa.ru' export type Chain = 'ETH' | 'BSC' | 'BTC' | 'TRX' | 'SOL' export interface FormattedAmount { raw: string formatted: string decimals: number usdPrice: number usdValue: number } export interface WalletBalanceData { chain: Chain address: string native: FormattedAmount tokens: Record } export interface PriceEntry { usd: number } export interface SendWalletPayload { to: string amount: string token?: string feeTier?: 'slow' | 'normal' | 'fast' } export interface SendWalletResponse { data: { txid: string; chain: Chain } } export interface WalletAddress { chain: Chain address: string derivationPath: string } export interface PortfolioChain { chain: Chain address: string native: FormattedAmount tokens: Record totalUsd: number stale: boolean lastUpdated: number } export interface PortfolioData { totalUsd: number hasErrors: boolean perChain: Record } export const CHAINS: Chain[] = ['ETH', 'BSC', 'BTC', 'TRX', 'SOL'] async function walletGet(path: string, allowRetry: boolean = true): Promise { const csrf = await getCsrfToken() const bearer = tokenStore.get() const res = await fetch(`${WALLET_API_URL}${path}`, { credentials: 'include', headers: { 'X-CSRF-Token': csrf, ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), }, }) if (res.status === 401 && allowRetry) { try { await refreshAccessToken() return walletGet(path, false) } catch { tokenStore.clear() throw new Error('Unauthorized') } } const data = await res.json() if (!res.ok) throw data return data as T } async function walletPost( path: string, body: unknown, allowRetry: boolean = true, extraHeaders: Record = {} ): Promise { const csrf = await getCsrfToken() const bearer = tokenStore.get() const res = await fetch(`${WALLET_API_URL}${path}`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrf, ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), ...extraHeaders, }, body: JSON.stringify(body), }) if (res.status === 401 && allowRetry) { try { await refreshAccessToken() return walletPost(path, body, false, extraHeaders) } catch { tokenStore.clear() throw new Error('Unauthorized') } } const data = await res.json() if (!res.ok) throw data return data as T } export async function getWalletAddresses(): Promise { const res = await walletGet<{ success: boolean; data: WalletAddress[] }>('/api/wallets') return res.data } export async function getWalletBalance(chain: Chain): Promise { const res = await walletGet<{ success: boolean; data: WalletBalanceData }>(`/api/wallets/${chain}/balance`) return res.data } export async function getPrices(symbols: string[]): Promise> { const res = await walletGet<{ success: boolean; data: Record }>( `/api/prices?symbols=${symbols.join(',')}` ) return res.data } export async function sendWallet(chain: Chain, payload: SendWalletPayload): Promise { return walletPost(`/api/wallets/${chain}/send`, payload) } export async function getPortfolio(): Promise { const res = await walletGet<{ success: boolean; data: PortfolioData }>('/api/wallets/portfolio') return res.data } // --- Legal-entity (b2b organizations) adapters --- // The b2b balances endpoint returns the same information as the cryptowallet // portfolio, but with snake_case keys, string-encoded numbers and an items[] // array instead of a perChain map. These adapters normalise it into the // existing PortfolioData / WalletAddress shapes so the wallet UI is reused as-is. const NATIVE_SYMBOL_TO_CHAIN: Record = { ETH: 'ETH', BNB: 'BSC', BTC: 'BTC', TRX: 'TRX', SOL: 'SOL', } /** Normalise a b2b chain/native_symbol into our Chain enum, or null if unknown. */ function normalizeChain(chain: string, nativeSymbol?: string): Chain | null { const upper = chain?.toUpperCase() if ((CHAINS as string[]).includes(upper)) return upper as Chain const bySymbol = nativeSymbol ? NATIVE_SYMBOL_TO_CHAIN[nativeSymbol.toUpperCase()] : undefined return bySymbol ?? null } function parseNum(value: string | number | null | undefined): number { const n = typeof value === 'number' ? value : parseFloat(value ?? '') return Number.isFinite(n) ? n : 0 } function toFormattedAmount(a: OrgAmountRaw): FormattedAmount { return { raw: a.raw, formatted: a.formatted, decimals: a.decimals, usdPrice: parseNum(a.usd_price), usdValue: parseNum(a.usd_value), } } function toPortfolioChain(item: OrgWalletBalance, chain: Chain): PortfolioChain { const tokens: Record = {} for (const [symbol, amount] of Object.entries(item.tokens ?? {})) { tokens[symbol] = toFormattedAmount(amount) } return { chain, address: item.address, native: toFormattedAmount(item.native), tokens, totalUsd: parseNum(item.total_usd), stale: false, lastUpdated: 0, } } export async function getOrgPortfolio(): Promise { const res = await getOrganizationBalances() const perChain = {} as Record for (const item of res.items ?? []) { const chain = normalizeChain(item.chain, item.native_symbol) if (!chain) continue perChain[chain] = toPortfolioChain(item, chain) } return { totalUsd: parseNum(res.total_usd), hasErrors: !!res.has_errors, perChain, } } const EMPTY_AMOUNT: FormattedAmount = { raw: '0', formatted: '0', decimals: 0, usdPrice: 0, usdValue: 0 } export async function getOrgWalletBalance(chain: Chain): Promise { const portfolio = await getOrgPortfolio() const c = portfolio.perChain[chain] if (!c) return { chain, address: '', native: EMPTY_AMOUNT, tokens: {} } return { chain, address: c.address, native: c.native, tokens: c.tokens } } export async function getOrgWalletAddresses(): Promise { const wallets = await getOrganizationWallets() const result: WalletAddress[] = [] for (const w of wallets ?? []) { const chain = normalizeChain(w.chain) if (!chain) continue result.push({ chain, address: w.address, derivationPath: w.derivation_path }) } return result } export interface TokenInfo { chain: string symbol: string name: string contract: string | null } export interface RelayQuotePayload { user: string recipient: string originChainId: number destinationChainId: number originCurrency: string destinationCurrency: string amount: string tradeType: 'EXACT_INPUT' } export interface RelayQuoteResponse { details: { currencyOut: { amountFormatted: string amountUsd: string } } fees: { gas: { amountUsd: string } } } export async function getTokensList(): Promise { const res = await walletGet<{ success: boolean; data: TokenInfo[] }>('/api/tokens') return res.data } export interface JumperToken { address: string chainId: number symbol: string decimals: number name: string coinKey?: string logoURI?: string priceUSD: string } export type JumperTokensMap = Record export async function getJumperTokens(): Promise { const res = await walletGet<{ tokens?: JumperTokensMap; data?: { tokens: JumperTokensMap } }>( '/api/jumper/tokens?chains=1,56,1151111081099710,728126428,20000000000001' ) return res.data?.tokens ?? res.tokens ?? {} } export interface JumperQuotePayload { fromChain: string toChain: string fromToken: string toToken: string fromAmount: string fromAddress: string toAddress: string slippage: number } export interface JumperQuoteToken { address: string chainId: number symbol: string decimals: number name: string logoURI?: string priceUSD: string } export interface JumperFeeCost { name: string description?: string token: JumperQuoteToken amount: string amountUSD: string percentage?: string included?: boolean } export interface JumperQuote { type: string id: string tool: string toolDetails: { key: string; name: string; logoURI?: string } action: { fromToken: JumperQuoteToken fromAmount: string toToken: JumperQuoteToken fromChainId: number toChainId: number slippage: number fromAddress: string toAddress: string } estimate: { tool: string approvalAddress?: string toAmountMin: string toAmount: string fromAmount: string feeCosts?: JumperFeeCost[] } } export async function getJumperQuote(payload: JumperQuotePayload): Promise { const qs = new URLSearchParams({ fromChain: payload.fromChain, toChain: payload.toChain, fromToken: payload.fromToken, toToken: payload.toToken, fromAmount: payload.fromAmount, fromAddress: payload.fromAddress, toAddress: payload.toAddress, slippage: String(payload.slippage), }).toString() const res = await walletGet( `/api/jumper/quote-best?${qs}` ) return (res.data?.body ?? res.body ?? res) as JumperQuote } export interface BridgeExecutePayload { provider: string fromChain: number toChain: number fromToken: string toToken: string fromAmount: string fromAddress: string toAddress: string acceptedMinOut?: string } export interface BridgeExecuteResult { provider: string fromChain: number toChain: number toolName: string feeTxid?: string feeAmount?: string bridgeTxid: string fromAmount: string toAmountMin: string fromAmountUSD?: string toAmountUSD?: string trackerUrl?: string } export async function executeBridge(payload: BridgeExecutePayload): Promise { const res = await walletPost<{ data?: { success: boolean; data: BridgeExecuteResult } }>( '/api/bridge/execute', payload, true, { 'Idempotency-Key': crypto.randomUUID() } ) return (res.data?.data ?? res) as BridgeExecuteResult } export async function getRelayQuote(payload: RelayQuotePayload): Promise { return walletPost('/api/relay/quote', payload) } export interface RelaySwapStep { id: string action: string description: string kind: string items: Array<{ status: string data: { from: string to: string data: string value: string chainId: number gas: string maxFeePerGas: string maxPriorityFeePerGas: string } check: { endpoint: string method: string } }> requestId: string } export interface RelaySwapResponse { steps: RelaySwapStep[] fees: RelayQuoteResponse['fees'] details: { operation: string sender: string recipient: string currencyIn: { amount: string; amountFormatted: string; amountUsd: string; currency: { symbol: string } } currencyOut: { amount: string; amountFormatted: string; amountUsd: string; currency: { symbol: string } } totalImpact: { usd: string; percent: string } rate: string timeEstimate: number } } export async function executeRelaySwap(payload: RelayQuotePayload): Promise { return walletPost('/api/relay/execute/swap', payload) } export async function signRawEvmTx( chain: 'ETH' | 'BSC', txData: RelaySwapStep['items'][0]['data'] ): Promise { const key = `relay-${chain.toLowerCase()}-${Date.now()}` return walletPost(`/api/wallets/${chain}/sign-raw-evm-tx`, txData, true, { 'Idempotency-Key': key }) } export async function signSolTx(txData: unknown): Promise { return walletPost('/api/wallets/SOL/sign-and-broadcast-tx', txData) } export interface TrxSwapQuotePayload { from: string to: string amountHuman: string } export interface TrxSwapQuoteData { quoteId: string expiresIn: number expectedOutFormatted: string minOutFormatted: string fees: { network: { amountFormatted: string; asset: string; amountUsd: number } } } export async function getTrxSwapQuote(payload: TrxSwapQuotePayload): Promise { const res = await walletPost<{ success: boolean; data: TrxSwapQuoteData }>( '/api/wallets/TRX/swap/quote', payload ) return res.data } export async function executeTrxSwap(quoteId: string): Promise { return walletPost( '/api/wallets/TRX/swap', { quoteId }, true, { 'Idempotency-Key': `trx-${Date.now()}` } ) } export async function createWallet(): Promise { await walletPost('/api/wallets/create', {}) } export async function revealMnemonic(): Promise { const res = await walletPost<{ success: boolean; data: { mnemonic: string } }>('/api/wallets/mnemonic/reveal', { confirm: 'I_UNDERSTAND_SEED_IS_SECRET' }) return res.data.mnemonic }