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,
FormFilledOutParams,
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 formFilledOut({
context,
params,
}: WalletMethodParams<FormFilledOutParams>) {
this.verifyInternalOrigin(context);
emitter.emit('formFilledOut', 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 FormFilledOutParams {
scope: 'Swap' | 'Bridge';
formData: AnalyticsFormData;
quote: Quote;
}

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;
formFilledOut: (data: FormFilledOutParams) => void;
}>();
20 changes: 20 additions & 0 deletions src/shared/analytics/analytics.background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,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 +328,25 @@ function trackAppEvents({ account }: { account: Account }) {
}
});

emitter.on('formFilledOut', async ({ scope, formData, quote }) => {
const analyticsData = await formDataToAnalytics(scope, {
formData,
quote,
currency: 'usd',
});
const actionType = scope === 'Bridge' ? 'Send' : 'Trade';
const params = createParams({
request_name: 'swap_form_filled_out',
screen_name: scope,
client_scope: scope,
action_type: actionType,
...analyticsData,
});
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
56 changes: 18 additions & 38 deletions src/shared/analytics/shared/addressActionToAnalytics.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
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,
createQuantityConverter,
toMaybeArr,
} from './helpers';

interface AnalyticsTransactionData {
action_type: string;
Expand All @@ -23,48 +25,27 @@ 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;
};
}

function createQuantityConverter(chain: Chain) {
return ({
asset: actionAsset,
quantity,
}: {
asset: ActionAsset;
quantity: string | null;
}): string | null => {
const asset = getFungibleAsset(actionAsset);
if (asset && quantity !== null) {
return baseToCommon(quantity, getDecimals({ asset, chain })).toFixed();
}
return null;
};
}

function getAssetName({ asset }: { asset: ActionAsset }) {
return getFungibleAsset(asset)?.name;
}
Expand All @@ -73,12 +54,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 All @@ -90,7 +65,12 @@ export function addressActionToAnalytics({
return null;
}
const chain = createChain(addressAction.transaction.chain);
const convertQuantity = createQuantityConverter(chain);

const quantityConverter = createQuantityConverter(chain);
const convertQuantity = ({ asset, quantity }: ActionAssetQuantity) => {
const fingibleAsset = getFungibleAsset(asset);
return quantityConverter({ asset: fingibleAsset, quantity });
};
const addAssetPrice = createPriceAdder(chain);

const outgoing = addressAction.content?.transfers?.outgoing;
Expand All @@ -114,7 +94,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
151 changes: 151 additions & 0 deletions src/shared/analytics/shared/formDataToAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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,
isSignificantValueLoss,
} from 'src/ui/pages/SwapForm/shared/price-impact';
import { getCommonQuantity } from 'src/modules/networks/asset';
import {
assetQuantityToValue,
createQuantityConverter,
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),
// Here this endpoint is used to fetch FDV (Fully Diluted Valuation) for analytics.
// While FDV can change quickly for new tokens, in practice backend/indexing services typically update this data less frequently.
// The main purpose of caching here is to reduce redundant requests while the user interacts with the form.
// This 30-minute cache should provide a good balance between data freshness and efficiency.
staleTime: 1000 * 60 * 30,
});
}

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 enough_balance = new BigNumber(spendPosition?.quantity || 0).gt(
quote.input_amount_estimation
);

const convertQuantity = createQuantityConverter(inputChain);

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

const assetAmountSent = convertQuantity({
asset: spendAsset,
quantity: quote.input_amount_estimation,
});
const assetAmountReceived = convertQuantity({
asset: receiveAsset,
quantity: quote.output_amount_estimation,
});

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

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

return {
usd_amount_sent: toMaybeArr([usdAmountSend]),
usd_amount_received: toMaybeArr([usdAmountReceived]),
asset_amount_sent: toMaybeArr([assetAmountSent]),
asset_amount_received: toMaybeArr([assetAmountReceived]),
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: convertQuantity({
asset: receiveAsset,
quantity: 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,
output_amount_color: outputAmountColor,
};
}
Loading