Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mezoification: Add base state and UI elements #3785

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
4 changes: 2 additions & 2 deletions background/constants/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export const ZK_SYNC: EVMNetwork = {
}

export const MEZO_TESTNET: EVMNetwork = {
name: "Matsnet",
name: "Mezo matsnet",
baseAsset: MEZO_BTC,
chainID: "31611",
family: "EVM",
Expand Down Expand Up @@ -169,7 +169,7 @@ export const NETWORK_BY_CHAIN_ID = {
}

export const TEST_NETWORK_BY_CHAIN_ID = new Set(
[SEPOLIA, ARBITRUM_SEPOLIA].map((network) => network.chainID),
[MEZO_TESTNET, SEPOLIA, ARBITRUM_SEPOLIA].map((network) => network.chainID),
)

// Networks that are not added to this struct will
Expand Down
28 changes: 28 additions & 0 deletions background/lib/mezo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Interface } from "ethers/lib/utils"
import { AnyEVMTransaction, sameNetwork } from "../networks"
import { MEZO_TESTNET } from "../constants"
import { sameEVMAddress } from "./utils"

const BORROWER_CONTRACT_ADDRESS = "0x20fAeA18B6a1D0FCDBCcFfFe3d164314744baF30"

const BorrowerABI = new Interface([
"function openTrove(uint256 _maxFeePercentage, uint256 debtAmount, uint256 _assetAmount, address _upperHint, address _lowerHint)",
])

// eslint-disable-next-line import/prefer-default-export
Copy link
Contributor

Choose a reason for hiding this comment

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

Lint disable is best accompanied by an explanation.

In this case my thought is that we probably move this into a Mezo or Mezo campaign service, which is where we deal with the Redux service's complexity as well (after we ship this, I mean). Wdyt?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That's a good point, this is only used for the campaign. I initially put this under /lib because I thought this would be also used by the enrichment service.

Copy link
Contributor

Choose a reason for hiding this comment

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

For sure. I think once we're enriching we'll probably export more anyway—and I wonder if the better approach here is actually making this file an ABI export file (background/lib/mezo/musd-trovemanager-abi.ts or so) and then we implement checkIsBorrowingTx directly where we need it and let enrichment do its own things with the ABI.

Anyway, none of that is blocking.

export const checkIsBorrowingTx = (tx: AnyEVMTransaction) => {
if (
!sameNetwork(tx.network, MEZO_TESTNET) ||
!tx.blockHash ||
!sameEVMAddress(tx.to, BORROWER_CONTRACT_ADDRESS)
) {
return false
}

try {
const data = BorrowerABI.decodeFunctionData("openTrove", tx.input ?? "")
return data.debtAmount > 0n
} catch (error) {
return false
}
}
8 changes: 8 additions & 0 deletions background/redux-slices/selectors/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ export const selectCustomNetworks = createSelector(
(network) => !DEFAULT_NETWORKS_BY_CHAIN_ID.has(network.chainID),
),
)

export const selectTestnetNetworks = createSelector(
selectEVMNetworks,
(evmNetworks) =>
evmNetworks.filter((network) =>
TEST_NETWORK_BY_CHAIN_ID.has(network.chainID),
),
)
5 changes: 5 additions & 0 deletions background/redux-slices/selectors/uiSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ export const selectShowingActivityDetail = createSelector(
},
)

export const selectActiveCampaigns = createSelector(
(state: RootState) => state.ui.activeCampaigns,
(campaigns) => campaigns,
)

