From de4243daa2a508323a75caf4b3b6bd72a5c5b89c Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Thu, 6 Feb 2025 12:17:01 +0100 Subject: [PATCH 01/27] chore: add redux params --- src/app/actions.ts | 33 +++++++----- src/app/reducers.ts | 21 +++++--- src/app/selectors.ts | 2 + src/app/types.ts | 17 +++++++ src/redux/migrations.ts | 7 +++ src/redux/store.test.ts | 4 +- src/redux/store.ts | 2 +- test/RootStateSchema.json | 104 ++++++++++++++++++++++++++++++++++++-- test/schemas.ts | 14 ++++- 9 files changed, 177 insertions(+), 27 deletions(-) create mode 100644 src/app/types.ts diff --git a/src/app/actions.ts b/src/app/actions.ts index 7aa9cf347c6..17a979c6229 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,6 +1,8 @@ import { BIOMETRY_TYPE } from '@divvi/react-native-keychain' import { RemoteConfigValues } from 'src/app/saga' +import { SupportedProtocol } from 'src/app/types' import { Screens } from 'src/navigator/Screens' +import { NetworkId } from 'src/transactions/types' // https://facebook.github.io/react-native/docs/appstate export enum AppState { @@ -11,7 +13,6 @@ export enum AppState { export enum Actions { SET_APP_STATE = 'APP/SET_APP_STATE', - SET_LOGGED_IN = 'APP/SET_LOGGED_IN', SET_SUPPORTED_BIOMETRY_TYPE = 'APP/SET_SUPPORTED_BIOMETRY_TYPE', OPEN_DEEP_LINK = 'APP/OPEN_DEEP_LINK', DEEP_LINK_DEFERRED = 'APP/DEEP_LINK_DEFERRED', @@ -34,6 +35,7 @@ export enum Actions { IN_APP_REVIEW_REQUESTED = 'APP/IN_APP_REVIEW_REQUESTED', NOTIFICATION_SPOTLIGHT_SEEN = 'APP/NOTIFICATION_SPOTLIGHT_SEEN', TOGGLE_HIDE_BALANCES = 'APP/TOGGLE_HIDE_BALANCES', + REGISTRATION_COMPLETED = 'APP/REGISTRATION_COMPLETED', } export interface SetAppState { @@ -41,11 +43,6 @@ export interface SetAppState { state: string } -interface SetLoggedIn { - type: Actions.SET_LOGGED_IN - loggedIn: boolean -} - interface SetSupportedBiometryType { type: Actions.SET_SUPPORTED_BIOMETRY_TYPE supportedBiometryType: BIOMETRY_TYPE | null @@ -157,9 +154,14 @@ interface ToggleHideBalances { type: Actions.TOGGLE_HIDE_BALANCES } +interface RegistrationCompleted { + type: Actions.REGISTRATION_COMPLETED + networkId: NetworkId + protocol: SupportedProtocol +} + export type ActionTypes = | SetAppState - | SetLoggedIn | SetSupportedBiometryType | OpenDeepLink | SetAnalyticsEnabled @@ -182,17 +184,13 @@ export type ActionTypes = | NotificationSpotlightSeen | ToggleHideBalances | DeepLinkDeferred + | RegistrationCompleted export const setAppState = (state: string): SetAppState => ({ type: Actions.SET_APP_STATE, state, }) -export const setLoggedIn = (loggedIn: boolean) => ({ - type: Actions.SET_LOGGED_IN, - loggedIn, -}) - export const setSupportedBiometryType = (supportedBiometryType: BIOMETRY_TYPE | null) => ({ type: Actions.SET_SUPPORTED_BIOMETRY_TYPE, supportedBiometryType, @@ -340,3 +338,14 @@ export const toggleHideBalances = (): ToggleHideBalances => { type: Actions.TOGGLE_HIDE_BALANCES, } } + +export const registrationCompleted = ( + networkId: NetworkId, + protocol: SupportedProtocol +): RegistrationCompleted => { + return { + type: Actions.REGISTRATION_COMPLETED, + networkId, + protocol, + } +} diff --git a/src/app/reducers.ts b/src/app/reducers.ts index 19f45bb37d4..7bb72f9220f 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -1,10 +1,12 @@ import { BIOMETRY_TYPE } from '@divvi/react-native-keychain' import { Platform } from 'react-native' import { Actions, ActionTypes, AppState } from 'src/app/actions' +import { SupportedProtocol } from 'src/app/types' import { DEEP_LINK_URL_SCHEME } from 'src/config' import { REMOTE_CONFIG_VALUES_DEFAULTS } from 'src/firebase/remoteConfigValuesDefaults' import { Screens } from 'src/navigator/Screens' import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' +import { NetworkId } from 'src/transactions/types' const PERSISTED_DEEP_LINKS = [ 'https://valoraapp.com/share', @@ -12,7 +14,6 @@ const PERSISTED_DEEP_LINKS = [ ] interface State { - loggedIn: boolean phoneNumberVerified: boolean analyticsEnabled: boolean requirePinOnAppOpen: boolean @@ -36,6 +37,9 @@ interface State { showNotificationSpotlight: boolean hideBalances: boolean pendingDeepLinks: PendingDeepLink[] + registrations: { + [protocol in SupportedProtocol]?: NetworkId[] + } } interface PendingDeepLink { @@ -44,7 +48,6 @@ interface PendingDeepLink { } const initialState = { - loggedIn: false, phoneNumberVerified: false, analyticsEnabled: true, requirePinOnAppOpen: false, @@ -66,6 +69,7 @@ const initialState = { showNotificationSpotlight: false, hideBalances: false, pendingDeepLinks: [], + registrations: {}, } function getPersistedDeepLinks(deepLinks: PendingDeepLink[]) { @@ -113,11 +117,6 @@ export const appReducer = ( appState, lastTimeBackgrounded, } - case Actions.SET_LOGGED_IN: - return { - ...state, - loggedIn: action.loggedIn, - } case Actions.SET_ANALYTICS_ENABLED: return { ...state, @@ -223,6 +222,14 @@ export const appReducer = ( (pendingDeepLink) => pendingDeepLink.url !== action.deepLink ), } + case Actions.REGISTRATION_COMPLETED: + return { + ...state, + registrations: { + ...state.registrations, + [action.protocol]: [...(state.registrations[action.protocol] ?? []), action.networkId], + }, + } default: return state } diff --git a/src/app/selectors.ts b/src/app/selectors.ts index 27761c06a9f..b5428ae4bfa 100644 --- a/src/app/selectors.ts +++ b/src/app/selectors.ts @@ -58,3 +58,5 @@ export const showNotificationSpotlightSelector = (state: RootState) => export const hideWalletBalancesSelector = (state: RootState) => state.app.hideBalances export const pendingDeepLinkSelector = (state: RootState) => state.app.pendingDeepLinks[0] ?? null + +export const getRegistrations = (state: RootState) => state.app.registrations diff --git a/src/app/types.ts b/src/app/types.ts new file mode 100644 index 00000000000..f7077498be5 --- /dev/null +++ b/src/app/types.ts @@ -0,0 +1,17 @@ +// TODO: import this from @divvy/mobile +export type SupportedProtocol = + | 'beefy' + | 'tether' + | 'somm' + | 'celo' + | 'aerodrome' + | 'velodrome' + | 'vana' + | 'curve' + | 'farcaster' + | 'mento' + | 'yearn' + | 'fonbnk' + | 'offchainlabs' + | 'euler' + | 'ubeswap' diff --git a/src/redux/migrations.ts b/src/redux/migrations.ts index 98b6ef09bb7..715c52fd0c5 100644 --- a/src/redux/migrations.ts +++ b/src/redux/migrations.ts @@ -2017,4 +2017,11 @@ export const migrations = { app: _.omit(state.app, 'sentryTracesSampleRate', 'sentryNetworkErrors'), i18n: _.omit(state.i18n, 'allowOtaTranslations'), }), + 244: (state: any) => ({ + ...state, + app: { + ..._.omit(state.app, 'loggedIn'), + registrations: {}, + }, + }), } diff --git a/src/redux/store.test.ts b/src/redux/store.test.ts index e0b0aae7b10..7d09d21fc73 100644 --- a/src/redux/store.test.ts +++ b/src/redux/store.test.ts @@ -143,7 +143,7 @@ describe('store state', () => { { "_persist": { "rehydrated": true, - "version": 243, + "version": 244, }, "account": { "acceptedTerms": false, @@ -189,11 +189,11 @@ describe('store state', () => { "inviterAddress": null, "lastTimeBackgrounded": 0, "locked": false, - "loggedIn": false, "pendingDeepLinks": [], "phoneNumberVerified": false, "pushNotificationRequestedUnixTime": 1692878055000, "pushNotificationsEnabled": false, + "registrations": {}, "requirePinOnAppOpen": false, "sessionId": "", "showNotificationSpotlight": true, diff --git a/src/redux/store.ts b/src/redux/store.ts index 251c11b1787..95ae021f404 100644 --- a/src/redux/store.ts +++ b/src/redux/store.ts @@ -26,7 +26,7 @@ const persistConfig: PersistConfig = { key: 'root', // default is -1, increment as we make migrations // See https://github.com/valora-inc/wallet/tree/main/WALLET.md#redux-state-migration - version: 243, + version: 244, keyPrefix: `reduxStore-`, // the redux-persist default is `persist:` which doesn't work with some file systems. storage: FSStorage(), blacklist: ['networkInfo', 'alert', 'imports', 'keylessBackup', transactionFeedV2Api.reducerPath], diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index 77fdf5ee60f..bf3aee49f3f 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -4723,9 +4723,6 @@ "locked": { "type": "boolean" }, - "loggedIn": { - "type": "boolean" - }, "pendingDeepLinks": { "items": { "$ref": "#/definitions/PendingDeepLink" @@ -4744,6 +4741,9 @@ "pushNotificationsEnabled": { "type": "boolean" }, + "registrations": { + "$ref": "#/definitions/{beefy?:NetworkId[]|undefined;tether?:NetworkId[]|undefined;somm?:NetworkId[]|undefined;celo?:NetworkId[]|undefined;aerodrome?:NetworkId[]|undefined;velodrome?:NetworkId[]|undefined;vana?:NetworkId[]|undefined;curve?:NetworkId[]|undefined;farcaster?:NetworkId[]|undefined;mento?:NetworkId[]|undefined;yearn?:NetworkId[]|undefined;fonbnk?:NetworkId[]|undefined;offchainlabs?:NetworkId[]|undefined;euler?:NetworkId[]|undefined;ubeswap?:NetworkId[]|undefined;}" + }, "requirePinOnAppOpen": { "type": "boolean" }, @@ -4784,11 +4784,11 @@ "inviterAddress", "lastTimeBackgrounded", "locked", - "loggedIn", "pendingDeepLinks", "phoneNumberVerified", "pushNotificationRequestedUnixTime", "pushNotificationsEnabled", + "registrations", "requirePinOnAppOpen", "sessionId", "showNotificationSpotlight", @@ -6783,6 +6783,102 @@ }, "type": "object" }, + "{beefy?:NetworkId[]|undefined;tether?:NetworkId[]|undefined;somm?:NetworkId[]|undefined;celo?:NetworkId[]|undefined;aerodrome?:NetworkId[]|undefined;velodrome?:NetworkId[]|undefined;vana?:NetworkId[]|undefined;curve?:NetworkId[]|undefined;farcaster?:NetworkId[]|undefined;mento?:NetworkId[]|undefined;yearn?:NetworkId[]|undefined;fonbnk?:NetworkId[]|undefined;offchainlabs?:NetworkId[]|undefined;euler?:NetworkId[]|undefined;ubeswap?:NetworkId[]|undefined;}": { + "additionalProperties": false, + "properties": { + "aerodrome": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "beefy": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "celo": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "curve": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "euler": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "farcaster": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "fonbnk": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "mento": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "offchainlabs": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "somm": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "tether": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "ubeswap": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "vana": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "velodrome": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + }, + "yearn": { + "items": { + "$ref": "#/definitions/NetworkId" + }, + "type": "array" + } + }, + "type": "object" + }, "{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;ifsc?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}": { "additionalProperties": false, "properties": { diff --git a/test/schemas.ts b/test/schemas.ts index 323cea2644a..19d68aa667b 100644 --- a/test/schemas.ts +++ b/test/schemas.ts @@ -3654,6 +3654,18 @@ export const v243Schema = { i18n: _.omit(v242Schema.i18n, 'allowOtaTranslations'), } +export const v244Schema = { + ...v243Schema, + _persist: { + ...v243Schema._persist, + version: 244, + }, + app: { + ..._.omit(v243Schema.app, 'loggedIn'), + registrations: {}, + }, +} + export function getLatestSchema(): Partial { - return v243Schema as Partial + return v244Schema as Partial } From 0c90d29c9696bb9cd3603ae3333c193ff28619cf Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Tue, 11 Feb 2025 16:01:26 +0100 Subject: [PATCH 02/27] fix: type --- src/app/actions.ts | 6 +- src/app/reducers.ts | 6 +- src/app/types.ts | 17 ----- src/divviProtocol/constants.ts | 24 +++++++ test/RootStateSchema.json | 112 ++++++++++++++------------------- 5 files changed, 77 insertions(+), 88 deletions(-) delete mode 100644 src/app/types.ts create mode 100644 src/divviProtocol/constants.ts diff --git a/src/app/actions.ts b/src/app/actions.ts index 17a979c6229..582b0d80ced 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,6 +1,6 @@ import { BIOMETRY_TYPE } from '@divvi/react-native-keychain' import { RemoteConfigValues } from 'src/app/saga' -import { SupportedProtocol } from 'src/app/types' +import { SupportedProtocolIds } from 'src/divviProtocol/constants' import { Screens } from 'src/navigator/Screens' import { NetworkId } from 'src/transactions/types' @@ -157,7 +157,7 @@ interface ToggleHideBalances { interface RegistrationCompleted { type: Actions.REGISTRATION_COMPLETED networkId: NetworkId - protocol: SupportedProtocol + protocol: SupportedProtocolIds } export type ActionTypes = @@ -341,7 +341,7 @@ export const toggleHideBalances = (): ToggleHideBalances => { export const registrationCompleted = ( networkId: NetworkId, - protocol: SupportedProtocol + protocol: SupportedProtocolIds ): RegistrationCompleted => { return { type: Actions.REGISTRATION_COMPLETED, diff --git a/src/app/reducers.ts b/src/app/reducers.ts index 7bb72f9220f..f8437913524 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -1,8 +1,8 @@ import { BIOMETRY_TYPE } from '@divvi/react-native-keychain' import { Platform } from 'react-native' import { Actions, ActionTypes, AppState } from 'src/app/actions' -import { SupportedProtocol } from 'src/app/types' import { DEEP_LINK_URL_SCHEME } from 'src/config' +import { SupportedProtocolIds } from 'src/divviProtocol/constants' import { REMOTE_CONFIG_VALUES_DEFAULTS } from 'src/firebase/remoteConfigValuesDefaults' import { Screens } from 'src/navigator/Screens' import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' @@ -38,7 +38,7 @@ interface State { hideBalances: boolean pendingDeepLinks: PendingDeepLink[] registrations: { - [protocol in SupportedProtocol]?: NetworkId[] + [networkId in NetworkId]?: SupportedProtocolIds[] } } @@ -227,7 +227,7 @@ export const appReducer = ( ...state, registrations: { ...state.registrations, - [action.protocol]: [...(state.registrations[action.protocol] ?? []), action.networkId], + [action.networkId]: [...(state.registrations[action.networkId] ?? []), action.protocol], }, } default: diff --git a/src/app/types.ts b/src/app/types.ts deleted file mode 100644 index f7077498be5..00000000000 --- a/src/app/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -// TODO: import this from @divvy/mobile -export type SupportedProtocol = - | 'beefy' - | 'tether' - | 'somm' - | 'celo' - | 'aerodrome' - | 'velodrome' - | 'vana' - | 'curve' - | 'farcaster' - | 'mento' - | 'yearn' - | 'fonbnk' - | 'offchainlabs' - | 'euler' - | 'ubeswap' diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts new file mode 100644 index 00000000000..f0c9ac356ec --- /dev/null +++ b/src/divviProtocol/constants.ts @@ -0,0 +1,24 @@ +import { Address, keccak256, stringToHex } from 'viem' + +export const REGISTRY_CONTRACT_ADDRESS: Address = '0x5a1a1027ac1d828e7415af7d797fba2b0cdd5575' + +export const supportedProtocolIdHashes: Record = { + [keccak256(stringToHex('beefy'))]: 'beefy', + [keccak256(stringToHex('tether'))]: 'tether', + [keccak256(stringToHex('somm'))]: 'somm', + [keccak256(stringToHex('celo'))]: 'celo', + [keccak256(stringToHex('aerodrome'))]: 'aerodrome', + [keccak256(stringToHex('velodrome'))]: 'velodrome', + [keccak256(stringToHex('vana'))]: 'vana', + [keccak256(stringToHex('curve'))]: 'curve', + [keccak256(stringToHex('farcaster'))]: 'farcaster', + [keccak256(stringToHex('mento'))]: 'mento', + [keccak256(stringToHex('yearn'))]: 'yearn', + [keccak256(stringToHex('fonbnk'))]: 'fonbnk', + [keccak256(stringToHex('offchainlabs'))]: 'offchainlabs', + [keccak256(stringToHex('euler'))]: 'euler', + [keccak256(stringToHex('ubeswap'))]: 'ubeswap', +} as const + +export type SupportedProtocolIds = + (typeof supportedProtocolIdHashes)[keyof typeof supportedProtocolIdHashes] diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index bf3aee49f3f..594c9a272fe 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -4742,7 +4742,7 @@ "type": "boolean" }, "registrations": { - "$ref": "#/definitions/{beefy?:NetworkId[]|undefined;tether?:NetworkId[]|undefined;somm?:NetworkId[]|undefined;celo?:NetworkId[]|undefined;aerodrome?:NetworkId[]|undefined;velodrome?:NetworkId[]|undefined;vana?:NetworkId[]|undefined;curve?:NetworkId[]|undefined;farcaster?:NetworkId[]|undefined;mento?:NetworkId[]|undefined;yearn?:NetworkId[]|undefined;fonbnk?:NetworkId[]|undefined;offchainlabs?:NetworkId[]|undefined;euler?:NetworkId[]|undefined;ubeswap?:NetworkId[]|undefined;}" + "$ref": "#/definitions/{\"celo-mainnet\"?:string[]|undefined;\"celo-alfajores\"?:string[]|undefined;\"ethereum-mainnet\"?:string[]|undefined;\"ethereum-sepolia\"?:string[]|undefined;\"arbitrum-one\"?:string[]|undefined;\"arbitrum-sepolia\"?:string[]|undefined;\"op-mainnet\"?:string[]|undefined;\"op-sepolia\"?:string[]|undefined;\"polygon-pos-mainnet\"?:string[]|undefined;\"polygon-pos-amoy\"?:string[]|undefined;\"base-mainnet\"?:string[]|undefined;\"base-sepolia\"?:string[]|undefined;}" }, "requirePinOnAppOpen": { "type": "boolean" @@ -6759,122 +6759,104 @@ ], "type": "object" }, - "{AccountNumber?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;MobileMoney?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;operator?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;DuniaWallet?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;IBANNumber?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;iban?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;IFSCAccount?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;ifsc?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;PIXAccount?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;keyType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;key?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;}": { - "additionalProperties": false, - "properties": { - "AccountNumber": { - "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" - }, - "DuniaWallet": { - "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" - }, - "IBANNumber": { - "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;iban?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" - }, - "IFSCAccount": { - "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;ifsc?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" - }, - "MobileMoney": { - "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;operator?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" - }, - "PIXAccount": { - "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;keyType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;key?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" - } - }, - "type": "object" - }, - "{beefy?:NetworkId[]|undefined;tether?:NetworkId[]|undefined;somm?:NetworkId[]|undefined;celo?:NetworkId[]|undefined;aerodrome?:NetworkId[]|undefined;velodrome?:NetworkId[]|undefined;vana?:NetworkId[]|undefined;curve?:NetworkId[]|undefined;farcaster?:NetworkId[]|undefined;mento?:NetworkId[]|undefined;yearn?:NetworkId[]|undefined;fonbnk?:NetworkId[]|undefined;offchainlabs?:NetworkId[]|undefined;euler?:NetworkId[]|undefined;ubeswap?:NetworkId[]|undefined;}": { + "{\"celo-mainnet\"?:string[]|undefined;\"celo-alfajores\"?:string[]|undefined;\"ethereum-mainnet\"?:string[]|undefined;\"ethereum-sepolia\"?:string[]|undefined;\"arbitrum-one\"?:string[]|undefined;\"arbitrum-sepolia\"?:string[]|undefined;\"op-mainnet\"?:string[]|undefined;\"op-sepolia\"?:string[]|undefined;\"polygon-pos-mainnet\"?:string[]|undefined;\"polygon-pos-amoy\"?:string[]|undefined;\"base-mainnet\"?:string[]|undefined;\"base-sepolia\"?:string[]|undefined;}": { "additionalProperties": false, "properties": { - "aerodrome": { + "arbitrum-one": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "beefy": { + "arbitrum-sepolia": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "celo": { + "base-mainnet": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "curve": { + "base-sepolia": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "euler": { + "celo-alfajores": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "farcaster": { + "celo-mainnet": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "fonbnk": { + "ethereum-mainnet": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "mento": { + "ethereum-sepolia": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "offchainlabs": { + "op-mainnet": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "somm": { + "op-sepolia": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "tether": { + "polygon-pos-amoy": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" }, - "ubeswap": { + "polygon-pos-mainnet": { "items": { - "$ref": "#/definitions/NetworkId" + "type": "string" }, "type": "array" + } + }, + "type": "object" + }, + "{AccountNumber?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;MobileMoney?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;operator?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;DuniaWallet?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;IBANNumber?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;iban?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;IFSCAccount?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;ifsc?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;PIXAccount?:{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;keyType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;key?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}|undefined;}": { + "additionalProperties": false, + "properties": { + "AccountNumber": { + "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" }, - "vana": { - "items": { - "$ref": "#/definitions/NetworkId" - }, - "type": "array" + "DuniaWallet": { + "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" }, - "velodrome": { - "items": { - "$ref": "#/definitions/NetworkId" - }, - "type": "array" + "IBANNumber": { + "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;iban?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" }, - "yearn": { - "items": { - "$ref": "#/definitions/NetworkId" - }, - "type": "array" + "IFSCAccount": { + "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountNumber?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;ifsc?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" + }, + "MobileMoney": { + "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;country?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;mobile?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;operator?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" + }, + "PIXAccount": { + "$ref": "#/definitions/{institutionName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;accountName?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;fiatAccountType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;keyType?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;key?:{regex:string;errorString:string;errorParams?:Record|undefined;}|undefined;}" } }, "type": "object" From dcbf216233800983444baa645e66b727657c1873 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Tue, 11 Feb 2025 16:29:36 +0100 Subject: [PATCH 03/27] fix: knip --- src/app/selectors.ts | 2 -- src/divviProtocol/constants.ts | 6 ++---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/app/selectors.ts b/src/app/selectors.ts index b5428ae4bfa..27761c06a9f 100644 --- a/src/app/selectors.ts +++ b/src/app/selectors.ts @@ -58,5 +58,3 @@ export const showNotificationSpotlightSelector = (state: RootState) => export const hideWalletBalancesSelector = (state: RootState) => state.app.hideBalances export const pendingDeepLinkSelector = (state: RootState) => state.app.pendingDeepLinks[0] ?? null - -export const getRegistrations = (state: RootState) => state.app.registrations diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts index f0c9ac356ec..2b6820fbb92 100644 --- a/src/divviProtocol/constants.ts +++ b/src/divviProtocol/constants.ts @@ -1,8 +1,6 @@ -import { Address, keccak256, stringToHex } from 'viem' +import { keccak256, stringToHex } from 'viem' -export const REGISTRY_CONTRACT_ADDRESS: Address = '0x5a1a1027ac1d828e7415af7d797fba2b0cdd5575' - -export const supportedProtocolIdHashes: Record = { +const supportedProtocolIdHashes: Record = { [keccak256(stringToHex('beefy'))]: 'beefy', [keccak256(stringToHex('tether'))]: 'tether', [keccak256(stringToHex('somm'))]: 'somm', From 58c18869c60670c718243d4b010c776c64a945cf Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 09:52:42 +0100 Subject: [PATCH 04/27] fix: ensure transaction sagas can handle registration transactions --- src/divviProtocol/abi/Registry.ts | 251 ++++++++++++++++++++++++++ src/divviProtocol/constants.ts | 4 +- src/divviProtocol/registerReferral.ts | 16 ++ src/earn/saga.ts | 9 +- src/jumpstart/saga.ts | 5 +- src/swap/saga.ts | 5 +- 6 files changed, 285 insertions(+), 5 deletions(-) create mode 100644 src/divviProtocol/abi/Registry.ts create mode 100644 src/divviProtocol/registerReferral.ts diff --git a/src/divviProtocol/abi/Registry.ts b/src/divviProtocol/abi/Registry.ts new file mode 100644 index 00000000000..b5fe3c3a7c3 --- /dev/null +++ b/src/divviProtocol/abi/Registry.ts @@ -0,0 +1,251 @@ +export const registryContractAbi = [ + { + inputs: [ + { + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + ], + name: 'ReferrerNotRegistered', + type: 'error', + }, + { + inputs: [ + { + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + { + internalType: 'address', + name: 'userAddress', + type: 'address', + }, + ], + name: 'UserAlreadyRegistered', + type: 'error', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + { + indexed: true, + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + { + indexed: true, + internalType: 'address', + name: 'userAddress', + type: 'address', + }, + ], + name: 'ReferralRegistered', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + { + indexed: true, + internalType: 'string[]', + name: 'protocolIds', + type: 'string[]', + }, + { + indexed: false, + internalType: 'uint256[]', + name: 'rewardRates', + type: 'uint256[]', + }, + { + indexed: false, + internalType: 'address', + name: 'rewardAddress', + type: 'address', + }, + ], + name: 'ReferrerRegistered', + type: 'event', + }, + { + inputs: [ + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + ], + name: 'getProtocols', + outputs: [ + { + internalType: 'string[]', + name: '', + type: 'string[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + ], + name: 'getReferrers', + outputs: [ + { + internalType: 'string[]', + name: '', + type: 'string[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + ], + name: 'getRewardAddress', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + ], + name: 'getRewardRate', + outputs: [ + { + internalType: 'uint256', + name: '', + type: 'uint256', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + ], + name: 'getUsers', + outputs: [ + { + internalType: 'address[]', + name: '', + type: 'address[]', + }, + { + internalType: 'uint256[]', + name: '', + type: 'uint256[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: 'referrerId', + type: 'string', + }, + { + internalType: 'string', + name: 'protocolId', + type: 'string', + }, + ], + name: 'registerReferral', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'string', + name: '_referrerId', + type: 'string', + }, + { + internalType: 'string[]', + name: '_protocolIds', + type: 'string[]', + }, + { + internalType: 'uint256[]', + name: '_rewardRates', + type: 'uint256[]', + }, + { + internalType: 'address', + name: '_rewardAddress', + type: 'address', + }, + ], + name: 'registerReferrer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, +] as const diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts index 2b6820fbb92..e0c09653abb 100644 --- a/src/divviProtocol/constants.ts +++ b/src/divviProtocol/constants.ts @@ -1,4 +1,6 @@ -import { keccak256, stringToHex } from 'viem' +import { Address, keccak256, stringToHex } from 'viem' + +export const REGISTRY_CONTRACT_ADDRESS: Address = '0x5a1a1027ac1d828e7415af7d797fba2b0cdd5575' const supportedProtocolIdHashes: Record = { [keccak256(stringToHex('beefy'))]: 'beefy', diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts new file mode 100644 index 00000000000..29270664f67 --- /dev/null +++ b/src/divviProtocol/registerReferral.ts @@ -0,0 +1,16 @@ +import { registryContractAbi } from 'src/divviProtocol/abi/Registry' +import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' +import { SerializableTransactionRequest } from 'src/viem/preparedTransactionSerialization' +import { TransactionRequest } from 'src/viem/prepareTransactions' +import { decodeFunctionData } from 'viem' + +export function isRegistrationTransaction(tx: TransactionRequest | SerializableTransactionRequest) { + return ( + tx.to === REGISTRY_CONTRACT_ADDRESS && + tx.data && + decodeFunctionData({ + abi: registryContractAbi, + data: tx.data, + }).functionName === 'registerReferral' + ) +} diff --git a/src/earn/saga.ts b/src/earn/saga.ts index 1c9c45c2355..31968cb89d1 100644 --- a/src/earn/saga.ts +++ b/src/earn/saga.ts @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js' import AppAnalytics from 'src/analytics/AppAnalytics' import { EarnEvents } from 'src/analytics/Events' import { EarnDepositTxsReceiptProperties } from 'src/analytics/Properties' +import { isRegistrationTransaction } from 'src/divviProtocol/registerReferral' import { depositCancel, depositError, @@ -80,7 +81,9 @@ export function* depositSubmitSaga(action: PayloadAction) { } = action.payload const depositTokenId = pool.dataProps.depositTokenId - const preparedTransactions = getPreparedTransactions(serializablePreparedTransactions) + const preparedTransactions = getPreparedTransactions( + serializablePreparedTransactions.filter((tx) => !isRegistrationTransaction(tx)) + ) const depositTokenInfo = yield* call(getTokenInfo, depositTokenId) const fromTokenInfo = yield* call(getTokenInfo, fromTokenId) @@ -312,7 +315,9 @@ export function* withdrawSubmitSaga(action: PayloadAction) { mode, } = action.payload const tokenId = pool.dataProps.depositTokenId - const preparedTransactions = getPreparedTransactions(serializablePreparedTransactions) + const preparedTransactions = getPreparedTransactions( + serializablePreparedTransactions.filter((tx) => !isRegistrationTransaction(tx)) + ) const tokenInfo = yield* call(getTokenInfo, tokenId) if (!tokenInfo) { diff --git a/src/jumpstart/saga.ts b/src/jumpstart/saga.ts index ede7b233e69..6b07203aff7 100644 --- a/src/jumpstart/saga.ts +++ b/src/jumpstart/saga.ts @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js' import walletJumpstart from 'src/abis/IWalletJumpstart' import AppAnalytics from 'src/analytics/AppAnalytics' import { JumpstartEvents } from 'src/analytics/Events' +import { isRegistrationTransaction } from 'src/divviProtocol/registerReferral' import { jumpstartLinkHandler } from 'src/jumpstart/jumpstartLinkHandler' import { JumpstarReclaimAction, @@ -242,7 +243,9 @@ export function* sendJumpstartTransactions( } const createStandbyTxHandlers = [] - const preparedTransactions = getPreparedTransactions(serializablePreparedTransactions) + const preparedTransactions = getPreparedTransactions( + serializablePreparedTransactions.filter((tx) => !isRegistrationTransaction(tx)) + ) // in this flow, there should only be 1 or 2 transactions. if there are 2 // transactions, the first one should be an approval. diff --git a/src/swap/saga.ts b/src/swap/saga.ts index 0ae1d1bfae5..b076d2fca2d 100644 --- a/src/swap/saga.ts +++ b/src/swap/saga.ts @@ -3,6 +3,7 @@ import BigNumber from 'bignumber.js' import AppAnalytics from 'src/analytics/AppAnalytics' import { SwapEvents } from 'src/analytics/Events' import { SwapTimeMetrics, SwapTxsReceiptProperties } from 'src/analytics/Properties' +import { isRegistrationTransaction } from 'src/divviProtocol/registerReferral' import { navigateHome } from 'src/navigator/NavigationService' import { CANCELLED_PIN_INPUT } from 'src/pincode/authentication' import { vibrateError } from 'src/styles/hapticFeedback' @@ -87,7 +88,9 @@ export function* swapSubmitSaga(action: PayloadAction) { } = quote const amountType = updatedField === Field.TO ? ('buyAmount' as const) : ('sellAmount' as const) const amount = swapAmount[updatedField] - const preparedTransactions = getPreparedTransactions(serializablePreparedTransactions) + const preparedTransactions = getPreparedTransactions( + serializablePreparedTransactions.filter((tx) => !isRegistrationTransaction(tx)) + ) const tokensById = yield* select((state) => tokensByIdSelector(state, getSupportedNetworkIds())) const fromToken = tokensById[fromTokenId] From cbbc09a7b9c220f6fad57ce7d8c7138dfd7c4a81 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:16:47 +0100 Subject: [PATCH 05/27] feat: integrate with divvy protocol --- src/app/selectors.ts | 2 + src/config.ts | 4 + src/divviProtocol/constants.ts | 2 +- src/divviProtocol/registerReferral.ts | 153 +++++++++++++++++++++++++- src/viem/index.ts | 21 ++++ src/viem/prepareTransactions.ts | 8 ++ src/viem/saga.ts | 31 +++++- 7 files changed, 215 insertions(+), 6 deletions(-) diff --git a/src/app/selectors.ts b/src/app/selectors.ts index 27761c06a9f..de05f618e23 100644 --- a/src/app/selectors.ts +++ b/src/app/selectors.ts @@ -58,3 +58,5 @@ export const showNotificationSpotlightSelector = (state: RootState) => export const hideWalletBalancesSelector = (state: RootState) => state.app.hideBalances export const pendingDeepLinkSelector = (state: RootState) => state.app.pendingDeepLinks[0] ?? null + +export const registrationsSelector = (state: RootState) => state.app.registrations diff --git a/src/config.ts b/src/config.ts index 37d442f1948..73b6d72bb8f 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,6 +6,7 @@ import { LoggerLevel } from 'src/utils/LoggerLevels' // eslint-disable-next-line import/no-relative-packages import { TORUS_SAPPHIRE_NETWORK } from '@toruslabs/constants' import { LaunchArguments } from 'react-native-launch-arguments' +import { SupportedProtocolIds } from 'src/divviProtocol/constants' import { HomeActionName } from 'src/home/types' import { ToggleableOnboardingFeatures } from 'src/onboarding/types' import { stringToBoolean } from 'src/utils/parsing' @@ -226,3 +227,6 @@ export const ENABLED_QUICK_ACTIONS = ( export const FETCH_FIATCONNECT_QUOTES = true export const WALLETCONNECT_UNIVERSAL_LINK = 'https://valoraapp.com/wc' + +export const DIVVI_PROTOCOL_IDS: SupportedProtocolIds[] = [] +export const DIVVI_REFERRER_ID: string | undefined = undefined diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts index e0c09653abb..f0c9ac356ec 100644 --- a/src/divviProtocol/constants.ts +++ b/src/divviProtocol/constants.ts @@ -2,7 +2,7 @@ import { Address, keccak256, stringToHex } from 'viem' export const REGISTRY_CONTRACT_ADDRESS: Address = '0x5a1a1027ac1d828e7415af7d797fba2b0cdd5575' -const supportedProtocolIdHashes: Record = { +export const supportedProtocolIdHashes: Record = { [keccak256(stringToHex('beefy'))]: 'beefy', [keccak256(stringToHex('tether'))]: 'tether', [keccak256(stringToHex('somm'))]: 'somm', diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 29270664f67..d9f320584e7 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -1,8 +1,21 @@ +import { registrationCompleted } from 'src/app/actions' +import { registrationsSelector } from 'src/app/selectors' +import { DIVVI_PROTOCOL_IDS, DIVVI_REFERRER_ID } from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' -import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' +import { REGISTRY_CONTRACT_ADDRESS, supportedProtocolIdHashes } from 'src/divviProtocol/constants' +import { store } from 'src/redux/store' +import { Network, NetworkId } from 'src/transactions/types' +import Logger from 'src/utils/Logger' +import { publicClient } from 'src/viem' +import { ViemWallet } from 'src/viem/getLockableWallet' import { SerializableTransactionRequest } from 'src/viem/preparedTransactionSerialization' import { TransactionRequest } from 'src/viem/prepareTransactions' -import { decodeFunctionData } from 'viem' +import networkConfig, { networkIdToNetwork } from 'src/web3/networkConfig' +import { walletAddressSelector } from 'src/web3/selectors' +import { call, put } from 'typed-redux-saga' +import { Address, decodeFunctionData, encodeFunctionData, parseEventLogs } from 'viem' + +const TAG = 'divviProtocol/registerReferral' export function isRegistrationTransaction(tx: TransactionRequest | SerializableTransactionRequest) { return ( @@ -14,3 +27,139 @@ export function isRegistrationTransaction(tx: TransactionRequest | SerializableT }).functionName === 'registerReferral' ) } + +export async function createRegistrationTransactions({ + networkId, +}: { + networkId: NetworkId +}): Promise { + const referrerId = DIVVI_REFERRER_ID + if (!referrerId) { + Logger.debug( + `${TAG}/createRegistrationTransactions`, + 'No referrer id set. Skipping registration transactions.' + ) + return [] + } + + // Caching registration status in Redux reduces on-chain checks but doesn’t guarantee + // it wasn’t completed in a previous install or session. + const completedRegistrations = new Set(registrationsSelector(store.getState())[networkId] ?? []) + const pendingRegistrations = DIVVI_PROTOCOL_IDS.filter( + (protocol) => !completedRegistrations.has(protocol) + ) + if (pendingRegistrations.length === 0) { + return [] + } + + // Ensure the referrer and protocol are registered before creating + // transactions to prevent gas estimation failures later. + const client = publicClient[networkIdToNetwork[networkId]] + const protocolsEligibleForRegistration: string[] = [] + const walletAddress = walletAddressSelector(store.getState()) as Address + await Promise.all( + pendingRegistrations.map(async (protocolId) => { + try { + const [registeredReferrers, registeredUsers] = await Promise.all([ + client.readContract({ + address: REGISTRY_CONTRACT_ADDRESS, + abi: registryContractAbi, + functionName: 'getReferrers', + args: [protocolId], + }), + // TODO: getUsers is not the correct call to get the registration + // status of the user for any given protocol, we need to modify this + // part once the correct call is available + client.readContract({ + address: REGISTRY_CONTRACT_ADDRESS, + abi: registryContractAbi, + functionName: 'getUsers', + args: [protocolId, referrerId], + }), + ]) + + if (!registeredReferrers.includes(referrerId)) { + Logger.error( + `${TAG}/createRegistrationTransactions`, + `Referrer "${referrerId}" is not registered for protocol "${protocolId}". Skipping registration transaction.` + ) + return + } + + if (registeredUsers[0].includes(walletAddress)) { + Logger.debug( + `${TAG}/createRegistrationTransactions`, + `Referral is already registered for protocol "${protocolId}". Skipping registration transaction.` + ) + store.dispatch(registrationCompleted(networkId, protocolId)) + return + } + + protocolsEligibleForRegistration.push(protocolId) + } catch (error) { + Logger.error( + `${TAG}/createRegistrationTransactions`, + `Error reading registered referrers for protocol "${protocolId}". Skipping registration transaction.`, + error + ) + } + }) + ) + + return protocolsEligibleForRegistration.map((protocolId) => ({ + to: REGISTRY_CONTRACT_ADDRESS, + data: encodeFunctionData({ + abi: registryContractAbi, + functionName: 'registerReferral', + args: [referrerId, protocolId], + }), + })) +} + +export function* sendPreparedRegistrationTransactions( + txs: TransactionRequest[], + network: Network, + wallet: ViemWallet, + nonce: number +) { + for (let i = 0; i < txs.length; i++) { + const signedTx = yield* call([wallet, 'signTransaction'], { + ...txs[i], + nonce: nonce++, + } as any) + const hash = yield* call([wallet, 'sendRawTransaction'], { + serializedTransaction: signedTx, + }) + + Logger.debug( + `${TAG}/sendPreparedRegistrationTransactions`, + 'Successfully sent transaction to the network', + hash + ) + + const receipt = yield* call([publicClient[network], 'waitForTransactionReceipt'], { + hash, + }) + + if (receipt.status === 'success') { + const parsedLogs = parseEventLogs({ + abi: registryContractAbi, + eventName: ['ReferralRegistered'], + logs: receipt.logs, + }) + + const protocolId = supportedProtocolIdHashes[parsedLogs[0].args.protocolId] + if (!protocolId) { + // this should never happen, since we specify the protocolId in the prepareTransactions step + Logger.error( + `${TAG}/sendPreparedRegistrationTransactions`, + `Unknown protocolId received from transaction ${hash}` + ) + throw new Error(`Unknown protocolId received from transaction ${hash}`) + } + yield* put(registrationCompleted(networkConfig.networkToNetworkId[network], protocolId)) + } + } + + return nonce +} diff --git a/src/viem/index.ts b/src/viem/index.ts index df6602e5719..e16792401ad 100644 --- a/src/viem/index.ts +++ b/src/viem/index.ts @@ -54,28 +54,48 @@ export const appViemTransports = { [Network.Arbitrum]: http(networkConfig.internalRpcUrl.arbitrum), } satisfies Record<(typeof INTERNAL_RPC_SUPPORTED_NETWORKS)[number], Transport> +const defaultPublicClientParams = { + // This enables call batching via multicall + // meaning client.call, client.readContract, etc. will batch calls (using multicall) + // when the promises are scheduled in the same event loop tick (or within `wait` ms) + // for instance when Promise.all is used + // Note: directly calling multiple client.multicall won't batch, they are sent separately + // See https://viem.sh/docs/clients/public.html#eth_call-aggregation-via-multicall + batch: { + multicall: { + wait: 0, + }, + }, +} + export const publicClient = { [Network.Celo]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain.celo, transport: viemTransports[Network.Celo], }), [Network.Ethereum]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain.ethereum, transport: viemTransports[Network.Ethereum], }), [Network.Arbitrum]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain.arbitrum, transport: viemTransports[Network.Arbitrum], }), [Network.Optimism]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain.optimism, transport: viemTransports[Network.Optimism], }), [Network.PolygonPoS]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain['polygon-pos'], transport: viemTransports[Network.PolygonPoS], }), [Network.Base]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain.base, transport: viemTransports[Network.Base], }), @@ -83,6 +103,7 @@ export const publicClient = { export const appPublicClient = { [Network.Arbitrum]: createPublicClient({ + ...defaultPublicClientParams, chain: networkConfig.viemChain.arbitrum, transport: appViemTransports[Network.Arbitrum], }), diff --git a/src/viem/prepareTransactions.ts b/src/viem/prepareTransactions.ts index 889d7ff9651..5ccdc11bf6c 100644 --- a/src/viem/prepareTransactions.ts +++ b/src/viem/prepareTransactions.ts @@ -3,6 +3,7 @@ import AppAnalytics from 'src/analytics/AppAnalytics' import { TransactionEvents } from 'src/analytics/Events' import { TransactionOrigin } from 'src/analytics/types' import { STATIC_GAS_PADDING } from 'src/config' +import { createRegistrationTransactions } from 'src/divviProtocol/registerReferral' import { NativeTokenBalance, TokenBalance, @@ -291,6 +292,13 @@ export async function prepareTransactions({ isGasSubsidized?: boolean origin: TransactionOrigin }): Promise { + const registrationTxs = await createRegistrationTransactions({ + networkId: feeCurrencies[0].networkId, + }) + if (registrationTxs.length > 0) { + baseTransactions.push(...registrationTxs) + } + if (!spendToken && spendTokenAmount.isGreaterThan(0)) { throw new Error( `prepareTransactions requires a spendToken if spendTokenAmount is greater than 0. spendTokenAmount: ${spendTokenAmount.toString()}` diff --git a/src/viem/saga.ts b/src/viem/saga.ts index 39b355938c6..c0455c6bdfe 100644 --- a/src/viem/saga.ts +++ b/src/viem/saga.ts @@ -1,3 +1,7 @@ +import { + isRegistrationTransaction, + sendPreparedRegistrationTransactions, +} from 'src/divviProtocol/registerReferral' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { CANCELLED_PIN_INPUT } from 'src/pincode/authentication' @@ -5,7 +9,7 @@ import { tokensByIdSelector } from 'src/tokens/selectors' import { BaseStandbyTransaction, addStandbyTransaction } from 'src/transactions/slice' import { NetworkId } from 'src/transactions/types' import Logger from 'src/utils/Logger' -import { getFeeCurrencyToken } from 'src/viem/prepareTransactions' +import { TransactionRequest, getFeeCurrencyToken } from 'src/viem/prepareTransactions' import { SerializableTransactionRequest, getPreparedTransactions, @@ -51,7 +55,17 @@ export function* sendPreparedTransactions( throw CANCELLED_PIN_INPUT } - if (serializablePreparedTransactions.length !== createBaseStandbyTransactions.length) { + const preparedTransactions: TransactionRequest[] = [] + const preparedRegistrationTransactions: TransactionRequest[] = [] + getPreparedTransactions(serializablePreparedTransactions).forEach((tx) => { + if (isRegistrationTransaction(tx)) { + preparedRegistrationTransactions.push(tx) + } else { + preparedTransactions.push(tx) + } + }) + + if (preparedTransactions.length !== createBaseStandbyTransactions.length) { throw new Error('Mismatch in number of prepared transactions and standby transaction creators') } @@ -75,7 +89,18 @@ export function* sendPreparedTransactions( blockTag: 'pending', }) - const preparedTransactions = getPreparedTransactions(serializablePreparedTransactions) + // if there are registration transactions, send them first so that the + // subsequent transactions can have the referral attribution, and update the nonce + if (preparedRegistrationTransactions.length > 0) { + nonce = yield* call( + sendPreparedRegistrationTransactions, + preparedRegistrationTransactions, + network, + wallet, + nonce + ) + } + const txHashes: Hash[] = [] for (let i = 0; i < preparedTransactions.length; i++) { const preparedTransaction = preparedTransactions[i] From d3984dd9c95d9bd7cd54b57f1e516f5526ec2e91 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:27:58 +0100 Subject: [PATCH 06/27] chore: rename registrations variable --- src/app/actions.ts | 14 +++++++------- src/app/reducers.ts | 15 +++++++++------ src/redux/migrations.ts | 2 +- src/redux/store.test.ts | 2 +- test/RootStateSchema.json | 8 ++++---- test/schemas.ts | 2 +- 6 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/app/actions.ts b/src/app/actions.ts index 582b0d80ced..349dd3ad45b 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -35,7 +35,7 @@ export enum Actions { IN_APP_REVIEW_REQUESTED = 'APP/IN_APP_REVIEW_REQUESTED', NOTIFICATION_SPOTLIGHT_SEEN = 'APP/NOTIFICATION_SPOTLIGHT_SEEN', TOGGLE_HIDE_BALANCES = 'APP/TOGGLE_HIDE_BALANCES', - REGISTRATION_COMPLETED = 'APP/REGISTRATION_COMPLETED', + DIVVI_REGISTRATION_COMPLETED = 'APP/DIVVI_REGISTRATION_COMPLETED', } export interface SetAppState { @@ -154,8 +154,8 @@ interface ToggleHideBalances { type: Actions.TOGGLE_HIDE_BALANCES } -interface RegistrationCompleted { - type: Actions.REGISTRATION_COMPLETED +interface DivviRegistrationCompleted { + type: Actions.DIVVI_REGISTRATION_COMPLETED networkId: NetworkId protocol: SupportedProtocolIds } @@ -184,7 +184,7 @@ export type ActionTypes = | NotificationSpotlightSeen | ToggleHideBalances | DeepLinkDeferred - | RegistrationCompleted + | DivviRegistrationCompleted export const setAppState = (state: string): SetAppState => ({ type: Actions.SET_APP_STATE, @@ -339,12 +339,12 @@ export const toggleHideBalances = (): ToggleHideBalances => { } } -export const registrationCompleted = ( +export const divviRegistrationCompleted = ( networkId: NetworkId, protocol: SupportedProtocolIds -): RegistrationCompleted => { +): DivviRegistrationCompleted => { return { - type: Actions.REGISTRATION_COMPLETED, + type: Actions.DIVVI_REGISTRATION_COMPLETED, networkId, protocol, } diff --git a/src/app/reducers.ts b/src/app/reducers.ts index f8437913524..00779ca1666 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -37,7 +37,7 @@ interface State { showNotificationSpotlight: boolean hideBalances: boolean pendingDeepLinks: PendingDeepLink[] - registrations: { + divviRegistrations: { [networkId in NetworkId]?: SupportedProtocolIds[] } } @@ -69,7 +69,7 @@ const initialState = { showNotificationSpotlight: false, hideBalances: false, pendingDeepLinks: [], - registrations: {}, + divviRegistrations: {}, } function getPersistedDeepLinks(deepLinks: PendingDeepLink[]) { @@ -222,12 +222,15 @@ export const appReducer = ( (pendingDeepLink) => pendingDeepLink.url !== action.deepLink ), } - case Actions.REGISTRATION_COMPLETED: + case Actions.DIVVI_REGISTRATION_COMPLETED: return { ...state, - registrations: { - ...state.registrations, - [action.networkId]: [...(state.registrations[action.networkId] ?? []), action.protocol], + divviRegistrations: { + ...state.divviRegistrations, + [action.networkId]: [ + ...(state.divviRegistrations[action.networkId] ?? []), + action.protocol, + ], }, } default: diff --git a/src/redux/migrations.ts b/src/redux/migrations.ts index 715c52fd0c5..d5c4112a017 100644 --- a/src/redux/migrations.ts +++ b/src/redux/migrations.ts @@ -2021,7 +2021,7 @@ export const migrations = { ...state, app: { ..._.omit(state.app, 'loggedIn'), - registrations: {}, + divviRegistrations: {}, }, }), } diff --git a/src/redux/store.test.ts b/src/redux/store.test.ts index 7d09d21fc73..8f30ad01fc7 100644 --- a/src/redux/store.test.ts +++ b/src/redux/store.test.ts @@ -179,6 +179,7 @@ describe('store state', () => { "activeScreen": "Main", "analyticsEnabled": true, "appState": "Active", + "divviRegistrations": {}, "fiatConnectCashInEnabled": false, "fiatConnectCashOutEnabled": false, "googleMobileServicesAvailable": undefined, @@ -193,7 +194,6 @@ describe('store state', () => { "phoneNumberVerified": false, "pushNotificationRequestedUnixTime": 1692878055000, "pushNotificationsEnabled": false, - "registrations": {}, "requirePinOnAppOpen": false, "sessionId": "", "showNotificationSpotlight": true, diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index 594c9a272fe..039e68372be 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -4687,6 +4687,9 @@ "appState": { "$ref": "#/definitions/AppState" }, + "divviRegistrations": { + "$ref": "#/definitions/{\"celo-mainnet\"?:string[]|undefined;\"celo-alfajores\"?:string[]|undefined;\"ethereum-mainnet\"?:string[]|undefined;\"ethereum-sepolia\"?:string[]|undefined;\"arbitrum-one\"?:string[]|undefined;\"arbitrum-sepolia\"?:string[]|undefined;\"op-mainnet\"?:string[]|undefined;\"op-sepolia\"?:string[]|undefined;\"polygon-pos-mainnet\"?:string[]|undefined;\"polygon-pos-amoy\"?:string[]|undefined;\"base-mainnet\"?:string[]|undefined;\"base-sepolia\"?:string[]|undefined;}" + }, "fiatConnectCashInEnabled": { "type": "boolean" }, @@ -4741,9 +4744,6 @@ "pushNotificationsEnabled": { "type": "boolean" }, - "registrations": { - "$ref": "#/definitions/{\"celo-mainnet\"?:string[]|undefined;\"celo-alfajores\"?:string[]|undefined;\"ethereum-mainnet\"?:string[]|undefined;\"ethereum-sepolia\"?:string[]|undefined;\"arbitrum-one\"?:string[]|undefined;\"arbitrum-sepolia\"?:string[]|undefined;\"op-mainnet\"?:string[]|undefined;\"op-sepolia\"?:string[]|undefined;\"polygon-pos-mainnet\"?:string[]|undefined;\"polygon-pos-amoy\"?:string[]|undefined;\"base-mainnet\"?:string[]|undefined;\"base-sepolia\"?:string[]|undefined;}" - }, "requirePinOnAppOpen": { "type": "boolean" }, @@ -4776,6 +4776,7 @@ "activeScreen", "analyticsEnabled", "appState", + "divviRegistrations", "fiatConnectCashInEnabled", "fiatConnectCashOutEnabled", "hapticFeedbackEnabled", @@ -4788,7 +4789,6 @@ "phoneNumberVerified", "pushNotificationRequestedUnixTime", "pushNotificationsEnabled", - "registrations", "requirePinOnAppOpen", "sessionId", "showNotificationSpotlight", diff --git a/test/schemas.ts b/test/schemas.ts index 19d68aa667b..9d659ac308a 100644 --- a/test/schemas.ts +++ b/test/schemas.ts @@ -3662,7 +3662,7 @@ export const v244Schema = { }, app: { ..._.omit(v243Schema.app, 'loggedIn'), - registrations: {}, + divviRegistrations: {}, }, } From 1b50b25e5869d47f93fa539dd419ecf8545d4e6e Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:31:04 +0100 Subject: [PATCH 07/27] fix: type safe protocol ids --- src/app/actions.ts | 8 +++---- src/app/reducers.ts | 6 +++--- src/divviProtocol/constants.ts | 39 ++++++++++++++++------------------ 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/src/app/actions.ts b/src/app/actions.ts index 349dd3ad45b..50b62bf4dd8 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -1,6 +1,6 @@ import { BIOMETRY_TYPE } from '@divvi/react-native-keychain' import { RemoteConfigValues } from 'src/app/saga' -import { SupportedProtocolIds } from 'src/divviProtocol/constants' +import { SupportedProtocolId } from 'src/divviProtocol/constants' import { Screens } from 'src/navigator/Screens' import { NetworkId } from 'src/transactions/types' @@ -157,7 +157,7 @@ interface ToggleHideBalances { interface DivviRegistrationCompleted { type: Actions.DIVVI_REGISTRATION_COMPLETED networkId: NetworkId - protocol: SupportedProtocolIds + protocolId: SupportedProtocolId } export type ActionTypes = @@ -341,11 +341,11 @@ export const toggleHideBalances = (): ToggleHideBalances => { export const divviRegistrationCompleted = ( networkId: NetworkId, - protocol: SupportedProtocolIds + protocolId: SupportedProtocolId ): DivviRegistrationCompleted => { return { type: Actions.DIVVI_REGISTRATION_COMPLETED, networkId, - protocol, + protocolId, } } diff --git a/src/app/reducers.ts b/src/app/reducers.ts index 00779ca1666..cb400d712b4 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -2,7 +2,7 @@ import { BIOMETRY_TYPE } from '@divvi/react-native-keychain' import { Platform } from 'react-native' import { Actions, ActionTypes, AppState } from 'src/app/actions' import { DEEP_LINK_URL_SCHEME } from 'src/config' -import { SupportedProtocolIds } from 'src/divviProtocol/constants' +import { SupportedProtocolId } from 'src/divviProtocol/constants' import { REMOTE_CONFIG_VALUES_DEFAULTS } from 'src/firebase/remoteConfigValuesDefaults' import { Screens } from 'src/navigator/Screens' import { getRehydratePayload, REHYDRATE, RehydrateAction } from 'src/redux/persist-helper' @@ -38,7 +38,7 @@ interface State { hideBalances: boolean pendingDeepLinks: PendingDeepLink[] divviRegistrations: { - [networkId in NetworkId]?: SupportedProtocolIds[] + [networkId in NetworkId]?: SupportedProtocolId[] } } @@ -229,7 +229,7 @@ export const appReducer = ( ...state.divviRegistrations, [action.networkId]: [ ...(state.divviRegistrations[action.networkId] ?? []), - action.protocol, + action.protocolId, ], }, } diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts index 2b6820fbb92..b26940bbe0a 100644 --- a/src/divviProtocol/constants.ts +++ b/src/divviProtocol/constants.ts @@ -1,22 +1,19 @@ -import { keccak256, stringToHex } from 'viem' +const supportedProtocolIds = [ + 'beefy', + 'tether', + 'somm', + 'celo', + 'aerodrome', + 'velodrome', + 'vana', + 'curve', + 'farcaster', + 'mento', + 'yearn', + 'fonbnk', + 'offchainlabs', + 'euler', + 'ubeswap', +] as const -const supportedProtocolIdHashes: Record = { - [keccak256(stringToHex('beefy'))]: 'beefy', - [keccak256(stringToHex('tether'))]: 'tether', - [keccak256(stringToHex('somm'))]: 'somm', - [keccak256(stringToHex('celo'))]: 'celo', - [keccak256(stringToHex('aerodrome'))]: 'aerodrome', - [keccak256(stringToHex('velodrome'))]: 'velodrome', - [keccak256(stringToHex('vana'))]: 'vana', - [keccak256(stringToHex('curve'))]: 'curve', - [keccak256(stringToHex('farcaster'))]: 'farcaster', - [keccak256(stringToHex('mento'))]: 'mento', - [keccak256(stringToHex('yearn'))]: 'yearn', - [keccak256(stringToHex('fonbnk'))]: 'fonbnk', - [keccak256(stringToHex('offchainlabs'))]: 'offchainlabs', - [keccak256(stringToHex('euler'))]: 'euler', - [keccak256(stringToHex('ubeswap'))]: 'ubeswap', -} as const - -export type SupportedProtocolIds = - (typeof supportedProtocolIdHashes)[keyof typeof supportedProtocolIdHashes] +export type SupportedProtocolId = (typeof supportedProtocolIds)[number] From 883b9382f8cef9b88f543c796ff3be2c5f6f3f71 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:41:15 +0100 Subject: [PATCH 08/27] fix: add try catch --- src/divviProtocol/registerReferral.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 29270664f67..98256c18162 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -5,12 +5,20 @@ import { TransactionRequest } from 'src/viem/prepareTransactions' import { decodeFunctionData } from 'viem' export function isRegistrationTransaction(tx: TransactionRequest | SerializableTransactionRequest) { - return ( - tx.to === REGISTRY_CONTRACT_ADDRESS && - tx.data && - decodeFunctionData({ - abi: registryContractAbi, - data: tx.data, - }).functionName === 'registerReferral' - ) + try { + return ( + tx.to === REGISTRY_CONTRACT_ADDRESS && + tx.data && + decodeFunctionData({ + abi: registryContractAbi, + data: tx.data, + }).functionName === 'registerReferral' + ) + } catch (error) { + // decodeFunctionData will throw if the data does not match any function in + // the abi, but this is unlikely to happen since we are checking the "to" + // address first. In any case, we should return false unless we are sure + // that the transaction is a registration transaction. + return false + } } From bb9564eac45abb6d558309c2c02fc9d29dcd5e0b Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:45:55 +0100 Subject: [PATCH 09/27] fix: root schema --- test/RootStateSchema.json | 208 +++++++++++++++++++++++++++++++++++++- 1 file changed, 206 insertions(+), 2 deletions(-) diff --git a/test/RootStateSchema.json b/test/RootStateSchema.json index 039e68372be..99d21a0edc5 100644 --- a/test/RootStateSchema.json +++ b/test/RootStateSchema.json @@ -4688,7 +4688,7 @@ "$ref": "#/definitions/AppState" }, "divviRegistrations": { - "$ref": "#/definitions/{\"celo-mainnet\"?:string[]|undefined;\"celo-alfajores\"?:string[]|undefined;\"ethereum-mainnet\"?:string[]|undefined;\"ethereum-sepolia\"?:string[]|undefined;\"arbitrum-one\"?:string[]|undefined;\"arbitrum-sepolia\"?:string[]|undefined;\"op-mainnet\"?:string[]|undefined;\"op-sepolia\"?:string[]|undefined;\"polygon-pos-mainnet\"?:string[]|undefined;\"polygon-pos-amoy\"?:string[]|undefined;\"base-mainnet\"?:string[]|undefined;\"base-sepolia\"?:string[]|undefined;}" + "$ref": "#/definitions/{\"celo-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"celo-alfajores\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"ethereum-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"ethereum-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"arbitrum-one\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"arbitrum-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"op-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"op-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"polygon-pos-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"polygon-pos-amoy\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"base-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"base-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;}" }, "fiatConnectCashInEnabled": { "type": "boolean" @@ -6759,77 +6759,281 @@ ], "type": "object" }, - "{\"celo-mainnet\"?:string[]|undefined;\"celo-alfajores\"?:string[]|undefined;\"ethereum-mainnet\"?:string[]|undefined;\"ethereum-sepolia\"?:string[]|undefined;\"arbitrum-one\"?:string[]|undefined;\"arbitrum-sepolia\"?:string[]|undefined;\"op-mainnet\"?:string[]|undefined;\"op-sepolia\"?:string[]|undefined;\"polygon-pos-mainnet\"?:string[]|undefined;\"polygon-pos-amoy\"?:string[]|undefined;\"base-mainnet\"?:string[]|undefined;\"base-sepolia\"?:string[]|undefined;}": { + "{\"celo-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"celo-alfajores\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"ethereum-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"ethereum-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"arbitrum-one\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"arbitrum-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"op-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"op-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"polygon-pos-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"polygon-pos-amoy\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"base-mainnet\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;\"base-sepolia\"?:(\"beefy\"|\"tether\"|\"somm\"|\"celo\"|\"aerodrome\"|\"velodrome\"|\"vana\"|\"curve\"|\"farcaster\"|\"mento\"|\"yearn\"|\"fonbnk\"|\"offchainlabs\"|\"euler\"|\"ubeswap\")[]|undefined;}": { "additionalProperties": false, "properties": { "arbitrum-one": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "arbitrum-sepolia": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "base-mainnet": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "base-sepolia": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "celo-alfajores": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "celo-mainnet": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "ethereum-mainnet": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "ethereum-sepolia": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "op-mainnet": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "op-sepolia": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "polygon-pos-amoy": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" }, "polygon-pos-mainnet": { "items": { + "enum": [ + "aerodrome", + "beefy", + "celo", + "curve", + "euler", + "farcaster", + "fonbnk", + "mento", + "offchainlabs", + "somm", + "tether", + "ubeswap", + "vana", + "velodrome", + "yearn" + ], "type": "string" }, "type": "array" From 47e2a1d281dcce073d04fbc7f9f68bd4a03e5dc6 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:48:51 +0100 Subject: [PATCH 10/27] fix: rename --- src/divviProtocol/registerReferral.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 0b309857ae1..b87435cbc92 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -1,4 +1,4 @@ -import { registrationCompleted } from 'src/app/actions' +import { divviRegistrationCompleted } from 'src/app/actions' import { registrationsSelector } from 'src/app/selectors' import { DIVVI_PROTOCOL_IDS, DIVVI_REFERRER_ID } from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' @@ -99,7 +99,7 @@ export async function createRegistrationTransactions({ `${TAG}/createRegistrationTransactions`, `Referral is already registered for protocol "${protocolId}". Skipping registration transaction.` ) - store.dispatch(registrationCompleted(networkId, protocolId)) + store.dispatch(divviRegistrationCompleted(networkId, protocolId)) return } @@ -165,7 +165,7 @@ export function* sendPreparedRegistrationTransactions( ) throw new Error(`Unknown protocolId received from transaction ${hash}`) } - yield* put(registrationCompleted(networkConfig.networkToNetworkId[network], protocolId)) + yield* put(divviRegistrationCompleted(networkConfig.networkToNetworkId[network], protocolId)) } } From ae376ff15a9bcbbcfa3211ae82e47d74a8a30131 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 10:50:23 +0100 Subject: [PATCH 11/27] fix: lint --- src/app/selectors.ts | 2 +- src/config.ts | 4 ++-- src/divviProtocol/registerReferral.ts | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/selectors.ts b/src/app/selectors.ts index de05f618e23..c5e9ed6c531 100644 --- a/src/app/selectors.ts +++ b/src/app/selectors.ts @@ -59,4 +59,4 @@ export const hideWalletBalancesSelector = (state: RootState) => state.app.hideBa export const pendingDeepLinkSelector = (state: RootState) => state.app.pendingDeepLinks[0] ?? null -export const registrationsSelector = (state: RootState) => state.app.registrations +export const divviRegistrationsSelector = (state: RootState) => state.app.divviRegistrations diff --git a/src/config.ts b/src/config.ts index 73b6d72bb8f..1e9425bc847 100644 --- a/src/config.ts +++ b/src/config.ts @@ -6,7 +6,7 @@ import { LoggerLevel } from 'src/utils/LoggerLevels' // eslint-disable-next-line import/no-relative-packages import { TORUS_SAPPHIRE_NETWORK } from '@toruslabs/constants' import { LaunchArguments } from 'react-native-launch-arguments' -import { SupportedProtocolIds } from 'src/divviProtocol/constants' +import { SupportedProtocolId } from 'src/divviProtocol/constants' import { HomeActionName } from 'src/home/types' import { ToggleableOnboardingFeatures } from 'src/onboarding/types' import { stringToBoolean } from 'src/utils/parsing' @@ -228,5 +228,5 @@ export const FETCH_FIATCONNECT_QUOTES = true export const WALLETCONNECT_UNIVERSAL_LINK = 'https://valoraapp.com/wc' -export const DIVVI_PROTOCOL_IDS: SupportedProtocolIds[] = [] +export const DIVVI_PROTOCOL_IDS: SupportedProtocolId[] = [] export const DIVVI_REFERRER_ID: string | undefined = undefined diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index b87435cbc92..31ce1e5970b 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -1,5 +1,5 @@ import { divviRegistrationCompleted } from 'src/app/actions' -import { registrationsSelector } from 'src/app/selectors' +import { divviRegistrationsSelector } from 'src/app/selectors' import { DIVVI_PROTOCOL_IDS, DIVVI_REFERRER_ID } from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS, supportedProtocolIdHashes } from 'src/divviProtocol/constants' @@ -52,7 +52,9 @@ export async function createRegistrationTransactions({ // Caching registration status in Redux reduces on-chain checks but doesn’t guarantee // it wasn’t completed in a previous install or session. - const completedRegistrations = new Set(registrationsSelector(store.getState())[networkId] ?? []) + const completedRegistrations = new Set( + divviRegistrationsSelector(store.getState())[networkId] ?? [] + ) const pendingRegistrations = DIVVI_PROTOCOL_IDS.filter( (protocol) => !completedRegistrations.has(protocol) ) From e1627e9675b9c142ca72fbacdffce2d1d24a7702 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 11:44:52 +0100 Subject: [PATCH 12/27] chore: add first tests --- src/divviProtocol/registerReferral.test.ts | 113 +++++++++++++++++++++ src/divviProtocol/registerReferral.ts | 4 +- 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/divviProtocol/registerReferral.test.ts diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts new file mode 100644 index 00000000000..56e70a80dbc --- /dev/null +++ b/src/divviProtocol/registerReferral.test.ts @@ -0,0 +1,113 @@ +import * as config from 'src/config' +import { registryContractAbi } from 'src/divviProtocol/abi/Registry' +import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' +import { createRegistrationTransactions } from 'src/divviProtocol/registerReferral' +import { store } from 'src/redux/store' +import { NetworkId } from 'src/transactions/types' +import { publicClient } from 'src/viem' +import { getMockStoreData } from 'test/utils' +import { mockAccount } from 'test/values' +import { encodeFunctionData } from 'viem' + +// Note: Statsig is not directly used by this module, but mocking it prevents +// require cycles from impacting the tests. +jest.mock('src/statsig') + +jest.mock('viem', () => ({ + ...jest.requireActual('viem'), + readContract: jest.fn(), +})) + +jest.mock('src/redux/store', () => ({ store: { getState: jest.fn() } })) +const mockStore = jest.mocked(store) +mockStore.getState.mockImplementation(getMockStoreData) +mockStore.dispatch = jest.fn() + +jest.mock('src/config') + +describe('createRegistrationTransactions', () => { + beforeEach(() => { + jest.clearAllMocks() + jest.mocked(config).DIVVI_PROTOCOL_IDS = ['beefy', 'somm'] + jest.mocked(config).DIVVI_REFERRER_ID = 'referrer-id' + }) + + it('returns no transactions if referrer id is not set', async () => { + jest.mocked(config).DIVVI_REFERRER_ID = undefined + + const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + expect(result).toEqual([]) + }) + + it('returns no transactions if all registrations are completed', async () => { + mockStore.getState.mockImplementationOnce(() => + getMockStoreData({ + app: { + divviRegistrations: { + [NetworkId['op-mainnet']]: ['beefy', 'somm'], + }, + }, + }) + ) + const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + expect(result).toEqual([]) + }) + + it('returns transactions for pending registrations only', async () => { + jest + .spyOn(publicClient.optimism, 'readContract') + .mockImplementation(async ({ functionName, args }) => { + if (functionName === 'getReferrers') { + return ['unrelated-referrer-id', 'referrer-id'] // Referrer is registered + } + if (functionName === 'getUsers' && args) { + if (args[0] === 'beefy') { + return [[], []] // User is not registered + } + return [[mockAccount], []] // User is registered + } + throw new Error('Unexpected read contract call.') + }) + + const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + expect(result).toEqual([ + { + data: encodeFunctionData({ + abi: registryContractAbi, + functionName: 'registerReferral', + args: ['referrer-id', 'beefy'], + }), + to: REGISTRY_CONTRACT_ADDRESS, + }, + ]) + }) + + it('handles errors in contract reads gracefully and returns no corresponding transactions', async () => { + jest + .spyOn(publicClient.optimism, 'readContract') + .mockImplementation(async ({ functionName, args }) => { + if (functionName === 'getReferrers') { + return ['unrelated-referrer-id', 'referrer-id'] // Referrer is registered + } + if (functionName === 'getUsers' && args) { + if (args[0] === 'beefy') { + return [[], []] // User is not registered + } + throw new Error('Read error for protocol') // simulate error for other protocols + } + throw new Error('Unexpected read contract call.') + }) + + const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + expect(result).toEqual([ + { + data: encodeFunctionData({ + abi: registryContractAbi, + functionName: 'registerReferral', + args: ['referrer-id', 'beefy'], + }), + to: REGISTRY_CONTRACT_ADDRESS, + }, + ]) + }) +}) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 31ce1e5970b..d9fdfcea7f6 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -96,7 +96,7 @@ export async function createRegistrationTransactions({ return } - if (registeredUsers[0].includes(walletAddress)) { + if (registeredUsers[0].some((address) => address.toLowerCase() === walletAddress)) { Logger.debug( `${TAG}/createRegistrationTransactions`, `Referral is already registered for protocol "${protocolId}". Skipping registration transaction.` @@ -109,7 +109,7 @@ export async function createRegistrationTransactions({ } catch (error) { Logger.error( `${TAG}/createRegistrationTransactions`, - `Error reading registered referrers for protocol "${protocolId}". Skipping registration transaction.`, + `Error reading registration state for protocol "${protocolId}". Skipping registration transaction.`, error ) } From 07614508b8ba61ddf4d3c58d4e95926d5d953cd6 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 11:54:28 +0100 Subject: [PATCH 13/27] chore: add extra test --- src/divviProtocol/registerReferral.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 56e70a80dbc..1edb9f34120 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -39,6 +39,23 @@ describe('createRegistrationTransactions', () => { expect(result).toEqual([]) }) + it('returns no transactions if referrer is not registered', async () => { + jest + .spyOn(publicClient.optimism, 'readContract') + .mockImplementation(async ({ functionName, args }) => { + if (functionName === 'getReferrers') { + return ['unrelated-referrer-id'] // Referrer is not registered + } + if (functionName === 'getUsers' && args) { + return [[], []] // User is not registered + } + throw new Error('Unexpected read contract call.') + }) + + const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + expect(result).toEqual([]) + }) + it('returns no transactions if all registrations are completed', async () => { mockStore.getState.mockImplementationOnce(() => getMockStoreData({ From 41707f0186af76f362745ca8af58203111d36cfe Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 12:27:59 +0100 Subject: [PATCH 14/27] chore: add more tests --- src/divviProtocol/registerReferral.test.ts | 96 +++++++++++++++++++++- src/divviProtocol/registerReferral.ts | 77 +++++++++-------- src/viem/saga.ts | 2 +- 3 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 1edb9f34120..401c995cc84 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -1,13 +1,21 @@ +import { expectSaga } from 'redux-saga-test-plan' +import * as matchers from 'redux-saga-test-plan/matchers' +import { throwError } from 'redux-saga-test-plan/providers' +import { divviRegistrationCompleted } from 'src/app/actions' import * as config from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' -import { createRegistrationTransactions } from 'src/divviProtocol/registerReferral' +import { + createRegistrationTransactions, + sendPreparedRegistrationTransactions, +} from 'src/divviProtocol/registerReferral' import { store } from 'src/redux/store' import { NetworkId } from 'src/transactions/types' import { publicClient } from 'src/viem' +import { ViemWallet } from 'src/viem/getLockableWallet' import { getMockStoreData } from 'test/utils' import { mockAccount } from 'test/values' -import { encodeFunctionData } from 'viem' +import { encodeFunctionData, parseEventLogs } from 'viem' // Note: Statsig is not directly used by this module, but mocking it prevents // require cycles from impacting the tests. @@ -16,6 +24,7 @@ jest.mock('src/statsig') jest.mock('viem', () => ({ ...jest.requireActual('viem'), readContract: jest.fn(), + parseEventLogs: jest.fn(), })) jest.mock('src/redux/store', () => ({ store: { getState: jest.fn() } })) @@ -25,6 +34,15 @@ mockStore.dispatch = jest.fn() jest.mock('src/config') +const mockBeefyRegistrationTx = { + data: encodeFunctionData({ + abi: registryContractAbi, + functionName: 'registerReferral', + args: ['referrer-id', 'beefy'], + }), + to: REGISTRY_CONTRACT_ADDRESS, +} + describe('createRegistrationTransactions', () => { beforeEach(() => { jest.clearAllMocks() @@ -128,3 +146,77 @@ describe('createRegistrationTransactions', () => { ]) }) }) + +describe('sendPreparedRegistrationTransactions', () => { + const mockViemWallet = { + account: { address: mockAccount }, + signTransaction: jest.fn(async () => '0xsignedTx'), + sendRawTransaction: jest.fn(async () => '0xhash'), + } as any as ViemWallet + + it('sends transactions and updates store on success', async () => { + const mockNonce = 157 + jest.mocked(parseEventLogs).mockReturnValue([ + { + args: { + protocolId: '0x62bd0dd2bb37b275249fe0ec6a61b0fb5adafd50d05a41adb9e1cbfd41ab0607', + }, + }, + ] as unknown as ReturnType) + + await expectSaga( + sendPreparedRegistrationTransactions, + [mockBeefyRegistrationTx], + NetworkId['op-mainnet'], + mockViemWallet, + mockNonce + ) + .provide([ + [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], + [matchers.call.fn(mockViemWallet.sendRawTransaction), '0xhash'], + [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'success' }], + ]) + .put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .returns(mockNonce + 1) + .run() + }) + + it('handles reverted transaction and does not update the store', async () => { + const mockNonce = 157 + + await expectSaga( + sendPreparedRegistrationTransactions, + [mockBeefyRegistrationTx], + NetworkId['op-mainnet'], + mockViemWallet, + mockNonce + ) + .provide([ + [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], + [matchers.call.fn(mockViemWallet.sendRawTransaction), '0xhash'], + [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'reverted' }], + ]) + .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .returns(mockNonce + 1) + .run() + }) + + it('does not throw on failures, and returns the incremented nonce', async () => { + const mockNonce = 157 + + await expectSaga( + sendPreparedRegistrationTransactions, + [mockBeefyRegistrationTx], + NetworkId['op-mainnet'], + mockViemWallet, + mockNonce + ) + .provide([ + [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], + [matchers.call.fn(mockViemWallet.sendRawTransaction), throwError(new Error('failure'))], + ]) + .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .returns(mockNonce + 1) + .run() + }) +}) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index d9fdfcea7f6..4bb706a1601 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -4,13 +4,13 @@ import { DIVVI_PROTOCOL_IDS, DIVVI_REFERRER_ID } from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS, supportedProtocolIdHashes } from 'src/divviProtocol/constants' import { store } from 'src/redux/store' -import { Network, NetworkId } from 'src/transactions/types' +import { NetworkId } from 'src/transactions/types' import Logger from 'src/utils/Logger' import { publicClient } from 'src/viem' import { ViemWallet } from 'src/viem/getLockableWallet' import { SerializableTransactionRequest } from 'src/viem/preparedTransactionSerialization' import { TransactionRequest } from 'src/viem/prepareTransactions' -import networkConfig, { networkIdToNetwork } from 'src/web3/networkConfig' +import { networkIdToNetwork } from 'src/web3/networkConfig' import { walletAddressSelector } from 'src/web3/selectors' import { call, put } from 'typed-redux-saga' import { Address, decodeFunctionData, encodeFunctionData, parseEventLogs } from 'viem' @@ -128,46 +128,57 @@ export async function createRegistrationTransactions({ export function* sendPreparedRegistrationTransactions( txs: TransactionRequest[], - network: Network, + networkId: NetworkId, wallet: ViemWallet, nonce: number ) { for (let i = 0; i < txs.length; i++) { - const signedTx = yield* call([wallet, 'signTransaction'], { - ...txs[i], - nonce: nonce++, - } as any) - const hash = yield* call([wallet, 'sendRawTransaction'], { - serializedTransaction: signedTx, - }) + try { + const signedTx = yield* call([wallet, 'signTransaction'], { + ...txs[i], + nonce: nonce++, + } as any) + const hash = yield* call([wallet, 'sendRawTransaction'], { + serializedTransaction: signedTx, + }) - Logger.debug( - `${TAG}/sendPreparedRegistrationTransactions`, - 'Successfully sent transaction to the network', - hash - ) + Logger.debug( + `${TAG}/sendPreparedRegistrationTransactions`, + 'Successfully sent transaction to the network', + hash + ) - const receipt = yield* call([publicClient[network], 'waitForTransactionReceipt'], { - hash, - }) + const receipt = yield* call( + [publicClient[networkIdToNetwork[networkId]], 'waitForTransactionReceipt'], + { + hash, + } + ) - if (receipt.status === 'success') { - const parsedLogs = parseEventLogs({ - abi: registryContractAbi, - eventName: ['ReferralRegistered'], - logs: receipt.logs, - }) + if (receipt.status === 'success') { + const parsedLogs = parseEventLogs({ + abi: registryContractAbi, + eventName: ['ReferralRegistered'], + logs: receipt.logs, + }) - const protocolId = supportedProtocolIdHashes[parsedLogs[0].args.protocolId] - if (!protocolId) { - // this should never happen, since we specify the protocolId in the prepareTransactions step - Logger.error( - `${TAG}/sendPreparedRegistrationTransactions`, - `Unknown protocolId received from transaction ${hash}` - ) - throw new Error(`Unknown protocolId received from transaction ${hash}`) + const protocolId = supportedProtocolIdHashes[parsedLogs[0].args.protocolId] + if (!protocolId) { + // this should never happen, since we specify the protocolId in the prepareTransactions step + Logger.error( + `${TAG}/sendPreparedRegistrationTransactions`, + `Unknown protocolId received from transaction ${hash}` + ) + continue + } + yield* put(divviRegistrationCompleted(networkId, protocolId)) } - yield* put(divviRegistrationCompleted(networkConfig.networkToNetworkId[network], protocolId)) + } catch (error) { + Logger.error( + `${TAG}/sendPreparedRegistrationTransactions`, + `Failed to send or parse prepared registration transaction`, + error + ) } } diff --git a/src/viem/saga.ts b/src/viem/saga.ts index c0455c6bdfe..b9d4f3e118e 100644 --- a/src/viem/saga.ts +++ b/src/viem/saga.ts @@ -95,7 +95,7 @@ export function* sendPreparedTransactions( nonce = yield* call( sendPreparedRegistrationTransactions, preparedRegistrationTransactions, - network, + networkId, wallet, nonce ) From e8c969e0edc6f93a3092bd188f95604f50c3a58c Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 12:31:14 +0100 Subject: [PATCH 15/27] fix: nonce --- src/divviProtocol/registerReferral.test.ts | 4 ++-- src/divviProtocol/registerReferral.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 401c995cc84..eedf931daba 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -201,7 +201,7 @@ describe('sendPreparedRegistrationTransactions', () => { .run() }) - it('does not throw on failures, and returns the incremented nonce', async () => { + it('does not throw on failures, and returns the original nonce', async () => { const mockNonce = 157 await expectSaga( @@ -216,7 +216,7 @@ describe('sendPreparedRegistrationTransactions', () => { [matchers.call.fn(mockViemWallet.sendRawTransaction), throwError(new Error('failure'))], ]) .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) - .returns(mockNonce + 1) + .returns(mockNonce) .run() }) }) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 4bb706a1601..81a1dc04fa5 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -136,7 +136,7 @@ export function* sendPreparedRegistrationTransactions( try { const signedTx = yield* call([wallet, 'signTransaction'], { ...txs[i], - nonce: nonce++, + nonce, } as any) const hash = yield* call([wallet, 'sendRawTransaction'], { serializedTransaction: signedTx, @@ -147,6 +147,7 @@ export function* sendPreparedRegistrationTransactions( 'Successfully sent transaction to the network', hash ) + nonce = nonce + 1 const receipt = yield* call( [publicClient[networkIdToNetwork[networkId]], 'waitForTransactionReceipt'], From c7663f41ccfb2b064d46e0d95fdf700e8561f4e9 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 12:42:54 +0100 Subject: [PATCH 16/27] fix: test title --- src/divviProtocol/registerReferral.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index eedf931daba..9b6af1eb547 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -201,7 +201,7 @@ describe('sendPreparedRegistrationTransactions', () => { .run() }) - it('does not throw on failures, and returns the original nonce', async () => { + it('does not throw on failure during sending to network, and returns the original nonce', async () => { const mockNonce = 157 await expectSaga( From 77397ce23b2705b1d2c5e782c7c42458950686b7 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 12:51:28 +0100 Subject: [PATCH 17/27] chore: add test --- src/viem/prepareTransactions.test.ts | 56 +++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/viem/prepareTransactions.test.ts b/src/viem/prepareTransactions.test.ts index 208b4ae15c4..4947f97be2c 100644 --- a/src/viem/prepareTransactions.test.ts +++ b/src/viem/prepareTransactions.test.ts @@ -1,6 +1,7 @@ import BigNumber from 'bignumber.js' import AppAnalytics from 'src/analytics/AppAnalytics' import { TransactionEvents } from 'src/analytics/Events' +import { createRegistrationTransactions } from 'src/divviProtocol/registerReferral' import { TokenBalanceWithAddress } from 'src/tokens/slice' import { Network, NetworkId } from 'src/transactions/types' import { estimateFeesPerGas } from 'src/viem/estimateFeesPerGas' @@ -10,8 +11,8 @@ import { getEstimatedGasFee, getFeeCurrency, getFeeCurrencyAddress, - getFeeCurrencyToken, getFeeCurrencyAndAmounts, + getFeeCurrencyToken, getFeeDecimals, getMaxGasFee, prepareERC20TransferTransaction, @@ -54,9 +55,11 @@ jest.mock('src/viem/index', () => ({ arbitrum: {} as unknown as jest.Mocked<(typeof publicClient)[Network.Arbitrum]>, }, })) +jest.mock('src/divviProtocol/registerReferral') beforeEach(() => { jest.clearAllMocks() + jest.mocked(createRegistrationTransactions).mockResolvedValue([]) }) describe('prepareTransactions module', () => { @@ -139,6 +142,57 @@ describe('prepareTransactions module', () => { } const mockPublicClient = {} as unknown as jest.Mocked<(typeof publicClient)[Network.Celo]> describe('prepareTransactions function', () => { + it('adds divvi registration transactions to the prepared transactions if there are any', async () => { + mocked(createRegistrationTransactions).mockResolvedValue([ + { + data: '0xregistrationData', + to: '0xregistrationTarget', + }, + ]) + mocked(estimateFeesPerGas).mockResolvedValue({ + maxFeePerGas: BigInt(1), + maxPriorityFeePerGas: BigInt(2), + baseFeePerGas: BigInt(1), + }) + mocked(estimateGas).mockResolvedValue(BigInt(500)) + + const result = await prepareTransactions({ + feeCurrencies: mockFeeCurrencies, + decreasedAmountGasFeeMultiplier: 1, + baseTransactions: [ + { + from: '0xfrom' as Address, + to: '0xto' as Address, + data: '0xdata', + }, + ], + origin: 'send', + }) + expect(result).toStrictEqual({ + type: 'possible', + transactions: [ + { + from: '0xfrom', + to: '0xto', + data: '0xdata', + gas: BigInt(500), + maxFeePerGas: BigInt(1), + maxPriorityFeePerGas: BigInt(2), + _baseFeePerGas: BigInt(1), + }, + { + data: '0xregistrationData', + to: '0xregistrationTarget', + gas: BigInt(500), + maxFeePerGas: BigInt(1), + maxPriorityFeePerGas: BigInt(2), + _baseFeePerGas: BigInt(1), + }, + ], + feeCurrency: mockFeeCurrencies[0], + }) + }) + it('throws if trying to sendAmount > sendToken balance', async () => { await expect(() => prepareTransactions({ From 80f271bb9b428c8b43f04327d02f0e2767c23756 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 13:33:49 +0100 Subject: [PATCH 18/27] chore: test --- src/viem/saga.test.ts | 72 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/viem/saga.test.ts b/src/viem/saga.test.ts index e72ae96e2f0..54b4642fde6 100644 --- a/src/viem/saga.test.ts +++ b/src/viem/saga.test.ts @@ -3,6 +3,8 @@ import { expectSaga } from 'redux-saga-test-plan' import * as matchers from 'redux-saga-test-plan/matchers' import { EffectProviders, StaticProvider, throwError } from 'redux-saga-test-plan/providers' import { call } from 'redux-saga/effects' +import { registryContractAbi } from 'src/divviProtocol/abi/Registry' +import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' import { BaseStandbyTransaction, addStandbyTransaction } from 'src/transactions/slice' import { NetworkId, TokenTransactionTypeV2 } from 'src/transactions/types' import { ViemWallet } from 'src/viem/getLockableWallet' @@ -20,8 +22,16 @@ import { mockCusdTokenId, mockQRCodeRecipient, } from 'test/values' +import { encodeFunctionData } from 'viem' import { getTransactionCount } from 'viem/actions' +const mockSendPreparedRegistrationTransactions = jest.fn() +jest.mock('src/divviProtocol/registerReferral', () => ({ + ...jest.requireActual('src/divviProtocol/registerReferral'), + sendPreparedRegistrationTransactions: (...args: any[]) => + mockSendPreparedRegistrationTransactions(...args), +})) + const preparedTransactions: TransactionRequest[] = [ { from: '0xa', @@ -148,6 +158,7 @@ describe('sendPreparedTransactions', () => { expect(mockViemWallet.sendRawTransaction).toHaveBeenNthCalledWith(2, { serializedTransaction: '0xmockSerializedTransaction2', }) + expect(mockSendPreparedRegistrationTransactions).not.toHaveBeenCalled() }) it('throws if the number of prepared transactions and standby transaction creators do not match', async () => { @@ -210,4 +221,65 @@ describe('sendPreparedTransactions', () => { .run() ).rejects.toThrowError('No account found in the wallet') }) + + it('sends the registration transactions first if there are any', async () => { + mockSendPreparedRegistrationTransactions.mockResolvedValue(11) + const mockPreparedRegistration: TransactionRequest = { + from: mockAccount, + to: REGISTRY_CONTRACT_ADDRESS, + data: encodeFunctionData({ + abi: registryContractAbi, + functionName: 'registerReferral', + args: ['some-referrer', 'some-protocol'], + }), + gas: BigInt(59_480), + maxFeePerGas: BigInt(12_000_000_000), + _baseFeePerGas: BigInt(6_000_000_000), + } + + await expectSaga( + sendPreparedTransactions, + serializablePreparedTransactions.concat( + getSerializablePreparedTransactions([mockPreparedRegistration]) + ), + networkConfig.defaultNetworkId, + mockCreateBaseStandbyTransactions + ) + .withState(createMockStore({}).getState()) + .provide(createDefaultProviders()) + .call(getViemWallet, networkConfig.viemChain.celo, false) + .put( + addStandbyTransaction({ + ...mockStandbyTransactions[0], + feeCurrencyId: mockCeloTokenId, + transactionHash: '0xmockTxHash1', + }) + ) + .put( + addStandbyTransaction({ + ...mockStandbyTransactions[1], + feeCurrencyId: mockCeloTokenId, + transactionHash: '0xmockTxHash2', + }) + ) + .returns(['0xmockTxHash1', '0xmockTxHash2']) + .run() + + expect(mockSendPreparedRegistrationTransactions).toHaveBeenCalledTimes(1) + expect(mockSendPreparedRegistrationTransactions).toHaveBeenCalledWith( + [mockPreparedRegistration], + networkConfig.defaultNetworkId, + mockViemWallet, + 10 + ) + expect(mockViemWallet.signTransaction).toHaveBeenCalledTimes(2) + expect(mockViemWallet.signTransaction).toHaveBeenNthCalledWith(1, { + ...preparedTransactions[0], + nonce: 11, // note that this is higher than the value provided in createDefaultProviders + }) + expect(mockViemWallet.signTransaction).toHaveBeenNthCalledWith(2, { + ...preparedTransactions[1], + nonce: 12, + }) + }) }) From 9fa65f970aacbf6f228c36e460ff5bc54ea122e3 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 16:06:09 +0100 Subject: [PATCH 19/27] fix: comments --- src/divviProtocol/registerReferral.test.ts | 24 +++++++++---- src/divviProtocol/registerReferral.ts | 10 +++--- src/viem/prepareTransactions.test.ts | 8 ++--- src/viem/prepareTransactions.ts | 4 +-- src/viem/saga.test.ts | 39 ++++++++++++---------- 5 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 9b6af1eb547..2342dfbaaf3 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -6,7 +6,7 @@ import * as config from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' import { - createRegistrationTransactions, + createRegistrationTransactionsIfNeeded, sendPreparedRegistrationTransactions, } from 'src/divviProtocol/registerReferral' import { store } from 'src/redux/store' @@ -43,7 +43,7 @@ const mockBeefyRegistrationTx = { to: REGISTRY_CONTRACT_ADDRESS, } -describe('createRegistrationTransactions', () => { +describe('createRegistrationTransactionsIfNeeded', () => { beforeEach(() => { jest.clearAllMocks() jest.mocked(config).DIVVI_PROTOCOL_IDS = ['beefy', 'somm'] @@ -53,7 +53,9 @@ describe('createRegistrationTransactions', () => { it('returns no transactions if referrer id is not set', async () => { jest.mocked(config).DIVVI_REFERRER_ID = undefined - const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + const result = await createRegistrationTransactionsIfNeeded({ + networkId: NetworkId['op-mainnet'], + }) expect(result).toEqual([]) }) @@ -70,7 +72,9 @@ describe('createRegistrationTransactions', () => { throw new Error('Unexpected read contract call.') }) - const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + const result = await createRegistrationTransactionsIfNeeded({ + networkId: NetworkId['op-mainnet'], + }) expect(result).toEqual([]) }) @@ -84,7 +88,9 @@ describe('createRegistrationTransactions', () => { }, }) ) - const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + const result = await createRegistrationTransactionsIfNeeded({ + networkId: NetworkId['op-mainnet'], + }) expect(result).toEqual([]) }) @@ -104,7 +110,9 @@ describe('createRegistrationTransactions', () => { throw new Error('Unexpected read contract call.') }) - const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + const result = await createRegistrationTransactionsIfNeeded({ + networkId: NetworkId['op-mainnet'], + }) expect(result).toEqual([ { data: encodeFunctionData({ @@ -133,7 +141,9 @@ describe('createRegistrationTransactions', () => { throw new Error('Unexpected read contract call.') }) - const result = await createRegistrationTransactions({ networkId: NetworkId['op-mainnet'] }) + const result = await createRegistrationTransactionsIfNeeded({ + networkId: NetworkId['op-mainnet'], + }) expect(result).toEqual([ { data: encodeFunctionData({ diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 81a1dc04fa5..e3cca3fbc1d 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -36,7 +36,7 @@ export function isRegistrationTransaction(tx: TransactionRequest | SerializableT } } -export async function createRegistrationTransactions({ +export async function createRegistrationTransactionsIfNeeded({ networkId, }: { networkId: NetworkId @@ -44,7 +44,7 @@ export async function createRegistrationTransactions({ const referrerId = DIVVI_REFERRER_ID if (!referrerId) { Logger.debug( - `${TAG}/createRegistrationTransactions`, + `${TAG}/createRegistrationTransactionsIfNeeded`, 'No referrer id set. Skipping registration transactions.' ) return [] @@ -90,7 +90,7 @@ export async function createRegistrationTransactions({ if (!registeredReferrers.includes(referrerId)) { Logger.error( - `${TAG}/createRegistrationTransactions`, + `${TAG}/createRegistrationTransactionsIfNeeded`, `Referrer "${referrerId}" is not registered for protocol "${protocolId}". Skipping registration transaction.` ) return @@ -98,7 +98,7 @@ export async function createRegistrationTransactions({ if (registeredUsers[0].some((address) => address.toLowerCase() === walletAddress)) { Logger.debug( - `${TAG}/createRegistrationTransactions`, + `${TAG}/createRegistrationTransactionsIfNeeded`, `Referral is already registered for protocol "${protocolId}". Skipping registration transaction.` ) store.dispatch(divviRegistrationCompleted(networkId, protocolId)) @@ -108,7 +108,7 @@ export async function createRegistrationTransactions({ protocolsEligibleForRegistration.push(protocolId) } catch (error) { Logger.error( - `${TAG}/createRegistrationTransactions`, + `${TAG}/createRegistrationTransactionsIfNeeded`, `Error reading registration state for protocol "${protocolId}". Skipping registration transaction.`, error ) diff --git a/src/viem/prepareTransactions.test.ts b/src/viem/prepareTransactions.test.ts index 4947f97be2c..978d85e8950 100644 --- a/src/viem/prepareTransactions.test.ts +++ b/src/viem/prepareTransactions.test.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js' import AppAnalytics from 'src/analytics/AppAnalytics' import { TransactionEvents } from 'src/analytics/Events' -import { createRegistrationTransactions } from 'src/divviProtocol/registerReferral' +import { createRegistrationTransactionsIfNeeded } from 'src/divviProtocol/registerReferral' import { TokenBalanceWithAddress } from 'src/tokens/slice' import { Network, NetworkId } from 'src/transactions/types' import { estimateFeesPerGas } from 'src/viem/estimateFeesPerGas' @@ -59,7 +59,7 @@ jest.mock('src/divviProtocol/registerReferral') beforeEach(() => { jest.clearAllMocks() - jest.mocked(createRegistrationTransactions).mockResolvedValue([]) + jest.mocked(createRegistrationTransactionsIfNeeded).mockResolvedValue([]) }) describe('prepareTransactions module', () => { @@ -142,8 +142,8 @@ describe('prepareTransactions module', () => { } const mockPublicClient = {} as unknown as jest.Mocked<(typeof publicClient)[Network.Celo]> describe('prepareTransactions function', () => { - it('adds divvi registration transactions to the prepared transactions if there are any', async () => { - mocked(createRegistrationTransactions).mockResolvedValue([ + it('adds divvi registration transactions to the prepared transactions if needed', async () => { + mocked(createRegistrationTransactionsIfNeeded).mockResolvedValue([ { data: '0xregistrationData', to: '0xregistrationTarget', diff --git a/src/viem/prepareTransactions.ts b/src/viem/prepareTransactions.ts index 5ccdc11bf6c..a55dd0b7bde 100644 --- a/src/viem/prepareTransactions.ts +++ b/src/viem/prepareTransactions.ts @@ -3,7 +3,7 @@ import AppAnalytics from 'src/analytics/AppAnalytics' import { TransactionEvents } from 'src/analytics/Events' import { TransactionOrigin } from 'src/analytics/types' import { STATIC_GAS_PADDING } from 'src/config' -import { createRegistrationTransactions } from 'src/divviProtocol/registerReferral' +import { createRegistrationTransactionsIfNeeded } from 'src/divviProtocol/registerReferral' import { NativeTokenBalance, TokenBalance, @@ -292,7 +292,7 @@ export async function prepareTransactions({ isGasSubsidized?: boolean origin: TransactionOrigin }): Promise { - const registrationTxs = await createRegistrationTransactions({ + const registrationTxs = await createRegistrationTransactionsIfNeeded({ networkId: feeCurrencies[0].networkId, }) if (registrationTxs.length > 0) { diff --git a/src/viem/saga.test.ts b/src/viem/saga.test.ts index 54b4642fde6..6712f4850b9 100644 --- a/src/viem/saga.test.ts +++ b/src/viem/saga.test.ts @@ -5,6 +5,7 @@ import { EffectProviders, StaticProvider, throwError } from 'redux-saga-test-pla import { call } from 'redux-saga/effects' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' +import { sendPreparedRegistrationTransactions } from 'src/divviProtocol/registerReferral' import { BaseStandbyTransaction, addStandbyTransaction } from 'src/transactions/slice' import { NetworkId, TokenTransactionTypeV2 } from 'src/transactions/types' import { ViemWallet } from 'src/viem/getLockableWallet' @@ -25,13 +26,6 @@ import { import { encodeFunctionData } from 'viem' import { getTransactionCount } from 'viem/actions' -const mockSendPreparedRegistrationTransactions = jest.fn() -jest.mock('src/divviProtocol/registerReferral', () => ({ - ...jest.requireActual('src/divviProtocol/registerReferral'), - sendPreparedRegistrationTransactions: (...args: any[]) => - mockSendPreparedRegistrationTransactions(...args), -})) - const preparedTransactions: TransactionRequest[] = [ { from: '0xa', @@ -125,6 +119,7 @@ describe('sendPreparedTransactions', () => { .withState(createMockStore({}).getState()) .provide(createDefaultProviders()) .call(getViemWallet, networkConfig.viemChain.celo, false) + .not.call(sendPreparedRegistrationTransactions) .put( addStandbyTransaction({ ...mockStandbyTransactions[0], @@ -158,7 +153,6 @@ describe('sendPreparedTransactions', () => { expect(mockViemWallet.sendRawTransaction).toHaveBeenNthCalledWith(2, { serializedTransaction: '0xmockSerializedTransaction2', }) - expect(mockSendPreparedRegistrationTransactions).not.toHaveBeenCalled() }) it('throws if the number of prepared transactions and standby transaction creators do not match', async () => { @@ -223,7 +217,6 @@ describe('sendPreparedTransactions', () => { }) it('sends the registration transactions first if there are any', async () => { - mockSendPreparedRegistrationTransactions.mockResolvedValue(11) const mockPreparedRegistration: TransactionRequest = { from: mockAccount, to: REGISTRY_CONTRACT_ADDRESS, @@ -246,8 +239,27 @@ describe('sendPreparedTransactions', () => { mockCreateBaseStandbyTransactions ) .withState(createMockStore({}).getState()) - .provide(createDefaultProviders()) + .provide([ + ...createDefaultProviders(), + [ + call( + sendPreparedRegistrationTransactions, + [mockPreparedRegistration], + networkConfig.defaultNetworkId, + mockViemWallet, + 10 + ), + 11, + ], + ]) .call(getViemWallet, networkConfig.viemChain.celo, false) + .call( + sendPreparedRegistrationTransactions, + [mockPreparedRegistration], + networkConfig.defaultNetworkId, + mockViemWallet, + 10 + ) .put( addStandbyTransaction({ ...mockStandbyTransactions[0], @@ -265,13 +277,6 @@ describe('sendPreparedTransactions', () => { .returns(['0xmockTxHash1', '0xmockTxHash2']) .run() - expect(mockSendPreparedRegistrationTransactions).toHaveBeenCalledTimes(1) - expect(mockSendPreparedRegistrationTransactions).toHaveBeenCalledWith( - [mockPreparedRegistration], - networkConfig.defaultNetworkId, - mockViemWallet, - 10 - ) expect(mockViemWallet.signTransaction).toHaveBeenCalledTimes(2) expect(mockViemWallet.signTransaction).toHaveBeenNthCalledWith(1, { ...preparedTransactions[0], From b26e9e693e5ec20e3a77419d9699f774596d34b4 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 12 Feb 2025 18:17:13 +0100 Subject: [PATCH 20/27] chore: spawn tx watcher --- src/divviProtocol/registerReferral.test.ts | 56 +++++++++++---------- src/divviProtocol/registerReferral.ts | 58 ++++++++++++---------- 2 files changed, 60 insertions(+), 54 deletions(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 2342dfbaaf3..4f04ed0dc18 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -7,6 +7,7 @@ import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' import { createRegistrationTransactionsIfNeeded, + monitorRegistrationTransaction, sendPreparedRegistrationTransactions, } from 'src/divviProtocol/registerReferral' import { store } from 'src/redux/store' @@ -15,7 +16,7 @@ import { publicClient } from 'src/viem' import { ViemWallet } from 'src/viem/getLockableWallet' import { getMockStoreData } from 'test/utils' import { mockAccount } from 'test/values' -import { encodeFunctionData, parseEventLogs } from 'viem' +import { encodeFunctionData, Hash, parseEventLogs } from 'viem' // Note: Statsig is not directly used by this module, but mocking it prevents // require cycles from impacting the tests. @@ -164,15 +165,8 @@ describe('sendPreparedRegistrationTransactions', () => { sendRawTransaction: jest.fn(async () => '0xhash'), } as any as ViemWallet - it('sends transactions and updates store on success', async () => { + it('sends transactions and spawns the monitor transaction saga', async () => { const mockNonce = 157 - jest.mocked(parseEventLogs).mockReturnValue([ - { - args: { - protocolId: '0x62bd0dd2bb37b275249fe0ec6a61b0fb5adafd50d05a41adb9e1cbfd41ab0607', - }, - }, - ] as unknown as ReturnType) await expectSaga( sendPreparedRegistrationTransactions, @@ -184,14 +178,14 @@ describe('sendPreparedRegistrationTransactions', () => { .provide([ [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], [matchers.call.fn(mockViemWallet.sendRawTransaction), '0xhash'], - [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'success' }], + [matchers.spawn.fn(monitorRegistrationTransaction), null], ]) - .put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .spawn(monitorRegistrationTransaction, '0xhash', NetworkId['op-mainnet']) .returns(mockNonce + 1) .run() }) - it('handles reverted transaction and does not update the store', async () => { + it('does not throw on failure during sending to network, and returns the original nonce', async () => { const mockNonce = 157 await expectSaga( @@ -203,30 +197,38 @@ describe('sendPreparedRegistrationTransactions', () => { ) .provide([ [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], - [matchers.call.fn(mockViemWallet.sendRawTransaction), '0xhash'], - [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'reverted' }], + [matchers.call.fn(mockViemWallet.sendRawTransaction), throwError(new Error('failure'))], ]) .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) - .returns(mockNonce + 1) + .returns(mockNonce) .run() }) +}) - it('does not throw on failure during sending to network, and returns the original nonce', async () => { - const mockNonce = 157 +describe('monitorRegistrationTransaction', () => { + it('updates the store on successful transaction', async () => { + jest.mocked(parseEventLogs).mockReturnValue([ + { + args: { + protocolId: '0x62bd0dd2bb37b275249fe0ec6a61b0fb5adafd50d05a41adb9e1cbfd41ab0607', // keccak256(stringToHex('beefy')) + }, + }, + ] as unknown as ReturnType) - await expectSaga( - sendPreparedRegistrationTransactions, - [mockBeefyRegistrationTx], - NetworkId['op-mainnet'], - mockViemWallet, - mockNonce - ) + await expectSaga(monitorRegistrationTransaction, '0xhash' as Hash, NetworkId['op-mainnet']) .provide([ - [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], - [matchers.call.fn(mockViemWallet.sendRawTransaction), throwError(new Error('failure'))], + [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'success' }], + ]) + .put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .run() + }) + + it('does not update the store on reverted transaction', async () => { + await expectSaga(monitorRegistrationTransaction, '0xhash' as Hash, NetworkId['op-mainnet']) + .provide([ + [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'reverted' }], ]) .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) - .returns(mockNonce) .run() }) }) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index e3cca3fbc1d..81547c08650 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -12,8 +12,8 @@ import { SerializableTransactionRequest } from 'src/viem/preparedTransactionSeri import { TransactionRequest } from 'src/viem/prepareTransactions' import { networkIdToNetwork } from 'src/web3/networkConfig' import { walletAddressSelector } from 'src/web3/selectors' -import { call, put } from 'typed-redux-saga' -import { Address, decodeFunctionData, encodeFunctionData, parseEventLogs } from 'viem' +import { call, put, spawn } from 'typed-redux-saga' +import { Address, decodeFunctionData, encodeFunctionData, Hash, parseEventLogs } from 'viem' const TAG = 'divviProtocol/registerReferral' @@ -149,31 +149,7 @@ export function* sendPreparedRegistrationTransactions( ) nonce = nonce + 1 - const receipt = yield* call( - [publicClient[networkIdToNetwork[networkId]], 'waitForTransactionReceipt'], - { - hash, - } - ) - - if (receipt.status === 'success') { - const parsedLogs = parseEventLogs({ - abi: registryContractAbi, - eventName: ['ReferralRegistered'], - logs: receipt.logs, - }) - - const protocolId = supportedProtocolIdHashes[parsedLogs[0].args.protocolId] - if (!protocolId) { - // this should never happen, since we specify the protocolId in the prepareTransactions step - Logger.error( - `${TAG}/sendPreparedRegistrationTransactions`, - `Unknown protocolId received from transaction ${hash}` - ) - continue - } - yield* put(divviRegistrationCompleted(networkId, protocolId)) - } + yield* spawn(monitorRegistrationTransaction, hash, networkId) } catch (error) { Logger.error( `${TAG}/sendPreparedRegistrationTransactions`, @@ -185,3 +161,31 @@ export function* sendPreparedRegistrationTransactions( return nonce } + +export function* monitorRegistrationTransaction(hash: Hash, networkId: NetworkId) { + const receipt = yield* call( + [publicClient[networkIdToNetwork[networkId]], 'waitForTransactionReceipt'], + { + hash, + } + ) + + if (receipt.status === 'success') { + const parsedLogs = parseEventLogs({ + abi: registryContractAbi, + eventName: ['ReferralRegistered'], + logs: receipt.logs, + }) + + const protocolId = supportedProtocolIdHashes[parsedLogs[0].args.protocolId] + if (!protocolId) { + // this should never happen, since we specify the protocolId in the prepareTransactions step + Logger.error( + `${TAG}/sendPreparedRegistrationTransactions`, + `Unknown protocolId received from transaction ${hash}` + ) + return + } + yield* put(divviRegistrationCompleted(networkId, protocolId)) + } +} From 9ff81a3ab5c711772051ecabda5f6965b82a4147 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Mon, 17 Feb 2025 18:21:51 +0100 Subject: [PATCH 21/27] refactor: single referral tx --- src/divviProtocol/abi/Registry.ts | 593 +++++++++++++++++++-- src/divviProtocol/registerReferral.test.ts | 18 +- src/divviProtocol/registerReferral.ts | 151 +++--- src/viem/prepareTransactions.test.ts | 6 +- src/viem/prepareTransactions.ts | 4 +- src/viem/saga.ts | 20 +- 6 files changed, 635 insertions(+), 157 deletions(-) diff --git a/src/divviProtocol/abi/Registry.ts b/src/divviProtocol/abi/Registry.ts index b5fe3c3a7c3..50f6027b82a 100644 --- a/src/divviProtocol/abi/Registry.ts +++ b/src/divviProtocol/abi/Registry.ts @@ -2,14 +2,78 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'address', + name: 'owner', + type: 'address', + }, + { + internalType: 'uint48', + name: 'transferDelay', + type: 'uint48', + }, + ], + stateMutability: 'nonpayable', + type: 'constructor', + }, + { + inputs: [], + name: 'AccessControlBadConfirmation', + type: 'error', + }, + { + inputs: [ + { + internalType: 'uint48', + name: 'schedule', + type: 'uint48', + }, + ], + name: 'AccessControlEnforcedDefaultAdminDelay', + type: 'error', + }, + { + inputs: [], + name: 'AccessControlEnforcedDefaultAdminRules', + type: 'error', + }, + { + inputs: [ + { + internalType: 'address', + name: 'defaultAdmin', + type: 'address', + }, + ], + name: 'AccessControlInvalidDefaultAdmin', + type: 'error', + }, + { + inputs: [ + { + internalType: 'address', + name: 'account', + type: 'address', + }, + { + internalType: 'bytes32', + name: 'neededRole', + type: 'bytes32', + }, + ], + name: 'AccessControlUnauthorizedAccount', + type: 'error', + }, + { + inputs: [ + { + internalType: 'bytes32', name: 'protocolId', - type: 'string', + type: 'bytes32', }, { - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, ], name: 'ReferrerNotRegistered', @@ -18,14 +82,30 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'uint8', + name: 'bits', + type: 'uint8', + }, + { + internalType: 'uint256', + name: 'value', + type: 'uint256', + }, + ], + name: 'SafeCastOverflowedUintDowncast', + type: 'error', + }, + { + inputs: [ + { + internalType: 'bytes32', name: 'protocolId', - type: 'string', + type: 'bytes32', }, { - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, { internalType: 'address', @@ -36,20 +116,70 @@ export const registryContractAbi = [ name: 'UserAlreadyRegistered', type: 'error', }, + { + anonymous: false, + inputs: [], + name: 'DefaultAdminDelayChangeCanceled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: 'uint48', + name: 'newDelay', + type: 'uint48', + }, + { + indexed: false, + internalType: 'uint48', + name: 'effectSchedule', + type: 'uint48', + }, + ], + name: 'DefaultAdminDelayChangeScheduled', + type: 'event', + }, + { + anonymous: false, + inputs: [], + name: 'DefaultAdminTransferCanceled', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + { + indexed: false, + internalType: 'uint48', + name: 'acceptSchedule', + type: 'uint48', + }, + ], + name: 'DefaultAdminTransferScheduled', + type: 'event', + }, { anonymous: false, inputs: [ { indexed: true, - internalType: 'string', + internalType: 'bytes32', name: 'protocolId', - type: 'string', + type: 'bytes32', }, { indexed: true, - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, { indexed: true, @@ -66,15 +196,15 @@ export const registryContractAbi = [ inputs: [ { indexed: true, - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, { - indexed: true, - internalType: 'string[]', + indexed: false, + internalType: 'bytes32[]', name: 'protocolIds', - type: 'string[]', + type: 'bytes32[]', }, { indexed: false, @@ -93,19 +223,186 @@ export const registryContractAbi = [ type: 'event', }, { + anonymous: false, inputs: [ { - internalType: 'string', - name: 'referrerId', - type: 'string', + indexed: true, + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'previousAdminRole', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'bytes32', + name: 'newAdminRole', + type: 'bytes32', + }, + ], + name: 'RoleAdminChanged', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleGranted', + type: 'event', + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + indexed: true, + internalType: 'address', + name: 'account', + type: 'address', + }, + { + indexed: true, + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + name: 'RoleRevoked', + type: 'event', + }, + { + inputs: [], + name: 'DEFAULT_ADMIN_ROLE', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'acceptDefaultAdminTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + ], + name: 'beginDefaultAdminTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'cancelDefaultAdminTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'uint48', + name: 'newDelay', + type: 'uint48', + }, + ], + name: 'changeDefaultAdminDelay', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'defaultAdmin', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'defaultAdminDelay', + outputs: [ + { + internalType: 'uint48', + name: '', + type: 'uint48', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'defaultAdminDelayIncreaseWait', + outputs: [ + { + internalType: 'uint48', + name: '', + type: 'uint48', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'providerId', + type: 'bytes32', }, ], name: 'getProtocols', outputs: [ { - internalType: 'string[]', + internalType: 'bytes32[]', name: '', - type: 'string[]', + type: 'bytes32[]', }, ], stateMutability: 'view', @@ -114,17 +411,17 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'bytes32', name: 'protocolId', - type: 'string', + type: 'bytes32', }, ], name: 'getReferrers', outputs: [ { - internalType: 'string[]', + internalType: 'bytes32[]', name: '', - type: 'string[]', + type: 'bytes32[]', }, ], stateMutability: 'view', @@ -133,9 +430,9 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, ], name: 'getRewardAddress', @@ -152,14 +449,14 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'bytes32', name: 'protocolId', - type: 'string', + type: 'bytes32', }, { - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, ], name: 'getRewardRate', @@ -176,14 +473,33 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + ], + name: 'getRoleAdmin', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', name: 'protocolId', - type: 'string', + type: 'bytes32', }, { - internalType: 'string', + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, ], name: 'getUsers', @@ -205,17 +521,132 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'grantRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'hasRole', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'address', + name: 'userAddress', + type: 'address', + }, + { + internalType: 'bytes32[]', + name: 'protocolIds', + type: 'bytes32[]', + }, + ], + name: 'isUserRegistered', + outputs: [ + { + internalType: 'bool[]', + name: '', + type: 'bool[]', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pendingDefaultAdmin', + outputs: [ + { + internalType: 'address', + name: 'newAdmin', + type: 'address', + }, + { + internalType: 'uint48', + name: 'schedule', + type: 'uint48', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'pendingDefaultAdminDelay', + outputs: [ + { + internalType: 'uint48', + name: 'newDelay', + type: 'uint48', + }, + { + internalType: 'uint48', + name: 'schedule', + type: 'uint48', + }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', name: 'referrerId', - type: 'string', + type: 'bytes32', }, { - internalType: 'string', - name: 'protocolId', - type: 'string', + internalType: 'bytes32[]', + name: 'protocolIds', + type: 'bytes32[]', }, ], - name: 'registerReferral', + name: 'registerReferrals', outputs: [], stateMutability: 'nonpayable', type: 'function', @@ -223,23 +654,23 @@ export const registryContractAbi = [ { inputs: [ { - internalType: 'string', - name: '_referrerId', - type: 'string', + internalType: 'bytes32', + name: 'referrerId', + type: 'bytes32', }, { - internalType: 'string[]', - name: '_protocolIds', - type: 'string[]', + internalType: 'bytes32[]', + name: 'protocolIds', + type: 'bytes32[]', }, { internalType: 'uint256[]', - name: '_rewardRates', + name: 'rewardRates', type: 'uint256[]', }, { internalType: 'address', - name: '_rewardAddress', + name: 'rewardAddress', type: 'address', }, ], @@ -248,4 +679,66 @@ export const registryContractAbi = [ stateMutability: 'nonpayable', type: 'function', }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'renounceRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'role', + type: 'bytes32', + }, + { + internalType: 'address', + name: 'account', + type: 'address', + }, + ], + name: 'revokeRole', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [], + name: 'rollbackDefaultAdminDelay', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [ + { + internalType: 'bytes4', + name: 'interfaceId', + type: 'bytes4', + }, + ], + name: 'supportsInterface', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool', + }, + ], + stateMutability: 'view', + type: 'function', + }, ] as const diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 4f04ed0dc18..e24d31f5bb1 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -6,9 +6,9 @@ import * as config from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' import { - createRegistrationTransactionsIfNeeded, + createRegistrationTransactionIfNeeded, monitorRegistrationTransaction, - sendPreparedRegistrationTransactions, + sendPreparedRegistrationTransaction, } from 'src/divviProtocol/registerReferral' import { store } from 'src/redux/store' import { NetworkId } from 'src/transactions/types' @@ -54,7 +54,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { it('returns no transactions if referrer id is not set', async () => { jest.mocked(config).DIVVI_REFERRER_ID = undefined - const result = await createRegistrationTransactionsIfNeeded({ + const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) expect(result).toEqual([]) @@ -73,7 +73,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { throw new Error('Unexpected read contract call.') }) - const result = await createRegistrationTransactionsIfNeeded({ + const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) expect(result).toEqual([]) @@ -89,7 +89,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { }, }) ) - const result = await createRegistrationTransactionsIfNeeded({ + const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) expect(result).toEqual([]) @@ -111,7 +111,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { throw new Error('Unexpected read contract call.') }) - const result = await createRegistrationTransactionsIfNeeded({ + const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) expect(result).toEqual([ @@ -142,7 +142,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { throw new Error('Unexpected read contract call.') }) - const result = await createRegistrationTransactionsIfNeeded({ + const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) expect(result).toEqual([ @@ -169,7 +169,7 @@ describe('sendPreparedRegistrationTransactions', () => { const mockNonce = 157 await expectSaga( - sendPreparedRegistrationTransactions, + sendPreparedRegistrationTransaction, [mockBeefyRegistrationTx], NetworkId['op-mainnet'], mockViemWallet, @@ -189,7 +189,7 @@ describe('sendPreparedRegistrationTransactions', () => { const mockNonce = 157 await expectSaga( - sendPreparedRegistrationTransactions, + sendPreparedRegistrationTransaction, [mockBeefyRegistrationTx], NetworkId['op-mainnet'], mockViemWallet, diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 81547c08650..a292250b01e 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -13,7 +13,15 @@ import { TransactionRequest } from 'src/viem/prepareTransactions' import { networkIdToNetwork } from 'src/web3/networkConfig' import { walletAddressSelector } from 'src/web3/selectors' import { call, put, spawn } from 'typed-redux-saga' -import { Address, decodeFunctionData, encodeFunctionData, Hash, parseEventLogs } from 'viem' +import { + Address, + decodeFunctionData, + encodeFunctionData, + Hash, + Hex, + parseEventLogs, + stringToHex, +} from 'viem' const TAG = 'divviProtocol/registerReferral' @@ -25,7 +33,7 @@ export function isRegistrationTransaction(tx: TransactionRequest | SerializableT decodeFunctionData({ abi: registryContractAbi, data: tx.data, - }).functionName === 'registerReferral' + }).functionName === 'registerReferrals' ) } catch (error) { // decodeFunctionData will throw if the data does not match any function in @@ -36,18 +44,18 @@ export function isRegistrationTransaction(tx: TransactionRequest | SerializableT } } -export async function createRegistrationTransactionsIfNeeded({ +export async function createRegistrationTransactionIfNeeded({ networkId, }: { networkId: NetworkId -}): Promise { +}): Promise { const referrerId = DIVVI_REFERRER_ID if (!referrerId) { Logger.debug( `${TAG}/createRegistrationTransactionsIfNeeded`, - 'No referrer id set. Skipping registration transactions.' + 'No referrer id set. Skipping registration transaction.' ) - return [] + return null } // Caching registration status in Redux reduces on-chain checks but doesn’t guarantee @@ -59,107 +67,84 @@ export async function createRegistrationTransactionsIfNeeded({ (protocol) => !completedRegistrations.has(protocol) ) if (pendingRegistrations.length === 0) { - return [] + return null } - // Ensure the referrer and protocol are registered before creating - // transactions to prevent gas estimation failures later. const client = publicClient[networkIdToNetwork[networkId]] - const protocolsEligibleForRegistration: string[] = [] const walletAddress = walletAddressSelector(store.getState()) as Address - await Promise.all( - pendingRegistrations.map(async (protocolId) => { - try { - const [registeredReferrers, registeredUsers] = await Promise.all([ - client.readContract({ - address: REGISTRY_CONTRACT_ADDRESS, - abi: registryContractAbi, - functionName: 'getReferrers', - args: [protocolId], - }), - // TODO: getUsers is not the correct call to get the registration - // status of the user for any given protocol, we need to modify this - // part once the correct call is available - client.readContract({ - address: REGISTRY_CONTRACT_ADDRESS, - abi: registryContractAbi, - functionName: 'getUsers', - args: [protocolId, referrerId], - }), - ]) - - if (!registeredReferrers.includes(referrerId)) { - Logger.error( - `${TAG}/createRegistrationTransactionsIfNeeded`, - `Referrer "${referrerId}" is not registered for protocol "${protocolId}". Skipping registration transaction.` - ) - return - } + const protocolsPendingRegistration = pendingRegistrations.map((protocol) => + stringToHex(protocol, { size: 32 }) + ) + const protocolsEligibleForRegistration: Hex[] = [] - if (registeredUsers[0].some((address) => address.toLowerCase() === walletAddress)) { - Logger.debug( - `${TAG}/createRegistrationTransactionsIfNeeded`, - `Referral is already registered for protocol "${protocolId}". Skipping registration transaction.` - ) - store.dispatch(divviRegistrationCompleted(networkId, protocolId)) - return - } + try { + const userRegistrationStatuses = await client.readContract({ + address: REGISTRY_CONTRACT_ADDRESS, + abi: registryContractAbi, + functionName: 'isUserRegistered', + args: [walletAddress, protocolsPendingRegistration], + }) - protocolsEligibleForRegistration.push(protocolId) - } catch (error) { - Logger.error( + userRegistrationStatuses.forEach((isRegistered, index) => { + if (isRegistered) { + Logger.debug( `${TAG}/createRegistrationTransactionsIfNeeded`, - `Error reading registration state for protocol "${protocolId}". Skipping registration transaction.`, - error + `User is already registered for protocol "${pendingRegistrations[index]}". Skipping registration transaction.` ) + store.dispatch(divviRegistrationCompleted(networkId, pendingRegistrations[index])) + } else { + protocolsEligibleForRegistration.push(protocolsPendingRegistration[index]) } }) - ) + } catch (error) { + Logger.error( + `${TAG}/createRegistrationTransactionsIfNeeded`, + `Error reading registration state. Skipping registration transaction.`, + error + ) + return null + } - return protocolsEligibleForRegistration.map((protocolId) => ({ + return { to: REGISTRY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: registryContractAbi, - functionName: 'registerReferral', - args: [referrerId, protocolId], + functionName: 'registerReferrals', + args: [stringToHex(referrerId, { size: 32 }), protocolsEligibleForRegistration], }), - })) + } } -export function* sendPreparedRegistrationTransactions( - txs: TransactionRequest[], +export function* sendPreparedRegistrationTransaction( + tx: TransactionRequest, networkId: NetworkId, wallet: ViemWallet, nonce: number ) { - for (let i = 0; i < txs.length; i++) { - try { - const signedTx = yield* call([wallet, 'signTransaction'], { - ...txs[i], - nonce, - } as any) - const hash = yield* call([wallet, 'sendRawTransaction'], { - serializedTransaction: signedTx, - }) + try { + const signedTx = yield* call([wallet, 'signTransaction'], { + tx, + nonce, + } as any) + const hash = yield* call([wallet, 'sendRawTransaction'], { + serializedTransaction: signedTx, + }) - Logger.debug( - `${TAG}/sendPreparedRegistrationTransactions`, - 'Successfully sent transaction to the network', - hash - ) - nonce = nonce + 1 + Logger.debug( + `${TAG}/sendPreparedRegistrationTransactions`, + 'Successfully sent transaction to the network', + hash + ) - yield* spawn(monitorRegistrationTransaction, hash, networkId) - } catch (error) { - Logger.error( - `${TAG}/sendPreparedRegistrationTransactions`, - `Failed to send or parse prepared registration transaction`, - error - ) - } + yield* spawn(monitorRegistrationTransaction, hash, networkId) + } catch (error) { + Logger.error( + `${TAG}/sendPreparedRegistrationTransactions`, + `Failed to send or parse prepared registration transaction`, + error + ) + throw error } - - return nonce } export function* monitorRegistrationTransaction(hash: Hash, networkId: NetworkId) { diff --git a/src/viem/prepareTransactions.test.ts b/src/viem/prepareTransactions.test.ts index 978d85e8950..f94a0f6309b 100644 --- a/src/viem/prepareTransactions.test.ts +++ b/src/viem/prepareTransactions.test.ts @@ -1,7 +1,7 @@ import BigNumber from 'bignumber.js' import AppAnalytics from 'src/analytics/AppAnalytics' import { TransactionEvents } from 'src/analytics/Events' -import { createRegistrationTransactionsIfNeeded } from 'src/divviProtocol/registerReferral' +import { createRegistrationTransactionIfNeeded } from 'src/divviProtocol/registerReferral' import { TokenBalanceWithAddress } from 'src/tokens/slice' import { Network, NetworkId } from 'src/transactions/types' import { estimateFeesPerGas } from 'src/viem/estimateFeesPerGas' @@ -59,7 +59,7 @@ jest.mock('src/divviProtocol/registerReferral') beforeEach(() => { jest.clearAllMocks() - jest.mocked(createRegistrationTransactionsIfNeeded).mockResolvedValue([]) + jest.mocked(createRegistrationTransactionIfNeeded).mockResolvedValue([]) }) describe('prepareTransactions module', () => { @@ -143,7 +143,7 @@ describe('prepareTransactions module', () => { const mockPublicClient = {} as unknown as jest.Mocked<(typeof publicClient)[Network.Celo]> describe('prepareTransactions function', () => { it('adds divvi registration transactions to the prepared transactions if needed', async () => { - mocked(createRegistrationTransactionsIfNeeded).mockResolvedValue([ + mocked(createRegistrationTransactionIfNeeded).mockResolvedValue([ { data: '0xregistrationData', to: '0xregistrationTarget', diff --git a/src/viem/prepareTransactions.ts b/src/viem/prepareTransactions.ts index a55dd0b7bde..6041ab95dd2 100644 --- a/src/viem/prepareTransactions.ts +++ b/src/viem/prepareTransactions.ts @@ -3,7 +3,7 @@ import AppAnalytics from 'src/analytics/AppAnalytics' import { TransactionEvents } from 'src/analytics/Events' import { TransactionOrigin } from 'src/analytics/types' import { STATIC_GAS_PADDING } from 'src/config' -import { createRegistrationTransactionsIfNeeded } from 'src/divviProtocol/registerReferral' +import { createRegistrationTransactionIfNeeded } from 'src/divviProtocol/registerReferral' import { NativeTokenBalance, TokenBalance, @@ -292,7 +292,7 @@ export async function prepareTransactions({ isGasSubsidized?: boolean origin: TransactionOrigin }): Promise { - const registrationTxs = await createRegistrationTransactionsIfNeeded({ + const registrationTxs = await createRegistrationTransactionIfNeeded({ networkId: feeCurrencies[0].networkId, }) if (registrationTxs.length > 0) { diff --git a/src/viem/saga.ts b/src/viem/saga.ts index b9d4f3e118e..35d8c709cca 100644 --- a/src/viem/saga.ts +++ b/src/viem/saga.ts @@ -1,6 +1,6 @@ import { isRegistrationTransaction, - sendPreparedRegistrationTransactions, + sendPreparedRegistrationTransaction, } from 'src/divviProtocol/registerReferral' import { navigate } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' @@ -56,10 +56,10 @@ export function* sendPreparedTransactions( } const preparedTransactions: TransactionRequest[] = [] - const preparedRegistrationTransactions: TransactionRequest[] = [] + let preparedRegistrationTransaction: TransactionRequest | null = null getPreparedTransactions(serializablePreparedTransactions).forEach((tx) => { if (isRegistrationTransaction(tx)) { - preparedRegistrationTransactions.push(tx) + preparedRegistrationTransaction = tx } else { preparedTransactions.push(tx) } @@ -89,15 +89,15 @@ export function* sendPreparedTransactions( blockTag: 'pending', }) - // if there are registration transactions, send them first so that the - // subsequent transactions can have the referral attribution, and update the nonce - if (preparedRegistrationTransactions.length > 0) { - nonce = yield* call( - sendPreparedRegistrationTransactions, - preparedRegistrationTransactions, + // if there is a registration transaction, send it first so that the + // subsequent transactions can have the referral attribution + if (preparedRegistrationTransaction) { + yield* call( + sendPreparedRegistrationTransaction, + preparedRegistrationTransaction, networkId, wallet, - nonce + nonce++ ) } From 655c798c8905945baec0c365c1f06014e7cd4c38 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Tue, 18 Feb 2025 14:29:32 +0100 Subject: [PATCH 22/27] fix: make it work with only one transaction --- src/app/actions.ts | 6 +-- src/app/reducers.ts | 2 +- src/divviProtocol/constants.ts | 8 +-- src/divviProtocol/registerReferral.ts | 71 ++++++++++++++++++--------- src/viem/prepareTransactions.ts | 6 +-- 5 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/app/actions.ts b/src/app/actions.ts index 50b62bf4dd8..06feea33bab 100644 --- a/src/app/actions.ts +++ b/src/app/actions.ts @@ -157,7 +157,7 @@ interface ToggleHideBalances { interface DivviRegistrationCompleted { type: Actions.DIVVI_REGISTRATION_COMPLETED networkId: NetworkId - protocolId: SupportedProtocolId + protocolIds: SupportedProtocolId[] } export type ActionTypes = @@ -341,11 +341,11 @@ export const toggleHideBalances = (): ToggleHideBalances => { export const divviRegistrationCompleted = ( networkId: NetworkId, - protocolId: SupportedProtocolId + protocolIds: SupportedProtocolId[] ): DivviRegistrationCompleted => { return { type: Actions.DIVVI_REGISTRATION_COMPLETED, networkId, - protocolId, + protocolIds, } } diff --git a/src/app/reducers.ts b/src/app/reducers.ts index cb400d712b4..04fa4d27cef 100644 --- a/src/app/reducers.ts +++ b/src/app/reducers.ts @@ -229,7 +229,7 @@ export const appReducer = ( ...state.divviRegistrations, [action.networkId]: [ ...(state.divviRegistrations[action.networkId] ?? []), - action.protocolId, + ...action.protocolIds, ], }, } diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts index 486ad0a01ac..a88e2de6257 100644 --- a/src/divviProtocol/constants.ts +++ b/src/divviProtocol/constants.ts @@ -1,8 +1,8 @@ -import { Address, keccak256, stringToHex } from 'viem' +import { Address } from 'viem' export const REGISTRY_CONTRACT_ADDRESS: Address = '0x5a1a1027ac1d828e7415af7d797fba2b0cdd5575' -const supportedProtocolIds = [ +export const supportedProtocolIds = [ 'beefy', 'tether', 'somm', @@ -21,7 +21,3 @@ const supportedProtocolIds = [ ] as const export type SupportedProtocolId = (typeof supportedProtocolIds)[number] - -export const supportedProtocolIdHashes = Object.fromEntries( - supportedProtocolIds.map((protocol) => [keccak256(stringToHex(protocol)), protocol]) -) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index a292250b01e..13db9d4c448 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -2,7 +2,11 @@ import { divviRegistrationCompleted } from 'src/app/actions' import { divviRegistrationsSelector } from 'src/app/selectors' import { DIVVI_PROTOCOL_IDS, DIVVI_REFERRER_ID } from 'src/config' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' -import { REGISTRY_CONTRACT_ADDRESS, supportedProtocolIdHashes } from 'src/divviProtocol/constants' +import { + REGISTRY_CONTRACT_ADDRESS, + SupportedProtocolId, + supportedProtocolIds, +} from 'src/divviProtocol/constants' import { store } from 'src/redux/store' import { NetworkId } from 'src/transactions/types' import Logger from 'src/utils/Logger' @@ -19,6 +23,7 @@ import { encodeFunctionData, Hash, Hex, + hexToString, parseEventLogs, stringToHex, } from 'viem' @@ -72,28 +77,27 @@ export async function createRegistrationTransactionIfNeeded({ const client = publicClient[networkIdToNetwork[networkId]] const walletAddress = walletAddressSelector(store.getState()) as Address - const protocolsPendingRegistration = pendingRegistrations.map((protocol) => + const pendingRegistrationsHex = pendingRegistrations.map((protocol) => stringToHex(protocol, { size: 32 }) ) - const protocolsEligibleForRegistration: Hex[] = [] + const referrerIdHex = stringToHex(referrerId, { size: 32 }) + + const protocolsToRegisterHex: Hex[] = [] + const protocolsAlreadyRegistered: SupportedProtocolId[] = [] try { - const userRegistrationStatuses = await client.readContract({ + const isUserRegisteredForProtocols = await client.readContract({ address: REGISTRY_CONTRACT_ADDRESS, abi: registryContractAbi, functionName: 'isUserRegistered', - args: [walletAddress, protocolsPendingRegistration], + args: [walletAddress, pendingRegistrationsHex], }) - userRegistrationStatuses.forEach((isRegistered, index) => { + isUserRegisteredForProtocols.forEach((isRegistered, index) => { if (isRegistered) { - Logger.debug( - `${TAG}/createRegistrationTransactionsIfNeeded`, - `User is already registered for protocol "${pendingRegistrations[index]}". Skipping registration transaction.` - ) - store.dispatch(divviRegistrationCompleted(networkId, pendingRegistrations[index])) + protocolsAlreadyRegistered.push(pendingRegistrations[index]) } else { - protocolsEligibleForRegistration.push(protocolsPendingRegistration[index]) + protocolsToRegisterHex.push(pendingRegistrationsHex[index]) } }) } catch (error) { @@ -105,12 +109,27 @@ export async function createRegistrationTransactionIfNeeded({ return null } + if (protocolsAlreadyRegistered.length > 0) { + Logger.debug( + `${TAG}/createRegistrationTransactionsIfNeeded`, + `User is already registered for protocols "${protocolsAlreadyRegistered.join( + ', ' + )}". Skipping registration for those protocols.` + ) + store.dispatch(divviRegistrationCompleted(networkId, protocolsAlreadyRegistered)) + } + + if (protocolsToRegisterHex.length === 0) { + return null + } + return { + from: walletAddress, to: REGISTRY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: registryContractAbi, functionName: 'registerReferrals', - args: [stringToHex(referrerId, { size: 32 }), protocolsEligibleForRegistration], + args: [referrerIdHex, protocolsToRegisterHex], }), } } @@ -123,7 +142,7 @@ export function* sendPreparedRegistrationTransaction( ) { try { const signedTx = yield* call([wallet, 'signTransaction'], { - tx, + ...tx, nonce, } as any) const hash = yield* call([wallet, 'sendRawTransaction'], { @@ -162,15 +181,19 @@ export function* monitorRegistrationTransaction(hash: Hash, networkId: NetworkId logs: receipt.logs, }) - const protocolId = supportedProtocolIdHashes[parsedLogs[0].args.protocolId] - if (!protocolId) { - // this should never happen, since we specify the protocolId in the prepareTransactions step - Logger.error( - `${TAG}/sendPreparedRegistrationTransactions`, - `Unknown protocolId received from transaction ${hash}` - ) - return - } - yield* put(divviRegistrationCompleted(networkId, protocolId)) + const registeredProtocolIds = parsedLogs + .map((log) => hexToString(log.args.protocolId)) + .filter((protocolId: string): protocolId is SupportedProtocolId => { + if ((supportedProtocolIds as readonly string[]).includes(protocolId)) { + return true + } else { + Logger.error( + `${TAG}/monitorRegistrationTransaction`, + `Received unsupported protocol id "${protocolId}" in ReferralRegistered event` + ) + return false + } + }) + yield* put(divviRegistrationCompleted(networkId, registeredProtocolIds)) } } diff --git a/src/viem/prepareTransactions.ts b/src/viem/prepareTransactions.ts index 6041ab95dd2..58ff67b5aff 100644 --- a/src/viem/prepareTransactions.ts +++ b/src/viem/prepareTransactions.ts @@ -292,11 +292,11 @@ export async function prepareTransactions({ isGasSubsidized?: boolean origin: TransactionOrigin }): Promise { - const registrationTxs = await createRegistrationTransactionIfNeeded({ + const registrationTx = await createRegistrationTransactionIfNeeded({ networkId: feeCurrencies[0].networkId, }) - if (registrationTxs.length > 0) { - baseTransactions.push(...registrationTxs) + if (registrationTx) { + baseTransactions.push(registrationTx) } if (!spendToken && spendTokenAmount.isGreaterThan(0)) { From d8de889c8d314936e83db81bb84c400666d51185 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Tue, 18 Feb 2025 14:49:27 +0100 Subject: [PATCH 23/27] fix: some tests --- src/divviProtocol/registerReferral.test.ts | 83 +++++++--------------- 1 file changed, 27 insertions(+), 56 deletions(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index e24d31f5bb1..fc20b4aa8a8 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -16,7 +16,7 @@ import { publicClient } from 'src/viem' import { ViemWallet } from 'src/viem/getLockableWallet' import { getMockStoreData } from 'test/utils' import { mockAccount } from 'test/values' -import { encodeFunctionData, Hash, parseEventLogs } from 'viem' +import { encodeFunctionData, Hash, parseEventLogs, stringToHex } from 'viem' // Note: Statsig is not directly used by this module, but mocking it prevents // require cycles from impacting the tests. @@ -35,11 +35,14 @@ mockStore.dispatch = jest.fn() jest.mock('src/config') +const beefyHex = stringToHex('beefy', { size: 32 }) +const sommHex = stringToHex('somm', { size: 32 }) +const referrerIdHex = stringToHex('referrer-id', { size: 32 }) const mockBeefyRegistrationTx = { data: encodeFunctionData({ abi: registryContractAbi, - functionName: 'registerReferral', - args: ['referrer-id', 'beefy'], + functionName: 'registerReferrals', + args: [referrerIdHex, [beefyHex]], }), to: REGISTRY_CONTRACT_ADDRESS, } @@ -51,35 +54,16 @@ describe('createRegistrationTransactionsIfNeeded', () => { jest.mocked(config).DIVVI_REFERRER_ID = 'referrer-id' }) - it('returns no transactions if referrer id is not set', async () => { + it('returns null if referrer id is not set', async () => { jest.mocked(config).DIVVI_REFERRER_ID = undefined const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) - expect(result).toEqual([]) + expect(result).toEqual(null) }) - it('returns no transactions if referrer is not registered', async () => { - jest - .spyOn(publicClient.optimism, 'readContract') - .mockImplementation(async ({ functionName, args }) => { - if (functionName === 'getReferrers') { - return ['unrelated-referrer-id'] // Referrer is not registered - } - if (functionName === 'getUsers' && args) { - return [[], []] // User is not registered - } - throw new Error('Unexpected read contract call.') - }) - - const result = await createRegistrationTransactionIfNeeded({ - networkId: NetworkId['op-mainnet'], - }) - expect(result).toEqual([]) - }) - - it('returns no transactions if all registrations are completed', async () => { + it('returns null if all registrations are completed', async () => { mockStore.getState.mockImplementationOnce(() => getMockStoreData({ app: { @@ -92,21 +76,18 @@ describe('createRegistrationTransactionsIfNeeded', () => { const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) - expect(result).toEqual([]) + expect(result).toEqual(null) }) - it('returns transactions for pending registrations only', async () => { + it('returns a transaction for pending registrations only, and updates the redux cache for registered protocols', async () => { jest .spyOn(publicClient.optimism, 'readContract') .mockImplementation(async ({ functionName, args }) => { if (functionName === 'getReferrers') { return ['unrelated-referrer-id', 'referrer-id'] // Referrer is registered } - if (functionName === 'getUsers' && args) { - if (args[0] === 'beefy') { - return [[], []] // User is not registered - } - return [[mockAccount], []] // User is registered + if (functionName === 'isUserRegistered' && args) { + return [true, false] // User is already registered for 'beefy' but not 'somm' } throw new Error('Unexpected read contract call.') }) @@ -114,16 +95,18 @@ describe('createRegistrationTransactionsIfNeeded', () => { const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) - expect(result).toEqual([ - { - data: encodeFunctionData({ - abi: registryContractAbi, - functionName: 'registerReferral', - args: ['referrer-id', 'beefy'], - }), - to: REGISTRY_CONTRACT_ADDRESS, - }, - ]) + expect(result).toEqual({ + data: encodeFunctionData({ + abi: registryContractAbi, + functionName: 'registerReferrals', + args: [referrerIdHex, [sommHex]], + }), + to: REGISTRY_CONTRACT_ADDRESS, + from: mockAccount.toLowerCase(), + }) + expect(mockStore.dispatch).toHaveBeenCalledWith( + divviRegistrationCompleted(NetworkId['op-mainnet'], ['beefy']) + ) }) it('handles errors in contract reads gracefully and returns no corresponding transactions', async () => { @@ -133,10 +116,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { if (functionName === 'getReferrers') { return ['unrelated-referrer-id', 'referrer-id'] // Referrer is registered } - if (functionName === 'getUsers' && args) { - if (args[0] === 'beefy') { - return [[], []] // User is not registered - } + if (functionName === 'isUserRegistered' && args) { throw new Error('Read error for protocol') // simulate error for other protocols } throw new Error('Unexpected read contract call.') @@ -145,16 +125,7 @@ describe('createRegistrationTransactionsIfNeeded', () => { const result = await createRegistrationTransactionIfNeeded({ networkId: NetworkId['op-mainnet'], }) - expect(result).toEqual([ - { - data: encodeFunctionData({ - abi: registryContractAbi, - functionName: 'registerReferral', - args: ['referrer-id', 'beefy'], - }), - to: REGISTRY_CONTRACT_ADDRESS, - }, - ]) + expect(result).toEqual(null) }) }) From 22996d68274f26ca4daee40ee7c0d913609d1b0d Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Tue, 18 Feb 2025 15:01:26 +0100 Subject: [PATCH 24/27] fix: more tests --- src/viem/prepareTransactions.test.ts | 12 +++++------- src/viem/saga.test.ts | 23 +++++++++++++---------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/viem/prepareTransactions.test.ts b/src/viem/prepareTransactions.test.ts index f94a0f6309b..237e98b3ecb 100644 --- a/src/viem/prepareTransactions.test.ts +++ b/src/viem/prepareTransactions.test.ts @@ -59,7 +59,7 @@ jest.mock('src/divviProtocol/registerReferral') beforeEach(() => { jest.clearAllMocks() - jest.mocked(createRegistrationTransactionIfNeeded).mockResolvedValue([]) + jest.mocked(createRegistrationTransactionIfNeeded).mockResolvedValue(null) }) describe('prepareTransactions module', () => { @@ -143,12 +143,10 @@ describe('prepareTransactions module', () => { const mockPublicClient = {} as unknown as jest.Mocked<(typeof publicClient)[Network.Celo]> describe('prepareTransactions function', () => { it('adds divvi registration transactions to the prepared transactions if needed', async () => { - mocked(createRegistrationTransactionIfNeeded).mockResolvedValue([ - { - data: '0xregistrationData', - to: '0xregistrationTarget', - }, - ]) + mocked(createRegistrationTransactionIfNeeded).mockResolvedValue({ + data: '0xregistrationData', + to: '0xregistrationTarget', + }) mocked(estimateFeesPerGas).mockResolvedValue({ maxFeePerGas: BigInt(1), maxPriorityFeePerGas: BigInt(2), diff --git a/src/viem/saga.test.ts b/src/viem/saga.test.ts index 6712f4850b9..acd04e82033 100644 --- a/src/viem/saga.test.ts +++ b/src/viem/saga.test.ts @@ -5,7 +5,7 @@ import { EffectProviders, StaticProvider, throwError } from 'redux-saga-test-pla import { call } from 'redux-saga/effects' import { registryContractAbi } from 'src/divviProtocol/abi/Registry' import { REGISTRY_CONTRACT_ADDRESS } from 'src/divviProtocol/constants' -import { sendPreparedRegistrationTransactions } from 'src/divviProtocol/registerReferral' +import { sendPreparedRegistrationTransaction } from 'src/divviProtocol/registerReferral' import { BaseStandbyTransaction, addStandbyTransaction } from 'src/transactions/slice' import { NetworkId, TokenTransactionTypeV2 } from 'src/transactions/types' import { ViemWallet } from 'src/viem/getLockableWallet' @@ -23,7 +23,7 @@ import { mockCusdTokenId, mockQRCodeRecipient, } from 'test/values' -import { encodeFunctionData } from 'viem' +import { encodeFunctionData, stringToHex } from 'viem' import { getTransactionCount } from 'viem/actions' const preparedTransactions: TransactionRequest[] = [ @@ -119,7 +119,7 @@ describe('sendPreparedTransactions', () => { .withState(createMockStore({}).getState()) .provide(createDefaultProviders()) .call(getViemWallet, networkConfig.viemChain.celo, false) - .not.call(sendPreparedRegistrationTransactions) + .not.call(sendPreparedRegistrationTransaction) .put( addStandbyTransaction({ ...mockStandbyTransactions[0], @@ -216,14 +216,17 @@ describe('sendPreparedTransactions', () => { ).rejects.toThrowError('No account found in the wallet') }) - it('sends the registration transactions first if there are any', async () => { + it('sends the registration transactions first if there is one', async () => { const mockPreparedRegistration: TransactionRequest = { from: mockAccount, to: REGISTRY_CONTRACT_ADDRESS, data: encodeFunctionData({ abi: registryContractAbi, - functionName: 'registerReferral', - args: ['some-referrer', 'some-protocol'], + functionName: 'registerReferrals', + args: [ + stringToHex('some-referrer', { size: 32 }), + [stringToHex('some-protocol', { size: 32 })], + ], }), gas: BigInt(59_480), maxFeePerGas: BigInt(12_000_000_000), @@ -243,8 +246,8 @@ describe('sendPreparedTransactions', () => { ...createDefaultProviders(), [ call( - sendPreparedRegistrationTransactions, - [mockPreparedRegistration], + sendPreparedRegistrationTransaction, + mockPreparedRegistration, networkConfig.defaultNetworkId, mockViemWallet, 10 @@ -254,8 +257,8 @@ describe('sendPreparedTransactions', () => { ]) .call(getViemWallet, networkConfig.viemChain.celo, false) .call( - sendPreparedRegistrationTransactions, - [mockPreparedRegistration], + sendPreparedRegistrationTransaction, + mockPreparedRegistration, networkConfig.defaultNetworkId, mockViemWallet, 10 From 15ddd07ed3ec2855630804146f1dc077be8afd16 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Tue, 18 Feb 2025 16:05:24 +0100 Subject: [PATCH 25/27] fix: tests --- src/divviProtocol/registerReferral.test.ts | 51 +++++++++++----------- src/divviProtocol/registerReferral.ts | 7 ++- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index fc20b4aa8a8..0fb44cf5c2a 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -83,9 +83,6 @@ describe('createRegistrationTransactionsIfNeeded', () => { jest .spyOn(publicClient.optimism, 'readContract') .mockImplementation(async ({ functionName, args }) => { - if (functionName === 'getReferrers') { - return ['unrelated-referrer-id', 'referrer-id'] // Referrer is registered - } if (functionName === 'isUserRegistered' && args) { return [true, false] // User is already registered for 'beefy' but not 'somm' } @@ -113,9 +110,6 @@ describe('createRegistrationTransactionsIfNeeded', () => { jest .spyOn(publicClient.optimism, 'readContract') .mockImplementation(async ({ functionName, args }) => { - if (functionName === 'getReferrers') { - return ['unrelated-referrer-id', 'referrer-id'] // Referrer is registered - } if (functionName === 'isUserRegistered' && args) { throw new Error('Read error for protocol') // simulate error for other protocols } @@ -141,7 +135,7 @@ describe('sendPreparedRegistrationTransactions', () => { await expectSaga( sendPreparedRegistrationTransaction, - [mockBeefyRegistrationTx], + mockBeefyRegistrationTx, NetworkId['op-mainnet'], mockViemWallet, mockNonce @@ -152,27 +146,27 @@ describe('sendPreparedRegistrationTransactions', () => { [matchers.spawn.fn(monitorRegistrationTransaction), null], ]) .spawn(monitorRegistrationTransaction, '0xhash', NetworkId['op-mainnet']) - .returns(mockNonce + 1) .run() }) - it('does not throw on failure during sending to network, and returns the original nonce', async () => { + it('throws on failure during sending to network', async () => { const mockNonce = 157 - await expectSaga( - sendPreparedRegistrationTransaction, - [mockBeefyRegistrationTx], - NetworkId['op-mainnet'], - mockViemWallet, - mockNonce - ) - .provide([ - [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], - [matchers.call.fn(mockViemWallet.sendRawTransaction), throwError(new Error('failure'))], - ]) - .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) - .returns(mockNonce) - .run() + await expect( + expectSaga( + sendPreparedRegistrationTransaction, + mockBeefyRegistrationTx, + NetworkId['op-mainnet'], + mockViemWallet, + mockNonce + ) + .provide([ + [matchers.call.fn(mockViemWallet.signTransaction), '0xsomeSerialisedTransaction'], + [matchers.call.fn(mockViemWallet.sendRawTransaction), throwError(new Error('failure'))], + ]) + .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], ['beefy'])) + .run() + ).rejects.toThrow() }) }) @@ -181,7 +175,12 @@ describe('monitorRegistrationTransaction', () => { jest.mocked(parseEventLogs).mockReturnValue([ { args: { - protocolId: '0x62bd0dd2bb37b275249fe0ec6a61b0fb5adafd50d05a41adb9e1cbfd41ab0607', // keccak256(stringToHex('beefy')) + protocolId: beefyHex, + }, + }, + { + args: { + protocolId: sommHex, }, }, ] as unknown as ReturnType) @@ -190,7 +189,7 @@ describe('monitorRegistrationTransaction', () => { .provide([ [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'success' }], ]) - .put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .put(divviRegistrationCompleted(NetworkId['op-mainnet'], ['beefy', 'somm'])) .run() }) @@ -199,7 +198,7 @@ describe('monitorRegistrationTransaction', () => { .provide([ [matchers.call.fn(publicClient.optimism.waitForTransactionReceipt), { status: 'reverted' }], ]) - .not.put(divviRegistrationCompleted(NetworkId['op-mainnet'], 'beefy')) + .not.put(divviRegistrationCompleted(expect.anything(), expect.anything())) .run() }) }) diff --git a/src/divviProtocol/registerReferral.ts b/src/divviProtocol/registerReferral.ts index 13db9d4c448..3937598b3d9 100644 --- a/src/divviProtocol/registerReferral.ts +++ b/src/divviProtocol/registerReferral.ts @@ -182,7 +182,7 @@ export function* monitorRegistrationTransaction(hash: Hash, networkId: NetworkId }) const registeredProtocolIds = parsedLogs - .map((log) => hexToString(log.args.protocolId)) + .map((log) => hexToString(log.args.protocolId, { size: 32 })) .filter((protocolId: string): protocolId is SupportedProtocolId => { if ((supportedProtocolIds as readonly string[]).includes(protocolId)) { return true @@ -194,6 +194,9 @@ export function* monitorRegistrationTransaction(hash: Hash, networkId: NetworkId return false } }) - yield* put(divviRegistrationCompleted(networkId, registeredProtocolIds)) + + if (registeredProtocolIds.length > 0) { + yield* put(divviRegistrationCompleted(networkId, registeredProtocolIds)) + } } } From 29ffa92b1b91892702e963aaedc9ba00a5e8ab4f Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 19 Feb 2025 09:02:55 +0100 Subject: [PATCH 26/27] chore: update contract address --- src/divviProtocol/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/divviProtocol/constants.ts b/src/divviProtocol/constants.ts index a88e2de6257..9b34f2a29e7 100644 --- a/src/divviProtocol/constants.ts +++ b/src/divviProtocol/constants.ts @@ -1,6 +1,6 @@ import { Address } from 'viem' -export const REGISTRY_CONTRACT_ADDRESS: Address = '0x5a1a1027ac1d828e7415af7d797fba2b0cdd5575' +export const REGISTRY_CONTRACT_ADDRESS: Address = '0xba9655677f4e42dd289f5b7888170bc0c7da8cdc' export const supportedProtocolIds = [ 'beefy', From 36215b0e964ab5ea859206f6405a6c26fe5bd6c3 Mon Sep 17 00:00:00 2001 From: Kathy Luo Date: Wed, 19 Feb 2025 09:07:34 +0100 Subject: [PATCH 27/27] chore: add one more test --- src/divviProtocol/registerReferral.test.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/divviProtocol/registerReferral.test.ts b/src/divviProtocol/registerReferral.test.ts index 0fb44cf5c2a..3df9ac63bdd 100644 --- a/src/divviProtocol/registerReferral.test.ts +++ b/src/divviProtocol/registerReferral.test.ts @@ -79,6 +79,25 @@ describe('createRegistrationTransactionsIfNeeded', () => { expect(result).toEqual(null) }) + it('returns null and updates redux if there is no cached redux status but the registrations have been done', async () => { + jest + .spyOn(publicClient.optimism, 'readContract') + .mockImplementation(async ({ functionName, args }) => { + if (functionName === 'isUserRegistered' && args) { + return [true, true] // User is already registered for both 'beefy' and 'somm' + } + throw new Error('Unexpected read contract call.') + }) + + const result = await createRegistrationTransactionIfNeeded({ + networkId: NetworkId['op-mainnet'], + }) + expect(result).toEqual(null) + expect(mockStore.dispatch).toHaveBeenCalledWith( + divviRegistrationCompleted(NetworkId['op-mainnet'], ['beefy', 'somm']) + ) + }) + it('returns a transaction for pending registrations only, and updates the redux cache for registered protocols', async () => { jest .spyOn(publicClient.optimism, 'readContract')