@avalabs/evm-module 0.0.15 → 0.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/.turbo/turbo-lint.log +1 -1
  3. package/.turbo/turbo-test.log +35 -15
  4. package/CHANGELOG.md +17 -0
  5. package/dist/index.cjs +31 -9
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +13 -2
  8. package/dist/index.d.ts +13 -2
  9. package/dist/index.js +28 -10
  10. package/dist/index.js.map +1 -1
  11. package/manifest.json +1 -1
  12. package/package.json +6 -5
  13. package/src/constants.ts +1 -0
  14. package/src/handlers/eth-send-transaction/eth-send-transaction.test.ts +233 -2
  15. package/src/handlers/eth-send-transaction/eth-send-transaction.ts +27 -8
  16. package/src/handlers/eth-sign/eth-sign.test.ts +320 -0
  17. package/src/handlers/eth-sign/eth-sign.ts +158 -0
  18. package/src/handlers/eth-sign/schemas/eth-sign-typed-data.ts +65 -0
  19. package/src/handlers/eth-sign/schemas/eth-sign.ts +9 -0
  20. package/src/handlers/eth-sign/schemas/parse-request-params/fixture.ts +47 -0
  21. package/src/handlers/eth-sign/schemas/parse-request-params/parse-request-params.test.ts +284 -0
  22. package/src/handlers/eth-sign/schemas/parse-request-params/parse-request-params.ts +94 -0
  23. package/src/handlers/eth-sign/schemas/personal-sign.ts +12 -0
  24. package/src/handlers/eth-sign/schemas/shared.ts +5 -0
  25. package/src/handlers/eth-sign/utils/beautify-message/beautify-message.test.ts +29 -0
  26. package/src/handlers/eth-sign/utils/beautify-message/beautify-message.ts +134 -0
  27. package/src/handlers/eth-sign/utils/is-typed-data-valid.ts +26 -0
  28. package/src/handlers/eth-sign/utils/typeguards.ts +10 -0
  29. package/src/handlers/get-balances/evm-balance-service/get-erc20-balances.test.ts +2 -2
  30. package/src/handlers/get-balances/evm-balance-service/get-erc20-balances.ts +4 -6
  31. package/src/handlers/get-balances/get-balances.test.ts +0 -5
  32. package/src/handlers/get-balances/get-balances.ts +14 -3
  33. package/src/handlers/get-balances/glacier-balance-service/get-erc20-balances.test.ts +0 -1
  34. package/src/handlers/get-balances/glacier-balance-service/get-erc20-balances.ts +10 -7
  35. package/src/handlers/get-tokens/get-tokens.test.ts +6 -6
  36. package/src/index.ts +1 -0
  37. package/src/module.ts +14 -0
  38. package/src/types.ts +9 -0
  39. package/src/utils/parse-erc20-transaction-type.ts +35 -0
  40. package/src/utils/process-transaction-simulation.test.ts +105 -0
  41. package/src/utils/process-transaction-simulation.ts +294 -0
  42. package/src/utils/scan-transaction.ts +63 -0
  43. package/tsconfig.json +6 -1