export const selectCurrentAddressNetwork = createSelector(
(state: RootState) => state.ui.selectedAccount,
(selectedAccount) => selectedAccount,
Expand Down
32 changes: 32 additions & 0 deletions background/redux-slices/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ export const defaultSettings = {
autoLockInterval: DEFAULT_AUTOLOCK_INTERVAL,
}

export type MezoClaimStatus =
| "not-eligible"
| "eligible"
| "claimed-sats"
| "borrowed"
| "campaign-complete"

export type UIState = {
selectedAccount: AddressOnNetwork
showingActivityDetailID: string | null
Expand All @@ -47,6 +54,13 @@ export type UIState = {
routeHistoryEntries?: Partial<Location>[]
slippageTolerance: number
accountSignerSettings: AccountSignerSettings[]
activeCampaigns: {
"mezo-claim"?: {
dateFrom: string
Copy link
Contributor

Choose a reason for hiding this comment

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

Guessing if we make this Date it won't (de)serialize?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yup, it will remain a string

Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we should give our serializer/deserializer the ability to deal with dates tbh (not as a blocker though).

dateTo: string
state: MezoClaimStatus
}
}
}

export type Events = {
Expand Down Expand Up @@ -78,6 +92,7 @@ export const initialState: UIState = {
snackbarMessage: "",
slippageTolerance: 0.01,
accountSignerSettings: [],
activeCampaigns: {},
}

const uiSlice = createSlice({
Expand Down Expand Up @@ -222,6 +237,22 @@ const uiSlice = createSlice({
...state,
settings: { ...state.settings, autoLockInterval: payload },
}),
updateCampaignState: <T extends keyof UIState["activeCampaigns"]>(
Copy link
Contributor

Choose a reason for hiding this comment

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

When we start pulling typeofs like this it's usually a sign to me that we should name the type, fwiw.

immerState: UIState,
{
payload,
}: {
payload: [T, Partial<UIState["activeCampaigns"][T]>]
},
) => {
const [campaignId, update] = payload

immerState.activeCampaigns ??= {}
immerState.activeCampaigns[campaignId] = {
...immerState.activeCampaigns[campaignId],
...update,
}
},
},
})

Expand All @@ -246,6 +277,7 @@ export const {
setSlippageTolerance,
setAccountsSignerSettings,
setAutoLockInterval,
updateCampaignState,
} = uiSlice.actions

export default uiSlice.reducer
Expand Down
16 changes: 14 additions & 2 deletions background/services/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ interface Events extends ServiceLifecycleEvents {
* handling sending and persistance concerns.
*/
export default class AnalyticsService extends BaseService<Events> {
#analyticsUUID: string | undefined = undefined

/*
* Create a new AnalyticsService. The service isn't initialized until
* startService() is called and resolved.
Expand Down Expand Up @@ -93,6 +95,7 @@ export default class AnalyticsService extends BaseService<Events> {

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

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

if (isEnabled) {
const { uuid, isNew } = await this.getOrCreateAnalyticsUUID()

browser.runtime.setUninstallURL(
process.env.NODE_ENV === "development"
? "about:blank"
Expand All @@ -126,6 +127,8 @@ export default class AnalyticsService extends BaseService<Events> {
await this.sendAnalyticsEvent(AnalyticsEvent.NEW_INSTALL)
}
}

this.#analyticsUUID = uuid
}

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

get analyticsUUID() {
Copy link
Contributor

Choose a reason for hiding this comment

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

From below it looks like we can lose this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you're referring to the error below, that's just to block access until service has initialized. This could happen during tests or if there's an attempt to get this data from within another service's initialization without awaiting service.started()

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I meant that it doesn't look like we're accessing this as a property anywhere—which is good, as leaking the UUID feels like leaking the private state of the analytics service and leaking the implementation detail of using PostHog.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmmm I'm not following, we need this id to get the wallet's campaign state 🤔

if (!this.#analyticsUUID) {
throw new Error(
"Attempted to access analytics UUID before service started",
)
}
return this.#analyticsUUID
}

async sendAnalyticsEvent(
eventName: AnalyticsEvent,
payload?: Record<string, unknown>,
Expand Down
19 changes: 12 additions & 7 deletions background/services/notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,8 @@ export default class NotificationsService extends BaseService<Events> {
// does guard this, but if that ends up not being true, browser.notifications
// will be undefined and all of this will explode.

this.preferenceService.emitter.on(
"initializeNotificationsPreferences",
async (isPermissionGranted) => {
this.isPermissionGranted = isPermissionGranted
},
)
this.isPermissionGranted =
await this.preferenceService.getShouldShowNotificationsPreferences()

this.preferenceService.emitter.on(
"setNotificationsPermission",
Expand Down Expand Up @@ -130,6 +126,7 @@ export default class NotificationsService extends BaseService<Events> {
message: string
contextMessage?: string
type?: browser.Notifications.TemplateType
onDismiss?: () => void
}
callback?: () => void
}) {
Expand All @@ -138,17 +135,25 @@ export default class NotificationsService extends BaseService<Events> {
}
const notificationId = uniqueId("notification-")

const { onDismiss = () => {}, ...createNotificationOptions } = options

const notificationOptions = {
type: "basic" as browser.Notifications.TemplateType,
iconUrl: TAHO_ICON_URL,
...options,
...createNotificationOptions,
}

if (typeof callback === "function") {
this.clickHandlers[notificationId] = callback
}

browser.notifications.create(notificationId, notificationOptions)

browser.notifications.onClosed.addListener((id, byUser) => {
if (id === notificationId && byUser) {
onDismiss()
}
})
}

public notifyXPDrop(callback?: () => void): void {
Expand Down
7 changes: 6 additions & 1 deletion background/services/preferences/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,17 @@ export type ManuallyDismissableItem =
| "analytics-enabled-banner"
| "copy-sensitive-material-warning"
| "testnet-portal-is-open-banner"

/**
* Items that the user will see once and will not be auto-displayed again. Can
* be used for tours, or for popups that can be retriggered but will not
* auto-display more than once.
*/
export type SingleShotItem = "default-connection-popover"
export type SingleShotItem =
| "default-connection-popover"
| "mezo-eligible-notification"
| "mezo-borrow-notification"
| "mezo-nft-notification"

/**
* Items that the user will view one time and either manually dismiss or that
Expand Down
15 changes: 3 additions & 12 deletions background/services/preferences/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import browser from "webextension-polyfill"
import { FiatCurrency } from "../../assets"
import { AddressOnNetwork, NameOnNetwork } from "../../accounts"
import { ServiceLifecycleEvents, ServiceCreatorFunction } from "../types"
Expand Down Expand Up @@ -277,18 +276,10 @@ export default class PreferenceService extends BaseService<Events> {
}

async setShouldShowNotifications(shouldShowNotifications: boolean) {
if (shouldShowNotifications) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would love to still make it so that this can only set the db state if the permission is actually granted, rather than having it be a blind state updater. i.e., it would be good if we could guard against setShouldShowNotifications(true) successfully updating the db if in fact the user denied the request.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmmm we can verify if permission has indeed been granted and guard that I think

const granted = await browser.permissions.request({
permissions: ["notifications"],
})

await this.db.setShouldShowNotifications(granted)
this.emitter.emit("setNotificationsPermission", granted)

return granted
}
await this.db.setShouldShowNotifications(shouldShowNotifications)
this.emitter.emit("setNotificationsPermission", shouldShowNotifications)

return false
return shouldShowNotifications
}

async getAccountSignerSettings(): Promise<AccountSignerSettings[]> {
Expand Down
Loading