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;
}>();
19 changes: 19 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,24 @@ function trackAppEvents({ account }: { account: Account }) {
}
});

emitter.on('formFilledOut', async ({ scope, formData, quote }) => {
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,
});
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
154 changes: 154 additions & 0 deletions src/shared/analytics/shared/formDataToAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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 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 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 priceImpact = calculatePriceImpact({
inputValue,
outputValue,
inputAsset: spendAsset,
outputAsset: receiveAsset,
});

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

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

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