diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 2499ca4a199e..192fe5d9facc 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -11833,6 +11833,10 @@
"value": {
"message": "Value"
},
+ "variationalReferralSubtitle": {
+ "message": "Get +15% Omni points and Silver tier for 90 days with a MetaMask referral code.",
+ "description": "The subtitle of the Variational referral confirmation screen"
+ },
"version": {
"message": "Version"
},
diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json
index 2499ca4a199e..192fe5d9facc 100644
--- a/app/_locales/en_GB/messages.json
+++ b/app/_locales/en_GB/messages.json
@@ -11833,6 +11833,10 @@
"value": {
"message": "Value"
},
+ "variationalReferralSubtitle": {
+ "message": "Get +15% Omni points and Silver tier for 90 days with a MetaMask referral code.",
+ "description": "The subtitle of the Variational referral confirmation screen"
+ },
"version": {
"message": "Version"
},
diff --git a/app/images/variational-referral.png b/app/images/variational-referral.png
new file mode 100644
index 000000000000..614550d43bfd
Binary files /dev/null and b/app/images/variational-referral.png differ
diff --git a/app/scripts/controllers/preferences-controller.test.ts b/app/scripts/controllers/preferences-controller.test.ts
index 5eaeae8af218..f5bcc31c1572 100644
--- a/app/scripts/controllers/preferences-controller.test.ts
+++ b/app/scripts/controllers/preferences-controller.test.ts
@@ -815,6 +815,7 @@ describe('preferences controller', () => {
"asterdex": {},
"gmx": {},
"hyperliquid": {},
+ "variational": {},
},
"securityAlertsEnabled": true,
"snapRegistryList": {},
@@ -899,6 +900,7 @@ describe('preferences controller', () => {
"asterdex": {},
"gmx": {},
"hyperliquid": {},
+ "variational": {},
},
"securityAlertsEnabled": true,
"showSidePanelMigrationToast": false,
@@ -984,6 +986,7 @@ describe('preferences controller', () => {
"asterdex": {},
"gmx": {},
"hyperliquid": {},
+ "variational": {},
},
"securityAlertsEnabled": true,
"showSidePanelMigrationToast": false,
@@ -1120,6 +1123,7 @@ describe('preferences controller', () => {
},
[DefiReferralPartner.GMX]: {},
[DefiReferralPartner.AsterDEX]: {},
+ [DefiReferralPartner.Variational]: {},
},
},
});
@@ -1144,6 +1148,7 @@ describe('preferences controller', () => {
},
[DefiReferralPartner.GMX]: {},
[DefiReferralPartner.AsterDEX]: {},
+ [DefiReferralPartner.Variational]: {},
},
},
});
@@ -1185,6 +1190,7 @@ describe('preferences controller', () => {
},
[DefiReferralPartner.GMX]: {},
[DefiReferralPartner.AsterDEX]: {},
+ [DefiReferralPartner.Variational]: {},
},
},
});
@@ -1208,6 +1214,7 @@ describe('preferences controller', () => {
},
[DefiReferralPartner.GMX]: {},
[DefiReferralPartner.AsterDEX]: {},
+ [DefiReferralPartner.Variational]: {},
},
},
});
@@ -1232,6 +1239,9 @@ describe('preferences controller', () => {
expect(
controller.state.referrals[DefiReferralPartner.AsterDEX],
).toStrictEqual({});
+ expect(
+ controller.state.referrals[DefiReferralPartner.Variational],
+ ).toStrictEqual({});
});
it('deep merges referrals state to add new partners while preserving existing data', () => {
diff --git a/app/scripts/controllers/preferences-controller.ts b/app/scripts/controllers/preferences-controller.ts
index ace8fd110403..88bb419dac26 100644
--- a/app/scripts/controllers/preferences-controller.ts
+++ b/app/scripts/controllers/preferences-controller.ts
@@ -209,6 +209,7 @@ export const getDefaultPreferencesControllerState =
[DefiReferralPartner.AsterDEX]: {},
[DefiReferralPartner.GMX]: {},
[DefiReferralPartner.Hyperliquid]: {},
+ [DefiReferralPartner.Variational]: {},
},
});
diff --git a/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts b/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts
index e29b704e1ab3..024e882e4f38 100644
--- a/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts
+++ b/app/scripts/messenger-client-init/messengers/legacy-background-api-service-messenger.ts
@@ -30,6 +30,7 @@ export function getLegacyBackgroundApiServiceMessenger(
'NetworkController:findNetworkClientIdByChainId',
'NetworkController:getNetworkClientById',
'NetworkController:getNetworkConfigurationByNetworkClientId',
+ 'SelectedNetworkController:getNetworkClientIdForDomain',
'NetworkController:getSelectedNetworkClient',
'NetworkController:addNetwork',
'NetworkController:setActiveNetwork',
diff --git a/app/scripts/services/legacy-background-api-service.test.ts b/app/scripts/services/legacy-background-api-service.test.ts
index 3d692d79e0c0..ef426a04c7f1 100644
--- a/app/scripts/services/legacy-background-api-service.test.ts
+++ b/app/scripts/services/legacy-background-api-service.test.ts
@@ -6259,6 +6259,7 @@ describe('LegacyBackgroundApiService', () => {
describe('DeFi referral', () => {
const HYPERLIQUID = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Hyperliquid];
const GMX = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.GMX];
+ const VARIATIONAL = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Variational];
const mockTabId = 140;
const mockPermittedAccount = '0x123';
const mockPermittedAccounts = [mockPermittedAccount, '0x456'];
@@ -6366,6 +6367,26 @@ describe('LegacyBackgroundApiService', () => {
);
}
+ /**
+ * Registers the network handlers used by the requiredChainId gate.
+ *
+ * @param rootMessenger - The root messenger to register handlers on.
+ * @param chainId - The active chain ID for the partner's domain.
+ */
+ function registerPartnerNetwork(
+ rootMessenger: RootMessenger,
+ chainId: string,
+ ) {
+ rootMessenger.registerActionHandler(
+ 'SelectedNetworkController:getNetworkClientIdForDomain',
+ jest.fn().mockReturnValue('network-client-id'),
+ );
+ rootMessenger.registerActionHandler(
+ 'NetworkController:getNetworkConfigurationByNetworkClientId',
+ jest.fn().mockReturnValue({ chainId }),
+ );
+ }
+
beforeEach(() => {
jest.mocked(trackEvent).mockClear();
jest.mocked(checkGmxHasReferralCode).mockReset().mockResolvedValue(false);
@@ -7057,6 +7078,69 @@ describe('LegacyBackgroundApiService', () => {
);
});
});
+
+ describe('requiredChainId gate', () => {
+ it('does not proceed when the active chain does not match requiredChainId', async () => {
+ const getPermittedAccounts = jest
+ .fn()
+ .mockResolvedValue(mockPermittedAccounts);
+ await withService(
+ { options: { getPermittedAccounts } },
+ async ({ rootMessenger }) => {
+ const handlers = registerReferralHandlers(rootMessenger, {
+ featureFlags: { [DefiReferralPartner.Variational]: true },
+ referrals: { [DefiReferralPartner.Variational]: {} },
+ });
+ registerPartnerNetwork(rootMessenger, '0x1');
+
+ await rootMessenger.call(
+ 'LegacyBackgroundApiService:handleDefiReferral',
+ VARIATIONAL,
+ mockTabId,
+ ReferralTriggerType.NewConnection,
+ );
+
+ expect(handlers.add).not.toHaveBeenCalled();
+ expect(handlers.addReferralPassedAccount).not.toHaveBeenCalled();
+ },
+ );
+ });
+
+ it('proceeds when the active chain matches requiredChainId', async () => {
+ const getPermittedAccounts = jest
+ .fn()
+ .mockResolvedValue(mockPermittedAccounts);
+ await withService(
+ { options: { getPermittedAccounts } },
+ async ({ rootMessenger }) => {
+ const handlers = registerReferralHandlers(rootMessenger, {
+ featureFlags: { [DefiReferralPartner.Variational]: true },
+ referrals: { [DefiReferralPartner.Variational]: {} },
+ });
+ registerPartnerNetwork(rootMessenger, '0xa4b1');
+
+ await rootMessenger.call(
+ 'LegacyBackgroundApiService:handleDefiReferral',
+ VARIATIONAL,
+ mockTabId,
+ ReferralTriggerType.NewConnection,
+ );
+
+ expect(handlers.add).toHaveBeenCalledWith({
+ origin: VARIATIONAL.origin,
+ type: VARIATIONAL.approvalType,
+ requestData: {
+ learnMoreUrl: VARIATIONAL.learnMoreUrl,
+ partnerId: DefiReferralPartner.Variational,
+ partnerName: VARIATIONAL.name,
+ selectedAddress: mockPermittedAccount,
+ },
+ shouldShowRequest: true,
+ });
+ },
+ );
+ });
+ });
});
describe('handleDefiReferralOnPermittedAccountsAdded', () => {
@@ -7329,6 +7413,7 @@ function getMessenger(
'NetworkController:findNetworkClientIdByChainId',
'NetworkController:getNetworkClientById',
'NetworkController:getNetworkConfigurationByNetworkClientId',
+ 'SelectedNetworkController:getNetworkClientIdForDomain',
'NetworkController:getSelectedNetworkClient',
'NetworkController:addNetwork',
'NetworkController:setActiveNetwork',
diff --git a/app/scripts/services/legacy-background-api-service.ts b/app/scripts/services/legacy-background-api-service.ts
index 02cae1021605..f3f6443cdb1c 100644
--- a/app/scripts/services/legacy-background-api-service.ts
+++ b/app/scripts/services/legacy-background-api-service.ts
@@ -23,6 +23,7 @@ import {
NetworkEnablementControllerState,
NetworkEnablementControllerStateChangeEvent,
} from '@metamask/network-enablement-controller';
+import { SelectedNetworkControllerGetNetworkClientIdForDomainAction } from '@metamask/selected-network-controller';
import {
add0x,
bytesToHex,
@@ -678,6 +679,7 @@ type AllowedActions =
| SeedlessOnboardingControllerSubmitPasswordAction
| SeedlessOnboardingControllerSyncLatestGlobalPasswordAction
| SeedlessOnboardingControllerUpdateBackupMetadataStateAction
+ | SelectedNetworkControllerGetNetworkClientIdForDomainAction
| GetSignatureState
| ShieldControllerClearStateAction
| ShieldControllerStartAction
@@ -4598,6 +4600,25 @@ export class LegacyBackgroundApiService {
return;
}
+ // If the partner requires a specific chain and user's chain doesn't match,
+ // return early to avoid the referral code potentially not being applied.
+ // Don't write any account status so that the prompt can show on the next
+ // trigger (NewConnection or OnNavigateConnectedTab) once the user has switched chain
+ if (partner.requiredChainId) {
+ const networkClientId = this.#messenger.call(
+ 'SelectedNetworkController:getNetworkClientIdForDomain',
+ partner.origin,
+ );
+ const networkConfig = this.#messenger.call(
+ 'NetworkController:getNetworkConfigurationByNetworkClientId',
+ networkClientId,
+ );
+ const currentChainId = networkConfig?.chainId;
+ if (currentChainId !== partner.requiredChainId) {
+ return;
+ }
+ }
+
const { activePermittedAddressOverride } = options;
const activePermittedAccount =
(activePermittedAddressOverride &&
diff --git a/shared/constants/app.ts b/shared/constants/app.ts
index 2c2d3898405e..973b0c0bb364 100644
--- a/shared/constants/app.ts
+++ b/shared/constants/app.ts
@@ -167,6 +167,7 @@ export const MESSAGE_TYPE = {
HYPERLIQUID_REFERRAL_CONSENT: 'hyperliquid_referral_consent',
ASTERDEX_REFERRAL_CONSENT: 'asterdex_referral_consent',
GMX_REFERRAL_CONSENT: 'gmx_referral_consent',
+ VARIATIONAL_REFERRAL_CONSENT: 'variational_referral_consent',
} as const;
export type MessageType = (typeof MESSAGE_TYPE)[keyof typeof MESSAGE_TYPE];
@@ -176,6 +177,8 @@ export const HYPERLIQUID_APPROVAL_TYPE =
MESSAGE_TYPE.HYPERLIQUID_REFERRAL_CONSENT;
export const ASTERDEX_APPROVAL_TYPE = MESSAGE_TYPE.ASTERDEX_REFERRAL_CONSENT;
export const GMX_APPROVAL_TYPE = MESSAGE_TYPE.GMX_REFERRAL_CONSENT;
+export const VARIATIONAL_APPROVAL_TYPE =
+ MESSAGE_TYPE.VARIATIONAL_REFERRAL_CONSENT;
export const SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES = {
confirmAccountCreation: 'snap_manageAccounts:confirmAccountCreation',
diff --git a/shared/constants/defi-referrals.ts b/shared/constants/defi-referrals.ts
index 6c003d65fa98..2c027069ab60 100644
--- a/shared/constants/defi-referrals.ts
+++ b/shared/constants/defi-referrals.ts
@@ -5,6 +5,7 @@ export enum DefiReferralPartner {
Hyperliquid = 'hyperliquid',
GMX = 'gmx',
AsterDEX = 'asterdex',
+ Variational = 'variational',
}
/**
@@ -32,6 +33,12 @@ export type DefiReferralPartnerConfig = {
approvalType: string;
/** Connection flow prior to showing the referral screen */
connectionFlow: ConnectionFlow;
+ /**
+ * When set, the referral prompt is suppressed if the user's active chain
+ * doesn't match this hex chain ID. This is used in cases where the partner's
+ * code application logic only works when on the required chain (e.g. Variational)
+ */
+ requiredChainId?: string;
};
/**
@@ -70,6 +77,16 @@ export const DEFI_REFERRAL_PARTNERS: Record<
approvalType: 'asterdex_referral_consent',
connectionFlow: 'permissions_then_signature',
},
+ [DefiReferralPartner.Variational]: {
+ id: DefiReferralPartner.Variational,
+ name: 'Variational',
+ origin: 'https://omni.variational.io',
+ referralUrl: 'https://omni.variational.io?ref=OMNIMETAMASK',
+ learnMoreUrl: 'https://docs.variational.io/omni/rewards/referrals',
+ approvalType: 'variational_referral_consent',
+ connectionFlow: 'permissions',
+ requiredChainId: '0xa4b1', // Arbitrum
+ },
};
/**
diff --git a/test/e2e/fixtures/default-fixture.json b/test/e2e/fixtures/default-fixture.json
index ce4e05a3d168..2be3bb494530 100644
--- a/test/e2e/fixtures/default-fixture.json
+++ b/test/e2e/fixtures/default-fixture.json
@@ -1057,7 +1057,8 @@
"referrals": {
"asterdex": {},
"gmx": {},
- "hyperliquid": {}
+ "hyperliquid": {},
+ "variational": {}
},
"securityAlertsEnabled": true,
"showSidePanelMigrationToast": false,
diff --git a/test/e2e/fixtures/onboarding-fixture.json b/test/e2e/fixtures/onboarding-fixture.json
index 41cf09ec2fbb..98fa0c5f4600 100644
--- a/test/e2e/fixtures/onboarding-fixture.json
+++ b/test/e2e/fixtures/onboarding-fixture.json
@@ -2046,7 +2046,8 @@
"referrals": {
"asterdex": {},
"gmx": {},
- "hyperliquid": {}
+ "hyperliquid": {},
+ "variational": {}
},
"securityAlertsEnabled": true,
"showSidePanelMigrationToast": false,
diff --git a/test/e2e/tests/settings/state-logs.json b/test/e2e/tests/settings/state-logs.json
index c81eb7bab3f3..9d9b61ef7971 100644
--- a/test/e2e/tests/settings/state-logs.json
+++ b/test/e2e/tests/settings/state-logs.json
@@ -1298,7 +1298,8 @@
"referrals": {
"asterdex": {},
"gmx": {},
- "hyperliquid": {}
+ "hyperliquid": {},
+ "variational": {}
},
"region": "string",
"requests": {
diff --git a/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx b/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx
index e6dc6265327d..ca87a149a0e1 100644
--- a/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx
+++ b/ui/pages/confirmations/confirmation/stories/defi-referral-consent.stories.tsx
@@ -3,6 +3,7 @@ import {
HYPERLIQUID_APPROVAL_TYPE,
GMX_APPROVAL_TYPE,
ASTERDEX_APPROVAL_TYPE,
+ VARIATIONAL_APPROVAL_TYPE,
} from '../../../../../shared/constants/app';
import {
DEFI_REFERRAL_PARTNERS,
@@ -15,6 +16,8 @@ const HYPERLIQUID_CONFIG =
DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Hyperliquid];
const GMX_CONFIG = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.GMX];
const ASTERDEX_CONFIG = DEFI_REFERRAL_PARTNERS[DefiReferralPartner.AsterDEX];
+const VARIATIONAL_CONFIG =
+ DEFI_REFERRAL_PARTNERS[DefiReferralPartner.Variational];
const STATE_MOCK_DEFAULT = {
metamask: {
@@ -23,6 +26,7 @@ const STATE_MOCK_DEFAULT = {
[DefiReferralPartner.Hyperliquid]: {},
[DefiReferralPartner.GMX]: {},
[DefiReferralPartner.AsterDEX]: {},
+ [DefiReferralPartner.Variational]: {},
},
},
},
@@ -103,3 +107,22 @@ export const AsterdexStory = (args: { selectedAddress: string }) => {
};
AsterdexStory.storyName = 'Asterdex';
+
+export const VariationalStory = (args: { selectedAddress: string }) => {
+ return (
+
+
+
+ );
+};
+
+VariationalStory.storyName = 'Variational';
diff --git a/ui/pages/confirmations/confirmation/templates/approval-templates.ts b/ui/pages/confirmations/confirmation/templates/approval-templates.ts
index 3f785f4c1e3b..c2a2b2248ebc 100644
--- a/ui/pages/confirmations/confirmation/templates/approval-templates.ts
+++ b/ui/pages/confirmations/confirmation/templates/approval-templates.ts
@@ -3,6 +3,7 @@ import {
HYPERLIQUID_APPROVAL_TYPE,
ASTERDEX_APPROVAL_TYPE,
GMX_APPROVAL_TYPE,
+ VARIATIONAL_APPROVAL_TYPE,
SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES,
} from '../../../../../shared/constants/app';
import createSnapAccount from './create-snap-account';
@@ -40,4 +41,5 @@ export const APPROVAL_TEMPLATES = {
[HYPERLIQUID_APPROVAL_TYPE]: defiReferralConsent,
[ASTERDEX_APPROVAL_TYPE]: defiReferralConsent,
[GMX_APPROVAL_TYPE]: defiReferralConsent,
+ [VARIATIONAL_APPROVAL_TYPE]: defiReferralConsent,
} as const;
diff --git a/ui/pages/confirmations/confirmation/templates/approval-types.ts b/ui/pages/confirmations/confirmation/templates/approval-types.ts
index bc0fe1cb4bc1..9e9aeecb5871 100644
--- a/ui/pages/confirmations/confirmation/templates/approval-types.ts
+++ b/ui/pages/confirmations/confirmation/templates/approval-types.ts
@@ -3,6 +3,7 @@ import {
HYPERLIQUID_APPROVAL_TYPE,
ASTERDEX_APPROVAL_TYPE,
GMX_APPROVAL_TYPE,
+ VARIATIONAL_APPROVAL_TYPE,
SNAP_MANAGE_ACCOUNTS_CONFIRMATION_TYPES,
} from '../../../../../shared/constants/app';
import type { IsEquivalent } from '../../../../../shared/types/type-level-utils';
@@ -29,6 +30,7 @@ export const TEMPLATED_CONFIRMATION_APPROVAL_TYPES = [
HYPERLIQUID_APPROVAL_TYPE,
ASTERDEX_APPROVAL_TYPE,
GMX_APPROVAL_TYPE,
+ VARIATIONAL_APPROVAL_TYPE,
] as const;
// ──────────────────────────────────────────────────────────────────────────
diff --git a/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx b/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx
index a2d3e4267727..897c9945b23d 100644
--- a/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx
+++ b/ui/pages/core/defi-referral-consent/defi-referral-consent-control.tsx
@@ -53,6 +53,7 @@ export const DefiReferralConsentControl: React.FC = ({
t('hyperliquidReferralSubtitle');
t('gmxReferralSubtitle');
t('asterdexReferralSubtitle');
+ t('variationalReferralSubtitle');
return (