Skip to content

Commit c1f0d6d

Browse files
committed
Adding code to capture metrics for uniswap shield
1 parent aed49b1 commit c1f0d6d

File tree

6 files changed

+283
-0
lines changed

6 files changed

+283
-0
lines changed

app/scripts/metamask-controller.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2892,6 +2892,10 @@ export default class MetamaskController extends EventEmitter {
28922892
this.controllerMessenger,
28932893
`${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.TRACK_METAMETRICS_EVENT}`,
28942894
),
2895+
[BridgeBackgroundAction.FETCH_QUOTES]: this.controllerMessenger.call.bind(
2896+
this.controllerMessenger,
2897+
`${BRIDGE_CONTROLLER_NAME}:${BridgeBackgroundAction.FETCH_QUOTES}`,
2898+
),
28952899

28962900
// Bridge Tx submission
28972901
[BridgeStatusAction.SUBMIT_TX]: this.controllerMessenger.call.bind(

ui/pages/confirmations/components/confirm/info/base-transaction-info/base-transaction-info.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { GasFeesSection } from '../shared/gas-fees-section/gas-fees-section';
66
import { TransactionDetails } from '../shared/transaction-details/transaction-details';
77
import { TransactionAccountDetails } from '../batch/transaction-account-details';
88
import { BatchSimulationDetails } from '../batch/batch-simulation-details/batch-simulation-details';
9+
import { DappSwapComparisonBanner } from '../shared/dapp-swap-comparison-banner/dapp-swap-comparison-banner';
10+
11+
const DAPP_SWAP_COMPARISON_ORIGIN = 'https://app.uniswap.org';
912

1013
const BaseTransactionInfo = () => {
1114
const { currentConfirmation: transactionMeta } =
@@ -17,6 +20,9 @@ const BaseTransactionInfo = () => {
1720

1821
return (
1922
<>
23+
{transactionMeta.origin === DAPP_SWAP_COMPARISON_ORIGIN && (
24+
<DappSwapComparisonBanner />
25+
)}
2026
<TransactionAccountDetails />
2127
<BatchSimulationDetails />
2228
<TransactionDetails />
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Hex } from '@metamask/utils';
2+
import { TransactionMeta } from '@metamask/transaction-controller';
3+
import { useCallback, useEffect } from 'react';
4+
5+
import { fetchQuotes } from '../../../../../../store/actions';
6+
import { useConfirmContext } from '../../../../context/confirm';
7+
import { useTransactionEventFragment } from '../../../../hooks/useTransactionEventFragment';
8+
import {
9+
getDataFromSwap,
10+
getBestQuote,
11+
getPercentageValue,
12+
getPercentageGasDifference,
13+
} from '../uniswap-shield-utils';
14+
15+
export function useDappSwapComparisonInfo() {
16+
const { currentConfirmation } = useConfirmContext<TransactionMeta>();
17+
const {
18+
chainId,
19+
id: transactionId,
20+
simulationData,
21+
txParams,
22+
} = currentConfirmation ?? {
23+
txParams: {},
24+
};
25+
const { data, gas, value: amount } = txParams ?? {};
26+
const { updateTransactionEventFragment } = useTransactionEventFragment();
27+
28+
const captureDappSwapComparisonMetricsProperties = useCallback(
29+
(properties: Record<string, string>) => {
30+
updateTransactionEventFragment(
31+
{
32+
properties: {
33+
uniswap_shield: properties,
34+
},
35+
},
36+
transactionId,
37+
);
38+
},
39+
[transactionId, updateTransactionEventFragment],
40+
);
41+
42+
useEffect(() => {
43+
captureDappSwapComparisonMetricsProperties({ loading: 'true' });
44+
45+
const { quotesInput, amountMin } = getDataFromSwap(chainId, amount, data);
46+
if (!quotesInput || !amountMin) {
47+
return;
48+
}
49+
50+
fetchQuotes(quotesInput).then((quotes) => {
51+
const selectedQuoteIndex = getBestQuote(quotes);
52+
53+
const percentageChangeInTokenAmount = getPercentageValue(
54+
quotes[selectedQuoteIndex].quote.destTokenAmount,
55+
amountMin,
56+
);
57+
58+
const percentageChangeInTokenMinAmount = getPercentageValue(
59+
quotes[selectedQuoteIndex].quote.minDestTokenAmount,
60+
amountMin,
61+
);
62+
63+
const percentageChangeInGas = getPercentageGasDifference(
64+
quotes[selectedQuoteIndex],
65+
gas as Hex,
66+
);
67+
68+
captureDappSwapComparisonMetricsProperties({
69+
percentage_change_in_token_amount: percentageChangeInTokenAmount,
70+
percentage_change_in_token_min_amount: percentageChangeInTokenMinAmount,
71+
percentage_change_in_gas: percentageChangeInGas,
72+
});
73+
});
74+
}, [
75+
amount,
76+
captureDappSwapComparisonMetricsProperties,
77+
chainId,
78+
data,
79+
gas,
80+
simulationData,
81+
]);
82+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import 'react';
2+
import { useDappSwapComparisonInfo } from '../../hooks/useDappSwapComparisonInfo';
3+
4+
// The component is conditionally included for uniswap origin
5+
// The only purpose of the component currently is to capture uniswap shield related metrics.
6+
7+
export const DappSwapComparisonBanner = () => {
8+
useDappSwapComparisonInfo();
9+
10+
return null;
11+
};
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { Hex } from '@metamask/utils';
2+
import { Interface, TransactionDescription } from '@ethersproject/abi';
3+
import { QuoteResponse } from '@metamask/bridge-controller';
4+
import { addHexPrefix } from 'ethereumjs-util';
5+
import { getNativeTokenAddress } from '@metamask/assets-controllers';
6+
7+
import { Numeric } from '../../../../../../shared/modules/Numeric';
8+
9+
export const ABI = [
10+
{
11+
constant: true,
12+
inputs: [
13+
{
14+
name: 'commands',
15+
type: 'bytes',
16+
},
17+
{
18+
name: 'inputs',
19+
type: 'bytes[]',
20+
},
21+
{
22+
name: 'deadline',
23+
type: 'uint256',
24+
},
25+
],
26+
name: 'execute',
27+
type: 'function',
28+
},
29+
{
30+
constant: true,
31+
inputs: [
32+
{
33+
name: 'commands',
34+
type: 'bytes',
35+
},
36+
{
37+
name: 'inputs',
38+
type: 'bytes[]',
39+
},
40+
],
41+
name: 'execute',
42+
type: 'function',
43+
},
44+
];
45+
46+
export const getWordsFromInput = (input: string) => {
47+
return input.slice(2).match(/.{1,64}/g) as string[];
48+
};
49+
50+
export const wordToAddress = (word: string) => {
51+
return addHexPrefix(word.slice(24));
52+
};
53+
54+
export const wordToAmount = (word: string) => {
55+
const amount = word.replace(/^0+/, '');
56+
return addHexPrefix(amount);
57+
};
58+
59+
export const parseTransactionData = (data?: string) => {
60+
const contractInterface = new Interface(ABI);
61+
62+
let parsedTransactionData: TransactionDescription;
63+
64+
try {
65+
parsedTransactionData = contractInterface.parseTransaction({
66+
data: data as Hex,
67+
});
68+
} catch (error) {
69+
return { inputs: [], commandBytes: [] };
70+
}
71+
72+
const commands = parsedTransactionData.args.commands as string;
73+
const inputs = parsedTransactionData.args.inputs as string[];
74+
const commandBytes = commands.slice(2).match(/.{1,2}/gu) as string[];
75+
76+
return { inputs, commandBytes };
77+
};
78+
79+
export const getDataFromSwap = (
80+
chainId: Hex,
81+
amount?: string,
82+
data?: string,
83+
) => {
84+
let quotesInput;
85+
let amountMin;
86+
const { commandBytes, inputs } = parseTransactionData(data);
87+
88+
const sweepIndex = commandBytes.findIndex(
89+
(commandByte) => commandByte === '04',
90+
);
91+
92+
if (sweepIndex >= 0) {
93+
const words = getWordsFromInput(inputs[sweepIndex]);
94+
amountMin = wordToAmount(words[2]);
95+
quotesInput = {
96+
walletAddress: wordToAddress(words[1]),
97+
srcChainId: chainId,
98+
destChainId: chainId,
99+
srcTokenAddress: getNativeTokenAddress(chainId),
100+
destTokenAddress: wordToAddress(words[0]),
101+
srcTokenAmount: amount ?? '0x0',
102+
gasIncluded: false,
103+
gasIncluded7702: false,
104+
};
105+
} else {
106+
const seaportIndex = commandBytes.findIndex(
107+
(commandByte) => commandByte === '10',
108+
);
109+
110+
if (seaportIndex >= 0) {
111+
const words = getWordsFromInput(inputs[seaportIndex]);
112+
amountMin = wordToAmount(words[13]);
113+
quotesInput = {
114+
walletAddress: wordToAddress(words[28]),
115+
srcChainId: chainId,
116+
destChainId: chainId,
117+
srcTokenAddress: wordToAddress(words[10]),
118+
destTokenAddress: wordToAddress(words[16]),
119+
srcTokenAmount: wordToAmount(words[12]),
120+
gasIncluded: false,
121+
gasIncluded7702: false,
122+
};
123+
}
124+
}
125+
126+
return { quotesInput, amountMin };
127+
};
128+
129+
export const getBestQuote = (quotes: QuoteResponse[]) => {
130+
let selectedQuoteIndex = -1;
131+
let highestMinDestTokenAmount = '0';
132+
133+
quotes.forEach((quote, index) => {
134+
if (
135+
new Numeric(quote.quote.minDestTokenAmount, 10).greaterThan(
136+
new Numeric(highestMinDestTokenAmount, 10),
137+
)
138+
) {
139+
highestMinDestTokenAmount = quote.quote.minDestTokenAmount;
140+
selectedQuoteIndex = index;
141+
}
142+
});
143+
144+
return selectedQuoteIndex;
145+
};
146+
147+
export const getPercentageValue = (value: string, baseValue: string) => {
148+
return new Numeric(value, 10)
149+
.minus(new Numeric(baseValue, 10))
150+
.divide(new Numeric(baseValue, 10))
151+
.times(100, 10)
152+
.toNumber()
153+
.toFixed(2);
154+
};
155+
156+
export const getPercentageGasDifference = (quote: QuoteResponse, gas: Hex) => {
157+
const totalGasInQuote =
158+
(quote.approval?.gasLimit ?? 0) + (quote.trade?.gasLimit ?? 0);
159+
160+
const totalGasInCurrentTransaction = new Numeric(gas ?? '0x0', 16).toNumber();
161+
162+
return (
163+
((totalGasInQuote - totalGasInCurrentTransaction) /
164+
totalGasInCurrentTransaction) *
165+
100
166+
).toFixed(2);
167+
};

ui/store/actions.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ import {
7474
Subscription,
7575
UpdatePaymentMethodOpts,
7676
} from '@metamask/subscription-controller';
77+
import {
78+
GenericQuoteRequest,
79+
L1GasFees,
80+
NonEvmFees,
81+
QuoteResponse,
82+
} from '@metamask/bridge-controller';
83+
7784
import { captureException } from '../../shared/lib/sentry';
7885
import { switchDirection } from '../../shared/lib/switch-direction';
7986
import {
@@ -7432,3 +7439,9 @@ export async function getLayer1GasFeeValue({
74327439
{ chainId, networkClientId, transactionParams },
74337440
]);
74347441
}
7442+
7443+
export async function fetchQuotes(
7444+
quoteRequest: GenericQuoteRequest,
7445+
): Promise<QuoteResponse[]> {
7446+
return await submitRequestToBackground('fetchQuotes', [quoteRequest]);
7447+
}

0 commit comments

Comments
 (0)