Skip to content

Commit 95f2063

Browse files
authored
Mezoification: Add base state and UI elements (#3785)
Adds base state, notification handlers and banner elements for upcoming campaign... Banners and notifications should change based on the state of the campaign. ## To Test - [ ] With `SUPPORT_MEZO_NETWORK=true` open the wallet, after a minute, a banner should appear - [ ] Clicking the banner should prompt for push notification permissions - [ ] If push notifications are enabled a notification should appear Latest build: [extension-builds-3785](https://github.com/tahowallet/extension/suites/35544139271/artifacts/2734391449) (as of Wed, 12 Mar 2025 01:15:33 GMT).
2 parents 96ee9cf + 8de91a2 commit 95f2063

32 files changed

+1001
-99
lines changed

background/constants/networks.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export const ZK_SYNC: EVMNetwork = {
104104
}
105105

106106
export const MEZO_TESTNET: EVMNetwork = {
107-
name: "Matsnet",
107+
name: "Mezo matsnet",
108108
baseAsset: MEZO_BTC,
109109
chainID: "31611",
110110
family: "EVM",
@@ -169,7 +169,7 @@ export const NETWORK_BY_CHAIN_ID = {
169169
}
170170

171171
export const TEST_NETWORK_BY_CHAIN_ID = new Set(
172-
[SEPOLIA, ARBITRUM_SEPOLIA].map((network) => network.chainID),
172+
[MEZO_TESTNET, SEPOLIA, ARBITRUM_SEPOLIA].map((network) => network.chainID),
173173
)
174174

175175
// Networks that are not added to this struct will

background/lib/mezo.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Interface } from "ethers/lib/utils"
2+
import { AnyEVMTransaction, sameNetwork } from "../networks"
3+
import { MEZO_TESTNET } from "../constants"
4+
import { sameEVMAddress } from "./utils"
5+
6+
const BORROWER_CONTRACT_ADDRESS = "0x20fAeA18B6a1D0FCDBCcFfFe3d164314744baF30"
7+
8+
const BorrowerABI = new Interface([
9+
"function openTrove(uint256 _maxFeePercentage, uint256 debtAmount, uint256 _assetAmount, address _upperHint, address _lowerHint)",
10+
])
11+
12+
// eslint-disable-next-line import/prefer-default-export
13+
export const checkIsBorrowingTx = (tx: AnyEVMTransaction) => {
14+
if (
15+
!sameNetwork(tx.network, MEZO_TESTNET) ||
16+
!tx.blockHash ||
17+
!sameEVMAddress(tx.to, BORROWER_CONTRACT_ADDRESS)
18+
) {
19+
return false
20+
}
21+
22+
try {
23+
const data = BorrowerABI.decodeFunctionData("openTrove", tx.input ?? "")
24+
return data.debtAmount > 0n
25+
} catch (error) {
26+
return false
27+
}
28+
}

background/networks.ts

+4
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,7 @@ export const isEnrichedEVMTransactionRequest = (
422422
transactionRequest: TransactionRequest,
423423
): transactionRequest is EnrichedEVMTransactionRequest =>
424424
"annotation" in transactionRequest
425+
426+
export const isConfirmedEVMTransaction = (
427+
transaction: AnyEVMTransaction,
428+
): transaction is ConfirmedEVMTransaction => "status" in transaction

background/redux-slices/selectors/networks.ts

+8
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ export const selectCustomNetworks = createSelector(
2828
(network) => !DEFAULT_NETWORKS_BY_CHAIN_ID.has(network.chainID),
2929
),
3030
)
31+
32+
export const selectTestnetNetworks = createSelector(
33+
selectEVMNetworks,
34+
(evmNetworks) =>
35+
evmNetworks.filter((network) =>
36+
TEST_NETWORK_BY_CHAIN_ID.has(network.chainID),
37+
),
38+
)

background/redux-slices/selectors/uiSelectors.ts

+5
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ export const selectShowingActivityDetail = createSelector(
3636
},
3737
)
3838

