Skip to content
14 changes: 13 additions & 1 deletion src/background/Wallet/Wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ import {
broadcastTransactionPatched,
checkEip712Tx,
} from 'src/modules/ethereum/account-abstraction/zksync-patch';
import type { DaylightEventParams, ScreenViewParams } from '../events';
import type {
DaylightEventParams,
FinalQuoteReceivedParams,
ScreenViewParams,
} from '../events';
import { emitter } from '../events';
import type { Credentials, SessionCredentials } from '../account/Credentials';
import { isSessionCredentials } from '../account/Credentials';
Expand Down Expand Up @@ -1500,6 +1504,14 @@ export class Wallet {
emitter.emit('screenView', params);
}

async finalQuoteReceived({
context,
params,
}: WalletMethodParams<FinalQuoteReceivedParams>) {
this.verifyInternalOrigin(context);
emitter.emit('finalQuoteReceived', params);
}

async daylightAction({
context,
params,
Expand Down
15 changes: 12 additions & 3 deletions src/background/events.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import type { ethers } from 'ethers';
import { createNanoEvents } from 'nanoevents';
import type { TypedData } from 'src/modules/ethereum/message-signing/TypedData';
import type { ChainId } from 'src/modules/ethereum/transactions/ChainId';
import type { AddEthereumChainParameter } from 'src/modules/ethereum/types/AddEthereumChainParameter';
import type { Chain } from 'src/modules/networks/Chain';
import type { AnalyticsFormData } from 'src/shared/analytics/shared/formDataToAnalytics';
import type { Quote } from 'src/shared/types/Quote';
import type {
MessageContextParams,
TransactionContextParams,
} from 'src/shared/types/SignatureContextParams';
import type { AddEthereumChainParameter } from 'src/modules/ethereum/types/AddEthereumChainParameter';
import type { ChainId } from 'src/modules/ethereum/transactions/ChainId';
import type { ButtonClickedParams } from 'src/shared/types/button-events';
import type { WindowType } from 'src/shared/types/UrlContext';
import type { ButtonClickedParams } from 'src/shared/types/button-events';
import type { State as GlobalPreferencesState } from './Wallet/GlobalPreferences';
import type { WalletOrigin } from './Wallet/model/WalletOrigin';
import type { WalletContainer } from './Wallet/model/types';
Expand All @@ -24,6 +26,12 @@ export interface ScreenViewParams {
windowType: WindowType;
}

export interface FinalQuoteReceivedParams {
quote: Quote;
formData: AnalyticsFormData;
scope: 'Swap' | 'Bridge';
}

export interface DaylightEventParams {
event_name: string;
address: string;
Expand Down Expand Up @@ -82,4 +90,5 @@ export const emitter = createNanoEvents<{
eip6963SupportDetected: (data: { origin: string }) => void;
uiClosed: (data: { url: string | null }) => void;
buttonClicked: (data: ButtonClickedParams) => void;
finalQuoteReceived: (data: FinalQuoteReceivedParams) => void;
}>();
26 changes: 26 additions & 0 deletions src/shared/analytics/analytics.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
isMnemonicContainer,
isPrivateKeyContainer,
} from '../types/validators';
import { invariant } from '../invariant';
import { getError } from '../errors/getError';
import { runtimeStore } from '../core/runtime-store';
import { productionVersion } from '../packageVersion';
Expand All @@ -34,6 +35,7 @@ import {
getChainBreakdown,
getOwnedWalletsPortolio,
} from './shared/mixpanel-data-helpers';
import { formDataToAnalytics } from './shared/formDataToAnalytics';

function queryWalletProvider(account: Account, address: string) {
const apiLayer = account.getCurrentWallet();
Expand Down Expand Up @@ -327,6 +329,30 @@ function trackAppEvents({ account }: { account: Account }) {
}
});

emitter.on('finalQuoteReceived', async ({ quote, formData, scope }) => {
invariant(
scope === 'Swap' || scope === 'Bridge',
'scope can be either Swap or Bridge'
);
const analyticsData = await formDataToAnalytics(scope, {
formData,
quote,
currency: 'usd',
});
const params = createParams({
request_name: 'swap_form_filled_out',
screen_name: scope,
client_scope: scope,
action_type: scope,
Copy link
Collaborator

Choose a reason for hiding this comment

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

action_type can be Trade or Send 🫠🫠🫠

Copy link
Contributor Author

@vyorkin vyorkin Apr 25, 2025

Choose a reason for hiding this comment

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

in general it can, but not in formFilledOut event handler
so the scope should be of type scope: "Swap" | "Bridge" in FormFilledOutParams (if I'm not missing smth)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll update this part in a minute

Copy link
Contributor Author

Choose a reason for hiding this comment

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

updated ✅

...analyticsData,
});
// Note that `finalQuoteReceived` is not exactly the same as `swap_form_filled_out` (or "Swap Form Filled Out").
// However, the analytics task specifically states: "Trigger the event when the client receives the final quote".
sendToMetabase('swap_form_filled_out', params);
const mixpanelParams = omit(params, ['request_name', 'wallet_address']);
mixpanelTrack(account, 'Transaction: Swap Form Filled Out', mixpanelParams);
});

emitter.on('firstScreenView', () => {
mixpanelTrack(account, 'General: Launch first time', {});
});
Expand Down
3 changes: 2 additions & 1 deletion src/shared/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ type MetabaseEvent =
| 'eip_6963_support'
| 'add_wallet'
| 'background_script_reloaded'
| 'hold_to_sign_prerefence';
| 'hold_to_sign_prerefence'
| 'swap_form_filled_out';

type BaseParams<E = MetabaseEvent> = { request_name: E };

Expand Down
27 changes: 8 additions & 19 deletions src/shared/analytics/shared/addressActionToAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { ActionAsset } from 'defi-sdk';
import { isTruthy } from 'is-truthy-ts';
import { getFungibleAsset } from 'src/modules/ethereum/transactions/actionAsset';
import type { AnyAddressAction } from 'src/modules/ethereum/transactions/addressAction';
import type { Chain } from 'src/modules/networks/Chain';
import { createChain } from 'src/modules/networks/Chain';
import { getDecimals } from 'src/modules/networks/asset';
import type { Quote } from 'src/shared/types/Quote';
import { baseToCommon } from 'src/shared/units/convert';
import { assetQuantityToValue, toMaybeArr } from './helpers';

interface AnalyticsTransactionData {
action_type: string;
Expand All @@ -23,28 +23,23 @@ interface AnalyticsTransactionData {
output_chain?: string;
}

interface AssetQuantity {
interface ActionAssetQuantity {
asset: ActionAsset;
quantity: string | null;
}

function assetQuantityToValue(
assetWithQuantity: AssetQuantity,
function actionAssetQuantityToValue(
assetWithQuantity: ActionAssetQuantity,
chain: Chain
): number {
const { asset: actionAsset, quantity } = assetWithQuantity;
const asset = getFungibleAsset(actionAsset);
if (asset && 'implementations' in asset && asset.price && quantity !== null) {
return baseToCommon(quantity, getDecimals({ asset, chain }))
.times(asset.price.value)
.toNumber();
}
return 0;
return assetQuantityToValue({ asset, quantity }, chain);
}

function createPriceAdder(chain: Chain) {
return (total: number, assetQuantity: AssetQuantity) => {
total += assetQuantityToValue(assetQuantity, chain);
return (total: number, assetQuantity: ActionAssetQuantity) => {
total += actionAssetQuantityToValue(assetQuantity, chain);
return total;
};
}
Expand Down Expand Up @@ -73,12 +68,6 @@ function getAssetAddress({ asset }: { asset: ActionAsset }) {
return getFungibleAsset(asset)?.asset_code;
}

function toMaybeArr<T>(
arr: (T | null | undefined)[] | null | undefined
): T[] | undefined {
return arr?.length ? arr.filter(isTruthy) : undefined;
}

export function addressActionToAnalytics({
addressAction,
quote,
Expand Down Expand Up @@ -114,7 +103,7 @@ export function addressActionToAnalytics({
const output_chain = quote.output_chain;
const zerion_fee_usd_amount =
feeAmount && asset
? assetQuantityToValue({ quantity: feeAmount, asset }, chain)
? actionAssetQuantityToValue({ quantity: feeAmount, asset }, chain)
: undefined;

return {
Expand Down
130 changes: 130 additions & 0 deletions src/shared/analytics/shared/formDataToAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import type {
CustomConfiguration,
EmptyAddressPosition,
} from '@zeriontech/transactions';
import BigNumber from 'bignumber.js';
import type { AddressPosition, Asset } from 'defi-sdk';
import { createChain } from 'src/modules/networks/Chain';
import { backgroundQueryClient } from 'src/modules/query-client/query-client.background';
import { ZerionAPI } from 'src/modules/zerion-api/zerion-api.background';
import type { Quote } from 'src/shared/types/Quote';
import {
calculatePriceImpact,
isHighValueLoss,
} from 'src/ui/pages/SwapForm/shared/price-impact';
import { getCommonQuantity } from 'src/modules/networks/asset';
import { assetQuantityToValue, toMaybeArr } from './helpers';

export interface AnalyticsFormData {
spendAsset: Asset;
receiveAsset: Asset;
spendPosition: AddressPosition | EmptyAddressPosition;
configuration: CustomConfiguration;
}

async function fetchAssetFullInfo(
params: Parameters<typeof ZerionAPI.assetGetFungibleFullInfo>[0]
) {
return backgroundQueryClient.fetchQuery({
queryKey: ['ZerionAPI.fetchAssetFullInfo', params],
queryFn: () => ZerionAPI.assetGetFungibleFullInfo(params),
staleTime: 1000 * 60 * 30, // 30 minutes
});
}

export async function formDataToAnalytics(
scope: 'Swap' | 'Bridge',
{
currency,
formData: { spendAsset, receiveAsset, spendPosition, configuration },
quote,
}: {
currency: string;
formData: AnalyticsFormData;
quote: Quote;
}
) {
const spendAssetInfo = await fetchAssetFullInfo({
fungibleId: spendAsset.asset_code,
currency,
});
const receiveAssetInfo = await fetchAssetFullInfo({
fungibleId: receiveAsset.asset_code,
currency,
});

const fdvAssetSent = spendAssetInfo.data.fungible.meta.fullyDilutedValuation;
const fdvAssetReceived =
receiveAssetInfo.data.fungible.meta.fullyDilutedValuation;

const zerion_fee_percentage = quote.protocol_fee;
const feeAmount = quote.protocol_fee_amount;
const inputChain = createChain(quote.input_chain);
const outputChain = createChain(quote.output_chain);
const zerion_fee_usd_amount = assetQuantityToValue(
{ quantity: feeAmount, asset: spendAsset },
inputChain
);
const usdAmountSend = assetQuantityToValue(
{ quantity: quote.input_amount_estimation, asset: spendAsset },
inputChain
);
const usdAmountReceived = assetQuantityToValue(
{ quantity: quote.output_amount_estimation, asset: receiveAsset },
outputChain
);

const inputValue = quote.input_amount_estimation;
const outputValue = quote.output_amount_estimation;

const enough_balance = new BigNumber(spendPosition?.quantity || 0).gt(
quote.input_amount_estimation
);

const bridgeFeeAmountInUsd =
scope === 'Bridge'
? getCommonQuantity({
baseQuantity: quote.bridge_fee_amount,
chain: createChain(quote.input_chain),
asset: spendAsset,
}).times(spendAsset.price?.value || 0)
: null;

const priceImpact = calculatePriceImpact({
inputValue,
outputValue,
inputAsset: spendAsset,
outputAsset: receiveAsset,
});

const isHighPriceImpact = priceImpact && isHighValueLoss(priceImpact);
const outputAmountColor = isHighPriceImpact ? 'red' : 'grey';

return {
usd_amount_sent: toMaybeArr([usdAmountSend]),
usd_amount_received: toMaybeArr([usdAmountReceived]),
asset_amount_sent: toMaybeArr([quote.input_amount_estimation]),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need to convert these values? Need to be checked

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right now it shows value in gwei, not in the token itself

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we convert values like these in addressActionToAnalytics so probably I should do the same here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

fixed, updated ✅

asset_amount_received: toMaybeArr([quote.output_amount_estimation]),
asset_name_sent: toMaybeArr([spendAsset?.name]),
asset_name_received: toMaybeArr([receiveAsset?.name]),
asset_address_sent: toMaybeArr([spendAsset?.asset_code]),
asset_address_received: toMaybeArr([receiveAsset?.asset_code]),
gas: quote.transaction?.gas,
network_fee: null, // TODO
gas_price: null, // TODO
guaranteed_output_amount: quote.guaranteed_output_amount,
zerion_fee_percentage,
zerion_fee_usd_amount,
input_chain: quote.input_chain,
output_chain: quote.output_chain ?? quote.input_chain,
slippage: configuration.slippage,
contract_type: quote.contract_metadata?.name,
enough_balance,
enough_allowance: Boolean(quote.transaction),
warning_was_shown: isHighPriceImpact,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Something is not right here
Zerts Screenshot 2025-04-24 at 17 56 38@2x
But I can't understand what exactly

Copy link
Contributor Author

@vyorkin vyorkin Apr 25, 2025

Choose a reason for hiding this comment

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

should be fixed, just checked locally ✅

fdv_asset_sent: fdvAssetSent,
fdv_asset_received: fdvAssetReceived,
bridge_fee_usd_amount: bridgeFeeAmountInUsd,
outputAmountColor,
Copy link
Collaborator

Choose a reason for hiding this comment

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

output_amount_color: outputAmountColor

Copy link
Collaborator

Choose a reason for hiding this comment

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

Also something is not right with these conditions
Zerts Screenshot 2025-04-24 at 17 54 04@2x

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ah, thanks! it should be

  const outputAmountColor =
    priceImpact && isSignificantValueLoss(priceImpact) ? 'red' : 'grey';

updated ✅

};
}
28 changes: 28 additions & 0 deletions src/shared/analytics/shared/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Asset } from 'defi-sdk';
import { isTruthy } from 'is-truthy-ts';
import type { Chain } from 'src/modules/networks/Chain';
import { getCommonQuantity } from 'src/modules/networks/asset';

export function toMaybeArr<T>(
arr: (T | null | undefined)[] | null | undefined
): T[] | undefined {
return arr?.filter(isTruthy) ?? undefined;
}

interface AssetQuantity {
asset: Asset | null;
quantity: string | null;
}

export function assetQuantityToValue(
assetWithQuantity: AssetQuantity,
chain: Chain
): number {
const { asset, quantity } = assetWithQuantity;
if (asset && 'implementations' in asset && asset.price && quantity !== null) {
return getCommonQuantity({ asset, chain, baseQuantity: quantity })
.times(asset.price.value)
.toNumber();
}
return 0;
}
Loading