@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.
- package/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-lint.log +1 -1
- package/.turbo/turbo-test.log +35 -15
- package/CHANGELOG.md +17 -0
- package/dist/index.cjs +31 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +28 -10
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +6 -5
- package/src/constants.ts +1 -0
- package/src/handlers/eth-send-transaction/eth-send-transaction.test.ts +233 -2
- package/src/handlers/eth-send-transaction/eth-send-transaction.ts +27 -8
- package/src/handlers/eth-sign/eth-sign.test.ts +320 -0
- package/src/handlers/eth-sign/eth-sign.ts +158 -0
- package/src/handlers/eth-sign/schemas/eth-sign-typed-data.ts +65 -0
- package/src/handlers/eth-sign/schemas/eth-sign.ts +9 -0
- package/src/handlers/eth-sign/schemas/parse-request-params/fixture.ts +47 -0
- package/src/handlers/eth-sign/schemas/parse-request-params/parse-request-params.test.ts +284 -0
- package/src/handlers/eth-sign/schemas/parse-request-params/parse-request-params.ts +94 -0
- package/src/handlers/eth-sign/schemas/personal-sign.ts +12 -0
- package/src/handlers/eth-sign/schemas/shared.ts +5 -0
- package/src/handlers/eth-sign/utils/beautify-message/beautify-message.test.ts +29 -0
- package/src/handlers/eth-sign/utils/beautify-message/beautify-message.ts +134 -0
- package/src/handlers/eth-sign/utils/is-typed-data-valid.ts +26 -0
- package/src/handlers/eth-sign/utils/typeguards.ts +10 -0
- package/src/handlers/get-balances/evm-balance-service/get-erc20-balances.test.ts +2 -2
- package/src/handlers/get-balances/evm-balance-service/get-erc20-balances.ts +4 -6
- package/src/handlers/get-balances/get-balances.test.ts +0 -5
- package/src/handlers/get-balances/get-balances.ts +14 -3
- package/src/handlers/get-balances/glacier-balance-service/get-erc20-balances.test.ts +0 -1
- package/src/handlers/get-balances/glacier-balance-service/get-erc20-balances.ts +10 -7
- package/src/handlers/get-tokens/get-tokens.test.ts +6 -6
- package/src/index.ts +1 -0
- package/src/module.ts +14 -0
- package/src/types.ts +9 -0
- package/src/utils/parse-erc20-transaction-type.ts +35 -0
- package/src/utils/process-transaction-simulation.test.ts +105 -0
- package/src/utils/process-transaction-simulation.ts +294 -0
- package/src/utils/scan-transaction.ts +63 -0
- 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
|
|
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
|
+
}
|
|
@@ -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:
|
|
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(
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
85
|
+
type: 'ERC20',
|
|
86
86
|
chainId: 1,
|
|
87
87
|
decimals: 18,
|
|
88
88
|
},
|
package/src/index.ts
CHANGED
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
|
+
};
|