39+
export const selectCampaigns = createSelector(
40+
(state: RootState) => state.ui.campaigns,
41+
(campaigns) => campaigns,
42+
)
43+
3944
export const selectCurrentAddressNetwork = createSelector(
4045
(state: RootState) => state.ui.selectedAccount,
4146
(selectedAccount) => selectedAccount,

background/redux-slices/ui.ts

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createSlice, createSelector } from "@reduxjs/toolkit"
22
import Emittery from "emittery"
33
import { AddressOnNetwork } from "../accounts"
4-
import { ETHEREUM } from "../constants"
4+
import { ETHEREUM, TEST_NETWORK_BY_CHAIN_ID } from "../constants"
55
import { AnalyticsEvent, OneTimeAnalyticsEvent } from "../lib/posthog"
66
import { EVMNetwork } from "../networks"
77
import { AnalyticsPreferences, DismissableItem } from "../services/preferences"
@@ -11,18 +11,25 @@ import { AccountState, addAddressNetwork } from "./accounts"
1111
import { createBackgroundAsyncThunk } from "./utils"
1212
import { UNIXTime } from "../types"
1313
import { DEFAULT_AUTOLOCK_INTERVAL } from "../services/preferences/defaults"
14+
import type { RootState } from "."
15+
import {
16+
CampaignIds,
17+
Campaigns,
18+
FilterCampaignsById,
19+
} from "../services/campaign/types"
1420

1521
export const defaultSettings = {
1622
hideDust: false,
1723
defaultWallet: false,
18-
showTestNetworks: false,
24+
showTestNetworks: true,
1925
showNotifications: undefined,
2026
collectAnalytics: false,
2127
showAnalyticsNotification: false,
2228
showUnverifiedAssets: false,
2329
hideBanners: false,
2430
useFlashbots: false,
2531
autoLockInterval: DEFAULT_AUTOLOCK_INTERVAL,
32+
campaigns: {},
2633
}
2734

2835
export type UIState = {
@@ -47,6 +54,16 @@ export type UIState = {
4754
routeHistoryEntries?: Partial<Location>[]
4855
slippageTolerance: number
4956
accountSignerSettings: AccountSignerSettings[]
57+
// Active user campaigns
58+
campaigns: {
59+
/**
60+
* Some hash used to invalidate cached data and update UI
61+
*/
62+
[campaignId in CampaignIds]?: FilterCampaignsById<
63+
Campaigns,
64+
campaignId
65+
>["data"]
66+
}
5067
}
5168

5269
export type Events = {
@@ -63,6 +80,8 @@ export type Events = {
6380
updateAnalyticsPreferences: Partial<AnalyticsPreferences>
6481
addCustomNetworkResponse: [string, boolean]
6582
updateAutoLockInterval: number
83+
toggleShowTestNetworks: boolean
84+
clearNotification: string
6685
}
6786

6887
export const emitter = new Emittery<Events>()
@@ -78,6 +97,7 @@ export const initialState: UIState = {
7897
snackbarMessage: "",
7998
slippageTolerance: 0.01,
8099
accountSignerSettings: [],
100+
campaigns: {},
81101
}
82102

83103
const uiSlice = createSlice({
@@ -222,6 +242,16 @@ const uiSlice = createSlice({
222242
...state,
223243
settings: { ...state.settings, autoLockInterval: payload },
224244
}),
245+
updateCampaignsState: (
246+
immerState: UIState,
247+
{
248+
payload,
249+
}: {
250+
payload: UIState["campaigns"]
251+
},
252+
) => {
253+
immerState.campaigns = payload
254+
},
225255
},
226256
})
227257

@@ -246,6 +276,7 @@ export const {
246276
setSlippageTolerance,
247277
setAccountsSignerSettings,
248278
setAutoLockInterval,
279+
updateCampaignsState,
249280
} = uiSlice.actions
250281

251282
export default uiSlice.reducer
@@ -273,6 +304,13 @@ export const deleteAnalyticsData = createBackgroundAsyncThunk(
273304
},
274305
)
275306

307+
export const clearNotification = createBackgroundAsyncThunk(
308+
"ui/clearNotification",
309+
async (id: string) => {
310+
await emitter.emit("clearNotification", id)
311+
},
312+
)
313+
276314
// Async thunk to bubble the setNewDefaultWalletValue action from store to emitter.
277315
export const setNewDefaultWalletValue = createBackgroundAsyncThunk(
278316
"ui/setNewDefaultWalletValue",
@@ -360,6 +398,22 @@ export const setSelectedNetwork = createBackgroundAsyncThunk(
360398
},
361399
)
362400

