diff --git a/packages/core-mobile/.eslintrc.js b/packages/core-mobile/.eslintrc.js index 48df8b0ef0..2b9e38ca70 100644 --- a/packages/core-mobile/.eslintrc.js +++ b/packages/core-mobile/.eslintrc.js @@ -20,8 +20,7 @@ module.exports = { 'android/app/build/**', 'expo-env.d.ts', 'ios/DerivedData', - 'app/utils/api/generated/**', - 'app/utils/apiClient/generated/**' + 'app/utils/api/generated/**' ], overrides: [ { diff --git a/packages/core-mobile/.gitignore b/packages/core-mobile/.gitignore index 0c1d238623..3ef5d16162 100644 --- a/packages/core-mobile/.gitignore +++ b/packages/core-mobile/.gitignore @@ -99,6 +99,4 @@ expo-env.d.ts /ios/config.xcconfig # generated api clients -app/utils/apiClient/generated/profileApi.client.ts -app/utils/apiClient/generated/balanceApi.client/ app/utils/api/generated \ No newline at end of file diff --git a/packages/core-mobile/aggregator-api.config.js b/packages/core-mobile/aggregator-api.config.js index bf715144a1..a8197a09a9 100644 --- a/packages/core-mobile/aggregator-api.config.js +++ b/packages/core-mobile/aggregator-api.config.js @@ -1,5 +1,6 @@ const isCI = process.env.APP_ENV === 'ci' +// use production environment on CI for stability const TOKEN_AGGREGATOR_SCHEMA_URL = isCI ? 'https://core-token-aggregator.avax.network/schema.json' : 'https://core-token-aggregator.avax-test.network/schema.json' diff --git a/packages/core-mobile/app/consts/reactQueryKeys.ts b/packages/core-mobile/app/consts/reactQueryKeys.ts index 608f80bb26..1c07ca643d 100644 --- a/packages/core-mobile/app/consts/reactQueryKeys.ts +++ b/packages/core-mobile/app/consts/reactQueryKeys.ts @@ -47,6 +47,10 @@ export enum ReactQueryKeys { // bridge BRIDGE_CONFIG = 'bridgeConfig', + // fusion + FUSION_SUPPORTED_CHAINS = 'fusionSupportedChains', + FUSION_TOKENS = 'fusionTokens', + // deposit AAVE_AVAILABLE_MARKETS = 'aaveAvailableMarkets', BENQI_AVAILABLE_MARKETS = 'benqiAvailableMarkets', diff --git a/packages/core-mobile/app/hooks/balance/useSupportedChains.ts b/packages/core-mobile/app/hooks/balance/useSupportedChains.ts index e288e2e3a4..01e2174ea5 100644 --- a/packages/core-mobile/app/hooks/balance/useSupportedChains.ts +++ b/packages/core-mobile/app/hooks/balance/useSupportedChains.ts @@ -1,7 +1,7 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query' import { ReactQueryKeys } from 'consts/reactQueryKeys' import { queryClient } from 'contexts/ReactQueryProvider' -import { getV1BalanceGetSupportedChains } from 'utils/apiClient/generated/balanceApi.client' +import { getV1BalanceGetSupportedChains } from 'utils/api/generated/balanceApi.client' import { balanceApiClient } from 'utils/api/clients/balanceApiClient' const STALE_TIME = 5 * 60 * 1000 // 5 minutes diff --git a/packages/core-mobile/app/hooks/networks/useNetworks.ts b/packages/core-mobile/app/hooks/networks/useNetworks.ts index 80ba0ca194..ecbebd0f61 100644 --- a/packages/core-mobile/app/hooks/networks/useNetworks.ts +++ b/packages/core-mobile/app/hooks/networks/useNetworks.ts @@ -48,7 +48,10 @@ export const useNetworks = () => { const chainId = parseInt(key) const network = rawNetworks[chainId] if (network && network.isTestnet === isDeveloperMode) { - reducedNetworks[chainId] = network + reducedNetworks[chainId] = { + ...network, + caip2ChainId: getCaip2ChainId(chainId) + } } return reducedNetworks }, @@ -61,7 +64,10 @@ export const useNetworks = () => { const network = _customNetworks[chainId] if (network && network.isTestnet === isDeveloperMode) { - reducedNetworks[chainId] = network + reducedNetworks[chainId] = { + ...network, + caip2ChainId: getCaip2ChainId(chainId) + } } return reducedNetworks }, @@ -70,7 +76,7 @@ export const useNetworks = () => { return { ...populatedNetworks, ...populatedCustomNetworks } }, [rawNetworks, _customNetworks, isDeveloperMode]) - const customNetworks = useMemo(() => { + const customNetworks: NetworkWithCaip2ChainId[] = useMemo(() => { if (networks === undefined) return [] const customNetworkChainIds = Object.values(_customNetworks).map( @@ -81,7 +87,7 @@ export const useNetworks = () => { ) }, [networks, _customNetworks]) - const enabledNetworks = useMemo(() => { + const enabledNetworks: NetworkWithCaip2ChainId[] = useMemo(() => { if (networks === undefined) return [] const lastTransactedChainIds = lastTransactedChains @@ -93,10 +99,10 @@ export const useNetworks = () => { const enabled = allChainIds.reduce((acc, chainId) => { const network = networks[chainId] if (network && network.isTestnet === isDeveloperMode) { - acc.push(network) + acc.push({ ...network, caip2ChainId: getCaip2ChainId(network.chainId) }) } return acc - }, [] as Network[]) + }, [] as NetworkWithCaip2ChainId[]) // sort all C/X/P networks to the top return enabled.sort((a, b) => { @@ -162,6 +168,13 @@ export const useNetworks = () => { [allNetworks] ) + const getEnabledNetworkByCaip2ChainId = useCallback( + (caip2ChainId: string) => { + return enabledNetworks.find(n => n.caip2ChainId === caip2ChainId) + }, + [enabledNetworks] + ) + const getFromPopulatedNetwork = useCallback( (chainId?: number) => { if (chainId === undefined || networks === undefined) return @@ -180,6 +193,7 @@ export const useNetworks = () => { getSomeNetworks, getNetwork, getNetworkByCaip2ChainId, + getEnabledNetworkByCaip2ChainId, getFromPopulatedNetwork, toggleNetwork } diff --git a/packages/core-mobile/app/new/features/meld/services/MeldService.ts b/packages/core-mobile/app/new/features/meld/services/MeldService.ts index d26c2da841..5eccd1fa52 100644 --- a/packages/core-mobile/app/new/features/meld/services/MeldService.ts +++ b/packages/core-mobile/app/new/features/meld/services/MeldService.ts @@ -35,12 +35,10 @@ class MeldService { countries }: MeldDefaultParams): Promise { return this.#meldApiClient.getCountries({ - queries: { - serviceProviders: serviceProviders?.join(','), - categories: categories.join(','), - accountFilter, - countries: countries?.join(',') - } + serviceProviders: serviceProviders?.join(','), + categories: categories.join(','), + accountFilter, + countries: countries?.join(',') }) } @@ -52,13 +50,11 @@ class MeldService { countries }: SearchFiatCurrenciesParams): Promise { return this.#meldApiClient.getFiatCurrencies({ - queries: { - serviceProviders: serviceProviders?.join(','), - categories: categories.join(','), - accountFilter, - countries: countries?.join(','), - fiatCurrencies: fiatCurrencies?.join(',') - } + serviceProviders: serviceProviders?.join(','), + categories: categories.join(','), + accountFilter, + countries: countries?.join(','), + fiatCurrencies: fiatCurrencies?.join(',') }) } @@ -69,15 +65,12 @@ class MeldService { cryptoCurrencies, accountFilter = true }: SearchCryptoCurrenciesParams): Promise { - const queries = { + return this.#meldApiClient.getCryptoCurrencies({ serviceProviders: serviceProviders?.join(','), categories: categories.join(','), accountFilter, countries: countries?.join(','), cryptoCurrencies: cryptoCurrencies?.join(',') - } - return this.#meldApiClient.getCryptoCurrencies({ - queries }) } @@ -87,14 +80,11 @@ class MeldService { accountFilter = true, cryptoCurrencies }: SearchServiceProvidersParams): Promise { - const queries = { + return this.#meldApiClient.getServiceProviders({ categories: categories.join(','), accountFilter, countries: countries?.join(','), cryptoCurrencies: cryptoCurrencies?.join(',') - } - return this.#meldApiClient.getServiceProviders({ - queries }) } @@ -103,12 +93,11 @@ class MeldService { countries, accountFilter = true }: SearchDefaultsByCountryParams): Promise { - const queries = { + return this.#meldApiClient.getDefaultsByCountry({ categories: categories.join(','), accountFilter, countries: countries?.join(',') - } - return this.#meldApiClient.getDefaultsByCountry({ queries }) + }) } async getPurchaseLimits({ @@ -120,7 +109,7 @@ class MeldService { cryptoCurrencyCodes, includeDetails = false }: GetTradeLimitsParams): Promise { - const queries = { + return this.#meldApiClient.getPurchaseLimits({ categories: categories.join(','), accountFilter, countries: countries?.join(','), @@ -128,8 +117,7 @@ class MeldService { fiatCurrencies: fiatCurrencies?.join(','), cryptoCurrencies: cryptoCurrencyCodes?.join(','), includeDetails - } - return this.#meldApiClient.getPurchaseLimits({ queries }) + }) } async getSellLimits({ @@ -141,7 +129,7 @@ class MeldService { cryptoCurrencyCodes, includeDetails = false }: GetTradeLimitsParams): Promise { - const queries = { + return this.#meldApiClient.getSellLimits({ categories: categories.join(','), accountFilter, countries: countries?.join(','), @@ -149,8 +137,7 @@ class MeldService { fiatCurrencies: fiatCurrencies?.join(','), cryptoCurrencies: cryptoCurrencyCodes?.join(','), includeDetails - } - return this.#meldApiClient.getSellLimits({ queries }) + }) } async searchPaymentMethods({ @@ -161,15 +148,14 @@ class MeldService { fiatCurrencies, cryptoCurrencyCodes }: SearchPaymentMethodsParams): Promise { - const queries = { + return this.#meldApiClient.getPaymentMethods({ categories: categories.join(','), accountFilter, countries: countries?.join(','), serviceProviders: serviceProviders?.join(','), fiatCurrencies: fiatCurrencies?.join(','), cryptoCurrencies: cryptoCurrencyCodes?.join(',') - } - return this.#meldApiClient.getPaymentMethods({ queries }) + }) } async createCryptoQuote({ @@ -236,9 +222,7 @@ class MeldService { }: { sessionId: string }): Promise { - return await this.#meldApiClient.fetchTransactionBySessionId({ - params: { id: sessionId } - }) + return await this.#meldApiClient.fetchTransactionBySessionId(sessionId) } } diff --git a/packages/core-mobile/app/new/features/meld/services/apiClient.ts b/packages/core-mobile/app/new/features/meld/services/apiClient.ts index f9f7ad020c..5c98c7ed5f 100644 --- a/packages/core-mobile/app/new/features/meld/services/apiClient.ts +++ b/packages/core-mobile/app/new/features/meld/services/apiClient.ts @@ -1,7 +1,10 @@ -import { Zodios } from '@zodios/core' import Config from 'react-native-config' import { z } from 'zod' import Logger from 'utils/Logger' +import { + fetchJson, + buildQueryString +} from 'utils/api/common/fetchWithValidation' import { CreateCryptoQuoteBodySchema, CreateCryptoQuoteSchema, @@ -23,252 +26,170 @@ if (!Config.PROXY_URL) const baseUrl = Config.PROXY_URL + '/proxy/meld' const sandboxBaseUrl = Config.PROXY_URL + '/proxy/meld-sandbox' +// Infer TypeScript types from Zod schemas +type SearchCountry = z.infer +type SearchFiatCurrency = z.infer +type SearchCryptoCurrency = z.infer +type SearchServiceProvider = z.infer +type SearchDefaultsByCountry = z.infer +type GetTradeLimits = z.infer +type SearchPaymentMethods = z.infer +type CreateCryptoQuoteBody = z.infer +type CreateCryptoQuote = z.infer +type CreateSessionWidgetBody = z.infer +type CreateSessionWidget = z.infer +type MeldTransaction = z.infer + +// Query parameters type +interface MeldQueryParams extends Record { + serviceProviders?: string + categories?: string + accountFilter?: boolean + countries?: string + fiatCurrencies?: string + cryptoCurrencies?: string +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -export const meldApiClient = (sandbox = false) => - new Zodios( - sandbox ? sandboxBaseUrl : baseUrl, - [ - { - method: 'get', - path: '/service-providers/properties/countries', - parameters: [ - { - name: 'serviceProviders', - type: 'Query', - schema: z.string().optional() - }, - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() } - ], - alias: 'getCountries', - response: z.array(SearchCountrySchema) - }, - { - method: 'get', - path: '/service-providers/properties/fiat-currencies', - parameters: [ - { - name: 'serviceProviders', - type: 'Query', - schema: z.string().optional() - }, - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { - name: 'fiatCurrencies', - type: 'Query', - schema: z.string().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() } - ], - alias: 'getFiatCurrencies', - response: z.array(SearchFiatCurrencySchema) - }, - { - method: 'get', - path: '/service-providers/properties/crypto-currencies', - parameters: [ - { - name: 'serviceProviders', - type: 'Query', - schema: z.string().optional() - }, - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() }, - { - name: 'cryptoCurrencies', - type: 'Query', - schema: z.string().optional() - } - ], - alias: 'getCryptoCurrencies', - response: z.array(SearchCryptoCurrencySchema) - }, - { - method: 'get', - path: '/service-providers', - parameters: [ - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() }, - { - name: 'cryptoCurrencies', - type: 'Query', - schema: z.string().optional() - } - ], - alias: 'getServiceProviders', - response: z.array(SearchServiceProviderSchema) - }, - { - method: 'get', - path: '/service-providers/properties/defaults/by-country', - parameters: [ - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() } - ], - alias: 'getDefaultsByCountry', - response: z.array(SearchDefaultsByCountrySchema) - }, - { - method: 'get', - path: '/service-providers/limits/fiat-currency-purchases', - parameters: [ - { - name: 'serviceProviders', - type: 'Query', - schema: z.string().optional() - }, - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() }, - { - name: 'fiatCurrencies', - type: 'Query', - schema: z.string().optional() - }, - { - name: 'cryptoCurrencies', - type: 'Query', - schema: z.string().optional() - } - ], - alias: 'getPurchaseLimits', - response: z.array(GetTradeLimitsSchema) - }, - { - method: 'get', - path: '/service-providers/limits/crypto-currency-sells', - parameters: [ - { - name: 'serviceProviders', - type: 'Query', - schema: z.string().optional() - }, - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() }, - { - name: 'fiatCurrencies', - type: 'Query', - schema: z.string().optional() - }, - { - name: 'cryptoCurrencies', - type: 'Query', - schema: z.string().optional() - } - ], - alias: 'getSellLimits', - response: z.array(GetTradeLimitsSchema) - }, - { - method: 'get', - path: '/service-providers/properties/payment-methods', - parameters: [ - { - name: 'serviceProviders', - type: 'Query', - schema: z.string().optional() - }, - { name: 'categories', type: 'Query', schema: z.string().optional() }, - { - name: 'accountFilter', - type: 'Query', - schema: z.boolean().optional() - }, - { name: 'countries', type: 'Query', schema: z.string().optional() }, - { - name: 'fiatCurrencies', - type: 'Query', - schema: z.string().optional() - }, - { - name: 'cryptoCurrencies', - type: 'Query', - schema: z.string().optional() - } - ], - alias: 'getPaymentMethods', - response: z.array(SearchPaymentMethodsSchema) - }, - { - method: 'post', - path: '/payments/crypto/quote', - parameters: [ - { - name: 'body', - type: 'Body', - schema: CreateCryptoQuoteBodySchema - } - ], - alias: 'createCryptoQuotes', - response: CreateCryptoQuoteSchema - }, - { - method: 'post', - path: '/crypto/session/widget', - parameters: [ - { - name: 'body', - type: 'Body', - schema: CreateSessionWidgetBodySchema - } - ], - alias: 'createSessionWidget', - response: CreateSessionWidgetSchema - }, - { - method: 'get', - path: '/payments/transactions/sessions/:id', - parameters: [ - { - name: 'id', - type: 'Path', - description: 'Session ID', - schema: z.string() - } - ], - alias: 'fetchTransactionBySessionId', - response: MeldTransactionSchema - } - ], - { - axiosConfig: { - headers: { - 'Content-Type': 'application/json' - } - } +export const meldApiClient = (sandbox = false) => { + const apiBaseUrl = sandbox ? sandboxBaseUrl : baseUrl + + return { + // GET /service-providers/properties/countries + getCountries: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/properties/countries${queryString}`, + { method: 'GET' }, + z.array(SearchCountrySchema) + ) + }, + + // GET /service-providers/properties/fiat-currencies + getFiatCurrencies: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/properties/fiat-currencies${queryString}`, + { method: 'GET' }, + z.array(SearchFiatCurrencySchema) + ) + }, + + // GET /service-providers/properties/crypto-currencies + getCryptoCurrencies: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/properties/crypto-currencies${queryString}`, + { method: 'GET' }, + z.array(SearchCryptoCurrencySchema) + ) + }, + + // GET /service-providers + getServiceProviders: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers${queryString}`, + { method: 'GET' }, + z.array(SearchServiceProviderSchema) + ) + }, + + // GET /service-providers/properties/defaults/by-country + getDefaultsByCountry: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/properties/defaults/by-country${queryString}`, + { method: 'GET' }, + z.array(SearchDefaultsByCountrySchema) + ) + }, + + // GET /service-providers/limits/fiat-currency-purchases + getPurchaseLimits: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/limits/fiat-currency-purchases${queryString}`, + { method: 'GET' }, + z.array(GetTradeLimitsSchema) + ) + }, + + // GET /service-providers/limits/crypto-currency-sells + getSellLimits: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/limits/crypto-currency-sells${queryString}`, + { method: 'GET' }, + z.array(GetTradeLimitsSchema) + ) + }, + + // GET /service-providers/properties/payment-methods + getPaymentMethods: async ( + params: MeldQueryParams = {} + ): Promise => { + const queryString = buildQueryString(params) + return fetchJson( + `${apiBaseUrl}/service-providers/properties/payment-methods${queryString}`, + { method: 'GET' }, + z.array(SearchPaymentMethodsSchema) + ) + }, + + // POST /payments/crypto/quote + createCryptoQuotes: async ( + body: CreateCryptoQuoteBody + ): Promise => { + return fetchJson( + `${apiBaseUrl}/payments/crypto/quote`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }, + CreateCryptoQuoteSchema + ) + }, + + // POST /crypto/session/widget + createSessionWidget: async ( + body: CreateSessionWidgetBody + ): Promise => { + return fetchJson( + `${apiBaseUrl}/crypto/session/widget`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }, + CreateSessionWidgetSchema + ) + }, + + // GET /payments/transactions/sessions/:id + fetchTransactionBySessionId: async ( + id: string + ): Promise => { + return fetchJson( + `${apiBaseUrl}/payments/transactions/sessions/${id}`, + { method: 'GET' }, + MeldTransactionSchema + ) } - ) + } +} diff --git a/packages/core-mobile/app/new/features/swap/services/JupiterService.ts b/packages/core-mobile/app/new/features/swap/services/JupiterService.ts index ba71bd4a56..b1bcfba3b2 100644 --- a/packages/core-mobile/app/new/features/swap/services/JupiterService.ts +++ b/packages/core-mobile/app/new/features/swap/services/JupiterService.ts @@ -64,14 +64,12 @@ class JupiterService { try { const data = await jupiterApi.getQuote({ - queries: { - inputMint, - outputMint, - swapMode, - amount: amount.toString(), - slippageBps: Math.round(slippageBps).toString(), - platformFeeBps: platformFeeBps?.toString() - }, + inputMint, + outputMint, + swapMode, + amount: amount.toString(), + slippageBps: Math.round(slippageBps).toString(), + platformFeeBps: platformFeeBps?.toString(), signal: abortSignal }) diff --git a/packages/core-mobile/app/new/features/swap/services/MarkrService.ts b/packages/core-mobile/app/new/features/swap/services/MarkrService.ts index 9a9d1a872e..6f3cd38a1e 100644 --- a/packages/core-mobile/app/new/features/swap/services/MarkrService.ts +++ b/packages/core-mobile/app/new/features/swap/services/MarkrService.ts @@ -1,7 +1,7 @@ import { Network } from '@avalabs/core-chains-sdk' import { fetch as expoFetch } from 'expo/fetch' -import { Zodios } from '@zodios/core' import { z } from 'zod' +import { fetchJsonWithExpo as fetchJson } from 'utils/api/common/fetchWithValidation' import { MARKR_EVM_PARTNER_ID } from '../consts' import { MarkrTransaction } from '../types' @@ -197,44 +197,40 @@ const GetSpenderAddressResponseSchema = z.object({ address: z.string() }) -const markrServiceClient = new Zodios( - ORCHESTRATOR_URL, - [ - { - method: 'post', - path: '/swap', - alias: 'swap', - parameters: [ - { - name: 'body', - type: 'Body', - schema: GetSwapTransactionBodySchema - } - ], - response: GetSwapTransactionResponseSchema - }, - { - method: 'get', - path: '/spender-address', - alias: 'getSpenderAddress', - parameters: [ - { - name: 'chainId', - type: 'Query', - schema: z.number() - } - ], - response: GetSpenderAddressResponseSchema - } - ], - { - axiosConfig: { - headers: { - 'Content-Type': 'application/json' - } - } +// Infer TypeScript types from Zod schemas +type GetSwapTransactionBody = z.infer +type GetSwapTransactionResponse = z.infer< + typeof GetSwapTransactionResponseSchema +> +type GetSpenderAddressResponse = z.infer + +const markrApiClient = { + // POST /swap + swap: async ( + body: GetSwapTransactionBody + ): Promise => { + return fetchJson( + `${ORCHESTRATOR_URL}/swap`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }, + GetSwapTransactionResponseSchema + ) + }, + + // GET /spender-address + getSpenderAddress: async ( + chainId: number + ): Promise => { + return fetchJson( + `${ORCHESTRATOR_URL}/spender-address?chainId=${chainId}`, + { method: 'GET' }, + GetSpenderAddressResponseSchema + ) } -) +} class MarkrService { async getSwapRateStream({ @@ -343,7 +339,7 @@ class MarkrService { from: string }): Promise { const { uuid } = quote - return await markrServiceClient.swap({ + return await markrApiClient.swap({ uuid, chainId: network.chainId, from, @@ -356,9 +352,7 @@ class MarkrService { } async getSpenderAddress({ chainId }: { chainId: number }): Promise { - const { address } = await markrServiceClient.getSpenderAddress({ - queries: { chainId } - }) + const { address } = await markrApiClient.getSpenderAddress(chainId) return address } } diff --git a/packages/core-mobile/app/new/features/swap/utils/svm/jupiterApi.client.ts b/packages/core-mobile/app/new/features/swap/utils/svm/jupiterApi.client.ts index 7cea92eff6..5e8ba450a4 100644 --- a/packages/core-mobile/app/new/features/swap/utils/svm/jupiterApi.client.ts +++ b/packages/core-mobile/app/new/features/swap/utils/svm/jupiterApi.client.ts @@ -1,43 +1,71 @@ -import { makeApi, Zodios } from '@zodios/core' import { z } from 'zod' -import { JUPITER_QUOTE_SCHEMA, JUPITER_TX_SCHEMA } from './schemas' +import { + fetchJson, + buildQueryString +} from 'utils/api/common/fetchWithValidation' +import { + JUPITER_QUOTE_SCHEMA, + JUPITER_TX_SCHEMA, + JupiterQuote, + JupiterTx +} from './schemas' -const endpoints = makeApi([ - { - method: 'get', - path: '/quote', - alias: 'getQuote', - parameters: [ - { name: 'inputMint', type: 'Query', schema: z.string() }, - { name: 'outputMint', type: 'Query', schema: z.string() }, - { name: 'swapMode', type: 'Query', schema: z.string() }, - { name: 'amount', type: 'Query', schema: z.string() }, - { name: 'slippageBps', type: 'Query', schema: z.string() }, - { name: 'platformFeeBps', type: 'Query', schema: z.string().optional() } - ], - response: JUPITER_QUOTE_SCHEMA +const JUPITER_BASE_URL = 'https://lite-api.jup.ag/swap/v1' + +// Swap request body schema +const SwapBodySchema = z.object({ + quoteResponse: JUPITER_QUOTE_SCHEMA, + userPublicKey: z.string(), + dynamicComputeUnitLimit: z.boolean(), + feeAccount: z.string().optional() +}) + +type SwapBody = z.infer + +export const jupiterApi = { + // GET /quote + getQuote: async ({ + inputMint, + outputMint, + swapMode, + amount, + slippageBps, + platformFeeBps, + signal + }: { + inputMint: string + outputMint: string + swapMode: string + amount: string + slippageBps: string + platformFeeBps?: string + signal?: AbortSignal + }): Promise => { + const queryString = buildQueryString({ + inputMint, + outputMint, + swapMode, + amount, + slippageBps, + platformFeeBps + }) + return fetchJson( + `${JUPITER_BASE_URL}/quote${queryString}`, + { method: 'GET', signal }, + JUPITER_QUOTE_SCHEMA + ) }, - { - method: 'post', - path: '/swap', - alias: 'swap', - requestFormat: 'json', - parameters: [ + + // POST /swap + swap: async (body: SwapBody): Promise => { + return fetchJson( + `${JUPITER_BASE_URL}/swap`, { - name: 'body', - type: 'Body', - schema: z.object({ - quoteResponse: JUPITER_QUOTE_SCHEMA, - userPublicKey: z.string(), - dynamicComputeUnitLimit: z.boolean(), - feeAccount: z.string().optional() - }) - } - ], - response: JUPITER_TX_SCHEMA + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body) + }, + JUPITER_TX_SCHEMA + ) } -]) - -const JUPITER_BASE_URL = 'https://lite-api.jup.ag/swap/v1' - -export const jupiterApi = new Zodios(JUPITER_BASE_URL, endpoints) +} diff --git a/packages/core-mobile/app/new/features/swapV2/consts.ts b/packages/core-mobile/app/new/features/swapV2/consts.ts index e875359338..9086d3bd75 100644 --- a/packages/core-mobile/app/new/features/swapV2/consts.ts +++ b/packages/core-mobile/app/new/features/swapV2/consts.ts @@ -1,3 +1,5 @@ +import Config from 'react-native-config' +import { Environment } from '@avalabs/unified-asset-transfer' import { BuildTxParams } from "features/swap/services/ParaswapService" /** @@ -93,3 +95,25 @@ export const MIN_SLIPPAGE_PERCENT = 0.1 * @example 50 -> 50% */ export const MAX_SLIPPAGE_PERCENT = 50 + +/** + * Markr API endpoint for Fusion SDK + */ +// TODO add to env variables once stable https://ava-labs.atlassian.net/browse/CP-13381 +export const MARKR_API_URL = + Config.MARKR_API_URL ?? 'https://proxy-api.avax.network/proxy/markr-staging' + +/** + * Determines the Fusion SDK environment based on app settings + */ +export function getFusionEnvironment( + isDeveloperMode: boolean, +): Environment { + // If developer mode is enabled, use TEST environment + if (isDeveloperMode) { + return Environment.TEST + } + + // Default to production environment + return Environment.PROD +} diff --git a/packages/core-mobile/app/new/features/swapV2/contexts/SwapContext.tsx b/packages/core-mobile/app/new/features/swapV2/contexts/SwapContext.tsx index 6ff930a9c5..c1127bd4ae 100644 --- a/packages/core-mobile/app/new/features/swapV2/contexts/SwapContext.tsx +++ b/packages/core-mobile/app/new/features/swapV2/contexts/SwapContext.tsx @@ -18,7 +18,7 @@ import { useQuotes, useSwapSelectedFromToken, useSwapSelectedToToken -} from '../store' +} from '../hooks/useZustandStore' import { getTokenAddress } from '../utils/getTokenAddress' const DEFAULT_SLIPPAGE = 0.2 diff --git a/packages/core-mobile/app/new/features/swapV2/hooks/useSupportedChains.ts b/packages/core-mobile/app/new/features/swapV2/hooks/useSupportedChains.ts new file mode 100644 index 0000000000..13580686a1 --- /dev/null +++ b/packages/core-mobile/app/new/features/swapV2/hooks/useSupportedChains.ts @@ -0,0 +1,87 @@ +import { useQuery } from '@tanstack/react-query' +import { useMemo } from 'react' +import { useSelector } from 'react-redux' +import type { Network } from '@avalabs/core-chains-sdk' +import { ReactQueryKeys } from 'consts/reactQueryKeys' +import { useNetworks } from 'hooks/networks/useNetworks' +import Logger from 'utils/Logger' +import { exponentialBackoff } from 'utils/reactQuery' +import { isAvalancheChainId } from 'services/network/utils/isAvalancheNetwork' +import { isSolanaNetwork } from 'utils/network/isSolanaNetwork' +import { selectIsSolanaSwapBlocked } from 'store/posthog' +import FusionService from '../services/FusionService' + +/** + * Stale time in milliseconds + */ +const STALE_TIME = 2 * 60 * 1000 // 2 minutes + +/** + * React hook to fetch supported chains from the Fusion SDK dynamically + * + * This hook: + * - Fetches supported chains from FusionService + * - Converts CAIP-2 chain IDs to app's Network objects from enabled networks + * - Returns only enabled networks that match developer mode + * - Uses React Query for caching and state management + * + * @returns Object containing chains array, loading state, and error state + */ +export function useSupportedChains(): { + chains: Network[] | undefined + isLoading: boolean + error: Error | null +} { + const { getEnabledNetworkByCaip2ChainId } = useNetworks() + const isSolanaSwapBlocked = useSelector(selectIsSolanaSwapBlocked) + + // Fetch raw CAIP-2 chain IDs from Fusion SDK + const { + data: caip2ChainIds, + isLoading, + error + } = useQuery({ + queryKey: [ReactQueryKeys.FUSION_SUPPORTED_CHAINS], + queryFn: async () => { + return FusionService.getSupportedChains() + }, + staleTime: STALE_TIME, + retry: 3, + retryDelay: exponentialBackoff(5000) + }) + + // Convert CAIP-2 IDs to Network objects from enabled networks + const chains = useMemo(() => { + if (!caip2ChainIds) return undefined + + const supportedNetworks = caip2ChainIds + .map(caip2Id => getEnabledNetworkByCaip2ChainId(caip2Id)) + .filter((network): network is Network => network !== undefined) + .filter(network => { + // Filter out Solana networks if Solana swap is blocked + return !(isSolanaSwapBlocked && isSolanaNetwork(network)) + }) + .sort((a, b) => { + // Avalanche C-Chain always first + const aIsAvalanche = isAvalancheChainId(a.chainId) + const bIsAvalanche = isAvalancheChainId(b.chainId) + + if (aIsAvalanche && !bIsAvalanche) return -1 + if (!aIsAvalanche && bIsAvalanche) return 1 + return 0 + }) + + Logger.info( + `Mapped to ${supportedNetworks.length} supported networks:`, + supportedNetworks.map(n => `${n.chainName} (${n.chainId})`) + ) + + return supportedNetworks + }, [caip2ChainIds, getEnabledNetworkByCaip2ChainId, isSolanaSwapBlocked]) + + return { + chains, + isLoading, + error: error as Error | null + } +} diff --git a/packages/core-mobile/app/new/features/swapV2/hooks/useSwapV2Tokens.ts b/packages/core-mobile/app/new/features/swapV2/hooks/useSwapV2Tokens.ts index b669468b3f..cc1c4a3c6b 100644 --- a/packages/core-mobile/app/new/features/swapV2/hooks/useSwapV2Tokens.ts +++ b/packages/core-mobile/app/new/features/swapV2/hooks/useSwapV2Tokens.ts @@ -7,12 +7,10 @@ import { LocalTokenWithBalance } from 'store/balance' import { getChainIdFromCaip2 } from 'utils/caip2ChainIds' import { selectActiveAccount } from 'store/account' import { useTokensWithBalanceByNetworkForAccount } from 'features/portfolio/hooks/useTokensWithBalanceByNetworkForAccount' -import useCChainNetwork from 'hooks/earn/useCChainNetwork' -import useSolanaNetwork from 'hooks/earn/useSolanaNetwork' +import { useNetworks } from 'hooks/networks/useNetworks' import { TokenType } from '@avalabs/vm-module-types' import { TokenUnit } from '@avalabs/core-utils-sdk' -import { isAvalancheCChainId } from 'services/network/utils/isAvalancheNetwork' -import { isSolanaChainId } from 'utils/network/isSolanaNetwork' +import { ReactQueryKeys } from 'consts/reactQueryKeys' import { mapApiTokenToLocal } from '../utils/mapApiTokenToLocal' import { getLocalTokenIdFromApi } from '../utils/getLocalTokenIdFromApi' @@ -42,10 +40,13 @@ export const useSwapV2Tokens = ( // Derive chainId from caip2Id const chainId = useMemo(() => getChainIdFromCaip2(caip2Id), [caip2Id]) const activeAccount = useSelector(selectActiveAccount) + const { getNetwork } = useNetworks() - // Get network configurations for native tokens - const cChainNetwork = useCChainNetwork() - const solanaNetwork = useSolanaNetwork() + // Get current network + const currentNetwork = useMemo( + () => (chainId ? getNetwork(chainId) : undefined), + [chainId, getNetwork] + ) // Get balances const balances = useTokensWithBalanceByNetworkForAccount( @@ -55,7 +56,7 @@ export const useSwapV2Tokens = ( // Fetch tokens from API const query = useQuery({ - queryKey: ['swapV2Tokens', caip2Id], + queryKey: [ReactQueryKeys.FUSION_TOKENS, caip2Id], queryFn: async () => { if (!caip2Id) return [] @@ -72,8 +73,7 @@ export const useSwapV2Tokens = ( // Transform and merge with balance data const tokens = useMemo((): LocalTokenWithBalance[] => { - if (!chainId || query.data === undefined || query.data.length === 0) - return [] + if (!chainId || query.data === undefined) return [] // Create balance lookup map by localId const balanceMap = new Map() @@ -83,21 +83,14 @@ export const useSwapV2Tokens = ( } }) - // Determine current network - const currentNetwork = isAvalancheCChainId(chainId) - ? cChainNetwork - : isSolanaChainId(chainId) - ? solanaNetwork - : null - // Create native token if network is available let nativeToken: LocalTokenWithBalance | null = null if (currentNetwork) { const symbol = currentNetwork.networkToken.symbol const decimals = currentNetwork.networkToken.decimals - const localId = `native-${symbol.toLowerCase()}` - const nativeBalanceData = balanceMap.get(localId) + const localId = `NATIVE-${symbol}` + const nativeBalanceData = balanceMap.get(localId.toLowerCase()) const balance = nativeBalanceData?.balance ?? 0n const balanceDisplayValue = new TokenUnit( @@ -125,16 +118,19 @@ export const useSwapV2Tokens = ( } } - // Map API tokens - const apiTokens = query.data.map(apiToken => { - const localId = getLocalTokenIdFromApi(apiToken) - const balanceData = balanceMap.get(localId) - return mapApiTokenToLocal(apiToken, chainId, balanceData) - }) + // Map API tokens (if any) + const apiTokens = + query.data.length > 0 + ? query.data.map(apiToken => { + const localId = getLocalTokenIdFromApi(apiToken) + const balanceData = balanceMap.get(localId.toLowerCase()) + return mapApiTokenToLocal(apiToken, chainId, balanceData) + }) + : [] // Add native token return nativeToken ? [nativeToken, ...apiTokens] : apiTokens - }, [query.data, balances, chainId, cChainNetwork, solanaNetwork]) + }, [query.data, balances, chainId, currentNetwork]) return { tokens, diff --git a/packages/core-mobile/app/new/features/swapV2/store.ts b/packages/core-mobile/app/new/features/swapV2/hooks/useZustandStore.ts similarity index 90% rename from packages/core-mobile/app/new/features/swapV2/store.ts rename to packages/core-mobile/app/new/features/swapV2/hooks/useZustandStore.ts index 0ceb944a97..cd3def41ed 100644 --- a/packages/core-mobile/app/new/features/swapV2/store.ts +++ b/packages/core-mobile/app/new/features/swapV2/hooks/useZustandStore.ts @@ -1,6 +1,6 @@ import { createZustandStore } from 'common/utils/createZustandStore' import { LocalTokenWithBalance } from 'store/balance' -import { NormalizedSwapQuoteResult } from './types' +import { NormalizedSwapQuoteResult } from '../types' export const useSwapSelectedFromToken = createZustandStore< LocalTokenWithBalance | undefined diff --git a/packages/core-mobile/app/new/features/swapV2/screens/SelectSwapV2TokenScreen.tsx b/packages/core-mobile/app/new/features/swapV2/screens/SelectSwapV2TokenScreen.tsx index 8e61c8ecbb..d81a4af8e5 100644 --- a/packages/core-mobile/app/new/features/swapV2/screens/SelectSwapV2TokenScreen.tsx +++ b/packages/core-mobile/app/new/features/swapV2/screens/SelectSwapV2TokenScreen.tsx @@ -1,3 +1,4 @@ +import React, { useMemo, useState, useCallback, useEffect, useRef } from 'react' import { ChainId, Network } from '@avalabs/core-chains-sdk' import { ActivityIndicator, @@ -15,16 +16,12 @@ import { ErrorState } from 'common/components/ErrorState' import { ListScreenV2 } from 'common/components/ListScreenV2' import { useRouter } from 'expo-router' import { LogoWithNetwork } from 'features/portfolio/assets/components/LogoWithNetwork' -import useCChainNetwork from 'hooks/earn/useCChainNetwork' -import useSolanaNetwork from 'hooks/earn/useSolanaNetwork' -import React, { useCallback, useEffect, useMemo, useState } from 'react' import { ListRenderItem } from '@shopify/flash-list' -import { useSelector } from 'react-redux' import { LocalTokenWithBalance } from 'store/balance' -import { selectIsSolanaSwapBlocked } from 'store/posthog' import { getCaip2ChainId } from 'utils/caip2ChainIds' import { useFilteredSwapTokens } from '../hooks/useFilteredSwapTokens' import { useSwapV2Tokens } from '../hooks/useSwapV2Tokens' +import { useSupportedChains } from '../hooks/useSupportedChains' export const SelectSwapV2TokenScreen = ({ selectedToken, @@ -43,30 +40,30 @@ export const SelectSwapV2TokenScreen = ({ const { back, canGoBack } = useRouter() const [searchText, setSearchText] = useState('') - // Get available networks - const cChainNetwork = useCChainNetwork() - const solanaNetwork = useSolanaNetwork() - const isSolanaSwapBlocked = useSelector(selectIsSolanaSwapBlocked) + // Get dynamically supported networks from Fusion SDK + const { chains: networks } = useSupportedChains() - // Network list - const networks = useMemo(() => { - const list = [cChainNetwork] - if (!isSolanaSwapBlocked && solanaNetwork) { - list.push(solanaNetwork) - } - return list.filter(Boolean) as Network[] - }, [cChainNetwork, solanaNetwork, isSolanaSwapBlocked]) - - // Selected network state (default to C-Chain or provided default) + // Selected network state (default to first network or provided default) const [selectedNetwork, setSelectedNetwork] = useState( - cChainNetwork + undefined ) + // Track if we've set the default network + const hasSetDefaultNetwork = useRef(false) + + // Set default network once when networks are loaded useEffect(() => { + if (!networks || networks.length === 0 || hasSetDefaultNetwork.current) + return + if (defaultNetworkChainId) { const found = networks.find(n => n.chainId === defaultNetworkChainId) - setSelectedNetwork(found) + setSelectedNetwork(found ?? networks[0]) + } else { + setSelectedNetwork(networks[0]) } + + hasSetDefaultNetwork.current = true }, [defaultNetworkChainId, networks]) // Get CAIP2 ID for selected network @@ -74,11 +71,8 @@ export const SelectSwapV2TokenScreen = ({ if (selectedNetwork) { return getCaip2ChainId(selectedNetwork.chainId) } - if (cChainNetwork) { - return getCaip2ChainId(cChainNetwork.chainId) - } return '' - }, [selectedNetwork, cChainNetwork]) + }, [selectedNetwork]) // Lazy load tokens for selected network (with balance data merged) const { tokens, isLoading } = useSwapV2Tokens(caip2Id) @@ -97,7 +91,7 @@ export const SelectSwapV2TokenScreen = ({ // Render network tabs const renderNetworkSelector = useCallback(() => { - if (networks.length <= 1) return null + if (!networks || networks.length <= 1) return null return ( { ) const handleSelectFromToken = useCallback((): void => { - // @ts-ignore TODO: make routes typesafe - navigate('/selectSwapV2FromToken') - }, [navigate]) + const tokenParams = toToken?.networkChainId + ? { networkChainId: toToken.networkChainId.toString() } + : {} + + navigate({ + // @ts-ignore TODO: make routes typesafe + pathname: '/selectSwapV2FromToken', + params: tokenParams + }) + }, [navigate, toToken]) const handleSelectToToken = useCallback((): void => { const tokenParams = fromToken?.networkChainId @@ -618,51 +618,6 @@ export const SwapScreen = (): JSX.Element => { ) }, [canSwap, handleSwap, swapInProcess]) - const prevFromToken = usePrevious(fromToken) - const prevToToken = usePrevious(toToken) - useEffect(() => { - // if both tokens are on the same chain, do nothing - if (fromToken?.networkChainId === toToken?.networkChainId) return - - const prevFrom = prevFromToken - const prevTo = prevToToken - - // determine which token actually changed - const isFromChanged = - prevFrom?.localId !== fromToken?.localId && !!fromToken - const isToChanged = - !isFromChanged && prevTo?.localId !== toToken?.localId && !!toToken - if (!isFromChanged && !isToChanged) return - - // pick the token that changed and compute the default counterpart - const changedToken = isFromChanged ? fromToken : toToken - const isAvaxChain = changedToken?.networkChainId === cChainNetwork?.chainId - const [baseId, pairId] = isAvaxChain - ? [AVAX_TOKEN_ID, USDC_AVALANCHE_C_TOKEN_ID] - : [SOLANA_TOKEN_LOCAL_ID, USDC_SOLANA_TOKEN_ID] - const targetLocalId = changedToken?.localId === baseId ? pairId : baseId - const defaultToken = swapList.find( - tk => tk.localId.toLowerCase() === targetLocalId.toLowerCase() - ) - - // update the opposite token - if (isFromChanged) { - setToToken(defaultToken) - } else { - setFromToken(defaultToken) - } - }, [ - fromToken, - toToken, - prevFromToken, - prevToToken, - cChainNetwork?.chainId, - solanaNetwork?.chainId, - swapList, - setFromToken, - setToToken - ]) - return ( { + try { + Logger.info('Initializing Fusion service', { + environment: config.environment, + enabledServices: config.enabledServices + }) + + const initializers = this.getServiceInitializers({ + btcFunctions: bitcoinProvider, + enabledServices: config.enabledServices, + signers + }) + + // Ensure at least one service is enabled + if (initializers.length === 0) { + Logger.warn('No Fusion services enabled. Skipping initialization.') + return + } + + // Create the TransferManager instance + this.#transferManager = await createTransferManager({ + environment: config.environment, + serviceInitializers: initializers as [ + ServiceInitializer, + ...ServiceInitializer[] + ] + }) + + Logger.info('Fusion service initialized successfully') + } catch (error) { + Logger.error('Failed to initialize Fusion service', error) + throw error + } + } + + /** + * Initialize with feature flags + * Helper method that determines enabled services from feature flags + */ + async initWithFeatureFlags({ + bitcoinProvider, + environment, + featureFlags, + signers + }: { + bitcoinProvider: BitcoinFunctions + environment: Environment + featureFlags: FeatureFlags + signers: FusionSigners + }): Promise { + const enabledServices = this.getEnabledServices(featureFlags) + + return this.init({ + bitcoinProvider, + config: { environment, enabledServices }, + signers + }) + } + + /** + * Get supported chains from the TransferManager + * Returns CAIP-2 chain IDs that are supported by the enabled services + * + * @returns Promise resolving to array of CAIP-2 chain IDs + */ + async getSupportedChains(): Promise { + try { + const chains = await this.transferManager.getSupportedChains() + Logger.info(`Fusion SDK supports ${chains.length} chains`, chains) + return chains + } catch (error) { + Logger.error('Failed to fetch supported chains from Fusion SDK', error) + throw error + } + } + + /** + * Check if the service is initialized + */ + isInitialized(): boolean { + return this.#transferManager !== null + } + + /** + * Cleanup and reset the service + */ + cleanup(): void { + this.#transferManager = null + Logger.info('Fusion service cleaned up') + } +} + +// Export a singleton instance +export default new FusionService() diff --git a/packages/core-mobile/app/new/features/swapV2/services/signers/BtcSigner.ts b/packages/core-mobile/app/new/features/swapV2/services/signers/BtcSigner.ts new file mode 100644 index 0000000000..03f676f0d1 --- /dev/null +++ b/packages/core-mobile/app/new/features/swapV2/services/signers/BtcSigner.ts @@ -0,0 +1,41 @@ +import { RpcMethod } from '@avalabs/vm-module-types' +import { BtcSigner } from '@avalabs/unified-asset-transfer' +import { getBitcoinCaip2ChainId } from 'utils/caip2ChainIds' +import { Request } from 'store/rpc/utils/createInAppRequest' +import Logger from 'utils/Logger' + +/** + * BTC Signer implementation for Fusion SDK + * + * This signer implements the BtcSigner interface required by the Fusion SDK + * for Bitcoin transaction signing operations. + */ +export function createBtcSigner( + request: Request, + isDeveloperMode = false +): BtcSigner { + const chainId = getBitcoinCaip2ChainId(!isDeveloperMode) + + return { + /** + * Sign a Bitcoin transaction with inputs and outputs + */ + sign: async ({ inputs, outputs }) => { + try { + const result = await request({ + method: RpcMethod.BITCOIN_SIGN_TRANSACTION, + params: { + inputs, + outputs + }, + chainId + }) + + return result as `0x${string}` + } catch (err) { + Logger.error('[fusion::btcSigner.sign]', err) + throw err + } + } + } +} diff --git a/packages/core-mobile/app/new/features/swapV2/services/signers/EvmSigner.ts b/packages/core-mobile/app/new/features/swapV2/services/signers/EvmSigner.ts new file mode 100644 index 0000000000..2ac8495f56 --- /dev/null +++ b/packages/core-mobile/app/new/features/swapV2/services/signers/EvmSigner.ts @@ -0,0 +1,77 @@ +import { hex, utf8 } from '@scure/base' +import { bigIntToHex } from '@ethereumjs/util' +import { RpcMethod } from '@avalabs/vm-module-types' +import { EvmSignerWithMessage } from '@avalabs/unified-asset-transfer' +import { getEvmCaip2ChainId } from 'utils/caip2ChainIds' +import { Request } from 'store/rpc/utils/createInAppRequest' +import { assert } from 'store/rpc/utils/assert' +import Logger from 'utils/Logger' + +/** + * EVM Signer implementation for Fusion SDK + * + * This signer implements the EvmSignerWithMessage interface required by the Fusion SDK, + * which includes both transaction signing and message signing capabilities. + */ +export function createEvmSigner(request: Request): EvmSignerWithMessage { + return { + /** + * Sign and send an EVM transaction + */ + sign: async ({ from, data, to, value, chainId }) => { + assert(to, 'Invalid transaction: missing "to" field') + assert(from, 'Invalid transaction: missing "from" field') + assert(data, 'Invalid transaction: missing "data" field') + assert(chainId, 'Invalid transaction: missing "chainId" field') + + try { + const result = await request({ + method: RpcMethod.ETH_SEND_TRANSACTION, + params: [ + { + from, + to, + data, + value: typeof value === 'bigint' ? bigIntToHex(value) : undefined, + chainId + } + ], + chainId: getEvmCaip2ChainId(Number(chainId)) + }) + + return result as `0x${string}` + } catch (err) { + Logger.error('[fusion::evmSigner.sign]', err) + throw err + } + }, + + /** + * Sign a message with the EVM account + * Required by EvmSignerWithMessage interface + */ + signMessage: async (data: { + message: string + address: `0x${string}` + chainId: number + }) => { + const { message, address, chainId } = data + + assert(message, 'Invalid message signing request: missing "message"') + assert(address, 'Invalid message signing request: missing "address"') + + try { + const result = await request({ + method: RpcMethod.PERSONAL_SIGN, + params: [`0x${hex.encode(utf8.decode(message))}`, address], + chainId: getEvmCaip2ChainId(chainId) + }) + + return result as `0x${string}` + } catch (err) { + Logger.error('[fusion::evmSigner.signMessage]', err) + throw err + } + } + } +} diff --git a/packages/core-mobile/app/new/features/swapV2/services/types.ts b/packages/core-mobile/app/new/features/swapV2/services/types.ts new file mode 100644 index 0000000000..d2a0596c8c --- /dev/null +++ b/packages/core-mobile/app/new/features/swapV2/services/types.ts @@ -0,0 +1,57 @@ +import type { + BitcoinFunctions, + BtcSigner, + Environment, + EvmSignerWithMessage, + ServiceType +} from '@avalabs/unified-asset-transfer' + +/** + * Configuration for initializing the Fusion SDK + */ +export interface FusionConfig { + environment: Environment + enabledServices: ServiceType[] +} + +/** + * Signers required for Fusion operations + */ +export interface FusionSigners { + evm: EvmSignerWithMessage + btc: BtcSigner +} + +/** + * Service interface for Fusion SDK operations + */ +export interface IFusionService { + /** + * Initialize the Fusion service with signers and configuration + */ + init({ + bitcoinProvider, + config, + signers + }: { + bitcoinProvider: BitcoinFunctions + config: FusionConfig + signers: FusionSigners + }): Promise + + /** + * Get supported chains from the Fusion SDK + * Returns CAIP-2 chain IDs that are supported by the enabled services + */ + getSupportedChains(): Promise + + /** + * Check if the service is initialized + */ + isInitialized(): boolean + + /** + * Cleanup and reset the service + */ + cleanup(): void +} diff --git a/packages/core-mobile/app/new/features/swapV2/store/listeners.ts b/packages/core-mobile/app/new/features/swapV2/store/listeners.ts new file mode 100644 index 0000000000..d9660b8813 --- /dev/null +++ b/packages/core-mobile/app/new/features/swapV2/store/listeners.ts @@ -0,0 +1,172 @@ +import { AppListenerEffectAPI, AppStartListening, RootState } from 'store/types' +import { onAppUnlocked, onLogOut, selectIsLocked } from 'store/app/slice' +import { + selectIsDeveloperMode, + toggleDeveloperMode +} from 'store/settings/advanced/slice' +import { isAnyOf } from '@reduxjs/toolkit' +import { + selectIsFusionEnabled, + selectIsFusionMarkrEnabled, + selectIsFusionAvalancheEvmEnabled, + selectIsFusionLombardBtcToBtcbEnabled, + selectIsFusionLombardBtcbToBtcEnabled, + setFeatureFlags +} from 'store/posthog/slice' +import Logger from 'utils/Logger' +import { createInAppRequest } from 'store/rpc/utils/createInAppRequest' +import { getBitcoinProvider } from 'services/network/utils/providerUtils' +import FusionService from '../services/FusionService' +import { getFusionEnvironment } from '../consts' +import { createEvmSigner } from '../services/signers/EvmSigner' +import { createBtcSigner } from '../services/signers/BtcSigner' + +/** + * Get the current state of all Fusion feature flags + */ +const getFusionFeatureStates = ( + state: RootState +): { + isFusionEnabled: boolean + isFusionMarkrEnabled: boolean + isFusionAvalancheEvmEnabled: boolean + isFusionLombardBtcToBtcbEnabled: boolean + isFusionLombardBtcbToBtcEnabled: boolean + isDeveloperMode: boolean +} => ({ + isFusionEnabled: selectIsFusionEnabled(state), + isFusionMarkrEnabled: selectIsFusionMarkrEnabled(state), + isFusionAvalancheEvmEnabled: selectIsFusionAvalancheEvmEnabled(state), + isFusionLombardBtcToBtcbEnabled: selectIsFusionLombardBtcToBtcbEnabled(state), + isFusionLombardBtcbToBtcEnabled: selectIsFusionLombardBtcbToBtcEnabled(state), + isDeveloperMode: selectIsDeveloperMode(state) +}) + +/** + * Determine if the Fusion service should be reinitialized + * based on state changes + */ +const shouldReinitializeFusion = ( + prevState: RootState, + currentState: RootState +): boolean => { + const prevFeatures = getFusionFeatureStates(prevState) + const currentFeatures = getFusionFeatureStates(currentState) + + // Reinitialize if any relevant feature flag or setting changed + return ( + prevFeatures.isFusionEnabled !== currentFeatures.isFusionEnabled || + prevFeatures.isFusionMarkrEnabled !== + currentFeatures.isFusionMarkrEnabled || + prevFeatures.isFusionAvalancheEvmEnabled !== + currentFeatures.isFusionAvalancheEvmEnabled || + prevFeatures.isFusionLombardBtcToBtcbEnabled !== + currentFeatures.isFusionLombardBtcToBtcbEnabled || + prevFeatures.isFusionLombardBtcbToBtcEnabled !== + currentFeatures.isFusionLombardBtcbToBtcEnabled || + prevFeatures.isDeveloperMode !== currentFeatures.isDeveloperMode + ) +} + +/** + * Initialize the Fusion service with current settings + */ +export const initFusionService = async ( + _action: any, // eslint-disable-line @typescript-eslint/no-explicit-any + listenerApi: AppListenerEffectAPI +): Promise => { + const state = listenerApi.getState() + const isLocked = selectIsLocked(state) + + if (isLocked) { + Logger.info('App is locked, skipping Fusion service initialization') + return + } + + const request = createInAppRequest(listenerApi.dispatch) + + // Check if already initialized and if reinitialization is needed + if (FusionService.isInitialized()) { + const prevState = listenerApi.getOriginalState() + + if (!shouldReinitializeFusion(prevState, state)) return + + // Cleanup before reinitializing + FusionService.cleanup() + } + + const featureStates = getFusionFeatureStates(state) + + // Don't initialize if Fusion is not enabled + if (!featureStates.isFusionEnabled) { + Logger.info('Fusion is disabled, skipping initialization') + return + } + + try { + Logger.info('Initializing Fusion service', featureStates) + + // Create signers + const evmSigner = createEvmSigner(request) + const btcSigner = createBtcSigner(request, featureStates.isDeveloperMode) + + // Determine environment + const environment = getFusionEnvironment(featureStates.isDeveloperMode) + + // Build feature flags object for the service + const featureFlags = { + 'fusion-markr': featureStates.isFusionMarkrEnabled, + 'fusion-avalanche-evm': featureStates.isFusionAvalancheEvmEnabled, + 'fusion-lombard-btc-to-btcb': + featureStates.isFusionLombardBtcToBtcbEnabled, + 'fusion-lombard-btcb-to-btc': + featureStates.isFusionLombardBtcbToBtcEnabled + } + + const bitcoinProvider = await getBitcoinProvider( + featureStates.isDeveloperMode + ) + + // Initialize the service + await FusionService.initWithFeatureFlags({ + bitcoinProvider, + environment, + featureFlags, + signers: { + evm: evmSigner, + btc: btcSigner + } + }) + + Logger.info('Fusion service initialized successfully', { + environment, + enabledServices: Object.entries(featureFlags) + .filter(([, enabled]) => enabled) + .map(([key]) => key) + }) + } catch (error) { + Logger.error('Failed to initialize Fusion service', error) + } +} + +export const cleanupFusionService = async ( + _action: any, // eslint-disable-line @typescript-eslint/no-explicit-any + _listenerApi: AppListenerEffectAPI +): Promise => { + FusionService.cleanup() +} + +/** + * Register Fusion service listeners + */ +export const addFusionListeners = (startListening: AppStartListening): void => { + startListening({ + matcher: isAnyOf(onAppUnlocked, toggleDeveloperMode, setFeatureFlags), + effect: initFusionService + }) + + startListening({ + actionCreator: onLogOut, + effect: cleanupFusionService + }) +} diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2FromToken/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2FromToken/index.tsx index 72bef46d79..66622f4487 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2FromToken/index.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2FromToken/index.tsx @@ -1,15 +1,21 @@ import React from 'react' +import { useLocalSearchParams } from 'expo-router' import { SelectSwapV2TokenScreen } from 'features/swapV2/screens/SelectSwapV2TokenScreen' -import { useSwapSelectedFromToken } from 'features/swapV2/store' +import { useSwapSelectedFromToken } from 'features/swapV2/hooks/useZustandStore' const SelectSwapV2FromTokenScreen = (): JSX.Element => { const [selectedFromToken, setSelectedFromToken] = useSwapSelectedFromToken() + const { networkChainId } = useLocalSearchParams<{ networkChainId?: string }>() return ( ) } diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2ToToken/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2ToToken/index.tsx index 34eb0286bb..e3eb3fac98 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2ToToken/index.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/selectSwapV2ToToken/index.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useLocalSearchParams } from 'expo-router' import { SelectSwapV2TokenScreen } from 'features/swapV2/screens/SelectSwapV2TokenScreen' -import { useSwapSelectedToToken } from 'features/swapV2/store' +import { useSwapSelectedToToken } from 'features/swapV2/hooks/useZustandStore' const SelectSwapV2ToTokenScreen = (): JSX.Element => { const [selectedToToken, setSelectedToToken] = useSwapSelectedToToken() diff --git a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/swapV2/pricingDetails/index.tsx b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/swapV2/pricingDetails/index.tsx index 8ac4a7fe78..4184615424 100644 --- a/packages/core-mobile/app/new/routes/(signedIn)/(modals)/swapV2/pricingDetails/index.tsx +++ b/packages/core-mobile/app/new/routes/(signedIn)/(modals)/swapV2/pricingDetails/index.tsx @@ -5,7 +5,7 @@ import { useQuotes, useSwapSelectedFromToken, useSwapSelectedToToken -} from 'features/swapV2/store' +} from 'features/swapV2/hooks/useZustandStore' export default (): JSX.Element => { const [fromToken] = useSwapSelectedFromToken() diff --git a/packages/core-mobile/app/services/balance/BalanceService.test.ts b/packages/core-mobile/app/services/balance/BalanceService.test.ts index ea8a246feb..81d5ed8de1 100644 --- a/packages/core-mobile/app/services/balance/BalanceService.test.ts +++ b/packages/core-mobile/app/services/balance/BalanceService.test.ts @@ -11,9 +11,9 @@ const mockLoadModuleByNetwork = jest.fn() const mockGetSupportedChainsFromCache = jest.fn() const mockMapBalanceResponseToLegacy = jest.fn() -jest.mock('utils/apiClient/balance/balanceApi', () => ({ - balanceApi: { - getBalancesStream: (...args: unknown[]) => mockGetBalancesStream(...args) +jest.mock('utils/api/clients/balanceApiClient', () => ({ + streamingBalanceApiClient: { + getBalances: (...args: unknown[]) => mockGetBalancesStream(...args) } })) diff --git a/packages/core-mobile/app/services/balance/BalanceService.ts b/packages/core-mobile/app/services/balance/BalanceService.ts index 827774884d..78e9096ea7 100644 --- a/packages/core-mobile/app/services/balance/BalanceService.ts +++ b/packages/core-mobile/app/services/balance/BalanceService.ts @@ -10,9 +10,9 @@ import { SPAN_STATUS_ERROR } from '@sentry/core' import SentryWrapper from 'services/sentry/SentryWrapper' import { Account } from 'store/account/types' import { getAddressByNetwork } from 'store/account/utils' -import { balanceApi } from 'utils/apiClient/balance/balanceApi' +import { streamingBalanceApiClient } from 'utils/api/clients/balanceApiClient' import { getSupportedChainsFromCache } from 'hooks/balance/useSupportedChains' -import { GetBalancesRequestBody } from 'utils/apiClient/generated/balanceApi.client' +import { GetBalancesRequestBody } from 'utils/api/generated/balanceApi.client' import { coingeckoInMemoryCache } from 'utils/coingeckoInMemoryCache' import Logger from 'utils/Logger' import { @@ -480,7 +480,9 @@ export class BalanceService { } try { - for await (const balance of balanceApi.getBalancesStream(body)) { + for await (const balance of streamingBalanceApiClient.getBalances( + body + )) { const id = 'id' in balance ? balance.id : undefined // Resolve to one or more matching accounts. @@ -632,7 +634,9 @@ export class BalanceService { } try { - for await (const balance of balanceApi.getBalancesStream(body)) { + for await (const balance of streamingBalanceApiClient.getBalances( + body + )) { const normalized = mapBalanceResponseToLegacy(account, balance) if (!normalized) continue diff --git a/packages/core-mobile/app/services/balance/utils/buildRequestItemsForAccounts.ts b/packages/core-mobile/app/services/balance/utils/buildRequestItemsForAccounts.ts index 1b62209fc0..bfea54dc09 100644 --- a/packages/core-mobile/app/services/balance/utils/buildRequestItemsForAccounts.ts +++ b/packages/core-mobile/app/services/balance/utils/buildRequestItemsForAccounts.ts @@ -14,7 +14,7 @@ import { EvmGetBalancesRequestItem, GetBalancesRequestBody, SvmGetBalancesRequestItem -} from 'utils/apiClient/generated/balanceApi.client' +} from 'utils/api/generated/balanceApi.client' /** * Maximum number of EVM references allowed per request item diff --git a/packages/core-mobile/app/services/balance/utils/mapBalanceResponseToLegacy.ts b/packages/core-mobile/app/services/balance/utils/mapBalanceResponseToLegacy.ts index 1ee16901d6..dd0dde82f3 100644 --- a/packages/core-mobile/app/services/balance/utils/mapBalanceResponseToLegacy.ts +++ b/packages/core-mobile/app/services/balance/utils/mapBalanceResponseToLegacy.ts @@ -12,7 +12,7 @@ import { NativeTokenBalance, PvmGetBalancesResponse, SvmGetBalancesResponse -} from 'utils/apiClient/generated/balanceApi.client' +} from 'utils/api/generated/balanceApi.client' import { TokenType } from '@avalabs/vm-module-types' import { Avalanche } from '@avalabs/core-wallets-sdk' import { AVAX_P_ID, AVAX_X_ID } from '../const' diff --git a/packages/core-mobile/app/services/browser/BrowserService.ts b/packages/core-mobile/app/services/browser/BrowserService.ts index 572797dda9..07196e4133 100644 --- a/packages/core-mobile/app/services/browser/BrowserService.ts +++ b/packages/core-mobile/app/services/browser/BrowserService.ts @@ -4,10 +4,7 @@ import { DeFiProtocolInformationObject } from './debankTypes' class BrowserService { static getDeFiProtocolInformationList = (): Promise< DeFiProtocolInformationObject[] - > => - browserApiClient.getDeFiProtocolInformationList({ - queries: { chain_id: 'avax' } - }) + > => browserApiClient.getDeFiProtocolInformationList('avax') } export default BrowserService diff --git a/packages/core-mobile/app/services/browser/apiClient.ts b/packages/core-mobile/app/services/browser/apiClient.ts index fe49a9edcd..c0afac227a 100644 --- a/packages/core-mobile/app/services/browser/apiClient.ts +++ b/packages/core-mobile/app/services/browser/apiClient.ts @@ -1,29 +1,32 @@ -import { Zodios } from '@zodios/core' import Config from 'react-native-config' import { z } from 'zod' import Logger from 'utils/Logger' +import { + fetchJson, + buildQueryString +} from 'utils/api/common/fetchWithValidation' import { DeFiProtocolInformationSchema } from './debankTypes' if (!Config.PROXY_URL) Logger.warn('PROXY_URL is missing') const baseUrl = Config.PROXY_URL + '/proxy/debank/v1' -export const browserApiClient = new Zodios( - baseUrl, - [ - { - method: 'get', - path: '/protocol/list', - parameters: [{ name: 'chain_id', type: 'Query', schema: z.string() }], - alias: 'getDeFiProtocolInformationList', - response: z.array(DeFiProtocolInformationSchema) - } - ], - { - axiosConfig: { - headers: { - 'Content-Type': 'application/json' - } - } +// Infer TypeScript types from Zod schemas +type DeFiProtocolInformation = z.infer + +export const browserApiClient = { + // GET /protocol/list + getDeFiProtocolInformationList: async ( + chain_id: string + ): Promise => { + const queryString = buildQueryString({ chain_id }) + return fetchJson( + `${baseUrl}/protocol/list${queryString}`, + { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, + z.array(DeFiProtocolInformationSchema) + ) } -) +} diff --git a/packages/core-mobile/app/services/defi/DeFiService.ts b/packages/core-mobile/app/services/defi/DeFiService.ts index 06e3e19c55..a1859fa7bb 100644 --- a/packages/core-mobile/app/services/defi/DeFiService.ts +++ b/packages/core-mobile/app/services/defi/DeFiService.ts @@ -19,13 +19,14 @@ class DeFiService { protocolId: string ): Promise => defiApiClient.getDeFiProtocol({ - queries: { id: userAddress, protocol_id: protocolId } + id: userAddress, + protocol_id: protocolId }) static getDeFiProtocolList = ( userAddress: string ): Promise => - defiApiClient.getDeFiProtocolList({ queries: { id: userAddress } }) + defiApiClient.getDeFiProtocolList(userAddress) static getExchangeRates = async (): Promise => { try { diff --git a/packages/core-mobile/app/services/defi/apiClient.ts b/packages/core-mobile/app/services/defi/apiClient.ts index f047a5b5bd..9ebfb4afbe 100644 --- a/packages/core-mobile/app/services/defi/apiClient.ts +++ b/packages/core-mobile/app/services/defi/apiClient.ts @@ -1,7 +1,10 @@ -import { Zodios } from '@zodios/core' import Config from 'react-native-config' import { z } from 'zod' import Logger from 'utils/Logger' +import { + fetchJson, + buildQueryString +} from 'utils/api/common/fetchWithValidation' import { DeFiChainSchema, DeFiProtocolSchema, @@ -13,41 +16,57 @@ if (!Config.PROXY_URL) Logger.warn('PROXY_URL is missing. Defi disabled.') const baseUrl = Config.PROXY_URL + '/proxy/debank/v1' -export const defiApiClient = new Zodios( - baseUrl, - [ - { - method: 'get', - path: '/chain/list', - alias: 'getSupportedChainList', - response: z.array(DeFiChainSchema) - }, - { - method: 'get', - path: '/user/protocol', - parameters: [ - { name: 'id', type: 'Query', schema: z.string() }, - { name: 'protocol_id', type: 'Query', schema: z.string() } - ], - alias: 'getDeFiProtocol', - response: DeFiProtocolSchema - }, - { - method: 'get', - path: '/user/all_simple_protocol_list', - parameters: [{ name: 'id', type: 'Query', schema: z.string() }], - alias: 'getDeFiProtocolList', - response: z.array(DeFiSimpleProtocolSchema) - } - ], - { - axiosConfig: { - headers: { - 'Content-Type': 'application/json' - } - } +// Infer TypeScript types from Zod schemas +type DeFiChain = z.infer +type DeFiProtocol = z.infer +type DeFiSimpleProtocol = z.infer +type ExchangeRate = z.infer + +export const defiApiClient = { + // GET /chain/list + getSupportedChainList: async (): Promise => { + return fetchJson( + `${baseUrl}/chain/list`, + { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, + z.array(DeFiChainSchema) + ) + }, + + // GET /user/protocol + getDeFiProtocol: async ({ + id, + protocol_id + }: { + id: string + protocol_id: string + }): Promise => { + const queryString = buildQueryString({ id, protocol_id }) + return fetchJson( + `${baseUrl}/user/protocol${queryString}`, + { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, + DeFiProtocolSchema + ) + }, + + // GET /user/all_simple_protocol_list + getDeFiProtocolList: async (id: string): Promise => { + const queryString = buildQueryString({ id }) + return fetchJson( + `${baseUrl}/user/all_simple_protocol_list${queryString}`, + { + method: 'GET', + headers: { 'Content-Type': 'application/json' } + }, + z.array(DeFiSimpleProtocolSchema) + ) } -) +} // https://github.com/fawazahmed0/exchange-api/blob/main/README.md#free-currency-exchange-rates-api // We're only loading exchange rates for USD at the moment. @@ -57,23 +76,24 @@ const CURRENCY_EXCHANGE_RATES_URL = const CURRENCY_EXCHANGE_RATES_FALLBACK_URL = 'https://latest.currency-api.pages.dev/v1/currencies/usd.min.json' -export const exchangeRateApiClient = new Zodios(CURRENCY_EXCHANGE_RATES_URL, [ - { - method: 'get', - path: '', - alias: 'getExchangeRates', - response: ExchangeRateSchema +export const exchangeRateApiClient = { + // GET / + getExchangeRates: async (): Promise => { + return fetchJson( + CURRENCY_EXCHANGE_RATES_URL, + { method: 'GET' }, + ExchangeRateSchema + ) } -]) +} -export const exchangeRateFallbackApiClient = new Zodios( - CURRENCY_EXCHANGE_RATES_FALLBACK_URL, - [ - { - method: 'get', - path: '', - alias: 'getExchangeRates', - response: ExchangeRateSchema - } - ] -) +export const exchangeRateFallbackApiClient = { + // GET / + getExchangeRates: async (): Promise => { + return fetchJson( + CURRENCY_EXCHANGE_RATES_FALLBACK_URL, + { method: 'GET' }, + ExchangeRateSchema + ) + } +} diff --git a/packages/core-mobile/app/services/defi/types.ts b/packages/core-mobile/app/services/defi/types.ts index 7d974cc95d..876272f2c3 100644 --- a/packages/core-mobile/app/services/defi/types.ts +++ b/packages/core-mobile/app/services/defi/types.ts @@ -96,7 +96,7 @@ export interface DeFiPerpetualItem extends BaseDeFiItem { export const ExchangeRateSchema = object({ date: string(), - usd: record(number()) + usd: record(string(), number()) }) export type ExchangeRate = z.infer diff --git a/packages/core-mobile/app/services/glacier/GlacierService.ts b/packages/core-mobile/app/services/glacier/GlacierService.ts index f87cda6801..9326947aa2 100644 --- a/packages/core-mobile/app/services/glacier/GlacierService.ts +++ b/packages/core-mobile/app/services/glacier/GlacierService.ts @@ -6,7 +6,7 @@ import { } from '@avalabs/glacier-sdk' import Config from 'react-native-config' import Logger from 'utils/Logger' -import { CORE_HEADERS } from 'utils/apiClient/constants' +import { CORE_HEADERS } from 'utils/api/constants' import { GlacierFetchHttpRequest } from './GlacierFetchHttpRequest' if (!Config.GLACIER_URL) diff --git a/packages/core-mobile/app/services/posthog/types.ts b/packages/core-mobile/app/services/posthog/types.ts index 931006aedc..5a15275877 100644 --- a/packages/core-mobile/app/services/posthog/types.ts +++ b/packages/core-mobile/app/services/posthog/types.ts @@ -46,7 +46,11 @@ export enum FeatureGates { GASLESS_INSTANT = 'gasless-instant', NEST_EGG_CAMPAIGN = 'nest-egg-campaign', NEST_EGG_NEW_SEEDLESS_ONLY = 'nest-egg-new-seedless-only', - FUSION = 'fusion' + FUSION = 'fusion', + FUSION_MARKR = 'fusion-markr', + FUSION_AVALANCHE_EVM = 'fusion-avalanche-evm', + FUSION_LOMBARD_BTC_TO_BTCB = 'fusion-lombard-btc-to-btcb', + FUSION_LOMBARD_BTCB_TO_BTC = 'fusion-lombard-btcb-to-btc' } export enum FeatureVars { diff --git a/packages/core-mobile/app/services/token/TokenService.ts b/packages/core-mobile/app/services/token/TokenService.ts index ba8d6daa73..3329917b53 100644 --- a/packages/core-mobile/app/services/token/TokenService.ts +++ b/packages/core-mobile/app/services/token/TokenService.ts @@ -308,11 +308,7 @@ export class TokenService { useCoingeckoProxy = false ): Promise { if (useCoingeckoProxy) { - return coingeckoProxyClient.marketDataByCoinId(undefined, { - params: { - id: coingeckoId - } - }) + return coingeckoProxyClient.marketDataByCoinId(coingeckoId) } return coinsInfo(coingeckoBasicClient, { coinId: coingeckoId @@ -332,14 +328,10 @@ export class TokenService { }): Promise { let rawData: ContractMarketChartResponse | undefined if (useCoingeckoProxy) { - rawData = await coingeckoProxyClient.marketChartByCoinId(undefined, { - params: { - id: coingeckoId - }, - queries: { - vs_currency: currency, - days: days?.toString() - } + rawData = await coingeckoProxyClient.marketChartByCoinId({ + id: coingeckoId, + vs_currency: currency, + days: days?.toString() ?? '1' }) } else { rawData = await coinsMarketChart(coingeckoBasicClient, { @@ -362,14 +354,12 @@ export class TokenService { CoinMarket[] | CoingeckoError > { if (useCoingeckoProxy) { - return coingeckoProxyClient.coinsMarket(undefined, { - queries: { - ids: coinIds?.join(','), - vs_currency: currency, - per_page: perPage, - page, - sparkline - } + return coingeckoProxyClient.coinsMarket({ + ids: coinIds?.join(','), + vs_currency: currency, + per_page: perPage, + page, + sparkline }) } return coinsMarket(coingeckoBasicClient, { @@ -393,15 +383,13 @@ export class TokenService { SimplePriceResponse | CoingeckoError > { if (useCoingeckoProxy) { - const rawData = await coingeckoProxyClient.simplePrice(undefined, { - queries: { - ids: coinIds?.join(','), - vs_currencies: currencies.join(','), - include_market_cap: String(marketCap), - include_24hr_vol: String(vol24), - include_24hr_change: String(change24), - include_last_updated_at: String(lastUpdated) - } + const rawData = await coingeckoProxyClient.simplePrice({ + ids: coinIds?.join(','), + vs_currencies: currencies.join(','), + include_market_cap: String(marketCap), + include_24hr_vol: String(vol24), + include_24hr_change: String(change24), + include_last_updated_at: String(lastUpdated) }) return transformSimplePriceResponse(rawData, currencies) } @@ -421,11 +409,7 @@ export class TokenService { useCoingeckoProxy = false ): Promise { if (useCoingeckoProxy) { - return coingeckoProxyClient.searchCoins(undefined, { - queries: { - query - } - }) + return coingeckoProxyClient.searchCoins(query) } return coinsSearch(coingeckoBasicClient, { query diff --git a/packages/core-mobile/app/services/token/coingeckoProxyClient.ts b/packages/core-mobile/app/services/token/coingeckoProxyClient.ts index 0d797d199a..c1dfa6e63a 100644 --- a/packages/core-mobile/app/services/token/coingeckoProxyClient.ts +++ b/packages/core-mobile/app/services/token/coingeckoProxyClient.ts @@ -1,99 +1,148 @@ -import { Zodios } from '@zodios/core' import Config from 'react-native-config' +import { z } from 'zod' import { + CoinMarket, CoinMarketSchema, + CoinsContractInfoResponse, CoinsContractInfoResponseSchema, + CoinsSearchResponse, CoinsSearchResponseSchema, + ContractMarketChartResponse, ContractMarketChartResponseSchema, + RawSimplePriceResponse, RawSimplePriceResponseSchema } from 'services/token/types' -import { boolean, number, string } from 'zod' import Logger from 'utils/Logger' +import { + fetchJson, + buildQueryString +} from 'utils/api/common/fetchWithValidation' if (!Config.PROXY_URL) Logger.warn('PROXY_URL is missing in env file. Coin prices are disabled.') const baseUrl = Config.PROXY_URL + '/proxy/coingecko' -export const coingeckoProxyClient = new Zodios( - baseUrl, - [ - { - method: 'post', - path: '/coins/markets', - parameters: [ - { name: 'vs_currency', type: 'Query', schema: string() }, - { name: 'ids', type: 'Query', schema: string().optional() }, - { name: 'per_page', type: 'Query', schema: number().optional() }, - { name: 'page', type: 'Query', schema: number().optional() }, - { name: 'sparkline', type: 'Query', schema: boolean().optional() } - ], - alias: 'coinsMarket', - response: CoinMarketSchema.array() - }, - { - method: 'post', - path: '/simple/price', - parameters: [ - { name: 'ids', type: 'Query', schema: string() }, - { name: 'vs_currencies', type: 'Query', schema: string() }, - { - name: 'include_market_cap', - type: 'Query', - schema: string().optional() - }, - { - name: 'include_24hr_vol', - type: 'Query', - schema: string().optional() - }, - { - name: 'include_24hr_change', - type: 'Query', - schema: string().optional() - }, - { - name: 'include_last_updated_at', - type: 'Query', - schema: string().optional() - } - ], - alias: 'simplePrice', - response: RawSimplePriceResponseSchema - }, - { - method: 'post', - path: '/coins/:id', - parameters: [{ name: 'id', type: 'Path', schema: string() }], - alias: 'marketDataByCoinId', - response: CoinsContractInfoResponseSchema - }, - { - method: 'post', - path: '/coins/:id/market_chart', - parameters: [ - { name: 'id', type: 'Path', schema: string() }, - { name: 'vs_currency', type: 'Query', schema: string() }, - { name: 'days', type: 'Query', schema: string() }, - { name: 'interval', type: 'Query', schema: string().optional() }, - { name: 'precision', type: 'Query', schema: string().optional() } - ], - alias: 'marketChartByCoinId', - response: ContractMarketChartResponseSchema - }, - { - method: 'post', - path: '/search', - parameters: [{ name: 'query', type: 'Query', schema: string() }], - alias: 'searchCoins', - response: CoinsSearchResponseSchema - } - ], - { - axiosConfig: { - headers: { - 'Content-Type': 'application/json' - } - } +export const coingeckoProxyClient = { + // POST /coins/markets + coinsMarket: async ({ + vs_currency, + ids, + per_page, + page, + sparkline + }: { + vs_currency: string + ids?: string + per_page?: number + page?: number + sparkline?: boolean + }): Promise => { + const queryString = buildQueryString({ + vs_currency, + ids, + per_page, + page, + sparkline + }) + return fetchJson( + `${baseUrl}/coins/markets${queryString}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, + z.array(CoinMarketSchema) + ) + }, + + // POST /simple/price + simplePrice: async ({ + ids, + vs_currencies, + include_market_cap, + include_24hr_vol, + include_24hr_change, + include_last_updated_at + }: { + ids: string + vs_currencies: string + include_market_cap?: string + include_24hr_vol?: string + include_24hr_change?: string + include_last_updated_at?: string + }): Promise => { + const queryString = buildQueryString({ + ids, + vs_currencies, + include_market_cap, + include_24hr_vol, + include_24hr_change, + include_last_updated_at + }) + return fetchJson( + `${baseUrl}/simple/price${queryString}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, + RawSimplePriceResponseSchema + ) + }, + + // POST /coins/:id + marketDataByCoinId: async ( + id: string + ): Promise => { + return fetchJson( + `${baseUrl}/coins/${id}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, + CoinsContractInfoResponseSchema + ) + }, + + // POST /coins/:id/market_chart + marketChartByCoinId: async ({ + id, + vs_currency, + days, + interval, + precision + }: { + id: string + vs_currency: string + days: string + interval?: string + precision?: string + }): Promise => { + const queryString = buildQueryString({ + vs_currency, + days, + interval, + precision + }) + return fetchJson( + `${baseUrl}/coins/${id}/market_chart${queryString}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, + ContractMarketChartResponseSchema + ) + }, + + // POST /search + searchCoins: async (query: string): Promise => { + const queryString = buildQueryString({ query }) + return fetchJson( + `${baseUrl}/search${queryString}`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }, + CoinsSearchResponseSchema + ) } -) +} diff --git a/packages/core-mobile/app/services/token/types.ts b/packages/core-mobile/app/services/token/types.ts index 3b4b4ad898..12dd534c51 100644 --- a/packages/core-mobile/app/services/token/types.ts +++ b/packages/core-mobile/app/services/token/types.ts @@ -51,9 +51,13 @@ const SimplePriceInCurrency = object({ vol24: number().optional().nullable() }) -const SimplePriceInCurrencyResponseSchema = record(SimplePriceInCurrency) +const SimplePriceInCurrencyResponseSchema = record( + string(), + SimplePriceInCurrency +) export const SimplePriceResponseSchema = record( + string(), SimplePriceInCurrencyResponseSchema ) @@ -260,7 +264,8 @@ export type CoinsInfoResponse = Omit< > export const RawSimplePriceResponseSchema = record( - record(number().nullable().optional()) + string(), + record(string(), number().nullable().optional()) ) export type RawSimplePriceResponse = z.infer< typeof RawSimplePriceResponseSchema diff --git a/packages/core-mobile/app/services/wallet/WalletService.tsx b/packages/core-mobile/app/services/wallet/WalletService.tsx index 3c3b138934..1daee69006 100644 --- a/packages/core-mobile/app/services/wallet/WalletService.tsx +++ b/packages/core-mobile/app/services/wallet/WalletService.tsx @@ -22,9 +22,9 @@ import { } from '@avalabs/vm-module-types' import { SpanName } from 'services/sentry/types' import { Curve } from 'utils/publicKeys' - -import { profileApi } from 'utils/apiClient/profile/profileApi' -import { GetAddressesResponse } from 'utils/apiClient/profile/types' +import { GetAddressesResponse } from 'utils/api/generated/profileApi.client/types.gen' +import { postV1GetAddresses } from 'utils/api/generated/profileApi.client' +import { profileApiClient } from 'utils/api/clients/profileApiClient' import { getAddressDerivationPath, isAvalancheTransactionRequest, @@ -347,12 +347,21 @@ class WalletService { }) try { - return await profileApi.postV1getAddresses({ - networkType: networkType, - extendedPublicKey: xpubXP, - isTestnet, - onlyWithActivity + const response = await postV1GetAddresses({ + client: profileApiClient, + body: { + networkType: networkType, + extendedPublicKey: xpubXP, + isTestnet, + onlyWithActivity + } }) + + if (!response.data) { + throw new Error('Failed to get addresses from postV1GetAddresses') + } + + return response.data } catch (err) { Logger.error(`[WalletService.ts][getAddressesFromXpubXP]${err}`) throw err diff --git a/packages/core-mobile/app/store/middleware/listener.ts b/packages/core-mobile/app/store/middleware/listener.ts index 677d0f1647..edc30cd2cd 100644 --- a/packages/core-mobile/app/store/middleware/listener.ts +++ b/packages/core-mobile/app/store/middleware/listener.ts @@ -18,6 +18,7 @@ import { addCurrencyListeners } from 'store/settings/currency/listeners' import { addMeldListeners } from 'store/meld/listeners' import { addBranchListeners } from 'store/branch/listener' import { addNestEggListeners } from 'store/nestEgg/listeners' +import { addFusionListeners } from 'new/features/swapV2/store/listeners' const listener = createListenerMiddleware({ onError: (error, errorInfo) => { @@ -62,6 +63,8 @@ addBranchListeners(startListening) addNestEggListeners(startListening) +addFusionListeners(startListening) + export const addAppListener = addListener as AppAddListener export { listener } diff --git a/packages/core-mobile/app/store/posthog/slice.ts b/packages/core-mobile/app/store/posthog/slice.ts index fb463ca0d7..96d1510516 100644 --- a/packages/core-mobile/app/store/posthog/slice.ts +++ b/packages/core-mobile/app/store/posthog/slice.ts @@ -525,6 +525,44 @@ export const selectIsFusionEnabled = (state: RootState): boolean => { ) } +export const selectIsFusionMarkrEnabled = (state: RootState): boolean => { + const { featureFlags } = state.posthog + return ( + featureFlags[FeatureGates.FUSION_MARKR] === true && + featureFlags[FeatureGates.EVERYTHING] === true + ) +} + +export const selectIsFusionAvalancheEvmEnabled = ( + state: RootState +): boolean => { + const { featureFlags } = state.posthog + return ( + featureFlags[FeatureGates.FUSION_AVALANCHE_EVM] === true && + featureFlags[FeatureGates.EVERYTHING] === true + ) +} + +export const selectIsFusionLombardBtcToBtcbEnabled = ( + state: RootState +): boolean => { + const { featureFlags } = state.posthog + return ( + featureFlags[FeatureGates.FUSION_LOMBARD_BTC_TO_BTCB] === true && + featureFlags[FeatureGates.EVERYTHING] === true + ) +} + +export const selectIsFusionLombardBtcbToBtcEnabled = ( + state: RootState +): boolean => { + const { featureFlags } = state.posthog + return ( + featureFlags[FeatureGates.FUSION_LOMBARD_BTCB_TO_BTC] === true && + featureFlags[FeatureGates.EVERYTHING] === true + ) +} + // actions export const { regenerateUserId, toggleAnalytics, setFeatureFlags } = posthogSlice.actions diff --git a/packages/core-mobile/app/store/posthog/types.ts b/packages/core-mobile/app/store/posthog/types.ts index 9e9fe2c77c..34f5364064 100644 --- a/packages/core-mobile/app/store/posthog/types.ts +++ b/packages/core-mobile/app/store/posthog/types.ts @@ -51,7 +51,11 @@ export const DefaultFeatureFlagConfig = { [FeatureGates.GASLESS_INSTANT]: true, [FeatureGates.NEST_EGG_CAMPAIGN]: false, [FeatureGates.NEST_EGG_NEW_SEEDLESS_ONLY]: false, - [FeatureGates.FUSION]: false + [FeatureGates.FUSION]: false, + [FeatureGates.FUSION_MARKR]: false, + [FeatureGates.FUSION_AVALANCHE_EVM]: false, + [FeatureGates.FUSION_LOMBARD_BTC_TO_BTCB]: false, + [FeatureGates.FUSION_LOMBARD_BTCB_TO_BTC]: false } export const initialState = { diff --git a/packages/core-mobile/app/store/rpc/handlers/account/avalanche_addAccount/util.ts b/packages/core-mobile/app/store/rpc/handlers/account/avalanche_addAccount/util.ts index b2e5bf4828..be8ceffd87 100644 --- a/packages/core-mobile/app/store/rpc/handlers/account/avalanche_addAccount/util.ts +++ b/packages/core-mobile/app/store/rpc/handlers/account/avalanche_addAccount/util.ts @@ -2,8 +2,7 @@ import { z } from 'zod' const requestParamsSchema = z.tuple([z.string()]).or(z.tuple([])) -export const parseRequestParams = ( - params: unknown -): z.SafeParseReturnType<[string] | [], [string] | []> => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const parseRequestParams = (params: unknown) => { return requestParamsSchema.safeParse(params) } diff --git a/packages/core-mobile/app/store/rpc/handlers/account/avalanche_renameAccount/utils.ts b/packages/core-mobile/app/store/rpc/handlers/account/avalanche_renameAccount/utils.ts index 62a6e97a35..e7ed55d623 100644 --- a/packages/core-mobile/app/store/rpc/handlers/account/avalanche_renameAccount/utils.ts +++ b/packages/core-mobile/app/store/rpc/handlers/account/avalanche_renameAccount/utils.ts @@ -2,8 +2,7 @@ import { z } from 'zod' const paramsSchema = z.tuple([z.string(), z.string()]) -export const parseRequestParams = ( - params: unknown -): z.SafeParseReturnType<[string, string], [string, string]> => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const parseRequestParams = (params: unknown) => { return paramsSchema.safeParse(params) } diff --git a/packages/core-mobile/app/store/rpc/handlers/account/avalanche_selectAccount/utils.ts b/packages/core-mobile/app/store/rpc/handlers/account/avalanche_selectAccount/utils.ts index 22ec1c5e8b..4e39dfdf4f 100644 --- a/packages/core-mobile/app/store/rpc/handlers/account/avalanche_selectAccount/utils.ts +++ b/packages/core-mobile/app/store/rpc/handlers/account/avalanche_selectAccount/utils.ts @@ -11,9 +11,9 @@ export const accountSchema = z.object({ addressCoreEth: z.string(), active: z.boolean(), id: z.string(), - type: z.nativeEnum(CoreAccountType), + type: z.enum(CoreAccountType), walletId: z.string(), - walletType: z.nativeEnum(WalletType) + walletType: z.enum(WalletType) }) const paramsSchema = z.tuple([z.string()]) @@ -22,9 +22,8 @@ const approveDataSchema = z.object({ account: accountSchema }) -export const parseRequestParams = ( - params: unknown -): z.SafeParseReturnType<[string], [string]> => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const parseRequestParams = (params: unknown) => { return paramsSchema.safeParse(params) } diff --git a/packages/core-mobile/app/store/rpc/handlers/avalanche_setDeveloperMode/utils.ts b/packages/core-mobile/app/store/rpc/handlers/avalanche_setDeveloperMode/utils.ts index bef5ba72ac..483265c4dd 100644 --- a/packages/core-mobile/app/store/rpc/handlers/avalanche_setDeveloperMode/utils.ts +++ b/packages/core-mobile/app/store/rpc/handlers/avalanche_setDeveloperMode/utils.ts @@ -2,8 +2,7 @@ import { z } from 'zod' const requestParamsSchema = z.tuple([z.boolean()]) -export const parseRequestParams = ( - params: unknown -): z.SafeParseReturnType<[boolean], [boolean]> => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const parseRequestParams = (params: unknown) => { return requestParamsSchema.safeParse(params) } diff --git a/packages/core-mobile/app/store/rpc/handlers/wallet_getNetworkState/utils.ts b/packages/core-mobile/app/store/rpc/handlers/wallet_getNetworkState/utils.ts index bef5ba72ac..483265c4dd 100644 --- a/packages/core-mobile/app/store/rpc/handlers/wallet_getNetworkState/utils.ts +++ b/packages/core-mobile/app/store/rpc/handlers/wallet_getNetworkState/utils.ts @@ -2,8 +2,7 @@ import { z } from 'zod' const requestParamsSchema = z.tuple([z.boolean()]) -export const parseRequestParams = ( - params: unknown -): z.SafeParseReturnType<[boolean], [boolean]> => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const parseRequestParams = (params: unknown) => { return requestParamsSchema.safeParse(params) } diff --git a/packages/core-mobile/app/store/rpc/handlers/wc_sessionRequest/utils.ts b/packages/core-mobile/app/store/rpc/handlers/wc_sessionRequest/utils.ts index 5e3c1cb330..6a759eb953 100644 --- a/packages/core-mobile/app/store/rpc/handlers/wc_sessionRequest/utils.ts +++ b/packages/core-mobile/app/store/rpc/handlers/wc_sessionRequest/utils.ts @@ -7,7 +7,7 @@ import { RpcMethod, CORE_WALLET_METHODS } from 'store/rpc/types' -import { SafeParseError, SafeParseSuccess, z, ZodArray } from 'zod' +import { z } from 'zod' import { WCSessionProposal } from 'store/walletConnectV2/types' import Logger from 'utils/Logger' import BlockaidService from 'services/blockaid/BlockaidService' @@ -136,38 +136,11 @@ export type NamespaceToApprove = z.infer const approveDataSchema = z.object({ selectedAccounts: z.array(coreAccountAddresses).nonempty(), - namespaces: z.record(namespaceToApproveSchema) + namespaces: z.record(z.string(), namespaceToApproveSchema) }) -export const parseApproveData: (data: unknown) => - | SafeParseSuccess<{ - selectedAccounts: ZodArray< - typeof coreAccountAddresses, - 'atleastone' - >['_output'] - namespaces: Record< - string, - { - chains?: string[] - methods: string[] - events: string[] - } - > - }> - | SafeParseError<{ - selectedAccounts: ZodArray< - typeof coreAccountAddresses, - 'atleastone' - >['_input'] - namespaces: Record< - string, - { - chains?: string[] - methods: string[] - events: string[] - } - > - }> = (data: unknown) => { +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const parseApproveData = (data: unknown) => { return approveDataSchema.safeParse(data) } diff --git a/packages/core-mobile/app/store/rpc/utils/assert.ts b/packages/core-mobile/app/store/rpc/utils/assert.ts new file mode 100644 index 0000000000..816974d47f --- /dev/null +++ b/packages/core-mobile/app/store/rpc/utils/assert.ts @@ -0,0 +1,24 @@ +import { rpcErrors } from '@metamask/rpc-errors' + +/** + * RPC assertion function + * Throws an RPC internal error if the value is falsy + * + * @param value - The value to check + * @param message - Optional error message + * @throws RPC internal error if value is falsy + * @example + * const foo: string | null = getValue() + * assert(foo, 'foo is required') + * // foo is now typed as string (non-nullable) + */ +export function assert( + value: T, + message?: string +): asserts value is NonNullable { + if (!value) { + throw rpcErrors.internal({ + data: { reason: message || 'Assertion failed' } + }) + } +} diff --git a/packages/core-mobile/app/utils/api/clients/aggregatedTokensApiClient.ts b/packages/core-mobile/app/utils/api/clients/aggregatedTokensApiClient.ts index 80f5b545c1..5c169e40ae 100644 --- a/packages/core-mobile/app/utils/api/clients/aggregatedTokensApiClient.ts +++ b/packages/core-mobile/app/utils/api/clients/aggregatedTokensApiClient.ts @@ -1,7 +1,7 @@ import Config from 'react-native-config' import queryString from 'query-string' -import { CORE_HEADERS } from 'utils/apiClient/constants' import Logger from 'utils/Logger' +import { CORE_HEADERS } from '../constants' import { createClient } from '../generated/tokenAggregator/aggregatorApi.client/client/client.gen' import { appCheckFetch } from '../common/appCheckFetch' diff --git a/packages/core-mobile/app/utils/api/clients/balanceApiClient.ts b/packages/core-mobile/app/utils/api/clients/balanceApiClient.ts index cec5de0400..abc608efd2 100644 --- a/packages/core-mobile/app/utils/api/clients/balanceApiClient.ts +++ b/packages/core-mobile/app/utils/api/clients/balanceApiClient.ts @@ -1,7 +1,21 @@ import Config from 'react-native-config' import { appCheckFetch } from 'utils/api/common/appCheckFetch' -import { CORE_HEADERS } from '../../apiClient/constants' -import { createClient } from '../../apiClient/generated/balanceApi.client/client' +import Logger from 'utils/Logger' +import { appCheckStreamingFetch } from 'utils/api/common/appCheckFetch' +import { CORE_HEADERS } from '../constants' +import { createClient } from '../../api/generated/balanceApi.client/client' +import { + GetBalancesRequestBody, + GetBalancesResponse +} from '../generated/balanceApi.client/types.gen' + +if (!Config.BALANCE_URL) Logger.warn('BALANCE_URL ENV is missing') + +export const BALANCE_URL = Config.BALANCE_URL + +const NEWLINE = '\n' + +const isDev = typeof __DEV__ === 'boolean' && __DEV__ /** * Balance API client configured with: @@ -10,10 +24,120 @@ import { createClient } from '../../apiClient/generated/balanceApi.client/client * - Core headers * * Use this client for non-streaming balance API requests. - * For streaming (get-balances), use balanceApi.getBalancesStream instead. + * For streaming (get-balances), use streamingBalanceApiClient.getBalances instead. */ export const balanceApiClient = createClient({ baseUrl: Config.BALANCE_URL, fetch: appCheckFetch as typeof fetch, headers: CORE_HEADERS }) + +export const streamingBalanceApiClient = { + // eslint-disable-next-line sonarjs/cognitive-complexity + getBalances: async function* ( + body: GetBalancesRequestBody + ): AsyncGenerator { + const res = await appCheckStreamingFetch( + `${BALANCE_URL}/v1/balance/get-balances`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...CORE_HEADERS + }, + body: JSON.stringify(body) + } + ) + + // Check if the response is successful + if (!res.ok) { + let errorMessage = `HTTP ${res.status}: ${res.statusText}` + try { + // Try to read error body if available + if (res.body) { + const reader = res.body.getReader() + const decoder = new TextDecoder() + let errorBody = '' + let done = false + while (!done) { + const { value, done: readerDone } = await reader.read() + done = readerDone + if (value) { + errorBody += decoder.decode(value, { stream: true }) + } + } + reader.releaseLock() + if (errorBody) { + try { + const errorJson = JSON.parse(errorBody) + errorMessage = + errorJson.message || errorJson.error || errorMessage + } catch { + errorMessage = errorBody + } + } + } + } catch (err) { + Logger.warn('Failed to read error response body', err) + } + throw new Error(errorMessage) + } + + if (!res.body) { + throw new Error('Stream unavailable (response.body missing)') + } + + const reader = res.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + let done = false + + // do measurement in DEV only + let startTime: number | undefined + if (isDev) { + startTime = Date.now() + Logger.info('📡 Streaming balances…') + } + + try { + while (!done) { + const { value, done: readerDone } = await reader.read() + done = readerDone + + if (value) { + buffer += decoder.decode(value, { stream: true }) + } + + // Process complete lines + let newlineIndex = buffer.indexOf(NEWLINE) + + while (newlineIndex !== -1) { + const rawLine = buffer.slice(0, newlineIndex).trim() + buffer = buffer.slice(newlineIndex + 1) + newlineIndex = buffer.indexOf(NEWLINE) + + if (!rawLine) continue + + // Remove "data: " prefix if present + const jsonLine = rawLine.startsWith('data:') + ? rawLine.slice(5).trim() + : rawLine + + try { + const parsed = JSON.parse(jsonLine) as GetBalancesResponse + yield parsed + } catch { + // Ignore malformed lines + } + } + } + } finally { + reader.releaseLock() + } + + if (isDev && startTime) { + const totalTime = Date.now() - startTime + Logger.info(`✅ Stream finished in ${totalTime}ms`) + } + } +} diff --git a/packages/core-mobile/app/utils/api/clients/glacierApiClient.ts b/packages/core-mobile/app/utils/api/clients/glacierApiClient.ts index 9530b46ca3..28ea0b08b4 100644 --- a/packages/core-mobile/app/utils/api/clients/glacierApiClient.ts +++ b/packages/core-mobile/app/utils/api/clients/glacierApiClient.ts @@ -1,9 +1,9 @@ import Config from 'react-native-config' import queryString from 'query-string' -import { CORE_HEADERS } from 'utils/apiClient/constants' import Logger from 'utils/Logger' import { createClient } from 'utils/api/generated/glacier/glacierApi.client/client' import { appCheckFetch } from '../common/appCheckFetch' +import { CORE_HEADERS } from '../constants' if (!Config.GLACIER_URL) Logger.warn( diff --git a/packages/core-mobile/app/utils/api/clients/profileApiClient.ts b/packages/core-mobile/app/utils/api/clients/profileApiClient.ts new file mode 100644 index 0000000000..772c90392f --- /dev/null +++ b/packages/core-mobile/app/utils/api/clients/profileApiClient.ts @@ -0,0 +1,22 @@ +import Config from 'react-native-config' +import Logger from 'utils/Logger' +import { appCheckFetch } from '../common/appCheckFetch' +import { CORE_HEADERS } from '../constants' +import { createClient } from '../generated/profileApi.client/client' + +if (!Config.CORE_PROFILE_URL) + Logger.warn( + 'CORE_PROFILE_URL is missing in env file. Profile API will not work properly.' + ) + +/** + * Profile API client configured with: + * - nitroFetch (via appCheckFetch) for better performance + * - AppCheck authentication with automatic retry + * - Core headers + */ +export const profileApiClient = createClient({ + baseUrl: Config.CORE_PROFILE_URL, + fetch: appCheckFetch as typeof fetch, + headers: CORE_HEADERS +}) diff --git a/packages/core-mobile/app/utils/api/common/fetchWithValidation.ts b/packages/core-mobile/app/utils/api/common/fetchWithValidation.ts new file mode 100644 index 0000000000..73c4876a43 --- /dev/null +++ b/packages/core-mobile/app/utils/api/common/fetchWithValidation.ts @@ -0,0 +1,73 @@ +import { fetch as nitroFetch } from 'react-native-nitro-fetch' +import { fetch as expoFetch } from 'expo/fetch' +import { z } from 'zod' + +/** + * Fetch helper with optional dev-only Zod validation. + * Uses nitroFetch by default for better performance. + */ +export const fetchJson = async ( + url: string, + options?: RequestInit, + schema?: z.ZodType +): Promise => { + const response = await nitroFetch( + url, + options as Parameters[1] + ) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + // Validate with Zod ONLY in development + if (__DEV__ && schema) { + return schema.parse(data) + } + + return data +} + +/** + * Fetch helper with expo fetch (supports streaming). + * Use this for endpoints that need ReadableStream support. + */ +export const fetchJsonWithExpo = async ( + url: string, + options?: RequestInit, + schema?: z.ZodType +): Promise => { + const response = await expoFetch( + url, + options as Parameters[1] + ) + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + + const data = await response.json() + + // Validate with Zod ONLY in development + if (__DEV__ && schema) { + return schema.parse(data) + } + + return data +} + +/** + * Helper to build query string from params object. + */ +export const buildQueryString = (params: Record): string => { + const filtered = Object.entries(params).filter(([_, v]) => v !== undefined) + if (filtered.length === 0) return '' + + const searchParams = new URLSearchParams() + filtered.forEach(([key, value]) => { + searchParams.append(key, String(value)) + }) + return `?${searchParams.toString()}` +} diff --git a/packages/core-mobile/app/utils/apiClient/constants.ts b/packages/core-mobile/app/utils/api/constants.ts similarity index 100% rename from packages/core-mobile/app/utils/apiClient/constants.ts rename to packages/core-mobile/app/utils/api/constants.ts diff --git a/packages/core-mobile/app/utils/apiClient/balance/balanceApi.ts b/packages/core-mobile/app/utils/apiClient/balance/balanceApi.ts deleted file mode 100644 index 15a4518478..0000000000 --- a/packages/core-mobile/app/utils/apiClient/balance/balanceApi.ts +++ /dev/null @@ -1,128 +0,0 @@ -import Config from 'react-native-config' -import Logger from 'utils/Logger' -import { appCheckStreamingFetch } from 'utils/api/common/appCheckFetch' -import { CORE_HEADERS } from '../constants' -import { - GetBalancesRequestBody, - GetBalancesResponse -} from '../generated/balanceApi.client/types.gen' - -if (!Config.BALANCE_URL) Logger.warn('BALANCE_URL ENV is missing') - -export const BALANCE_URL = Config.BALANCE_URL - -const NEWLINE = '\n' - -const isDev = typeof __DEV__ === 'boolean' && __DEV__ - -const balanceApi = { - // eslint-disable-next-line sonarjs/cognitive-complexity - getBalancesStream: async function* ( - body: GetBalancesRequestBody - ): AsyncGenerator { - const res = await appCheckStreamingFetch( - `${BALANCE_URL}/v1/balance/get-balances`, - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...CORE_HEADERS - }, - body: JSON.stringify(body) - } - ) - - // Check if the response is successful - if (!res.ok) { - let errorMessage = `HTTP ${res.status}: ${res.statusText}` - try { - // Try to read error body if available - if (res.body) { - const reader = res.body.getReader() - const decoder = new TextDecoder() - let errorBody = '' - let done = false - while (!done) { - const { value, done: readerDone } = await reader.read() - done = readerDone - if (value) { - errorBody += decoder.decode(value, { stream: true }) - } - } - reader.releaseLock() - if (errorBody) { - try { - const errorJson = JSON.parse(errorBody) - errorMessage = - errorJson.message || errorJson.error || errorMessage - } catch { - errorMessage = errorBody - } - } - } - } catch (err) { - Logger.warn('Failed to read error response body', err) - } - throw new Error(errorMessage) - } - - if (!res.body) { - throw new Error('Stream unavailable (response.body missing)') - } - - const reader = res.body.getReader() - const decoder = new TextDecoder() - let buffer = '' - let done = false - - // do measurement in DEV only - let startTime: number | undefined - if (isDev) { - startTime = Date.now() - Logger.info('📡 Streaming balances…') - } - - try { - while (!done) { - const { value, done: readerDone } = await reader.read() - done = readerDone - - if (value) { - buffer += decoder.decode(value, { stream: true }) - } - - // Process complete lines - let newlineIndex = buffer.indexOf(NEWLINE) - - while (newlineIndex !== -1) { - const rawLine = buffer.slice(0, newlineIndex).trim() - buffer = buffer.slice(newlineIndex + 1) - newlineIndex = buffer.indexOf(NEWLINE) - - if (!rawLine) continue - - // Remove "data: " prefix if present - const jsonLine = rawLine.startsWith('data:') - ? rawLine.slice(5).trim() - : rawLine - - try { - const parsed = JSON.parse(jsonLine) as GetBalancesResponse - yield parsed - } catch { - // Ignore malformed lines - } - } - } - } finally { - reader.releaseLock() - } - - if (isDev && startTime) { - const totalTime = Date.now() - startTime - Logger.info(`✅ Stream finished in ${totalTime}ms`) - } - } -} - -export { balanceApi } diff --git a/packages/core-mobile/app/utils/apiClient/profile/profileApi.ts b/packages/core-mobile/app/utils/apiClient/profile/profileApi.ts deleted file mode 100644 index ae4ed0fc24..0000000000 --- a/packages/core-mobile/app/utils/apiClient/profile/profileApi.ts +++ /dev/null @@ -1,60 +0,0 @@ -import Config from 'react-native-config' -import Logger from 'utils/Logger' -import queryString from 'query-string' -import AppCheckService from 'services/fcm/AppCheckService' -import { - createApiClient, - api as noOpApiClient -} from '../generated/profileApi.client' -import { CORE_HEADERS } from '../constants' - -if (!Config.CORE_PROFILE_URL) Logger.warn('CORE_PROFILE_URL ENV is missing') - -export const CORE_PROFILE_URL = Config.CORE_PROFILE_URL - -let profileApi: ReturnType - -if (CORE_PROFILE_URL) { - profileApi = createApiClient(CORE_PROFILE_URL, { - axiosConfig: { - headers: CORE_HEADERS, - // Use query-string's stringify with arrayFormat 'comma' - // so that array parameters are serialized as comma-separated values, - // e.g. txTypes=AddPermissionlessDelegatorTx,AddDelegatorTx, - // instead of the default repeated keys (txTypes[]=...). - paramsSerializer: params => - queryString.stringify(params, { arrayFormat: 'comma' }) - }, - validate: __DEV__ - }) - - profileApi.axios.interceptors.request.use(async config => { - const appCheckToken = await AppCheckService.getToken() - config.headers['X-Firebase-AppCheck'] = appCheckToken.token - return config - }) - - profileApi.axios.interceptors.response.use( - response => response, - async error => { - const originalRequest = error.config - if ( - (error.response?.status === 401 || error.response?.status === 403) && - !originalRequest._retry - ) { - originalRequest._retry = true - Logger.warn( - 'AppCheck token rejected (profileApi), retrying with fresh token' - ) - const { token } = await AppCheckService.getToken(true) - originalRequest.headers['X-Firebase-AppCheck'] = token - return profileApi.axios(originalRequest) - } - return Promise.reject(error) - } - ) -} else { - profileApi = noOpApiClient -} - -export { profileApi } diff --git a/packages/core-mobile/app/utils/apiClient/profile/types.ts b/packages/core-mobile/app/utils/apiClient/profile/types.ts deleted file mode 100644 index ac9f1abca3..0000000000 --- a/packages/core-mobile/app/utils/apiClient/profile/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { z } from 'zod' -import { schemas } from '../generated/profileApi.client' - -export type GetAddressesResponse = z.infer diff --git a/packages/core-mobile/app/utils/apiClient/scripts/fixZodIntersections.js b/packages/core-mobile/app/utils/apiClient/scripts/fixZodIntersections.js deleted file mode 100644 index 8541311e04..0000000000 --- a/packages/core-mobile/app/utils/apiClient/scripts/fixZodIntersections.js +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env node - -/* eslint-disable no-console */ - -/** - * This script patches incorrect openapi-zod-client output like: - * z.string().and(z.string()).min(...).max(...) - * - * Usage: - * node fixZodIntersections.ts path/to/generated.ts - */ - -const fs = require('fs') -const path = require('path') - -const inputPath = process.argv[2] - -if (!inputPath) { - console.error('❌ No file path supplied to fixZodIntersections.ts') - console.error(' Example:') - console.error( - ' node fixZodIntersections.ts ./app/utils/apiClient/generated/profileApi.client.ts' - ) - process.exit(1) -} - -const file = path.resolve(process.cwd(), inputPath) - -if (!fs.existsSync(file)) { - console.error(`❌ File not found: ${file}`) - process.exit(1) -} - -let code = fs.readFileSync(file, 'utf8') - -// Fix any "z.string().and(z.string())" occurrences -code = code.replace( - /z\.string\(\)\.and\(z\.string\(\)\)((?:\.\w+\([^()]*\))*)/g, - (_match, tail) => `z.string()${tail}` -) - -fs.writeFileSync(file, code, 'utf8') - -console.log(`✔ Fixed Zod intersections in: ${file}`) diff --git a/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.test.ts b/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.test.ts index 3459e5b3d5..654e8449be 100644 --- a/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.test.ts +++ b/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.test.ts @@ -1,7 +1,7 @@ import { NetworkVMType } from '@avalabs/core-chains-sdk' import WalletService from 'services/wallet/WalletService' import { WalletType } from 'services/wallet/types' -import { GetAddressesResponse } from '../apiClient/profile/types' +import { GetAddressesResponse } from 'utils/api/generated/profileApi.client/types.gen' import { getAddressesFromXpubXP } from './getAddressesFromXpubXP' jest.mock('services/wallet/WalletService') diff --git a/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.ts b/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.ts index 0efd2b980c..165615f020 100644 --- a/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.ts +++ b/packages/core-mobile/app/utils/getAddressesFromXpubXP/getAddressesFromXpubXP.ts @@ -10,7 +10,7 @@ import ModuleManager from 'vmModule/ModuleManager' import { AVALANCHE_DERIVATION_PATH_PREFIX, Curve } from 'utils/publicKeys' import { toSegments } from 'utils/toSegments' import { AddressIndex } from '@avalabs/types' -import { GetAddressesResponse } from '../apiClient/profile/types' +import { GetAddressesResponse } from 'utils/api/generated/profileApi.client/types.gen' type GetAddressesFromXpubParams = { isDeveloperMode: boolean diff --git a/packages/core-mobile/app/utils/reactQuery.ts b/packages/core-mobile/app/utils/reactQuery.ts new file mode 100644 index 0000000000..17ddc9eaf7 --- /dev/null +++ b/packages/core-mobile/app/utils/reactQuery.ts @@ -0,0 +1,23 @@ +/** + * React Query utility functions + */ + +/** + * Exponential backoff with a maximum delay cap for React Query retries + * + * @param maxDelayMs - Maximum delay in milliseconds (default: 5000ms) + * @returns Function compatible with React Query's retryDelay option + * + * @example + * useQuery({ + * queryKey: ['data'], + * queryFn: fetchData, + * retry: 3, + * retryDelay: exponentialBackoff(5000) // 1s, 2s, 4s + * }) + */ +export const exponentialBackoff = (maxDelayMs = 5000) => { + return (attemptIndex: number): number => { + return Math.min(1000 * 2 ** attemptIndex, maxDelayMs) + } +} diff --git a/packages/core-mobile/app/utils/zodToCamelCase.ts b/packages/core-mobile/app/utils/zodToCamelCase.ts index 7390a7d515..14ab448a50 100644 --- a/packages/core-mobile/app/utils/zodToCamelCase.ts +++ b/packages/core-mobile/app/utils/zodToCamelCase.ts @@ -1,10 +1,12 @@ -import z, { ZodEffects } from 'zod' +import { z } from 'zod' import camelcaseKeys from 'camelcase-keys' -import { CamelCasedPropertiesDeep } from 'type-fest' +import type { CamelCasedPropertiesDeep } from 'type-fest' -export const zodToCamelCase = ( - zod: T -): ZodEffects> => - zod.transform( - val => camelcaseKeys(val, { deep: true }) as CamelCasedPropertiesDeep +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const zodToCamelCase = (schema: T) => + schema.transform( + val => + camelcaseKeys(val as Record, { + deep: true + }) as CamelCasedPropertiesDeep ) diff --git a/packages/core-mobile/app/vmModule/ModuleManager.ts b/packages/core-mobile/app/vmModule/ModuleManager.ts index 7cc5117bc7..57e3dcafba 100644 --- a/packages/core-mobile/app/vmModule/ModuleManager.ts +++ b/packages/core-mobile/app/vmModule/ModuleManager.ts @@ -21,10 +21,7 @@ import { getEvmCaip2ChainId, getSolanaCaip2ChainId } from 'utils/caip2ChainIds' -import { - APPLICATION_NAME, - APPLICATION_VERSION -} from 'utils/apiClient/constants' +import { APPLICATION_NAME, APPLICATION_VERSION } from 'utils/api/constants' import { DerivationPath } from '@avalabs/core-wallets-sdk' import { emptyAddresses } from 'utils/publicKeys' import { WalletType } from 'services/wallet/types' diff --git a/packages/core-mobile/balance-api.config.js b/packages/core-mobile/balance-api.config.js index 7972119805..a7412c906c 100644 --- a/packages/core-mobile/balance-api.config.js +++ b/packages/core-mobile/balance-api.config.js @@ -1,5 +1,12 @@ +const isCI = process.env.APP_ENV === 'ci' + +// use production environment on CI for stability +const BALANCE_SCHEMA_URL = isCI + ? 'https://core-balance-api.avax.network/schema.json' + : 'https://core-balance-api.avax-test.network/schema.json' + export default { - input: 'https://core-balance-api.avax-test.network/schema.json', - output: './app/utils/apiClient/generated/balanceApi.client', + input: BALANCE_SCHEMA_URL, + output: './app/utils/api/generated/balanceApi.client', plugins: ['@hey-api/client-fetch'] } diff --git a/packages/core-mobile/glacier-api.config.js b/packages/core-mobile/glacier-api.config.js index 81c0c698f8..520a7a4918 100644 --- a/packages/core-mobile/glacier-api.config.js +++ b/packages/core-mobile/glacier-api.config.js @@ -1,5 +1,6 @@ const isCI = process.env.APP_ENV === 'ci' +// use production environment on CI for stability const GLACIER_SCHEMA_URL = isCI ? 'https://glacier-api.avax.network/api-json' : 'https://glacier-api-dev.avax.network/api-json' diff --git a/packages/core-mobile/metro.config.js b/packages/core-mobile/metro.config.js index b977191af0..deb15ed1f0 100644 --- a/packages/core-mobile/metro.config.js +++ b/packages/core-mobile/metro.config.js @@ -48,12 +48,20 @@ const baseConfig = { if (moduleName.startsWith('@lombard.finance/sdk')) { const newContext = { ...context, - unstable_enablePackageExports: true, - unstable_conditionNames: ['require', 'import'], - preferNativePlatform: true + unstable_enablePackageExports: true } return context.resolveRequest(newContext, moduleName, platform) } + + // Enable package exports only for @avalabs/unified-asset-transfer + if (moduleName.startsWith('@avalabs/unified-asset-transfer')) { + const newContext = { + ...context, + unstable_enablePackageExports: true + } + return context.resolveRequest(newContext, moduleName, platform) + } + if (moduleName.startsWith('@ledgerhq/cryptoassets-evm-signatures')) { return context.resolveRequest( context, diff --git a/packages/core-mobile/package.json b/packages/core-mobile/package.json index ff440525d0..18dc568c5e 100644 --- a/packages/core-mobile/package.json +++ b/packages/core-mobile/package.json @@ -39,6 +39,7 @@ "@avalabs/k2-alpine": "workspace:*", "@avalabs/svm-module": "3.1.3", "@avalabs/types": "3.1.0-alpha.71", + "@avalabs/unified-asset-transfer": "0.0.0-project-fusion-pr-20260211175355", "@avalabs/vm-module-types": "3.1.3", "@babel/runtime": "7.25.7", "@bitcoinerlab/secp256k1": "1.2.0", @@ -115,7 +116,6 @@ "@walletconnect/react-native-compat": "2.11.0", "@walletconnect/types": "2.17.2", "@walletconnect/utils": "2.17.2", - "@zodios/core": "10.9.6", "assert": "2.1.0", "axios": "1.12.2", "base-64": "1.0.0", @@ -251,7 +251,7 @@ "web3": "4.16.0", "xss": "1.0.15", "zeego": "3.0.6", - "zod": "3.23.8", + "zod": "4.1.12", "zustand": "5.0.6" }, "devDependencies": { diff --git a/packages/core-mobile/profile-api.config.js b/packages/core-mobile/profile-api.config.js new file mode 100644 index 0000000000..a864ab72f6 --- /dev/null +++ b/packages/core-mobile/profile-api.config.js @@ -0,0 +1,12 @@ +const isCI = process.env.APP_ENV === 'ci' + +// use production environment on CI for stability +const PROFILE_SCHEMA_URL = isCI + ? 'https://core-profile-api.avax.network/schema.json' + : 'https://core-profile-api.avax-test.network/schema.json' + +export default { + input: PROFILE_SCHEMA_URL, + output: './app/utils/api/generated/profileApi.client', + plugins: ['@hey-api/client-fetch'] +} diff --git a/packages/core-mobile/scripts/codegen.js b/packages/core-mobile/scripts/codegen.js index 26f01b6b71..663e7e74d9 100644 --- a/packages/core-mobile/scripts/codegen.js +++ b/packages/core-mobile/scripts/codegen.js @@ -4,12 +4,6 @@ const path = require('path') const root = path.resolve(__dirname, '..') -const isCI = process.env.APP_ENV === 'ci' - -const PROFILE_SCHEMA_URL = isCI - ? 'https://core-profile-api.avax.network/schema.json' - : 'https://core-profile-api.avax-test.network/schema.json' - function run(cmd) { execSync(cmd, { stdio: 'inherit', @@ -19,7 +13,7 @@ function run(cmd) { function main() { // ensure output dir - run('mkdir -p app/utils/apiClient/generated') + run('mkdir -p app/utils/api/generated') // contracts run( @@ -28,15 +22,9 @@ function main() { './node_modules/@openzeppelin/contracts/build/contracts/ERC721.json ' + './node_modules/@openzeppelin/contracts/build/contracts/ERC1155.json' ) - // profile API (+ patch script) - run( - `npx openapi-zod-client '${PROFILE_SCHEMA_URL}' ` + - "-o './app/utils/apiClient/generated/profileApi.client.ts'" - ) - run( - 'node ./app/utils/apiClient/scripts/fixZodIntersections.js ' + - './app/utils/apiClient/generated/profileApi.client.ts' - ) + + // profile API + run('npx @hey-api/openapi-ts -f profile-api.config.js') // balance API run('npx @hey-api/openapi-ts -f balance-api.config.js') diff --git a/yarn.lock b/yarn.lock index 1e50bb52f2..006fe7a7e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -514,6 +514,7 @@ __metadata: "@avalabs/svm-module": 3.1.3 "@avalabs/tsconfig-mobile": "workspace:*" "@avalabs/types": 3.1.0-alpha.71 + "@avalabs/unified-asset-transfer": 0.0.0-project-fusion-pr-20260211175355 "@avalabs/vm-module-types": 3.1.3 "@babel/core": 7.28.0 "@babel/plugin-proposal-nullish-coalescing-operator": 7.18.6 @@ -636,7 +637,6 @@ __metadata: "@wdio/spec-reporter": 9.20.0 "@wdio/types": 9.20.0 "@welldone-software/why-did-you-render": 8.0.3 - "@zodios/core": 10.9.6 appium: 3.1.2 appium-doctor: 1.16.2 appium-uiautomator2-driver: 6.7.9 @@ -807,7 +807,7 @@ __metadata: webdriverio: 9.23.0 xss: 1.0.15 zeego: 3.0.6 - zod: 3.23.8 + zod: 4.1.12 zustand: 5.0.6 languageName: unknown linkType: soft @@ -1089,6 +1089,22 @@ __metadata: languageName: node linkType: hard +"@avalabs/unified-asset-transfer@npm:0.0.0-project-fusion-pr-20260211175355": + version: 0.0.0-project-fusion-pr-20260211175355 + resolution: "@avalabs/unified-asset-transfer@npm:0.0.0-project-fusion-pr-20260211175355" + dependencies: + "@lombard.finance/sdk": ^4.1.0 + "@scure/base": ^1.0.1 + coinselect: ^3.1.13 + es-toolkit: ^1.42.0 + peerDependencies: + "@solana/kit": ^5.0.0 + viem: ^2.38.0 + zod: ^4.1.0 + checksum: cad64b61189a29c04010833c51d7242632d1872571bc21427dbde3fbf307bb250c3906535b368be771d1699e0551e1dbd8e2ef19e87e5782224450a340ba1857 + languageName: node + linkType: hard + "@avalabs/vm-module-types@npm:3.1.3": version: 3.1.3 resolution: "@avalabs/vm-module-types@npm:3.1.3" @@ -9629,6 +9645,16 @@ __metadata: languageName: node linkType: hard +"@lombard.finance/sdk-common@npm:3.4.0": + version: 3.4.0 + resolution: "@lombard.finance/sdk-common@npm:3.4.0" + peerDependencies: + "@bitcoinerlab/secp256k1": ^1.2.0 + bitcoinjs-lib: ^6.1.5 + checksum: e0ea4685270ca69b16c04f8f41c330f977d76e18518d175d4d249300afd9eeb741f3bd258e4569195ff16b6d5aba953ae804e026d2c85a7bab6292f5031c4b60 + languageName: node + linkType: hard + "@lombard.finance/sdk-common@npm:^3.3.1": version: 3.3.1 resolution: "@lombard.finance/sdk-common@npm:3.3.1" @@ -9658,6 +9684,23 @@ __metadata: languageName: node linkType: hard +"@lombard.finance/sdk@npm:^4.1.0": + version: 4.3.0 + resolution: "@lombard.finance/sdk@npm:4.3.0" + dependencies: + "@lombard.finance/sdk-common": 3.4.0 + isows: ^1.0.7 + peerDependencies: + "@bitcoinerlab/secp256k1": ^1.2.0 + "@layerzerolabs/lz-v2-utilities": ^3.0.17 + axios: ^1 + bignumber.js: ^9 + bitcoinjs-lib: ^6.1.5 + viem: ^2.21.0 + checksum: 407e7fdc70e6b4120f4b5074d11634dc27a3af73584d4cb9dd02820e820c2dfdd7d45fb3f476158b8984ebbf564b425ed0859fe31aa9d6fb43e3170c574da542 + languageName: node + linkType: hard + "@metamask/abi-utils@npm:^2.0.2, @metamask/abi-utils@npm:^2.0.4": version: 2.0.4 resolution: "@metamask/abi-utils@npm:2.0.4" @@ -12722,6 +12765,13 @@ __metadata: languageName: node linkType: hard +"@scure/base@npm:^1.0.1, @scure/base@npm:~1.2.5": + version: 1.2.6 + resolution: "@scure/base@npm:1.2.6" + checksum: 1058cb26d5e4c1c46c9cc0ae0b67cc66d306733baf35d6ebdd8ddaba242b80c3807b726e3b48cb0411bb95ec10d37764969063ea62188f86ae9315df8ea6b325 + languageName: node + linkType: hard + "@scure/base@npm:^1.1.3, @scure/base@npm:~1.1.3, @scure/base@npm:~1.1.6": version: 1.1.7 resolution: "@scure/base@npm:1.1.7" @@ -12743,13 +12793,6 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:~1.2.5": - version: 1.2.6 - resolution: "@scure/base@npm:1.2.6" - checksum: 1058cb26d5e4c1c46c9cc0ae0b67cc66d306733baf35d6ebdd8ddaba242b80c3807b726e3b48cb0411bb95ec10d37764969063ea62188f86ae9315df8ea6b325 - languageName: node - linkType: hard - "@scure/bip32@npm:1.3.1": version: 1.3.1 resolution: "@scure/bip32@npm:1.3.1" @@ -16878,16 +16921,6 @@ __metadata: languageName: node linkType: hard -"@zodios/core@npm:10.9.6": - version: 10.9.6 - resolution: "@zodios/core@npm:10.9.6" - peerDependencies: - axios: ^0.x || ^1.0.0 - zod: ^3.x - checksum: 482a80bd4da661734f9ed276a92d9275fa1b76011bef26ede4a851e4c495d9103e6c9456074b9e50e4c89ad7e83fcead6ea45ffba615e6ff54b8088433ba47fc - languageName: node - linkType: hard - "@zxing/browser@npm:^0.1.1": version: 0.1.5 resolution: "@zxing/browser@npm:0.1.5" @@ -20313,7 +20346,7 @@ __metadata: languageName: node linkType: hard -"coinselect@npm:3.1.13": +"coinselect@npm:3.1.13, coinselect@npm:^3.1.13": version: 3.1.13 resolution: "coinselect@npm:3.1.13" checksum: 47731f5dcb4bcf1ab0c906dff5e79ef4c6f9d64acd3d93cf8c3d47ef6c5a0f5530892c68fc840e398db3d5491ab2aa4143110cca056b642c383c066f7942b511 @@ -22983,6 +23016,18 @@ __metadata: languageName: node linkType: hard +"es-toolkit@npm:^1.42.0": + version: 1.44.0 + resolution: "es-toolkit@npm:1.44.0" + dependenciesMeta: + "@trivago/prettier-plugin-sort-imports@4.3.0": + unplugged: true + prettier-plugin-sort-re-exports@0.0.1: + unplugged: true + checksum: cd1d0f791aa2329450bd27f090e3dd933558fd1888ad17e911db3ddadbee7020d2727d2d5f819502b3b015535e46d74554365f7e3b9f04c52180fff0be3f2cdd + languageName: node + linkType: hard + "es6-promise@npm:4.2.8": version: 4.2.8 resolution: "es6-promise@npm:4.2.8" @@ -34826,7 +34871,7 @@ react-native-webview@ava-labs/react-native-webview: peerDependencies: react: "*" react-native: "*" - checksum: d6880e5ce50f78dafc5b51a87916e4c1e9925357c4f21cc20e5891311800297da97ba5a1b8d1f70381816d32e65d81cc635fe988d584dbf94d4b57a7ee509e3b + checksum: d6ceb75e1d0f5755370d6d91f67902bd2b8a23a3ca43cb853d2964b43798f30477e8ab0223ea8b620534fbdf5f56d39550f40d24bec2d2398c323ccfe8a5a556 languageName: node linkType: hard @@ -41609,6 +41654,13 @@ react-native-webview@ava-labs/react-native-webview: languageName: node linkType: hard +"zod@npm:4.1.12": + version: 4.1.12 + resolution: "zod@npm:4.1.12" + checksum: 91174acc7d2ca5572ad522643474ddd60640cf6877b5d76e5d583eb25e3c4072c6f5eb92ab94f231ec5ce61c6acdfc3e0166de45fb1005b1ea54986b026b765f + languageName: node + linkType: hard + "zod@npm:^3.21.4": version: 3.24.1 resolution: "zod@npm:3.24.1"