Skip to content

Commit de72100

Browse files
committed
Feat/add additional analytics to swaps (#2057)
* feat(extension): send total value in ada as success event on swap * feat(extension): add exact amount check on collateral value * feat(extension): auto-reclaim collateral if amount too large * feat(extension): add optional check on inMemoryWallet utxos * fix(extension): add check if unspendable utxos present in wallet when auto-reclaiming collateral
1 parent b440de8 commit de72100

File tree

5 files changed

+72
-5
lines changed

5 files changed

+72
-5
lines changed

apps/browser-extension-wallet/src/hooks/useCollateral.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const useCollateral = (): UseCollateralReturn => {
5151

5252
useEffect(() => {
5353
const isPureUtxoWithEnoughCoins = (utxo: Cardano.Utxo): boolean =>
54-
!utxo[1].value?.assets && utxo[1].value.coins >= COLLATERAL_AMOUNT_LOVELACES;
54+
!utxo[1].value?.assets && utxo[1].value.coins === COLLATERAL_AMOUNT_LOVELACES;
5555

5656
const checkCollateral = async (): Promise<void> => {
5757
if (!inMemoryWallet?.utxo?.available$) return;

apps/browser-extension-wallet/src/routes/PopupView.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import { removePreloaderIfExists } from '@utils/remove-reloader-if-exists';
1818
import { ENHANCED_ANALYTICS_OPT_IN_STATUS_LS_KEY } from '@providers/AnalyticsProvider/config';
1919
import { EnhancedAnalyticsOptInStatus } from '@providers/AnalyticsProvider/analyticsTracker';
2020
import { useAnalyticsContext } from '@providers';
21+
import { useObservable } from '@lace/common';
22+
import { autoReclaimLargeCollateralUtxos } from '@src/utils/collateral-utils';
2123

2224
dayjs.extend(duration);
2325

@@ -41,6 +43,12 @@ export const PopupView = (): React.ReactElement => {
4143
initialHdDiscoveryCompleted
4244
} = useWalletStore();
4345

46+
const unspendable = useObservable(inMemoryWallet?.balance?.utxo.unspendable$);
47+
48+
useEffect(() => {
49+
autoReclaimLargeCollateralUtxos(inMemoryWallet);
50+
}, [unspendable, inMemoryWallet]);
51+
4452
const [{ lastMnemonicVerification, mnemonicVerificationFrequency, chainName }] = useAppSettingsContext();
4553
const backgroundServices = useBackgroundServiceAPIContext();
4654

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Cardano } from '@cardano-sdk/core';
2+
import { ObservableWallet } from '@cardano-sdk/wallet';
3+
import { firstValueFrom, take } from 'rxjs';
4+
import { COLLATERAL_AMOUNT_LOVELACES } from './constants';
5+
6+
/**
7+
* Checks if a UTXO is unspendable (collateral) with too high coin lockup
8+
* @param utxo - The UTXO to check
9+
* @returns true if the UTXO has no assets and coins exceed the collateral amount threshold
10+
*/
11+
const isUnspendableWithTooHighCoinLockup = (utxo: Cardano.Utxo): boolean =>
12+
!utxo[1].value?.assets && utxo[1].value.coins > COLLATERAL_AMOUNT_LOVELACES;
13+
14+
/**
15+
* Automatically reclaims collateral UTXOs that are too large
16+
* If any unspendable UTXOs have a coin value over COLLATERAL_AMOUNT_LOVELACES,
17+
* they are automatically removed from the unspendable set to reclaim them
18+
* @param inMemoryWallet - The wallet instance
19+
*/
20+
export const autoReclaimLargeCollateralUtxos = async (inMemoryWallet: ObservableWallet): Promise<void> => {
21+
// Guard: return early if wallet is not available
22+
if (!inMemoryWallet?.utxo?.unspendable$) {
23+
return;
24+
}
25+
26+
// if we've got utxos OVER COLLATERAL_AMOUNT_LOVELACES automatically reclaim them
27+
const collateral = await firstValueFrom(inMemoryWallet.utxo.unspendable$.pipe(take(1)));
28+
const matchingUnspendableUtxos = collateral.filter((o) => isUnspendableWithTooHighCoinLockup(o));
29+
30+
if (matchingUnspendableUtxos.length > 0) {
31+
// Remove the matching unspendable UTXOs by setting unspendable to only the UTXOs that don't match
32+
const remainingUnspendableUtxos = collateral.filter((o) => !isUnspendableWithTooHighCoinLockup(o));
33+
await inMemoryWallet.utxo.setUnspendable(remainingUnspendableUtxos);
34+
}
35+
};

apps/browser-extension-wallet/src/views/browser-view/features/swaps/components/SwapProvider.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
import { SwapsContainer } from './SwapContainer';
2727
import { DropdownList } from './drawers';
2828
import { usePostHogClientContext } from '@providers/PostHogClientProvider';
29+
import { useBackgroundServiceAPIContext } from '@providers/BackgroundServiceAPI';
2930
import { useTranslation } from 'react-i18next';
3031
import { TFunction } from 'i18next';
3132
import { storage } from 'webextension-polyfill';
@@ -134,6 +135,8 @@ export const SwapsProvider = (): React.ReactElement => {
134135
const utxos = useObservable(inMemoryWallet.utxo.available$);
135136
const collateral = useObservable(inMemoryWallet.utxo.unspendable$);
136137
const addresses = useObservable(inMemoryWallet.addresses$);
138+
const { coinPrices } = useBackgroundServiceAPIContext();
139+
const tokenPrices = useObservable(coinPrices.tokenPrices$);
137140

138141
// swaps interface
139142
const [tokenA, setTokenA] = useState<DropdownList>();
@@ -437,13 +440,26 @@ export const SwapsProvider = (): React.ReactElement => {
437440
);
438441

439442
const sendSuccessPosthogEvent = useCallback(() => {
443+
let totalValueTransferInAda = 0;
444+
445+
// If swapping from ADA, the quantity is already in ADA
446+
if (tokenA.id === 'lovelace') {
447+
totalValueTransferInAda = Number(quantity);
448+
} else {
449+
// For other tokens, get the token price in ADA and multiply by quantity
450+
const tokenPrice = tokenPrices?.tokens.get(tokenA.id as Wallet.Cardano.AssetId);
451+
const priceInAda = tokenPrice?.price?.priceInAda || 0;
452+
totalValueTransferInAda = Number(quantity) * priceInAda;
453+
}
454+
440455
posthog.sendEvent(PostHogAction.SwapsSignSuccess, {
441456
tokenIn: tokenB,
442457
tokenOut: tokenA,
443458
quantity,
444-
targetSlippage: targetSlippage.toString()
459+
targetSlippage: targetSlippage.toString(),
460+
totalValueTransferInAda: totalValueTransferInAda.toString()
445461
});
446-
}, [tokenA, tokenB, posthog, quantity, targetSlippage]);
462+
}, [tokenA, tokenB, posthog, quantity, targetSlippage, tokenPrices]);
447463

448464
const signAndSubmitSwapRequest = useCallback(async () => {
449465
const unableToSignErrorText = t('swaps.error.unableToSign');

apps/browser-extension-wallet/src/views/browser-view/routes/index.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import { DAppExplorer } from '@views/browser/features/dapp/explorer/components/D
4040
import { useFatalError } from '@hooks/useFatalError';
4141
import { Crash } from '@components/ErrorBoundary';
4242
import { useIsPosthogClientInitialized } from '@providers/PostHogClientProvider/useIsPosthogClientInitialized';
43-
import { logger } from '@lace/common';
43+
import { logger, useObservable } from '@lace/common';
4444
import { VotingLayout } from '../features/voting-beta';
4545
import { catchAndBrandExtensionApiError } from '@utils/catch-and-brand-extension-api-error';
4646
import { removePreloaderIfExists } from '@utils/remove-reloader-if-exists';
@@ -50,6 +50,7 @@ import { useNotificationsCenterConfig } from '@hooks/useNotificationsCenterConfi
5050
import { NotificationDetailsContainer, NotificationsCenter } from '../features/notifications-center';
5151
import { SwapsProvider } from '../features/swaps';
5252
import { WalletType } from '@cardano-sdk/web-extension';
53+
import { autoReclaimLargeCollateralUtxos } from '@src/utils/collateral-utils';
5354

5455
export const defaultRoutes: RouteMap = [
5556
{
@@ -150,7 +151,8 @@ export const BrowserViewRoutes = ({ routesMap = defaultRoutes }: { routesMap?: R
150151
initialHdDiscoveryCompleted,
151152
isSharedWallet,
152153
environmentName,
153-
isBitcoinWallet
154+
isBitcoinWallet,
155+
inMemoryWallet
154156
} = useWalletStore();
155157
const [{ chainName }] = useAppSettingsContext();
156158
const [isLoadingWalletInfo, setIsLoadingWalletInfo] = useState(true);
@@ -161,6 +163,12 @@ export const BrowserViewRoutes = ({ routesMap = defaultRoutes }: { routesMap?: R
161163
const isVotingCenterEnabled = !!GOV_TOOLS_URLS[environmentName];
162164
const { isNotificationsCenterEnabled } = useNotificationsCenterConfig();
163165

166+
const unspendable = useObservable(inMemoryWallet?.balance?.utxo.unspendable$);
167+
168+
useEffect(() => {
169+
autoReclaimLargeCollateralUtxos(inMemoryWallet);
170+
}, [unspendable, inMemoryWallet]);
171+
164172
const availableRoutes = routesMap.filter((route) => {
165173
if (route.path === routes.staking && isSharedWallet) return false;
166174
if (route.path === routes.voting && !isVotingCenterEnabled) return false;

0 commit comments

Comments
 (0)