Skip to content

Commit f312e0c

Browse files
authored
Merge pull request #2164 from AmbireTech/release/v2.88
Release / v2.88.0
2 parents bb170ff + f0ae639 commit f312e0c

53 files changed

Lines changed: 1305 additions & 663 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ jobs:
2828
uses: actions/checkout@v3
2929

3030
- name: Install Node.js ⚙️
31-
uses: actions/setup-node@v3
31+
uses: actions/setup-node@v6
3232
with:
33-
node-version: 22.12.0
33+
node-version-file: '.nvmrc'
3434

3535
- name: Install NPM packages ♾️🕳️
3636
run: npm ci
@@ -47,5 +47,5 @@ jobs:
4747
REACT_APP_PIMLICO_API_KEY: ${{ secrets.REACT_APP_PIMLICO_API_KEY }}
4848
REACT_APP_CANDIDE_API_KEY: ${{ secrets.REACT_APP_CANDIDE_API_KEY }}
4949
REACT_APP_ETHERSPOT_API_KEY: ${{ secrets.REACT_APP_ETHERSPOT_API_KEY }}
50-
50+
5151
run: npm run jest

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
22.12.0
1+
24.13.0

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"version": "2.87.2",
2+
"version": "2.88.3",
33
"name": "ambire-common",
44
"description": "Common ground for the Ambire apps",
55
"scripts": {

src/classes/recurringTimeout/recurringTimeout.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,48 @@ describe('RecurringTimeout', () => {
102102
await jest.advanceTimersByTimeAsync(200)
103103
expect(fn).toHaveBeenCalledTimes(2)
104104
})
105+
test('should allow overlap when configured: next fn run starts even if previous is running', async () => {
106+
// The second call should overlap with the first, but after both complete,
107+
// there should be only one scheduled loop
108+
const first = createDeferred()
109+
const second = createDeferred()
110+
const fn = jest
111+
.fn()
112+
.mockImplementationOnce(() => first.promise)
113+
.mockImplementationOnce(() => second.promise)
114+
115+
const t = new RecurringTimeout(fn, 200)
116+
t.start({ runImmediately: true, allowOverlap: true })
117+
118+
await jest.advanceTimersByTimeAsync(0) // wait for the debounce timeout
119+
expect(fn).toHaveBeenCalledTimes(1)
120+
121+
// Trigger restart while the first is still running
122+
// without allowOverlap, this would not schedule a new run
123+
t.restart({ runImmediately: true, allowOverlap: true })
124+
await jest.advanceTimersByTimeAsync(0) // wait for the debounce timeout
125+
expect(fn).toHaveBeenCalledTimes(2)
126+
127+
// Resolve first
128+
first.resolve()
129+
await Promise.resolve() // this await the promise of the fn to run its .then/.catch/.finally
130+
131+
// No new run should be scheduled yet
132+
await jest.advanceTimersByTimeAsync(1000)
133+
expect(fn).toHaveBeenCalledTimes(2)
134+
135+
// Resolve second
136+
second.resolve()
137+
await Promise.resolve() // this await the promise of the fn to run its .then/.catch/.finally
138+
139+
// Now a new run should be scheduled
140+
// Purposefully advance more than 200ms to ensure the timer is not
141+
// affected by small inaccuracies
142+
await jest.advanceTimersByTimeAsync(300)
143+
// Expect 3!!! If there is a bug that makes loop() schedule multiple
144+
// timeouts, this will catch it.
145+
expect(fn).toHaveBeenCalledTimes(3)
146+
})
105147
test('stop should prevent further fn runs and clears timer', async () => {
106148
const d1 = createDeferred()
107149
const fn = jest.fn(() => d1.promise)

src/classes/recurringTimeout/recurringTimeout.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,31 @@
55

66
import EventEmitter from '../../controllers/eventEmitter/eventEmitter'
77

8+
type RecurringTimeoutStartOptions = {
9+
timeout?: number
10+
/**
11+
* Whether to run the function immediately upon starting,
12+
* instead of waiting for the first timeout interval.
13+
*/
14+
runImmediately?: boolean
15+
/**
16+
* Whether to allow the starting of a new function execution
17+
* even if the previous one is still running. There will still
18+
* be only one interval scheduled at a time.
19+
*
20+
* @example
21+
* - Execution 1 starts
22+
* - Execution 2 starts while Execution 1 is still running
23+
* - Execution 1 completes - a new execution is not scheduled. Simply
24+
* the promise of Execution 1 is resolved.
25+
* - Execution 2 completes - a new execution is scheduled.
26+
*/
27+
allowOverlap?: boolean
28+
}
29+
830
export interface IRecurringTimeout {
9-
start: (options?: { timeout?: number; runImmediately?: boolean }) => void
10-
restart: (options?: { timeout?: number; runImmediately?: boolean }) => void
31+
start: (options?: RecurringTimeoutStartOptions) => void
32+
restart: (options?: RecurringTimeoutStartOptions) => void
1133
stop: () => void
1234
updateTimeout: (options: { timeout: number }) => void
1335
running: boolean
@@ -42,7 +64,7 @@ export class RecurringTimeout implements IRecurringTimeout {
4264
promise: Promise<void> | undefined
4365

4466
// collapse multiple start/restart calls in the same tick
45-
#pendingStart?: { timeout?: number; runImmediately?: boolean }
67+
#pendingStart?: RecurringTimeoutStartOptions
4668

4769
startScheduled = false
4870

@@ -62,7 +84,7 @@ export class RecurringTimeout implements IRecurringTimeout {
6284
this.currentTimeout = timeout
6385
}
6486

65-
start(opts: { timeout?: number; runImmediately?: boolean } = {}) {
87+
start(opts: RecurringTimeoutStartOptions = {}) {
6688
this.#scheduleStart(opts)
6789
}
6890

@@ -71,43 +93,42 @@ export class RecurringTimeout implements IRecurringTimeout {
7193
this.#reset()
7294
}
7395

74-
restart(opts: { timeout?: number; runImmediately?: boolean } = {}) {
96+
restart(opts: RecurringTimeoutStartOptions = {}) {
7597
this.#reset()
7698
this.#scheduleStart(opts)
7799
}
78100

79101
async #loop() {
102+
this.fnExecutionsCount += 1
103+
const currentCount = this.fnExecutionsCount
104+
80105
try {
81106
this.promise = this.#fn()
82-
this.fnExecutionsCount += 1
83107
await this.promise
84108
} catch (err: any) {
85109
if (!this.promise) return
86110
console.error('Recurring task error:', err)
87111
if (this.#emitError)
88112
this.#emitError({ error: err, message: 'Recurring task failed', level: 'minor' })
89113
} finally {
90-
if (this.promise) {
114+
// If fnExecutionsCount has changed, it means `restart` was called during the execution of fn,
115+
// so we shouldn't schedule the next loop here.
116+
if (this.promise && this.fnExecutionsCount === currentCount) {
91117
if (this.running) this.#timeoutId = setTimeout(this.#loop.bind(this), this.currentTimeout)
92118
this.promise = undefined
93119
}
94120
}
95121
}
96122

97-
#scheduleStart(
98-
opts: {
99-
timeout?: number
100-
runImmediately?: boolean
101-
} = {}
102-
) {
123+
#scheduleStart(opts: RecurringTimeoutStartOptions = {}) {
103124
if (this.running) return
104125
this.#pendingStart = opts // collect latest opts for this tick
105126
if (this.startScheduled) return
106127
this.startScheduled = true
107128

108129
queueMicrotask(() => {
109130
this.startScheduled = false
110-
const { timeout: newTimeout, runImmediately } = this.#pendingStart || {}
131+
const { timeout: newTimeout, runImmediately, allowOverlap } = this.#pendingStart || {}
111132
this.#pendingStart = undefined
112133

113134
this.running = true
@@ -116,7 +137,8 @@ export class RecurringTimeout implements IRecurringTimeout {
116137

117138
if (newTimeout) this.updateTimeout({ timeout: newTimeout })
118139

119-
if (this.promise) return // prevents multiple executions in one tick
140+
// Prevents starting a new loop if the previous one is still running
141+
if (this.promise && !allowOverlap) return
120142

121143
if (runImmediately) {
122144
this.#loop()

src/consts/coingecko.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ZeroAddress } from 'ethers'
22

33
import { Network } from '../interfaces/network'
4-
import { WALLET_STAKING_ADDR, WALLET_TOKEN } from './addresses'
4+
import { STK_WALLET, WALLET_STAKING_ADDR, WALLET_TOKEN } from './addresses'
55

66
const COINGECKO_API_BASE_URL = 'https://api.coingecko.com/api/v3/coins/'
77
const COINGECKO_BASE_URL = 'https://www.coingecko.com/en/coins/'
@@ -21,8 +21,8 @@ export function geckoIdMapper(address: string, network: Network): string | null
2121
* CoinGecko (so that they are aliased to existing tokens).
2222
*/
2323
export function geckoTokenAddressMapper(address: string) {
24-
// xWALLET is missing on CoinGecko, so alias it to WALLET token (that exists on CoinGecko)
25-
if (address === WALLET_STAKING_ADDR) return WALLET_TOKEN
24+
// stkWALLET and xWALLET are missing on CoinGecko, so alias to WALLET token (which exists)
25+
if ([STK_WALLET, WALLET_STAKING_ADDR].includes(address)) return WALLET_TOKEN
2626

2727
return address
2828
}

src/consts/dapps/dapps.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const dappIdsToBeRemoved = new Set([
1313
'curve.fi', // curve.fi was moved to curve.finance
1414
'app.ether.fi', // app.ether.fi was moved to ether.fi
1515
'core.app', // not supported,
16+
'new.bungee.exchange', // got left hanging since Bungee moved to bungee.exchange
17+
'multitx.bungee.exchange', // got left hanging since Bungee moved to bungee.exchange
1618
// unsupported or outdated protocols returned by DefiLlama
1719
'bridge.base.org',
1820
'binance.com',
@@ -419,7 +421,7 @@ export const defiLlamaProtocolIdsToExclude: string[] = [
419421

420422
export const categoriesNotToFilterOut = ['DEX Aggregator']
421423
export const categoriesToExclude = ['CEX', 'Developer Tools']
422-
export const dappsNotToFilterOutByDomain = ['snapshot.box']
424+
export const dappsNotToFilterOutByDomain = ['snapshot.box', 'bungee.exchange']
423425

424426
export const CATEGORY_MAP: Record<string, string> = {
425427
'AI Agents': 'AI Agents',

src/controllers/accountPicker/accountPicker.test.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,11 @@ import {
1515
DERIVATION_OPTIONS,
1616
SMART_ACCOUNT_SIGNER_KEY_DERIVATION_OFFSET
1717
} from '../../consts/derivation'
18-
import { networks } from '../../consts/networks'
1918
import { Account } from '../../interfaces/account'
2019
import { IProvidersController } from '../../interfaces/provider'
2120
import { Storage } from '../../interfaces/storage'
2221
import { isSmartAccount } from '../../libs/account/account'
2322
import { getPrivateKeyFromSeed, KeyIterator } from '../../libs/keyIterator/keyIterator'
24-
import { getRpcProvider } from '../../services/provider'
2523
import wait from '../../utils/wait'
2624
import { AccountsController } from '../accounts/accounts'
2725
import { KeystoreController } from '../keystore/keystore'
@@ -73,22 +71,15 @@ const prepareTest = async () => {
7371
storage: storageCtrl,
7472
fetch,
7573
relayerUrl,
76-
onAddOrUpdateNetworks: (nets) => {
77-
nets.forEach((n) => {
78-
providersCtrl.setProvider(n)
79-
})
74+
useTempProvider: (props, cb) => {
75+
return providersCtrl.useTempProvider(props, cb)
8076
},
81-
onRemoveNetwork: (id) => {
82-
providersCtrl.removeProvider(id)
83-
}
77+
onAddOrUpdateNetworks: () => {}
8478
})
85-
providersCtrl = new ProvidersController(networksCtrl, storageCtrl)
86-
87-
providersCtrl.providers = Object.fromEntries(
88-
networks.map((network) => [network.chainId, getRpcProvider(network.rpcUrls, network.chainId)])
89-
)
9079
const { uiManager } = mockUiManager()
9180
const uiCtrl = new UiController({ uiManager })
81+
providersCtrl = new ProvidersController(networksCtrl, storageCtrl, uiCtrl)
82+
9283
const keystoreController = new KeystoreController('default', storageCtrl, {}, uiCtrl)
9384

9485
const accountsCtrl = new AccountsController(

0 commit comments

Comments
 (0)