Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
dbcafc8
refactor: staggers when http calls to blockchain api are done, implem…
ganchoradkov Jul 4, 2025
a645f52
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 4, 2025
5bf4c40
fix: sendpage tests
ganchoradkov Jul 4, 2025
817990c
fix: wallet send select token view tests
ganchoradkov Jul 4, 2025
679ba68
fix: swap tests and improper interval handling
ganchoradkov Jul 4, 2025
606c558
fix: swaps ui test
ganchoradkov Jul 4, 2025
19da2b6
fix: interval and cache clear
ganchoradkov Jul 4, 2025
95a4a44
fix: sets swap token amount
ganchoradkov Jul 14, 2025
935f842
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 14, 2025
a19bb80
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 15, 2025
3b27984
fix: `watchTokensAndValues` race cond
ganchoradkov Jul 15, 2025
e51c78d
Merge branch 'refactor/balance-calls' of github.com:WalletConnect/web…
ganchoradkov Jul 15, 2025
63d4493
chore: adds additional validation
ganchoradkov Jul 15, 2025
8411dc1
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 16, 2025
59c6e8d
Merge branch 'refactor/balance-calls' of github.com:WalletConnect/web…
ganchoradkov Jul 16, 2025
51929d9
fix: unsubscribe from listeners
ganchoradkov Jul 16, 2025
1270b6c
fix: duplicate subs
ganchoradkov Jul 16, 2025
3325817
chore: rm redundant test
ganchoradkov Jul 16, 2025
39e64fc
chore: rm unused import
ganchoradkov Jul 16, 2025
bc8988f
chore: reverts event listener, to be addressed in a separate pr
ganchoradkov Jul 16, 2025
54b9b09
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 22, 2025
30f0650
refactor: moves magic number into a const
ganchoradkov Jul 22, 2025
4fad310
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 22, 2025
562ee26
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 22, 2025
b38222a
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 22, 2025
07ac10f
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 22, 2025
72e60e8
Merge branch 'main' into refactor/balance-calls
ganchoradkov Jul 23, 2025
0992e53
refactor: replaces magic numbers
ganchoradkov Jul 23, 2025
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
4 changes: 3 additions & 1 deletion apps/laboratory/tests/wallet-features.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ walletFeaturesTest('it should initialize swap as expected', async () => {
await page.openAccount()
const walletFeatureButton = await page.getWalletFeaturesButton('swaps')
await walletFeatureButton.click()
await expect(page.page.getByTestId('swap-input-sourceToken')).toHaveValue('1')
await expect(page.page.getByTestId('swap-input-sourceToken')).toHaveValue('0')
await expect(page.page.getByTestId('swap-input-token-sourceToken')).toHaveText('ETH')
await expect(page.page.getByTestId('swap-action-button')).toHaveText('Select token')
await page.page.getByTestId('swap-input-sourceToken').fill('1')
await page.page.getByTestId('swap-select-token-button-toToken').click()
await page.page
.getByTestId('swap-select-token-search-input')
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/utils/SafeLocalStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export type SafeLocalStorageItems = {
'@appkit/preferred_account_types': string
'@appkit/connections': string
'@appkit/disconnected_connector_ids': string
'@appkit/history_transactions_cache': string
'@appkit/token_price_cache': string
'@appkit/recent_emails': string
/*
* DO NOT CHANGE: @walletconnect/universal-provider requires us to set this specific key
Expand Down Expand Up @@ -54,6 +56,8 @@ export const SafeLocalStorageKeys = {
PREFERRED_ACCOUNT_TYPES: '@appkit/preferred_account_types',
CONNECTIONS: '@appkit/connections',
DISCONNECTED_CONNECTOR_IDS: '@appkit/disconnected_connector_ids',
HISTORY_TRANSACTIONS_CACHE: '@appkit/history_transactions_cache',
TOKEN_PRICE_CACHE: '@appkit/token_price_cache',
RECENT_EMAILS: '@appkit/recent_emails'
} as const satisfies Record<string, keyof SafeLocalStorageItems>

Expand Down
34 changes: 32 additions & 2 deletions packages/controllers/src/controllers/BlockchainApiController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,15 @@ export const BlockchainApiController = {
return { data: [], next: undefined }
}

return BlockchainApiController.get<BlockchainApiTransactionsResponse>({
const transactionsCache = StorageUtil.getTransactionsCacheForAddress({
address: account,
chainId
})
if (transactionsCache) {
return transactionsCache as BlockchainApiTransactionsResponse
}

const result = await BlockchainApiController.get<BlockchainApiTransactionsResponse>({
path: `/v1/account/${account}/history`,
params: {
cursor,
Expand All @@ -250,6 +258,15 @@ export const BlockchainApiController = {
signal,
cache
})

StorageUtil.updateTransactionsCache({
address: account,
chainId,
timestamp: Date.now(),
transactions: result
})

return result
},

async fetchSwapQuote({ amount, userAddress, from, to, gasPrice }: BlockchainApiSwapQuoteRequest) {
Expand Down Expand Up @@ -299,7 +316,12 @@ export const BlockchainApiController = {
return { fungibles: [] }
}

return state.api.post<BlockchainApiTokenPriceResponse>({
const tokenPriceCache = StorageUtil.getTokenPriceCacheForAddresses(addresses)
if (tokenPriceCache) {
return tokenPriceCache as BlockchainApiTokenPriceResponse
}

const result = await state.api.post<BlockchainApiTokenPriceResponse>({
path: '/v1/fungible/price',
body: {
currency: 'usd',
Expand All @@ -310,6 +332,14 @@ export const BlockchainApiController = {
'Content-Type': 'application/json'
}
})

StorageUtil.updateTokenPriceCache({
addresses,
timestamp: Date.now(),
tokenPrice: result
})

return result
},

async fetchSwapAllowance({ tokenAddress, userAddress }: BlockchainApiSwapAllowanceRequest) {
Expand Down
2 changes: 1 addition & 1 deletion packages/controllers/src/controllers/SwapController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ const controller = {
if (networkToken) {
state.networkTokenSymbol = networkToken.symbol
SwapController.setSourceToken(networkToken)
SwapController.setSourceTokenAmount('1')
SwapController.setSourceTokenAmount('0')
}
},

Expand Down
145 changes: 144 additions & 1 deletion packages/controllers/src/utils/StorageUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import type {
BlockchainApiBalanceResponse,
BlockchainApiIdentityResponse,
BlockchainApiLookupEnsName,
BlockchainApiTokenPriceResponse,
BlockchainApiTransactionsResponse,
ConnectionStatus,
PreferredAccountTypes,
SocialProvider,
Expand All @@ -33,7 +35,9 @@ export const StorageUtil = {
portfolio: 30000,
nativeBalance: 30000,
ens: 300000,
identity: 300000
identity: 300000,
transactionsHistory: 15000,
tokenPrice: 15000
},
isCacheExpired(timestamp: number, cacheExpiry: number) {
return Date.now() - timestamp > cacheExpiry
Expand Down Expand Up @@ -567,6 +571,7 @@ export const StorageUtil = {
SafeLocalStorage.removeItem(SafeLocalStorageKeys.NATIVE_BALANCE_CACHE)
SafeLocalStorage.removeItem(SafeLocalStorageKeys.ENS_CACHE)
SafeLocalStorage.removeItem(SafeLocalStorageKeys.IDENTITY_CACHE)
SafeLocalStorage.removeItem(SafeLocalStorageKeys.HISTORY_TRANSACTIONS_CACHE)
} catch {
console.info('Unable to clear address cache')
}
Expand Down Expand Up @@ -765,5 +770,143 @@ export const StorageUtil = {
}

return false
},
getTransactionsCache() {
try {
const result = SafeLocalStorage.getItem(SafeLocalStorageKeys.HISTORY_TRANSACTIONS_CACHE)

return result ? JSON.parse(result) : {}
} catch {
console.info('Unable to get transactions cache')
}

return {}
},

getTransactionsCacheForAddress({ address, chainId = '' }: { address: string; chainId?: string }) {
try {
const cache = StorageUtil.getTransactionsCache()
const transactionsCache = cache[address]?.[chainId]

// We want to discard cache if it's older than the cache expiry
if (
transactionsCache &&
!this.isCacheExpired(transactionsCache.timestamp, this.cacheExpiry.transactionsHistory)
) {
return transactionsCache.transactions
}
StorageUtil.removeTransactionsCache({ address, chainId })
} catch {
console.info('Unable to get transactions cache')
}

return undefined
},
updateTransactionsCache({
address,
chainId = '',
timestamp,
transactions
}: {
address: string
chainId?: string
timestamp: number
transactions: BlockchainApiTransactionsResponse
}) {
try {
const cache = StorageUtil.getTransactionsCache()
cache[address] = {
...cache[address],
[chainId]: {
timestamp,
transactions
}
}
SafeLocalStorage.setItem(
SafeLocalStorageKeys.HISTORY_TRANSACTIONS_CACHE,
JSON.stringify(cache)
)
} catch {
console.info('Unable to update transactions cache', {
address,
chainId,
timestamp,
transactions
})
}
},
removeTransactionsCache({ address, chainId }: { address: string; chainId: string }) {
try {
const cache = StorageUtil.getTransactionsCache()
const addressCache = cache?.[address] || {}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [chainId]: _removed, ...updatedChainData } = addressCache

SafeLocalStorage.setItem(
SafeLocalStorageKeys.HISTORY_TRANSACTIONS_CACHE,
JSON.stringify({
...cache,
[address]: updatedChainData
})
)
} catch {
console.info('Unable to remove transactions cache', { address, chainId })
}
},
getTokenPriceCache() {
try {
const result = SafeLocalStorage.getItem(SafeLocalStorageKeys.TOKEN_PRICE_CACHE)

return result ? JSON.parse(result) : {}
} catch {
console.info('Unable to get token price cache')
}

return {}
},
getTokenPriceCacheForAddresses(addresses: string[]) {
try {
const cache = StorageUtil.getTokenPriceCache()
const tokenPriceCache = cache[addresses.join(',')]
if (
tokenPriceCache &&
!this.isCacheExpired(tokenPriceCache.timestamp, this.cacheExpiry.tokenPrice)
) {
return tokenPriceCache.tokenPrice
}
StorageUtil.removeTokenPriceCache(addresses)
} catch {
console.info('Unable to get token price cache for addresses', addresses)
}

return undefined
},
updateTokenPriceCache(params: {
addresses: string[]
timestamp: number
tokenPrice: BlockchainApiTokenPriceResponse
}) {
try {
const cache = StorageUtil.getTokenPriceCache()
cache[params.addresses.join(',')] = {
timestamp: params.timestamp,
tokenPrice: params.tokenPrice
}
SafeLocalStorage.setItem(SafeLocalStorageKeys.TOKEN_PRICE_CACHE, JSON.stringify(cache))
} catch {
console.info('Unable to update token price cache', params)
}
},
removeTokenPriceCache(addresses: string[]) {
try {
const cache = StorageUtil.getTokenPriceCache()
SafeLocalStorage.setItem(
SafeLocalStorageKeys.TOKEN_PRICE_CACHE,
JSON.stringify({ ...cache, [addresses.join(',')]: undefined })
)
} catch {
console.info('Unable to remove token price cache', addresses)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ const networkTokenAddress = 'eip155:137:0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
// AVAX
const toTokenAddress = 'eip155:137:0x2c89bbc92bd86f8075d1decc58c7f4e0107f286b'

const sourceTokenAmount = '1'

// - Setup ---------------------------------------------------------------------
beforeAll(async () => {
const mockAdapter = {
Expand All @@ -70,7 +72,6 @@ beforeAll(async () => {

ChainController.setActiveCaipNetwork(caipNetwork)
AccountController.setCaipAddress(caipAddress, chain)

vi.spyOn(BlockchainApiController, 'fetchSwapTokens').mockResolvedValue(tokensResponse)
vi.spyOn(BlockchainApiController, 'getBalance').mockResolvedValue(balanceResponse)
vi.spyOn(BlockchainApiController, 'fetchSwapQuote').mockResolvedValue(swapQuoteResponse)
Expand Down Expand Up @@ -98,6 +99,8 @@ describe('SwapController', () => {
})

it('should calculate swap values as expected', async () => {
SwapController.setSourceTokenAmount(sourceTokenAmount)

await SwapController.swapTokens()

expect(SwapController.state.gasPriceInUSD).toEqual(0.00648630001383744)
Expand Down
64 changes: 59 additions & 5 deletions packages/scaffold-ui/src/views/w3m-swap-view/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ export class W3mSwapView extends LitElement {

@state() private fetchError = SwapController.state.fetchError

@state() private lastTokenPriceUpdate = 0

private minTokenPriceUpdateInterval = 10_000

// -- Lifecycle ----------------------------------------- //
public constructor() {
super()
Expand Down Expand Up @@ -136,6 +140,10 @@ export class W3mSwapView extends LitElement {
this.toTokenPriceInUSD = newState.toTokenPriceInUSD
this.inputError = newState.inputError
this.fetchError = newState.fetchError

if (newState.sourceToken && newState.toToken) {
this.watchTokensAndValues()
}
})
]
)
Expand All @@ -150,6 +158,7 @@ export class W3mSwapView extends LitElement {
public override disconnectedCallback() {
this.unsubscribe.forEach(unsubscribe => unsubscribe?.())
clearInterval(this.interval)
document?.removeEventListener('visibilitychange', this.visibilityChangeHandler)
}

// -- Render -------------------------------------------- //
Expand All @@ -162,12 +171,57 @@ export class W3mSwapView extends LitElement {
}

// -- Private ------------------------------------------- //
private watchTokensAndValues() {

private visibilityChangeHandler = () => {
if (document?.hidden) {
clearInterval(this.interval)
this.interval = undefined
} else {
this.startTokenPriceInterval()
}
}

private subscribeToVisibilityChange() {
document?.removeEventListener('visibilitychange', this.visibilityChangeHandler)
document?.addEventListener('visibilitychange', this.visibilityChangeHandler)
}

private startTokenPriceInterval = () => {
if (
this.interval &&
Date.now() - this.lastTokenPriceUpdate < this.minTokenPriceUpdateInterval
) {
return
}

// Quick fetch tokens and values if last update is more than 10 seconds ago
if (
this.lastTokenPriceUpdate &&
Date.now() - this.lastTokenPriceUpdate > this.minTokenPriceUpdateInterval
) {
this.fetchTokensAndValues()
}
clearInterval(this.interval)
this.interval = setInterval(() => {
SwapController.getNetworkTokenPrice()
SwapController.getMyTokensWithBalance()
SwapController.swapTokens()
}, 10_000)
this.fetchTokensAndValues()
}, this.minTokenPriceUpdateInterval)
}

private watchTokensAndValues = () => {
// Only fetch tokens and values if source and to token are set
if (!this.sourceToken || !this.toToken) {
return
}

this.subscribeToVisibilityChange()
this.startTokenPriceInterval()
}

private fetchTokensAndValues() {
SwapController.getNetworkTokenPrice()
SwapController.getMyTokensWithBalance()
SwapController.swapTokens()
this.lastTokenPriceUpdate = Date.now()
}

private templateSwap() {
Expand Down
Loading
Loading