Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/core-mobile/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ POSTHOG_FEATURE_FLAGS_KEY=
PROXY_URL=
GLACIER_URL=
GLACIER_API_KEY=
# Core API key for core-proxy-api (dev/E2E: authorizes + bypasses rate limits; 1Password "front end eng" vault -> "Core API keys")
CORE_API_KEY=

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should we call it CORE_PROXY_API_KEY? There are multiple Core Apis we use

Suggested change
CORE_API_KEY=
CORE_PROXY_API_KEY=


SENTRY_DSN=

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ describe('approvalMethods', () => {
const AVALANCHE_SIGNING = [
'avalanche_sendTransaction',
'avalanche_signTransaction',
'avalanche_signMessage'
'avalanche_signMessage',
// added to the RpcMethod enum in vm-module-types 4.x (agent identity)
'avalanche_declareAgentIdentity'
]
const CROSS_NAMESPACE = [
'solana_signTransaction',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Network } from '@avalabs/core-chains-sdk'
import { NetworkContractToken } from '@avalabs/vm-module-types'
import { NetworkContractToken, TokenType } from '@avalabs/vm-module-types'
import { runAfterInteractions } from 'utils/runAfterInteractions'
import ModuleManager from 'vmModule/ModuleManager'
import { mapToVmNetwork } from 'vmModule/utils/mapToVmNetwork'
Expand All @@ -15,5 +15,10 @@ export const getNetworkContractTokens = async (
return module.getTokens(mapToVmNetwork(network))
})

return tokens ?? []
// Hypercore spot tokens are not contract tokens (no EVM address) and are
// not supported by this app's token pipeline.
return (tokens ?? []).filter(
(token): token is NetworkContractToken =>
token.type !== TokenType.HYPERCORE_SPOT
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ const resolveDecimals = (
return DEFAULT_TOKEN_DECIMALS
}

// Hypercore spot tokens carry no EVM address, hence the `in` guard.
const getBalanceDataAddress = (
balanceData: LocalTokenWithBalance | undefined
): string | undefined =>
balanceData && 'address' in balanceData ? balanceData.address : undefined

export const buildLocalToken = ({
accountTokens,
tokenInfo,
Expand Down Expand Up @@ -60,7 +66,7 @@ export const buildLocalToken = ({

const address = isNative
? ''
: balanceData?.address ?? tokenInfo.platforms?.[caip2Id] ?? ''
: getBalanceDataAddress(balanceData) ?? tokenInfo.platforms?.[caip2Id] ?? ''

// NATIVE_DECIMALS keys AVAX at 18 (C-Chain), but native AVAX on P/X-Chain is
// 9-decimal nAVAX. Override so balances/amounts render at the chain's actual
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,5 +163,15 @@ describe('getTokenAddress', () => {
'0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7'
)
})

it('should return empty string for hypercore spot tokens (no address field)', () => {
const hypercoreToken = {
type: TokenType.HYPERCORE_SPOT,
symbol: 'HYPE',
index: 150
} as unknown as TokenWithBalance

expect(getTokenAddress(hypercoreToken)).toBe('')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@ export const getTokenAddress = (token?: TokenWithBalance): string => {
if (!token) {
return ''
}
return token.type === TokenType.NATIVE ? token.symbol : token.address
if (token.type === TokenType.NATIVE) {
return token.symbol
}
// Hypercore spot tokens have no EVM address and are not swappable.
return 'address' in token ? token.address : ''
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ export function getLocalTokenId(
return fallbackTokenId
}

if (!token.address) {
// Hypercore spot tokens carry no address by design (identified by index) —
// fall back without logging.
if (token.type === TokenType.HYPERCORE_SPOT) {
return fallbackTokenId
}

if (!('address' in token) || !token.address) {
Logger.error('Token address is missing', { token })
return fallbackTokenId
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
NetworkContractToken,
TokenType
} from '@avalabs/vm-module-types'
import Logger from 'utils/Logger'
import { getLocalTokenId } from './getLocalTokenId'

describe('getLocalTokenId', () => {
Expand All @@ -26,4 +27,16 @@ describe('getLocalTokenId', () => {
| NetworkContractToken
expect(getLocalTokenId(token)).toBe('ERC20-USDC')
})

it('falls back to type-symbol for hypercore spot tokens without logging an error', () => {
const errorSpy = jest.spyOn(Logger, 'error')
const token = {
type: TokenType.HYPERCORE_SPOT,
symbol: 'HYPE',
index: 150
} as unknown as TokenWithBalance | NetworkContractToken
expect(getLocalTokenId(token)).toBe('HYPERCORE_SPOT-HYPE')
expect(errorSpy).not.toHaveBeenCalled()
errorSpy.mockRestore()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import Config from 'react-native-config'
import Logger from 'utils/Logger'
import { CORE_HEADERS } from 'utils/api/constants'
import { getCoreAuthHeaders } from 'utils/api/common/getCoreAuthHeaders'
import { GlacierFetchHttpRequest } from './GlacierFetchHttpRequest'

/**
Expand All @@ -36,7 +37,12 @@ class GlacierService {
private glacierSdk = new Glacier(
{
BASE: Config.GLACIER_URL,
HEADERS: CORE_HEADERS
// Resolved per request: the Glacier proxy (core-proxy-api) requires an
// AppCheck token (or Core API key) on every call.
HEADERS: async () => ({
...CORE_HEADERS,
...(await getCoreAuthHeaders())
})
},
GlacierFetchHttpRequest
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { fetch as expoFetch } from 'expo/fetch'
import AppCheckService from 'services/fcm/AppCheckService'
import Logger from 'utils/Logger'

const APPCHECK_HEADER = 'X-Firebase-AppCheck'
export const APPCHECK_HEADER = 'X-Firebase-AppCheck'

const shouldRetry = (status: number): boolean =>
status === 401 || status === 403
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import AppCheckService from 'services/fcm/AppCheckService'
import { getCoreAuthHeaders } from './getCoreAuthHeaders'

jest.mock('services/fcm/AppCheckService', () => ({
getToken: jest.fn()
}))

jest.mock('react-native-config', () => ({
CORE_API_KEY: undefined
}))

const Config = require('react-native-config')

const mockGetToken = AppCheckService.getToken as jest.MockedFunction<
typeof AppCheckService.getToken
>

describe('getCoreAuthHeaders', () => {
beforeEach(() => {
jest.clearAllMocks()
Config.CORE_API_KEY = undefined
})

it('returns the AppCheck token header', async () => {
mockGetToken.mockResolvedValue({ token: 'appcheck-token' } as Awaited<
ReturnType<typeof AppCheckService.getToken>
>)

await expect(getCoreAuthHeaders()).resolves.toEqual({
'X-Firebase-AppCheck': 'appcheck-token'
})
})

it('includes the Core API key header when configured', async () => {
Config.CORE_API_KEY = 'core-api-key'
mockGetToken.mockResolvedValue({ token: 'appcheck-token' } as Awaited<
ReturnType<typeof AppCheckService.getToken>
>)

await expect(getCoreAuthHeaders()).resolves.toEqual({
'X-Firebase-AppCheck': 'appcheck-token',
'x-core-api-key': 'core-api-key'
})
})

it('re-reads the token on every call', async () => {
mockGetToken
.mockResolvedValueOnce({ token: 'token-1' } as Awaited<
ReturnType<typeof AppCheckService.getToken>
>)
.mockResolvedValueOnce({ token: 'token-2' } as Awaited<
ReturnType<typeof AppCheckService.getToken>
>)

await expect(getCoreAuthHeaders()).resolves.toEqual({
'X-Firebase-AppCheck': 'token-1'
})
await expect(getCoreAuthHeaders()).resolves.toEqual({
'X-Firebase-AppCheck': 'token-2'
})
})
})
25 changes: 25 additions & 0 deletions packages/core-mobile/app/utils/api/common/getCoreAuthHeaders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import Config from 'react-native-config'
import AppCheckService from 'services/fcm/AppCheckService'
import { APPCHECK_HEADER } from 'utils/api/common/appCheckFetch'

const CORE_API_KEY_HEADER = 'x-core-api-key'

/**
* Resolves the auth headers required by core-proxy-api (e.g. the Glacier
* proxy): a Firebase AppCheck token, plus the Core API key when one is
* configured (dev/E2E builds — it both authorizes and bypasses rate limits).
*
* Meant to be invoked per request (e.g. as a glacier-sdk HEADERS resolver or
* the vm-modules `runtime.getAuthHeaders`) so the short-lived AppCheck token
* is re-read on every call. Unlike appCheckFetch, callers of this resolver
* get no 401-retry-with-fresh-token — the token cache makes that rare.
*/
export const getCoreAuthHeaders = async (): Promise<Record<string, string>> => {
const { token } = await AppCheckService.getToken()
return {
[APPCHECK_HEADER]: token,
...(Config.CORE_API_KEY
? { [CORE_API_KEY_HEADER]: Config.CORE_API_KEY }
: {})
}
}
1 change: 1 addition & 0 deletions packages/core-mobile/app/utils/publicKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const emptyAddresses = (): Record<NetworkVMType, string> => ({
[NetworkVMType.CoreEth]: '',
[NetworkVMType.EVM]: '',
[NetworkVMType.HVM]: '',
[NetworkVMType.HYPERCORE]: '',
[NetworkVMType.PVM]: '',
[NetworkVMType.SVM]: ''
})
Expand Down
14 changes: 11 additions & 3 deletions packages/core-mobile/app/vmModule/ModuleManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import Logger from 'utils/Logger'
import {
Environment,
Module,
ConstructorParams
ConstructorParams,
RuntimeParams
} from '@avalabs/vm-module-types'
import { getCoreAuthHeaders } from 'utils/api/common/getCoreAuthHeaders'
import {
NetworkVMType,
Network,
Expand Down Expand Up @@ -88,7 +90,9 @@ class ModuleManager {

const environment = isDev ? Environment.DEV : Environment.PRODUCTION

const moduleInitParams: ConstructorParams = {
const moduleInitParams: ConstructorParams & {
runtime: Required<Pick<RuntimeParams, 'getAuthHeaders'>>
} = {
environment,
approvalController,
appInfo: {
Expand All @@ -97,7 +101,11 @@ class ModuleManager {
},
runtime: {
fetch: global.fetch,
httpAgent: new http.Agent()
httpAgent: new http.Agent(),
// Required by EvmModule/AvalancheModule: their internal Glacier calls
// go through core-proxy-api, which needs AppCheck (or a Core API key)
// on every request.
getAuthHeaders: getCoreAuthHeaders
}
}

Expand Down
10 changes: 5 additions & 5 deletions packages/core-mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@
"appium:smokeAndroid": "IS_SMOKE=true PLATFORM=android wdio run ./e2e-appium/wdio.conf.ts --mochaOpts.grep='\\[(s|S)moke\\]'"
},
"dependencies": {
"@avalabs/avalanche-module": "3.9.0",
"@avalabs/avalanche-module": "4.0.0",
"@avalabs/avalanchejs": "5.1.0-alpha.5",
"@avalabs/bitcoin-module": "3.9.0",
"@avalabs/bitcoin-module": "4.0.0",
"@avalabs/bridge-unified": "4.4.1",
"@avalabs/core-chains-sdk": "3.1.0-canary.672f5c3.0",
"@avalabs/core-coingecko-sdk": "3.1.0-canary.672f5c3.0",
Expand All @@ -38,15 +38,15 @@
"@avalabs/core-wallets-sdk": "3.1.0-canary.672f5c3.0",
"@avalabs/crypto-nitro": "0.3.2",
"@avalabs/crypto-sdk": "1.0.2",
"@avalabs/evm-module": "3.9.0",
"@avalabs/evm-module": "4.0.0",
"@avalabs/fusion-sdk": "0.0.0-feat-enable-hvm-big-blocks-20260720201517",
"@avalabs/glacier-sdk": "3.1.0-canary.672f5c3.0",
"@avalabs/k2-alpine": "workspace:*",
"@avalabs/perps-sdk": "0.0.0-fix-perps-unified-portfolio-ba-20260716132651",
"@avalabs/prediction-market-sdk": "0.3.0",
"@avalabs/svm-module": "3.9.0",
"@avalabs/svm-module": "4.0.0",
"@avalabs/types": "3.1.0-canary.672f5c3.0",
"@avalabs/vm-module-types": "3.9.0",
"@avalabs/vm-module-types": "4.0.0",
"@babel/runtime": "7.25.7",
"@bitcoinerlab/secp256k1": "1.2.0",
"@blockaid/client": "0.48.0",
Expand Down
Loading
Loading