401+
export const toggleShowTestNetworks = createBackgroundAsyncThunk(
402+
"ui/toggleShowTestNetworks",
403+
async (updatedValue: boolean, { dispatch, getState }) => {
404+
const state = getState() as RootState
405+
406+
const currentNetwork = state.ui.selectedAccount.network
407+
408+
// If user is on one of the built-in test networks, don't leave them stranded
409+
if (!updatedValue && TEST_NETWORK_BY_CHAIN_ID.has(currentNetwork.chainID)) {
410+
dispatch(setSelectedNetwork(ETHEREUM))
411+
}
412+
413+
await emitter.emit("toggleShowTestNetworks", updatedValue)
414+
},
415+
)
416+
363417
export const refreshBackgroundPage = createBackgroundAsyncThunk(
364418
"ui/refreshBackgroundPage",
365419
async () => {

background/services/analytics/index.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ interface Events extends ServiceLifecycleEvents {
3131
* handling sending and persistance concerns.
3232
*/
3333
export default class AnalyticsService extends BaseService<Events> {
34+
#analyticsUUID: string | undefined = undefined
35+
3436
/*
3537
* Create a new AnalyticsService. The service isn't initialized until
3638
* startService() is called and resolved.
@@ -93,6 +95,7 @@ export default class AnalyticsService extends BaseService<Events> {
9395

9496
protected override async internalStartService(): Promise<void> {
9597
await super.internalStartService()
98+
const { uuid, isNew } = await this.getOrCreateAnalyticsUUID()
9699

97100
let { isEnabled, hasDefaultOnBeenTurnedOn } =
98101
await this.preferenceService.getAnalyticsPreferences()
@@ -114,8 +117,6 @@ export default class AnalyticsService extends BaseService<Events> {
114117
}
115118

116119
if (isEnabled) {
117-
const { uuid, isNew } = await this.getOrCreateAnalyticsUUID()
118-
119120
browser.runtime.setUninstallURL(
120121
process.env.NODE_ENV === "development"
121122
? "about:blank"
@@ -126,6 +127,8 @@ export default class AnalyticsService extends BaseService<Events> {
126127
await this.sendAnalyticsEvent(AnalyticsEvent.NEW_INSTALL)
127128
}
128129
}
130+
131+
this.#analyticsUUID = uuid
129132
}
130133

131134
protected override async internalStopService(): Promise<void> {
@@ -134,6 +137,15 @@ export default class AnalyticsService extends BaseService<Events> {
134137
await super.internalStopService()
135138
}
136139

140+
get analyticsUUID() {
141+
if (!this.#analyticsUUID) {
142+
throw new Error(
143+
"Attempted to access analytics UUID before service started",
144+
)
145+
}
146+
return this.#analyticsUUID
147+
}
148+
137149
async sendAnalyticsEvent(
138150
eventName: AnalyticsEvent,
139151
payload?: Record<string, unknown>,

background/services/campaign/db.ts

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Dexie from "dexie"
2+
import { CampaignIds, Campaigns, FilterCampaignsById } from "./types"
3+
4+
export class CampaignDatabase extends Dexie {
5+
private campaigns!: Dexie.Table<Campaigns, CampaignIds>
6+
7+
constructor() {
8+
super("taho/campaigns")
9+
10+
this.version(1).stores({
11+
campaigns: "&id",
12+
})
13+
}
14+
15+
async getActiveCampaigns() {
16+
return this.campaigns
17+
.toCollection()
18+
.filter((campaign) => campaign.enabled)
19+
.toArray()
20+
}
21+
22+
async getCampaignData<K extends CampaignIds>(
23+
id: K,
24+
): Promise<FilterCampaignsById<Campaigns, K> | undefined> {
25+
return this.campaigns.get(id) as Promise<
26+
FilterCampaignsById<Campaigns, K> | undefined
27+
>
28+
}
29+
30+
async upsertCampaign(campaign: Campaigns): Promise<void> {
31+
await this.campaigns.put(campaign)
32+
}
33+
34+
async updateCampaignData<K extends CampaignIds>(
35+
id: K,
36+
data: Partial<FilterCampaignsById<Campaigns, K>["data"]>,
37+
): Promise<void> {
38+
await this.campaigns.toCollection().modify((campaign) => {
39+
if (campaign.id === id) {
40+
Object.assign(campaign, { data })
41+
}
42+
})
43+
}
44+
}
45+
46+
export async function getOrCreateDB(): Promise<CampaignDatabase> {
47+
return new CampaignDatabase()
48+
}

0 commit comments

Comments
 (0)