@@ -48,7 +48,6 @@ describe('get-balances', () => {
48
48
  balanceDisplayValue: '1',
49
49
  balanceInCurrency: 1,
50
50
  priceInCurrency: 1,
51
- contractType: 'ERC-20',
52
51
  type: TokenType.ERC20,
53
52
  change24: 0,
54
53
  marketCap: 0,
@@ -89,7 +88,6 @@ describe('get-balances', () => {
89
88
  balanceInCurrency: 1,
90
89
  chainId: 1,
91
90
  change24: 0,
92
- contractType: 'ERC-20',
93
91
  decimals: 18,
94
92
  logoUri: 'https://s3.us-east-2.amazonaws.com/nomics-api/static/images/currencies/dai.svg',
95
93
  marketCap: 0,
@@ -138,7 +136,6 @@ describe('get-balances', () => {
138
136
  symbol: 'DAI2',
139
137
  decimals: 18,
140
138
  logoUri: 'https://s3.us-east-2.amazonaws.com/nomics-api/static/images/currencies/dai.svg',
141
- contractType: 'ERC-20',
142
139
  type: TokenType.ERC20,
143
140
  },
144
141
  ]),
@@ -178,7 +175,6 @@ describe('get-balances', () => {
178
175
  balanceDisplayValue: '1',
179
176
  balanceInCurrency: 1,
180
177
  priceInCurrency: 1,
181
- contractType: 'ERC-20',
182
178
  type: TokenType.ERC20,
183
179
  change24: 0,
184
180
  marketCap: 0,
@@ -219,7 +215,6 @@ describe('get-balances', () => {
219
215
  balanceInCurrency: 1,
220
216
  chainId: 2,
221
217
  change24: 0,
222
- contractType: 'ERC-20',
223
218
  decimals: 18,
224
219
  logoUri: 'https://s3.us-east-2.amazonaws.com/nomics-api/static/images/currencies/dai.svg',
225
220
  marketCap: 0,
@@ -1,4 +1,11 @@
1
- import type { GetBalancesResponse, GetBalancesParams, Storage } from '@avalabs/vm-module-types';
1
+ import {
2
+ type GetBalancesResponse,
3
+ type GetBalancesParams,
4
+ type Storage,
5
+ TokenType,
6
+ type NetworkContractToken,
7
+ type ERC20Token,
8
+ } from '@avalabs/vm-module-types';
2
9
  import { getErc20Balances } from './evm-balance-service/get-erc20-balances';
3
10
  import { TokenService } from '@internal/utils';
4
11
  import { getProvider } from '../../utils/get-provider';
@@ -37,7 +44,7 @@ export const getBalances = async ({
37
44
  });
38
45
 
39
46
  const erc20Tokens = await getErc20BalancesFromGlacier({
40
- customTokens,
47
+ customTokens: customTokens.filter(isERC20Token),
41
48
  glacierService,
42
49
  currency,
43
50
  chainId,
@@ -80,7 +87,7 @@ export const getBalances = async ({
80
87
  tokenService,
81
88
  address,
82
89
  currency,
83
- tokens: allTokens,
90
+ tokens: allTokens.filter(isERC20Token),
84
91
  });
85
92
 
86
93
  return {
@@ -107,3 +114,7 @@ export const getBalances = async ({
107
114
 
108
115
  return filterBalances;
109
116
  };
117
+
118
+ function isERC20Token(token: NetworkContractToken): token is ERC20Token {
119
+ return token.type === TokenType.ERC20;
120
+ }
@@ -50,7 +50,6 @@ describe('get-erc20-balances', () => {
50
50
  balanceDisplayValue: '1',
51
51
  balanceInCurrency: 1000,
52
52
  priceInCurrency: 1000,
53
- contractType: 'ERC-20',
54
53
  type: 'ERC20',
55
54
  change24: 0,
56
55
  marketCap: 0,
@@ -2,12 +2,13 @@ import { balanceToDisplayValue, bnToBig } from '@avalabs/utils-sdk';
2
2
  import {
3
3
  TokenType,
4
4
  type TokenWithBalanceERC20,
5
- type NetworkContractToken,
6
5
  type TokenWithBalance,
6
+ type ERC20Token,
7
7
  } from '@avalabs/vm-module-types';
8
8
  import { CurrencyCode, Erc20TokenBalance } from '@avalabs/glacier-sdk';
9
9
  import BN from 'bn.js';
10
10
  import type { EvmGlacierService } from '../../../services/glacier-service/glacier-service';
11
+ import { DEFAULT_DECIMALS } from '../../../constants';
11
12
 
12
13
  export const getErc20Balances = async ({
13
14
  glacierService,
@@ -20,7 +21,7 @@ export const getErc20Balances = async ({
20
21
  address: string;
21
22
  currency: string;
22
23
  chainId: number;
23
- customTokens: NetworkContractToken[];
24
+ customTokens: ERC20Token[];
24
25
  }): Promise<Record<string, TokenWithBalance>> => {
25
26
  const tokensWithBalance: TokenWithBalanceERC20[] = [];
26
27
  /**
@@ -37,7 +38,9 @@ export const getErc20Balances = async ({
37
38
  pageToken: nextPageToken,
38
39
  });
39
40
 
40
- tokensWithBalance.push(...convertErc20ToTokenWithBalance(response.erc20TokenBalances, Number(chainId)));
41
+ tokensWithBalance.push(
42
+ ...convertErc20TokenWithBalanceToTokenWithBalance(response.erc20TokenBalances, Number(chainId)),
43
+ );
41
44
  nextPageToken = response.nextPageToken;
42
45
  } while (nextPageToken);
43
46
 
@@ -47,7 +50,7 @@ export const getErc20Balances = async ({
47
50
  * tokens are only used for swap, bridge and tx parsing.
48
51
  */
49
52
  return [
50
- ...convertNetworkTokenToTokenWithBalance(customTokens),
53
+ ...convertErc20TokenToTokenWithBalance(customTokens),
51
54
  ...tokensWithBalance, // this needs to be second in the list so it overwrites its zero balance counterpart if there is one
52
55
  ].reduce(
53
56
  (acc, token) => {
@@ -57,10 +60,11 @@ export const getErc20Balances = async ({
57
60
  );
58
61
  };
59
62
 
60
- const convertNetworkTokenToTokenWithBalance = (tokens: NetworkContractToken[]): TokenWithBalanceERC20[] => {
63
+ const convertErc20TokenToTokenWithBalance = (tokens: ERC20Token[]): TokenWithBalanceERC20[] => {
61
64
  return tokens.map((token) => {
62
65
  return {
63
66
  ...token,
67
+ decimals: token.decimals ?? DEFAULT_DECIMALS,
64
68
  type: TokenType.ERC20,
65
69
  balance: new BN(0),
66
70
  balanceInCurrency: 0,
@@ -74,7 +78,7 @@ const convertNetworkTokenToTokenWithBalance = (tokens: NetworkContractToken[]):
74
78
  });
75
79
  };
76
80
 
77
- const convertErc20ToTokenWithBalance = (
81
+ const convertErc20TokenWithBalanceToTokenWithBalance = (
78
82
  tokenBalances: Erc20TokenBalance[],
79
83
  chainId: number,
80
84
  ): TokenWithBalanceERC20[] => {
@@ -97,7 +101,6 @@ const convertErc20ToTokenWithBalance = (
97
101
  balanceDisplayValue,
98
102
  balanceInCurrency,
99
103
  priceInCurrency,
100
- contractType: 'ERC-20',
101
104
  type: TokenType.ERC20,
102
105
  change24: 0,
103
106
  marketCap: 0,
@@ -28,7 +28,7 @@ describe('get-tokens', () => {
28
28
  address: '0xB31f66AA3C1e785363F0875A1B74E27b85FD66c7',
29
29
  name: 'Wrapped AVAX',
30
30
  symbol: 'WAVAX',
31
- contractType: 'ERC-20',
31
+ type: 'ERC20',
32
32
  chainId: 43114,
33
33
  decimals: 18,
34
34
  },
@@ -36,7 +36,7 @@ describe('get-tokens', () => {
36
36
  address: '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E',
37
37
  name: 'USD Coin',
38
38
  symbol: 'USDC',
39
- contractType: 'ERC-20',
39
+ type: 'ERC20',
40
40
  chainId: 43114,
41
41
  decimals: 6,
42
42
  },
@@ -58,7 +58,7 @@ describe('get-tokens', () => {
58
58
  address: '0xdAC17F958D2ee523a2206206994597C13D831ec7',
59
59
  name: 'Tether USD',
60
60
  symbol: 'USDT',
61
- contractType: 'ERC-20',
61
+ type: 'ERC20',
62
62
  chainId: 1,
63
63
  decimals: 6,
64
64
  },
@@ -66,7 +66,7 @@ describe('get-tokens', () => {
66
66
  address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
67
67
  name: 'USDCoin',
68
68
  symbol: 'USDC',
69
- contractType: 'ERC-20',
69
+ type: 'ERC20',
70
70
  chainId: 1,
71
71
  decimals: 6,
72
72
  },
@@ -74,7 +74,7 @@ describe('get-tokens', () => {
74
74
  address: '0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599',
75
75
  name: 'Wrapped BTC',
76
76
  symbol: 'WBTC',
77
- contractType: 'ERC-20',
77
+ type: 'ERC20',
78
78
  chainId: 1,
79
79
  decimals: 8,
80
80
  },
@@ -82,7 +82,7 @@ describe('get-tokens', () => {
82
82
  address: '0x514910771AF9Ca656af840dff83E8264EcF986CA',
83
83
  name: 'ChainLink Token',
84
84
  symbol: 'LINK',
85
- contractType: 'ERC-20',
85
+ type: 'ERC20',
86
86
  chainId: 1,
87
87
  decimals: 18,
88
88
  },
package/src/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export * from './module';
2
2
  export * from './types';
3
+ export * from './handlers/eth-sign/utils/typeguards';
package/src/module.ts CHANGED
@@ -20,6 +20,7 @@ import { ethSendTransaction } from './handlers/eth-send-transaction/eth-send-tra
20
20
  import { getBalances } from './handlers/get-balances/get-balances';
21
21
  import { getEnv } from './env';
22
22
  import { EvmGlacierService } from './services/glacier-service/glacier-service';
23
+ import { ethSign } from './handlers/eth-sign/eth-sign';
23
24
 
24
25
  export class EvmModule implements Module {
25
26
  #glacierService: EvmGlacierService;
@@ -97,6 +98,19 @@ export class EvmModule implements Module {
97
98
  request,
98
99
  network,
99
100
  approvalController: this.#approvalController,
101
+ proxyApiUrl: this.#proxyApiUrl,
102
+ });
103
+ case RpcMethod.PERSONAL_SIGN:
104
+ case RpcMethod.ETH_SIGN:
105
+ case RpcMethod.SIGN_TYPED_DATA:
106
+ case RpcMethod.SIGN_TYPED_DATA_V1:
107
+ case RpcMethod.SIGN_TYPED_DATA_V3:
108
+ case RpcMethod.SIGN_TYPED_DATA_V4:
109
+ return ethSign({
110
+ request,
111
+ network,
112
+ approvalController: this.#approvalController,
113
+ proxyApiUrl: this.#proxyApiUrl,
100
114
  });
101
115
  default:
102
116
  return { error: rpcErrors.methodNotSupported(`Method ${request.method} not supported`) };
package/src/types.ts CHANGED
@@ -14,3 +14,12 @@ export type TransactionParams = {
14
14
  nonce?: string;
15
15
  chainId?: string;
16
16
  };
17
+
18
+ export enum ERC20TransactionType {
19
+ TOTAL_SUPPLY = 'totalSupply',
20
+ BALANCE_OF = 'balanceOf',
21
+ TRANSFER = 'transfer',
22
+ TRANSFER_FROM = 'transferFrom',
23
+ APPROVE = 'approve',
24
+ ALLOWANCE = 'allowance',
25
+ }
@@ -0,0 +1,35 @@
1
+ import { Interface } from 'ethers';
2
+ import ERC20 from '@openzeppelin/contracts/build/contracts/ERC20.json';
3
+ import { ERC20TransactionType } from '../types';
4
+
5
+ export const parseERC20TransactionType = (transaction: {
6
+ data?: string;
7
+ value?: string;
8
+ }): ERC20TransactionType | undefined => {
9
+ if (!transaction.data) {
10
+ return undefined;
11
+ }
12
+
13
+ try {
14
+ const contractInterface = new Interface(ERC20.abi);
15
+
16
+ const description = contractInterface.parseTransaction({
17
+ data: transaction.data,
18
+ value: transaction.value,
19
+ });
20
+
21
+ const functionName = description?.name ?? description?.fragment?.name;
22
+
23
+ if (functionName && isERC20TransactionType(functionName)) {
24
+ return functionName;
25
+ }
26
+
27
+ return undefined;
28
+ } catch (e) {
29
+ return undefined;
30
+ }
31
+ };
32
+
33
+ function isERC20TransactionType(value: string): value is ERC20TransactionType {
34
+ return Object.values(ERC20TransactionType).includes(value as ERC20TransactionType);
35
+ }
@@ -0,0 +1,105 @@
1
+ import Blockaid from '@blockaid/client';
2
+ import { processBalanceChange } from './process-transaction-simulation';
3
+
4
+ jest.mock('@blockaid/client', () => {
5
+ return jest.fn().mockImplementation(() => {
6
+ return {};
7
+ });
8
+ });
9
+
10
+ describe('processBalanceChange', () => {
11
+ it('should sort asset diffs correctly within ins and outs', () => {
12
+ const assetDiffs: Blockaid.AssetDiff[] = [
13
+ {
14
+ asset: {
15
+ type: 'ERC20',
16
+ address: '0xTokenAddress1',
17
+ name: 'TokenName1',
18
+ symbol: 'TKN1',
19
+ decimals: 18,
20
+ logo_url: 'logo_url1',
21
+ },
22
+ in: [
23
+ { value: '1', usd_price: '1', raw_value: '0x1' },
24
+ { value: '2', usd_price: '2', raw_value: '0x2' },
25
+ ],
26
+ out: [
27
+ { value: '3', usd_price: '3', raw_value: '0x3' },
28
+ { value: '1', usd_price: '1', raw_value: '0x1' },
29
+ ],
30
+ },
31
+ {
32
+ asset: {
33
+ type: 'ERC20',
34
+ address: '0xTokenAddress2',
35
+ name: 'TokenName2',
36
+ symbol: 'TKN2',
37
+ decimals: 18,
38
+ logo_url: 'logo_url2',
39
+ },
40
+ in: [{ value: '4', usd_price: '4', raw_value: '0x4' }],
41
+ out: [{ value: '5', usd_price: '5', raw_value: '0x5' }],
42
+ },
43
+ ];
44
+
45
+ const result = processBalanceChange(assetDiffs);
46
+
47
+ // Verify sorting logic for 'ins'
48
+ expect(result?.ins).toEqual([
49
+ {
50
+ token: {
51
+ type: 'ERC20',
52
+ address: '0xTokenAddress2',
53
+ name: 'TokenName2',
54
+ symbol: 'TKN2',
55
+ decimals: 18,
56
+ logoUri: 'logo_url2',
57
+ },
58
+ items: [{ displayValue: '4', usdPrice: '4' }],
59
+ },
60
+ {
61
+ token: {
62
+ type: 'ERC20',
63
+ address: '0xTokenAddress1',
64
+ name: 'TokenName1',
65
+ symbol: 'TKN1',
66
+ decimals: 18,
67
+ logoUri: 'logo_url1',
68
+ },
69
+ items: [
70
+ { displayValue: '1', usdPrice: '1' },
71
+ { displayValue: '2', usdPrice: '2' },
72
+ ],
73
+ },
74
+ ]);
75
+
76
+ // Verify sorting logic for 'outs'
77
+ expect(result?.outs).toEqual([
78
+ {
79
+ token: {
80
+ type: 'ERC20',
81
+ address: '0xTokenAddress2',
82
+ name: 'TokenName2',
83
+ symbol: 'TKN2',
84
+ decimals: 18,
85
+ logoUri: 'logo_url2',
86
+ },
87
+ items: [{ displayValue: '5', usdPrice: '5' }],
88
+ },
89
+ {
90
+ token: {
91
+ type: 'ERC20',
92
+ address: '0xTokenAddress1',
93
+ name: 'TokenName1',
94
+ symbol: 'TKN1',
95
+ decimals: 18,
96
+ logoUri: 'logo_url1',
97
+ },
98
+ items: [
99
+ { displayValue: '3', usdPrice: '3' },
100
+ { displayValue: '1', usdPrice: '1' },
101
+ ],
102
+ },
103
+ ]);
104
+ });
105
+ });
@@ -0,0 +1,294 @@
1
+ import Blockaid from '@blockaid/client';
2
+ import type { TransactionParams } from '../types';
3
+ import {
4
+ type NetworkContractToken,
5
+ type NetworkToken,
6
+ TokenType,
7
+ AlertType,
8
+ type Alert,
9
+ type BalanceChange,
10
+ type TokenApproval,
11
+ type TokenDiff,
12
+ type TokenDiffItem,
13
+ type TokenApprovals,
14
+ type RpcRequest,
15
+ RpcMethod,
16
+ } from '@avalabs/vm-module-types';
17
+ import { balanceToDisplayValue, numberToBN } from '@avalabs/utils-sdk';
18
+ import { isHexString } from 'ethers';
19
+ import { scanJsonRpc, scanTransaction } from './scan-transaction';
20
+
21
+ export const processTransactionSimulation = async ({
22
+ request,
23
+ dAppUrl,
24
+ params,
25
+ chainId,
26
+ proxyApiUrl,
27
+ }: {
28
+ request: RpcRequest;
29
+ dAppUrl?: string;
30
+ params: TransactionParams;
31
+ chainId: number;
32
+ proxyApiUrl: string;
33
+ }) => {
34
+ const { validation, simulation } = await scanTransaction({
35
+ proxyApiUrl,
36
+ chainId,
37
+ params,
38
+ domain: dAppUrl,
39
+ });
40
+
41
+ let alert: Alert | undefined;
42
+ if (!validation || validation.result_type === 'Error' || validation.result_type === 'Warning') {
43
+ alert = {
44
+ type: AlertType.WARNING,
45
+ details: {
46
+ title: 'Suspicious Transaction',
47
+ description: 'Use caution, this transaction may be malicious.',
48
+ },
49
+ };
50
+ } else if (validation.result_type === 'Malicious') {
51
+ alert = {
52
+ type: AlertType.DANGER,
53
+ details: {
54
+ title: 'Scam Transaction',
55
+ description: 'This transaction is malicious, do not proceed.',
56
+ actionTitles: {
57
+ reject: 'Reject Transaction',
58
+ proceed: 'Proceed Anyway',
59
+ },
60
+ },
61
+ };
62
+ }
63
+
64
+ let balanceChange: BalanceChange | undefined;
65
+ let tokenApprovals: TokenApprovals | undefined;
66
+
67
+ if (simulation?.status === 'Success') {
68
+ tokenApprovals = processTokenApprovals(request, simulation.account_summary.exposures);
69
+ balanceChange = processBalanceChange(simulation.account_summary.assets_diffs);
70
+ }
71
+
72
+ return { alert, balanceChange, tokenApprovals };
73
+ };
74
+
75
+ const processTokenApprovals = (
76
+ request: RpcRequest,
77
+ exposures: Blockaid.AddressAssetExposure[],
78
+ ): TokenApprovals | undefined => {
79
+ const approvals: TokenApproval[] = [];
80
+
81
+ for (const exposurePerAsset of exposures) {
82
+ const token = convertAssetToNetworkContractToken(exposurePerAsset.asset);
83
+ if (!token) {
84
+ continue;
85
+ }
86
+
87
+ for (const [spenderAddress, exposurePerSpender] of Object.entries(exposurePerAsset.spenders)) {
88
+ if (exposurePerSpender.exposure.length === 0) {
89
+ approvals.push({
90
+ token,
91
+ spenderAddress,
92
+ logoUri: token.logoUri,
93
+ });
94
+ } else {
95
+ for (const exposure of exposurePerSpender.exposure) {
96
+ if ('raw_value' in exposure) {
97
+ approvals.push({
98
+ token,
99
+ spenderAddress,
100
+ value: exposure.raw_value,
101
+ usdPrice: exposure.usd_price,
102
+ logoUri: token.logoUri,
103
+ });
104
+ } else {
105
+ approvals.push({
106
+ token,
107
+ spenderAddress,
108
+ logoUri: exposure.logo_url,
109
+ usdPrice: exposure.usd_price,
110
+ });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ if (approvals.length === 0) {
118
+ return undefined;
119
+ }
120
+
121
+ const isEditable =
122
+ approvals.length === 1 &&
123
+ approvals[0]?.token.type === TokenType.ERC20 &&
124
+ request.method === RpcMethod.ETH_SEND_TRANSACTION;
125
+
126
+ return { isEditable, approvals };
127
+ };
128
+
129
+ export const processBalanceChange = (assetDiffs: Blockaid.AssetDiff[]): BalanceChange | undefined => {
130
+ const ins = processAssetDiffs(assetDiffs, 'in');
131
+ const outs = processAssetDiffs(assetDiffs, 'out');
132
+
133
+ if (ins.length === 0 && outs.length === 0) {
134
+ return undefined;
135
+ }
136
+
137
+ return { ins, outs };
138
+ };
139
+
140
+ const processAssetDiffs = (assetDiffs: Blockaid.AssetDiff[], type: 'in' | 'out'): TokenDiff[] => {
141
+ return (
142
+ assetDiffs
143
+ .filter((assetDiff) => assetDiff[type].length > 0)
144
+ // sort asset diffs by length of in/out array
145
+ // this is done to ensure that the token with multiple in/out values are displayed last,
146
+ // to put them in groups with appropriate UI(i.e. accordion), after single in/out tokens
147
+ .sort((a, b) => a[type].length - b[type].length)
148
+ .map((assetDiff) => {
149
+ const asset = assetDiff.asset;
150
+ // convert blockaid asset to network token
151
+ const token: NetworkToken | NetworkContractToken | undefined =
152
+ 'address' in asset ? convertAssetToNetworkContractToken(asset) : convertNativeAssetToToken(asset);
153
+ if (!token) {
154
+ return undefined;
155
+ }
156
+
157
+ const items = assetDiff[type]
158
+ .map((diff) => {
159
+ let displayValue;
160
+ if ('value' in diff && diff.value) {
161
+ if ('decimals' in token) {
162
+ const valueBN = numberToBN(diff.value, token.decimals);
163
+ displayValue = balanceToDisplayValue(valueBN, token.decimals);
164
+ } else if (isHexString(diff.value)) {
165
+ // for some token (like ERC1155) blockaid returns value in hex format
166
+ displayValue = parseInt(diff.value, 16).toString();
167
+ }
168
+ } else if ('type' in token && token.type === TokenType.ERC721) {
169
+ // for ERC721 type token, we just display 1 to indicate that a single NFT will be transferred
170
+ displayValue = '1';
171
+ }
172
+
173
+ return displayValue ? { displayValue, usdPrice: diff.usd_price } : undefined;
174
+ })
175
+ .filter((x): x is TokenDiffItem => x !== undefined);
176
+
177
+ return { token, items };
178
+ })
179
+ .filter((x): x is TokenDiff => x !== undefined)
180
+ );
181
+ };
182
+
183
+ const convertAssetToNetworkContractToken = (
184
+ asset:
185
+ | Blockaid.Erc20TokenDetails
186
+ | Blockaid.Erc1155TokenDetails
187
+ | Blockaid.Erc721TokenDetails
188
+ | Blockaid.NonercTokenDetails,
189
+ ): NetworkContractToken | undefined => {
190
+ let token: NetworkContractToken | undefined;
191
+ if (asset.type === 'ERC20') {
192
+ token = {
193
+ type: TokenType.ERC20,
194
+ address: asset.address,
195
+ decimals: asset.decimals,
196
+ name: asset.name ?? asset.symbol ?? '',
197
+ symbol: asset.symbol ?? '',
198
+ logoUri: asset.logo_url,
199
+ };
200
+ } else if (asset.type === 'ERC1155') {
201
+ token = {
202
+ type: TokenType.ERC1155,
203
+ address: asset.address,
204
+ logoUri: asset.logo_url,
205
+ name: asset.name,
206
+ symbol: asset.symbol,
207
+ };
208
+ } else if (asset.type === 'ERC721') {
209
+ token = {
210
+ type: TokenType.ERC721,
211
+ address: asset.address,
212
+ logoUri: asset.logo_url,
213
+ name: asset.name,
214
+ symbol: asset.symbol,
215
+ };
216
+ } else if (asset.type === 'NONERC') {
217
+ token = {
218
+ type: TokenType.NONERC,
219
+ address: asset.address,
220
+ logoUri: asset.logo_url,
221
+ name: asset.name,
222
+ symbol: asset.symbol,
223
+ };
224
+ }
225
+
226
+ return token;
227
+ };
228
+
229
+ const convertNativeAssetToToken = (asset: Blockaid.NativeAssetDetails): NetworkToken => {
230
+ return {
231
+ name: asset.name ?? '',
232
+ symbol: asset.symbol ?? '',
233
+ decimals: asset.decimals,
234
+ description: '',
235
+ logoUri: asset.logo_url,
236
+ };
237
+ };
238
+
239
+ export const processJsonRpcSimulation = async ({
240
+ request,
241
+ dAppUrl,
242
+ accountAddress,
243
+ chainId,
244
+ data,
245
+ proxyApiUrl,
246
+ }: {
247
+ request: RpcRequest;
248
+ dAppUrl?: string;
249
+ accountAddress: string;
250
+ data: { method: string; params: unknown };
251
+ chainId: number;
252
+ proxyApiUrl: string;
253
+ }) => {
254
+ const { validation, simulation } = await scanJsonRpc({
255
+ proxyApiUrl,
256
+ chainId,
257
+ accountAddress,
258
+ data: data as Blockaid.Evm.JsonRpcScanParams.Data,
259
+ domain: dAppUrl,
260
+ });
261
+
262
+ let alert: Alert | undefined;
263
+ if (!validation || validation.result_type === 'Error' || validation.result_type === 'Warning') {
264
+ alert = {
265
+ type: AlertType.WARNING,
266
+ details: {
267
+ title: 'Suspicious Transaction',
268
+ description: 'Use caution, this transaction may be malicious.',
269
+ },
270
+ };
271
+ } else if (validation.result_type === 'Malicious') {
272
+ alert = {
273
+ type: AlertType.DANGER,
274
+ details: {
275
+ title: 'Scam Transaction',
276
+ description: 'This transaction is malicious, do not proceed.',
277
+ actionTitles: {
278
+ reject: 'Reject Transaction',
279
+ proceed: 'Proceed Anyway',
280
+ },
281
+ },
282
+ };
283
+ }
284
+
285
+ let balanceChange: BalanceChange | undefined;
286
+ let tokenApprovals: TokenApprovals | undefined;
287
+
288
+ if (simulation?.status === 'Success') {
289
+ tokenApprovals = processTokenApprovals(request, simulation.account_summary.exposures);
290
+ balanceChange = processBalanceChange(simulation.account_summary.assets_diffs);
291
+ }
292
+
293
+ return { alert, balanceChange, tokenApprovals };
294
+ };