Skip to content

Commit

Permalink
feat: measure transaction time to confirmation through handlers CP-87…
Browse files Browse the repository at this point in the history
…83 (#26)

Co-authored-by: ryanml <[email protected]>
  • Loading branch information
gergelylovas and ryanml committed Aug 22, 2024
1 parent 38c2ef1 commit 8c2a78c
Show file tree
Hide file tree
Showing 13 changed files with 778 additions and 178 deletions.
20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,16 @@
"@avalabs/avalanchejs": "4.0.5",
"@avalabs/bitcoin-module": "0.1.4",
"@avalabs/bridge-unified": "2.1.0",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.3",
"@avalabs/core-chains-sdk": "3.1.0-alpha.3",
"@avalabs/core-coingecko-sdk": "3.1.0-alpha.3",
"@avalabs/core-covalent-sdk": "3.1.0-alpha.3",
"@avalabs/core-etherscan-sdk": "3.1.0-alpha.3",
"@avalabs/core-snowtrace-sdk": "3.1.0-alpha.3",
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.3",
"@avalabs/core-utils-sdk": "3.1.0-alpha.3",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.3",
"@avalabs/glacier-sdk": "3.1.0-alpha.3",
"@avalabs/core-bridge-sdk": "3.1.0-alpha.4",
"@avalabs/core-chains-sdk": "3.1.0-alpha.4",
"@avalabs/core-coingecko-sdk": "3.1.0-alpha.4",
"@avalabs/core-covalent-sdk": "3.1.0-alpha.4",
"@avalabs/core-etherscan-sdk": "3.1.0-alpha.4",
"@avalabs/core-snowtrace-sdk": "3.1.0-alpha.4",
"@avalabs/core-token-prices-sdk": "3.1.0-alpha.4",
"@avalabs/core-utils-sdk": "3.1.0-alpha.4",
"@avalabs/core-wallets-sdk": "3.1.0-alpha.4",
"@avalabs/glacier-sdk": "3.1.0-alpha.4",
"@avalabs/hw-app-avalanche": "0.14.1",
"@avalabs/core-k2-components": "4.18.0-alpha.47",
"@avalabs/types": "3.1.0-alpha.3",
Expand Down
18 changes: 12 additions & 6 deletions src/background/services/actions/ActionsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ describe('background/services/actions/ActionsService.ts', () => {
actionId: 'uuid',
} as any;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { displayData, ...mockActionWithoutDisplaydata } = mockAction;

beforeEach(() => {
jest.resetAllMocks();
// jest is having issues mocking non static getters
Expand Down Expand Up @@ -277,7 +280,10 @@ describe('background/services/actions/ActionsService.ts', () => {
expect(eventListener).toHaveBeenCalledTimes(1);
expect(eventListener).toHaveBeenCalledWith({
type: ActionCompletedEventType.ERROR,
action: action,
action: {
...mockActionWithoutDisplaydata,
method: 'method-with-no-handler',
},
result: ethErrors.rpc.internal('Request handler not found'),
});
expect(storageService.save).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -340,7 +346,7 @@ describe('background/services/actions/ActionsService.ts', () => {
expect(eventListener).toHaveBeenCalledTimes(1);
expect(eventListener).toHaveBeenCalledWith({
type: ActionCompletedEventType.ERROR,
action: mockAction,
action: mockActionWithoutDisplaydata,
result: new Error('someError'),
});
expect(storageService.save).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -375,7 +381,7 @@ describe('background/services/actions/ActionsService.ts', () => {
expect(eventListener).toHaveBeenCalledTimes(1);
expect(eventListener).toHaveBeenCalledWith({
type: ActionCompletedEventType.COMPLETED,
action: mockAction,
action: mockActionWithoutDisplaydata,
result: ['ADDRESS'],
});
expect(storageService.save).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -406,7 +412,7 @@ describe('background/services/actions/ActionsService.ts', () => {
expect(eventListener).toHaveBeenCalledTimes(1);
expect(eventListener).toHaveBeenCalledWith({
type: ActionCompletedEventType.ERROR,
action: mockAction,
action: mockActionWithoutDisplaydata,
result: ethErrors.provider.userRejectedRequest(),
});
expect(storageService.save).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -438,7 +444,7 @@ describe('background/services/actions/ActionsService.ts', () => {
expect(eventListener).toHaveBeenCalledTimes(1);
expect(eventListener).toHaveBeenCalledWith({
type: ActionCompletedEventType.ERROR,
action: mockAction,
action: mockActionWithoutDisplaydata,
result: ethErrors.rpc.internal(new Error('very big error')),
});
expect(storageService.save).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -470,7 +476,7 @@ describe('background/services/actions/ActionsService.ts', () => {
expect(eventListener).toHaveBeenCalledTimes(1);
expect(eventListener).toHaveBeenCalledWith({
type: ActionCompletedEventType.COMPLETED,
action: mockAction,
action: mockActionWithoutDisplaydata,
result: ['ADDRESS'],
});
expect(storageService.save).toHaveBeenCalledTimes(1);
Expand Down
8 changes: 5 additions & 3 deletions src/background/services/actions/ActionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,14 +104,16 @@ export class ActionsService implements OnStorageReady {
) {
await this.removeAction(id);

// We dont want display data to be emitted. Sometimes it can not be serialized.
delete action.displayData;
// We dont want display data to be emitted. Sometimes it can not be serialized and it's content is internal to Core
// Make sure not to modify the original action object
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { displayData, ...actionWithoutDisplayData } = action;

this.eventEmitter.emit(ActionsEvent.ACTION_COMPLETED, {
type: isSuccess
? ActionCompletedEventType.COMPLETED
: ActionCompletedEventType.ERROR,
action,
action: actionWithoutDisplayData,
result,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,22 @@ import { ChainId } from '@avalabs/core-chains-sdk';
import { encryptAnalyticsData } from '../../analytics/utils/encryptAnalyticsData';
import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow';
import { buildRpcCall } from '@src/tests/test-utils';
import { measureDuration } from '@src/utils/measureDuration';

jest.mock('@avalabs/avalanchejs');
jest.mock('@avalabs/core-wallets-sdk');
jest.mock('../utils/getProvidedUtxos');
jest.mock('../../analytics/utils/encryptAnalyticsData');
jest.mock('@src/background/runtime/openApprovalWindow');
jest.mock('@src/utils/measureDuration', () => {
const measureDurationMock = {
start: jest.fn(),
end: jest.fn(),
};
return {
measureDuration: () => measureDurationMock,
};
});

describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts', () => {
const env = process.env;
Expand Down Expand Up @@ -88,6 +98,7 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
};
const providerMock = {
issueTxHex: issueTxHexMock,
waitForTransaction: jest.fn(),
};
const utxosMock = [{ utxoId: '1' }, { utxoId: '2' }];

Expand Down Expand Up @@ -117,7 +128,7 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
(UnsignedTx.fromJSON as jest.Mock).mockReturnValue(unsignedTxMock);
(EVMUnsignedTx.fromJSON as jest.Mock).mockReturnValue(unsignedTxMock);
signMock.mockReturnValue({ signedTx: 'baz' });
getAvalancheNetworkXPMock.mockReturnValue({});
getAvalancheNetworkXPMock.mockReturnValue({ rpcUrl: 'RPCURL' });
issueTxHexMock.mockResolvedValue({ txID: 1 });
getAvalanceProviderXPMock.mockResolvedValue(providerMock);
getAddressesMock.mockReturnValue([]);
Expand Down Expand Up @@ -417,6 +428,9 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
unsignedTxJson,
},
params: {},
site: {
domain: 'core.app',
},
} as Action;

it('returns error when there are multiple addresses without indices', async () => {
Expand Down Expand Up @@ -532,7 +546,7 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
externalIndices: undefined,
internalIndices: undefined,
},
{},
{ rpcUrl: 'RPCURL' },
frontendTabId,
'avalanche_sendTransaction'
);
Expand Down Expand Up @@ -577,7 +591,7 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
externalIndices: undefined,
internalIndices: undefined,
},
{},
{ rpcUrl: 'RPCURL' },
frontendTabId,
'avalanche_sendTransaction'
);
Expand Down Expand Up @@ -627,7 +641,7 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
externalIndices: [0, 1],
internalIndices: [2, 3],
},
{},
{ rpcUrl: 'RPCURL' },
frontendTabId,
'avalanche_sendTransaction'
);
Expand All @@ -652,5 +666,106 @@ describe('src/background/services/wallet/handlers/avalanche_sendTransaction.ts',
})
);
});

it('measures and reports time to confirmation', async () => {
const signedTxHex = '0x000142';

let resolveWaitforTransaction;
providerMock.waitForTransaction.mockReturnValue(
new Promise((resolve) => {
resolveWaitforTransaction = resolve;
})
);
const measurement = measureDuration();
jest.mocked(measurement.end).mockReturnValue(1000);
hasAllSignaturesMock.mockReturnValueOnce(true);
(Avalanche.signedTxToHex as jest.Mock).mockReturnValueOnce(signedTxHex);

await handler.onActionApproved(
pendingActionMock,
{},
onSuccessMock,
onErrorMock,
frontendTabId
);

expect(measurement.start).toHaveBeenCalled();
expect(providerMock.waitForTransaction).toHaveBeenCalledTimes(1);
expect(providerMock.waitForTransaction).toHaveBeenCalledWith(
1,
'AVM',
60000
);
expect(
analyticsServicePosthogMock.captureEncryptedEvent
).toHaveBeenCalledTimes(1);

resolveWaitforTransaction(undefined);

await new Promise(process.nextTick);

expect(measurement.end).toHaveBeenCalled();

expect(
analyticsServicePosthogMock.captureEncryptedEvent
).toHaveBeenCalledTimes(2);
expect(
analyticsServicePosthogMock.captureEncryptedEvent
).toHaveBeenNthCalledWith(2, {
name: 'TransactionTimeToConfirmation',
properties: {
chainId: 4503599627370468,
duration: 1000,
rpcUrl: 'RPCURL',
site: 'core.app',
txType: undefined,
},
windowId: undefined,
});
});

it('ends measurement if waiting for confirmation fails', async () => {
const signedTxHex = '0x000142';

let rejectWaitforTransaction;
providerMock.waitForTransaction.mockReturnValue(
new Promise((_, reject) => {
rejectWaitforTransaction = reject;
})
);
const measurement = measureDuration();
jest.mocked(measurement.end).mockReturnValue(1000);
hasAllSignaturesMock.mockReturnValueOnce(true);
(Avalanche.signedTxToHex as jest.Mock).mockReturnValueOnce(signedTxHex);

await handler.onActionApproved(
pendingActionMock,
{},
onSuccessMock,
onErrorMock,
frontendTabId
);

expect(measurement.start).toHaveBeenCalled();
expect(providerMock.waitForTransaction).toHaveBeenCalledTimes(1);
expect(providerMock.waitForTransaction).toHaveBeenCalledWith(
1,
'AVM',
60000
);
expect(
analyticsServicePosthogMock.captureEncryptedEvent
).toHaveBeenCalledTimes(1);

rejectWaitforTransaction(new Error('some error'));

await new Promise(process.nextTick);

expect(measurement.end).toHaveBeenCalled();

expect(
analyticsServicePosthogMock.captureEncryptedEvent
).toHaveBeenCalledTimes(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import getProvidedUtxos from '../utils/getProvidedUtxos';
import { AnalyticsServicePosthog } from '../../analytics/AnalyticsServicePosthog';
import { ChainId } from '@avalabs/core-chains-sdk';
import { openApprovalWindow } from '@src/background/runtime/openApprovalWindow';
import { measureDuration } from '@src/utils/measureDuration';

type TxParams = {
transactionHex: string;
Expand Down Expand Up @@ -229,8 +230,9 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler<

const usedAddress = this.#getAddressForVM(vm);
const usedNetwork = this.#getChainIdForVM(vm);

const measurement = measureDuration();
try {
measurement.start();
// Parse the json into a tx object
const unsignedTx =
vm === EVM
Expand All @@ -250,18 +252,23 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler<
);
}

const network = this.networkService.getAvalancheNetworkXP();
const prov = await this.networkService.getAvalanceProviderXP();
const { txHash, signedTx } = await this.walletService.sign(
{
tx: unsignedTx,
externalIndices,
internalIndices,
},
this.networkService.getAvalancheNetworkXP(),
network,
frontendTabId,
DAppProviderRequest.AVALANCHE_SEND_TRANSACTION
);

let transactionHash: string;
if (typeof txHash === 'string') {
transactionHash = txHash;

this.analyticsServicePosthog.captureEncryptedEvent({
name: 'avalanche_sendTransaction_success',
windowId: crypto.randomUUID(),
Expand Down Expand Up @@ -290,7 +297,6 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler<
);

// Submit the transaction and return the tx id
const prov = await this.networkService.getAvalanceProviderXP();
const res = await prov.issueTxHex(signedTransactionHex, vm);

this.analyticsServicePosthog.captureEncryptedEvent({
Expand All @@ -303,9 +309,38 @@ export class AvalancheSendTransactionHandler extends DAppRequestHandler<
},
});

transactionHash = res.txID;

onSuccess(res.txID);
} else {
onError(new Error('Signing error, invalid result'));
return;
}

prov
.waitForTransaction(transactionHash, vm, 60000)
.then(() => {
const duration = measurement.end();
this.analyticsServicePosthog.captureEncryptedEvent({
name: 'TransactionTimeToConfirmation',
windowId: crypto.randomUUID(),
properties: {
duration,
txType: unsignedTx.getTx()._type,
chainId: usedNetwork,
rpcUrl: network.rpcUrl,
site: pendingAction.site?.domain,
},
});
})
.catch(() => {
// clean up pending measurement
measurement.end();
});
} catch (e) {
// clean up pending measurement
measurement.end();

this.analyticsServicePosthog.captureEncryptedEvent({
name: 'avalanche_sendTransaction_failed',
windowId: crypto.randomUUID(),
Expand Down
Loading

0 comments on commit 8c2a78c

Please sign in to comment.