1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-01 13:47:06 +01:00
metamask-extension/shared/modules/transaction.utils.js
Mark Stacey 4f66dc948f
Update @metamask/controllers to v33 (#16493)
The controllers package has been updated to v33. The only breaking
change in this release was to rename the term "collectible" to "NFT"
wherever it appeared in the API.

Changes in this PR have been kept minimal; additional renaming can be
done in separate PRs. This PR only updates the controller names,
controller state, controller methods, and any direct references to
these things. NFTs are still called "collectibles" in most places.
2022-11-15 15:19:42 -03:30

283 lines
9.9 KiB
JavaScript

import { isHexString } from 'ethereumjs-util';
import { ethers } from 'ethers';
import { abiERC721, abiERC20, abiERC1155 } from '@metamask/metamask-eth-abis';
import log from 'loglevel';
import {
ASSET_TYPES,
TOKEN_STANDARDS,
TRANSACTION_TYPES,
} from '../constants/transaction';
import { readAddressAsContract } from './contract-utils';
import { isEqualCaseInsensitive } from './string-utils';
/**
* @typedef { 'transfer' | 'approve' | 'setapprovalforall' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes
*/
/**
* @typedef {object} InferTransactionTypeResult
* @property {InferrableTransactionTypes} type - The type of transaction
* @property {string} getCodeResponse - The contract code, in hex format if
* it exists. '0x0' or '0x' are also indicators of non-existent contract
* code
*/
/**
* @typedef EthersContractCall
* @type object
* @property {any[]} args - The args/params to the function call.
* An array-like object with numerical and string indices.
* @property {string} name - The name of the function.
* @property {string} signature - The function signature.
* @property {string} sighash - The function signature hash.
* @property {EthersBigNumber} value - The ETH value associated with the call.
* @property {FunctionFragment} functionFragment - The Ethers function fragment
* representation of the function.
*/
const erc20Interface = new ethers.utils.Interface(abiERC20);
const erc721Interface = new ethers.utils.Interface(abiERC721);
const erc1155Interface = new ethers.utils.Interface(abiERC1155);
export function transactionMatchesNetwork(transaction, chainId, networkId) {
if (typeof transaction.chainId !== 'undefined') {
return transaction.chainId === chainId;
}
return transaction.metamaskNetworkId === networkId;
}
/**
* Determines if the maxFeePerGas and maxPriorityFeePerGas fields are supplied
* and valid inputs. This will return false for non hex string inputs.
*
* @param {import("../constants/transaction").TransactionMeta} transaction -
* the transaction to check
* @returns {boolean} true if transaction uses valid EIP1559 fields
*/
export function isEIP1559Transaction(transaction) {
return (
isHexString(transaction?.txParams?.maxFeePerGas) &&
isHexString(transaction?.txParams?.maxPriorityFeePerGas)
);
}
/**
* Determine if the maxFeePerGas and maxPriorityFeePerGas fields are not
* supplied and that the gasPrice field is valid if it is provided. This will
* return false if gasPrice is a non hex string.
*
* @param {import("../constants/transaction").TransactionMeta} transaction -
* the transaction to check
* @returns {boolean} true if transaction uses valid Legacy fields OR lacks
* EIP1559 fields
*/
export function isLegacyTransaction(transaction) {
return (
typeof transaction.txParams.maxFeePerGas === 'undefined' &&
typeof transaction.txParams.maxPriorityFeePerGas === 'undefined' &&
(typeof transaction.txParams.gasPrice === 'undefined' ||
isHexString(transaction.txParams.gasPrice))
);
}
/**
* Determine if a transactions gas fees in txParams match those in its dappSuggestedGasFees property
*
* @param {import("../constants/transaction").TransactionMeta} transaction -
* the transaction to check
* @returns {boolean} true if both the txParams and dappSuggestedGasFees are objects with truthy gas fee properties,
* and those properties are strictly equal
*/
export function txParamsAreDappSuggested(transaction) {
const { gasPrice, maxPriorityFeePerGas, maxFeePerGas } =
transaction?.txParams || {};
return (
(gasPrice && gasPrice === transaction?.dappSuggestedGasFees?.gasPrice) ||
(maxPriorityFeePerGas &&
maxFeePerGas &&
transaction?.dappSuggestedGasFees?.maxPriorityFeePerGas ===
maxPriorityFeePerGas &&
transaction?.dappSuggestedGasFees?.maxFeePerGas === maxFeePerGas)
);
}
/**
* Attempts to decode transaction data using ABIs for three different token standards: ERC20, ERC721, ERC1155.
* The data will decode correctly if the transaction is an interaction with a contract that matches one of these
* contract standards
*
* @param data - encoded transaction data
* @returns {EthersContractCall | undefined}
*/
export function parseStandardTokenTransactionData(data) {
try {
return erc20Interface.parseTransaction({ data });
} catch {
// ignore and next try to parse with erc721 ABI
}
try {
return erc721Interface.parseTransaction({ data });
} catch {
// ignore and next try to parse with erc1155 ABI
}
try {
return erc1155Interface.parseTransaction({ data });
} catch {
// ignore and return undefined
}
return undefined;
}
/**
* Determines the contractCode of the transaction by analyzing the txParams.
*
* @param {object} txParams - Parameters for the transaction
* @param {EthQuery} query - EthQuery instance
* @returns {InferTransactionTypeResult}
*/
export async function determineTransactionContractCode(txParams, query) {
const { to } = txParams;
const { contractCode } = await readAddressAsContract(query, to);
return contractCode;
}
/**
* Determines the type of the transaction by analyzing the txParams.
* This method will return one of the types defined in shared/constants/transactions
* It will never return TRANSACTION_TYPE_CANCEL or TRANSACTION_TYPE_RETRY as these
* represent specific events that we control from the extension and are added manually
* at transaction creation.
*
* @param {object} txParams - Parameters for the transaction
* @param {EthQuery} query - EthQuery instance
* @returns {InferTransactionTypeResult}
*/
export async function determineTransactionType(txParams, query) {
const { data, to } = txParams;
let name;
try {
({ name } = data && parseStandardTokenTransactionData(data));
} catch (error) {
log.debug('Failed to parse transaction data.', error, data);
}
let result;
let contractCode;
if (data && !to) {
result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
} else {
const { contractCode: resultCode, isContractAddress } =
await readAddressAsContract(query, to);
contractCode = resultCode;
if (isContractAddress) {
const tokenMethodName = [
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM,
].find((methodName) => isEqualCaseInsensitive(methodName, name));
result =
data && tokenMethodName
? tokenMethodName
: TRANSACTION_TYPES.CONTRACT_INTERACTION;
} else {
result = TRANSACTION_TYPES.SIMPLE_SEND;
}
}
return { type: result, getCodeResponse: contractCode };
}
const INFERRABLE_TRANSACTION_TYPES = [
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
TRANSACTION_TYPES.CONTRACT_INTERACTION,
TRANSACTION_TYPES.SIMPLE_SEND,
];
/**
* Given a transaction meta object, determine the asset type that the
* transaction is dealing with, as well as the standard for the token if it
* is a token transaction.
*
* @param {import('../constants/transaction').TransactionMeta} txMeta -
* transaction meta object
* @param {EthQuery} query - EthQuery instance
* @param {Function} getTokenStandardAndDetails - function to get token
* standards and details.
* @returns {{ assetType: string, tokenStandard: string}}
*/
export async function determineTransactionAssetType(
txMeta,
query,
getTokenStandardAndDetails,
) {
// If the transaction type is already one of the inferrable types, then we do
// not need to re-establish the type.
let inferrableType = txMeta.type;
if (INFERRABLE_TRANSACTION_TYPES.includes(txMeta.type) === false) {
// Because we will deal with all types of transactions (including swaps)
// we want to get an inferrable type of transaction that isn't special cased
// that way we can narrow the number of logic gates required.
const result = await determineTransactionType(txMeta.txParams, query);
inferrableType = result.type;
}
// If the inferred type of the transaction is one of those that are part of
// the token contract standards, we can use the getTokenStandardAndDetails
// method to get the asset type.
const isTokenMethod = [
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
].find((methodName) => methodName === inferrableType);
if (
isTokenMethod ||
// We can also check any contract interaction type to see if the to address
// is a token contract. If it isn't, then the method will throw and we can
// fall through to the other checks.
inferrableType === TRANSACTION_TYPES.CONTRACT_INTERACTION
) {
try {
// We don't need a balance check, so the second parameter to
// getTokenStandardAndDetails is omitted.
const details = await getTokenStandardAndDetails(txMeta.txParams.to);
if (details.standard) {
return {
assetType:
details.standard === TOKEN_STANDARDS.ERC20
? ASSET_TYPES.TOKEN
: ASSET_TYPES.NFT,
tokenStandard: details.standard,
};
}
} catch {
// noop, We expect errors here but we don't need to report them or do
// anything in response.
}
}
// If the transaction is interacting with a contract but isn't a token method
// we use the 'UNKNOWN' value to show that it isn't a transaction sending any
// particular asset.
if (inferrableType === TRANSACTION_TYPES.CONTRACT_INTERACTION) {
return {
assetType: ASSET_TYPES.UNKNOWN,
tokenStandard: TOKEN_STANDARDS.NONE,
};
}
return { assetType: ASSET_TYPES.NATIVE, tokenStandard: TOKEN_STANDARDS.NONE };
}