Merge pull request #15173 from MetaMask/Version-v10.18.0
Version v10.18.0 RCfeature/default_network_editable
commit
cea02f8fe0
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 78 KiB |
@ -0,0 +1,146 @@ |
||||
{ |
||||
"data": { |
||||
"AppStateController": { |
||||
"mkrMigrationReminderTimestamp": null |
||||
}, |
||||
"CachedBalancesController": { |
||||
"cachedBalances": { |
||||
"4": {} |
||||
} |
||||
}, |
||||
"CurrencyController": { |
||||
"conversionDate": 1575697244.188, |
||||
"conversionRate": 149.61, |
||||
"currentCurrency": "usd", |
||||
"nativeCurrency": "ETH" |
||||
}, |
||||
"IncomingTransactionsController": { |
||||
"incomingTransactions": {}, |
||||
"incomingTxLastFetchedBlocksByNetwork": { |
||||
"goerli": null, |
||||
"kovan": null, |
||||
"mainnet": null, |
||||
"rinkeby": 5570536 |
||||
} |
||||
}, |
||||
"KeyringController": { |
||||
"vault": "{\"data\":\"s6TpYjlUNsn7ifhEFTkuDGBUM1GyOlPrim7JSjtfIxgTt8/6MiXgiR/CtFfR4dWW2xhq85/NGIBYEeWrZThGdKGarBzeIqBfLFhw9n509jprzJ0zc2Rf+9HVFGLw+xxC4xPxgCS0IIWeAJQ+XtGcHmn0UZXriXm8Ja4kdlow6SWinB7sr/WM3R0+frYs4WgllkwggDf2/Tv6VHygvLnhtzp6hIJFyTjh+l/KnyJTyZW1TkZhDaNDzX3SCOHT\",\"iv\":\"FbeHDAW5afeWNORfNJBR0Q==\",\"salt\":\"TxZ+WbCW6891C9LK/hbMAoUsSEW1E8pyGLVBU6x5KR8=\"}" |
||||
}, |
||||
"NetworkController": { |
||||
"network": "1337", |
||||
"provider": { |
||||
"nickname": "Localhost 8545", |
||||
"rpcUrl": "http://localhost:8545", |
||||
"chainId": "0x539", |
||||
"ticker": "ETH", |
||||
"type": "rpc" |
||||
} |
||||
}, |
||||
"NotificationController": { |
||||
"notifications": { |
||||
"1": { |
||||
"isShown": true |
||||
}, |
||||
"3": { |
||||
"isShown": true |
||||
}, |
||||
"5": { |
||||
"isShown": true |
||||
}, |
||||
"6": { |
||||
"isShown": true |
||||
}, |
||||
"8": { |
||||
"isShown": true |
||||
}, |
||||
"12": { |
||||
"isShown": true |
||||
} |
||||
} |
||||
}, |
||||
"OnboardingController": { |
||||
"onboardingTabs": {}, |
||||
"seedPhraseBackedUp": false |
||||
}, |
||||
"PermissionsMetadata": { |
||||
"domainMetadata": { |
||||
"metamask.github.io": { |
||||
"icon": null, |
||||
"name": "M E T A M A S K M E S H T E S T" |
||||
} |
||||
}, |
||||
"permissionsHistory": {}, |
||||
"permissionsLog": [ |
||||
{ |
||||
"id": 746677923, |
||||
"method": "eth_accounts", |
||||
"methodType": "restricted", |
||||
"origin": "metamask.github.io", |
||||
"request": { |
||||
"id": 746677923, |
||||
"jsonrpc": "2.0", |
||||
"method": "eth_accounts", |
||||
"origin": "metamask.github.io", |
||||
"params": [] |
||||
}, |
||||
"requestTime": 1575697241368, |
||||
"response": { |
||||
"id": 746677923, |
||||
"jsonrpc": "2.0", |
||||
"result": [] |
||||
}, |
||||
"responseTime": 1575697241370, |
||||
"success": true |
||||
} |
||||
] |
||||
}, |
||||
"PreferencesController": { |
||||
"accountTokens": { |
||||
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { |
||||
"rinkeby": [], |
||||
"ropsten": [] |
||||
} |
||||
}, |
||||
"assetImages": {}, |
||||
"completedOnboarding": true, |
||||
"dismissSeedBackUpReminder": true, |
||||
"currentLocale": "en", |
||||
"featureFlags": { |
||||
"showIncomingTransactions": true, |
||||
"transactionTime": false, |
||||
"sendHexData": true |
||||
}, |
||||
"firstTimeFlowType": "create", |
||||
"forgottenPassword": false, |
||||
"frequentRpcListDetail": [], |
||||
"identities": { |
||||
"0x5cfe73b6021e818b776b421b1c4db2474086a7e1": { |
||||
"address": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", |
||||
"name": "Account 1" |
||||
} |
||||
}, |
||||
"knownMethodData": {}, |
||||
"lostIdentities": {}, |
||||
"metaMetricsId": null, |
||||
"participateInMetaMetrics": false, |
||||
"preferences": { |
||||
"useNativeCurrencyAsPrimaryCurrency": true |
||||
}, |
||||
"selectedAddress": "0x5cfe73b6021e818b776b421b1c4db2474086a7e1", |
||||
"suggestedTokens": {}, |
||||
"tokens": [], |
||||
"useBlockie": false, |
||||
"useNonceField": false, |
||||
"usePhishDetect": true, |
||||
"useTokenDetection": true |
||||
}, |
||||
"config": {}, |
||||
"firstTimeInfo": { |
||||
"date": 1575697234195, |
||||
"version": "7.7.0" |
||||
} |
||||
}, |
||||
"meta": { |
||||
"version": 40 |
||||
} |
||||
} |
@ -1,3 +1,3 @@ |
||||
module.exports = { |
||||
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/1.0.0', |
||||
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/2.0.0', |
||||
}; |
||||
|
@ -0,0 +1,329 @@ |
||||
const { strict: assert } = require('assert'); |
||||
const { convertToHexValue, withFixtures } = require('../helpers'); |
||||
|
||||
const hexPrefixedAddress = '0x2f318C334780961FB129D2a6c30D0763d9a5C970'; |
||||
const nonHexPrefixedAddress = hexPrefixedAddress.substring(2); |
||||
|
||||
describe('Send ETH to a 40 character hexadecimal address', function () { |
||||
const ganacheOptions = { |
||||
accounts: [ |
||||
{ |
||||
secretKey: |
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||
balance: convertToHexValue(25000000000000000000), |
||||
}, |
||||
], |
||||
}; |
||||
it('should ensure the address is prefixed with 0x when pasted and should send ETH to a valid hexadecimal address', async function () { |
||||
await withFixtures( |
||||
{ |
||||
fixtures: 'imported-account', |
||||
ganacheOptions, |
||||
title: this.test.title, |
||||
failOnConsoleError: false, |
||||
}, |
||||
async ({ driver }) => { |
||||
await driver.navigate(); |
||||
await driver.fill('#password', 'correct horse battery staple'); |
||||
await driver.press('#password', driver.Key.ENTER); |
||||
|
||||
// Send ETH
|
||||
await driver.clickElement('[data-testid="eth-overview-send"]'); |
||||
|
||||
// Paste address without hex prefix
|
||||
await driver.pasteIntoField( |
||||
'input[placeholder="Search, public address (0x), or ENS"]', |
||||
nonHexPrefixedAddress, |
||||
); |
||||
await driver.waitForSelector({ |
||||
css: '.ens-input__selected-input__title', |
||||
text: hexPrefixedAddress, |
||||
}); |
||||
await driver.wait(async () => { |
||||
const sendDialogMsgs = await driver.findElements( |
||||
'.send-v2__form div.dialog', |
||||
); |
||||
return sendDialogMsgs.length === 1; |
||||
}, 10000); |
||||
await driver.clickElement({ text: 'Next', tag: 'button' }); |
||||
|
||||
// Confirm transaction
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' }); |
||||
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||
const sendTransactionListItem = await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item', |
||||
); |
||||
await sendTransactionListItem.click(); |
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' }); |
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)'); |
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement( |
||||
'.nickname-popover__public-address', |
||||
); |
||||
assert.equal(await publicAddress.getText(), hexPrefixedAddress); |
||||
}, |
||||
); |
||||
}); |
||||
it('should ensure the address is prefixed with 0x when typed and should send ETH to a valid hexadecimal address', async function () { |
||||
await withFixtures( |
||||
{ |
||||
fixtures: 'imported-account', |
||||
ganacheOptions, |
||||
title: this.test.title, |
||||
failOnConsoleError: false, |
||||
}, |
||||
async ({ driver }) => { |
||||
await driver.navigate(); |
||||
await driver.fill('#password', 'correct horse battery staple'); |
||||
await driver.press('#password', driver.Key.ENTER); |
||||
|
||||
// Send ETH
|
||||
await driver.clickElement('[data-testid="eth-overview-send"]'); |
||||
|
||||
// Type address without hex prefix
|
||||
await driver.fill( |
||||
'input[placeholder="Search, public address (0x), or ENS"]', |
||||
nonHexPrefixedAddress, |
||||
); |
||||
await driver.waitForSelector({ |
||||
css: '.ens-input__selected-input__title', |
||||
text: hexPrefixedAddress, |
||||
}); |
||||
await driver.wait(async () => { |
||||
const sendDialogMsgs = await driver.findElements( |
||||
'.send-v2__form div.dialog', |
||||
); |
||||
return sendDialogMsgs.length === 1; |
||||
}, 10000); |
||||
await driver.clickElement({ text: 'Next', tag: 'button' }); |
||||
|
||||
// Confirm transaction
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' }); |
||||
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||
const sendTransactionListItem = await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item', |
||||
); |
||||
await sendTransactionListItem.click(); |
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' }); |
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)'); |
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement( |
||||
'.nickname-popover__public-address', |
||||
); |
||||
assert.equal(await publicAddress.getText(), hexPrefixedAddress); |
||||
}, |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('Send ERC20 to a 40 character hexadecimal address', function () { |
||||
const ganacheOptions = { |
||||
accounts: [ |
||||
{ |
||||
secretKey: |
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||
balance: convertToHexValue(25000000000000000000), |
||||
}, |
||||
], |
||||
}; |
||||
it('should ensure the address is prefixed with 0x when pasted and should send TST to a valid hexadecimal address', async function () { |
||||
await withFixtures( |
||||
{ |
||||
dapp: true, |
||||
fixtures: 'connected-state', |
||||
ganacheOptions, |
||||
title: this.test.title, |
||||
failOnConsoleError: false, |
||||
}, |
||||
async ({ driver }) => { |
||||
await driver.navigate(); |
||||
await driver.fill('#password', 'correct horse battery staple'); |
||||
await driver.press('#password', driver.Key.ENTER); |
||||
|
||||
// Create TST
|
||||
await driver.openNewPage('http://127.0.0.1:8080/'); |
||||
await driver.clickElement('#createToken'); |
||||
await driver.waitUntilXWindowHandles(3); |
||||
let windowHandles = await driver.getAllWindowHandles(); |
||||
const extension = windowHandles[0]; |
||||
const dapp = await driver.switchToWindowWithTitle( |
||||
'E2E Test Dapp', |
||||
windowHandles, |
||||
); |
||||
await driver.switchToWindowWithTitle( |
||||
'MetaMask Notification', |
||||
windowHandles, |
||||
); |
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' }); |
||||
await driver.waitUntilXWindowHandles(2); |
||||
await driver.switchToWindow(extension); |
||||
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||
await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', |
||||
{ timeout: 10000 }, |
||||
); |
||||
|
||||
// Add token
|
||||
await driver.switchToWindow(dapp); |
||||
await driver.clickElement('#watchAsset'); |
||||
await driver.waitUntilXWindowHandles(3); |
||||
windowHandles = await driver.getAllWindowHandles(); |
||||
await driver.switchToWindowWithTitle( |
||||
'MetaMask Notification', |
||||
windowHandles, |
||||
); |
||||
await driver.clickElement({ text: 'Add Token', tag: 'button' }); |
||||
await driver.waitUntilXWindowHandles(2); |
||||
await driver.switchToWindow(extension); |
||||
|
||||
// Send TST
|
||||
await driver.clickElement('[data-testid="home__asset-tab"]'); |
||||
await driver.clickElement('.token-cell'); |
||||
await driver.clickElement('[data-testid="eth-overview-send"]'); |
||||
|
||||
// Paste address without hex prefix
|
||||
await driver.pasteIntoField( |
||||
'input[placeholder="Search, public address (0x), or ENS"]', |
||||
nonHexPrefixedAddress, |
||||
); |
||||
await driver.waitForSelector({ |
||||
css: '.ens-input__selected-input__title', |
||||
text: hexPrefixedAddress, |
||||
}); |
||||
await driver.wait(async () => { |
||||
const sendDialogMsgs = await driver.findElements( |
||||
'.send-v2__form div.dialog', |
||||
); |
||||
return sendDialogMsgs.length === 1; |
||||
}, 10000); |
||||
await driver.delay(2000); |
||||
await driver.clickElement({ text: 'Next', tag: 'button' }); |
||||
|
||||
// Confirm transaction
|
||||
await driver.waitForSelector({ |
||||
css: '.confirm-page-container-summary__title', |
||||
text: '0 TST', |
||||
}); |
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' }); |
||||
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||
await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', |
||||
{ timeout: 10000 }, |
||||
); |
||||
const sendTransactionListItem = await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', |
||||
); |
||||
await sendTransactionListItem.click(); |
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' }); |
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)'); |
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement( |
||||
'.nickname-popover__public-address', |
||||
); |
||||
assert.equal(await publicAddress.getText(), hexPrefixedAddress); |
||||
}, |
||||
); |
||||
}); |
||||
it('should ensure the address is prefixed with 0x when typed and should send TST to a valid hexadecimal address', async function () { |
||||
await withFixtures( |
||||
{ |
||||
dapp: true, |
||||
fixtures: 'connected-state', |
||||
ganacheOptions, |
||||
title: this.test.title, |
||||
failOnConsoleError: false, |
||||
}, |
||||
async ({ driver }) => { |
||||
await driver.navigate(); |
||||
await driver.fill('#password', 'correct horse battery staple'); |
||||
await driver.press('#password', driver.Key.ENTER); |
||||
|
||||
// Create TST
|
||||
await driver.openNewPage('http://127.0.0.1:8080/'); |
||||
await driver.clickElement('#createToken'); |
||||
await driver.waitUntilXWindowHandles(3); |
||||
let windowHandles = await driver.getAllWindowHandles(); |
||||
const extension = windowHandles[0]; |
||||
const dapp = await driver.switchToWindowWithTitle( |
||||
'E2E Test Dapp', |
||||
windowHandles, |
||||
); |
||||
await driver.switchToWindowWithTitle( |
||||
'MetaMask Notification', |
||||
windowHandles, |
||||
); |
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' }); |
||||
await driver.waitUntilXWindowHandles(2); |
||||
await driver.switchToWindow(extension); |
||||
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||
await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', |
||||
{ timeout: 10000 }, |
||||
); |
||||
|
||||
// Add token
|
||||
await driver.switchToWindow(dapp); |
||||
await driver.clickElement('#watchAsset'); |
||||
await driver.waitUntilXWindowHandles(3); |
||||
windowHandles = await driver.getAllWindowHandles(); |
||||
await driver.switchToWindowWithTitle( |
||||
'MetaMask Notification', |
||||
windowHandles, |
||||
); |
||||
await driver.clickElement({ text: 'Add Token', tag: 'button' }); |
||||
await driver.waitUntilXWindowHandles(2); |
||||
await driver.switchToWindow(extension); |
||||
|
||||
// Send TST
|
||||
await driver.clickElement('[data-testid="home__asset-tab"]'); |
||||
await driver.clickElement('.token-cell'); |
||||
await driver.clickElement('[data-testid="eth-overview-send"]'); |
||||
|
||||
// Type address without hex prefix
|
||||
await driver.fill( |
||||
'input[placeholder="Search, public address (0x), or ENS"]', |
||||
nonHexPrefixedAddress, |
||||
); |
||||
await driver.waitForSelector({ |
||||
css: '.ens-input__selected-input__title', |
||||
text: hexPrefixedAddress, |
||||
}); |
||||
await driver.wait(async () => { |
||||
const sendDialogMsgs = await driver.findElements( |
||||
'.send-v2__form div.dialog', |
||||
); |
||||
return sendDialogMsgs.length === 1; |
||||
}, 10000); |
||||
await driver.delay(2000); |
||||
await driver.clickElement({ text: 'Next', tag: 'button' }); |
||||
|
||||
// Confirm transaction
|
||||
await driver.waitForSelector({ |
||||
css: '.confirm-page-container-summary__title', |
||||
text: '0 TST', |
||||
}); |
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' }); |
||||
await driver.clickElement('[data-testid="home__activity-tab"]'); |
||||
await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(2)', |
||||
{ timeout: 10000 }, |
||||
); |
||||
const sendTransactionListItem = await driver.waitForSelector( |
||||
'.transaction-list__completed-transactions .transaction-list-item:nth-of-type(1)', |
||||
); |
||||
await sendTransactionListItem.click(); |
||||
await driver.clickElement({ text: 'Activity log', tag: 'summary' }); |
||||
await driver.clickElement('.sender-to-recipient__name:nth-of-type(2)'); |
||||
|
||||
// Verify address in activity log
|
||||
const publicAddress = await driver.findElement( |
||||
'.nickname-popover__public-address', |
||||
); |
||||
assert.equal(await publicAddress.getText(), hexPrefixedAddress); |
||||
}, |
||||
); |
||||
}); |
||||
}); |
@ -0,0 +1,65 @@ |
||||
const { strict: assert } = require('assert'); |
||||
const { promises: fs } = require('fs'); |
||||
const { convertToHexValue, withFixtures } = require('../helpers'); |
||||
|
||||
const downloadsFolder = `${process.cwd()}/test-artifacts/downloads`; |
||||
|
||||
const createDownloadFolder = async () => { |
||||
await fs.rm(downloadsFolder, { recursive: true, force: true }); |
||||
await fs.mkdir(downloadsFolder, { recursive: true }); |
||||
}; |
||||
|
||||
const stateLogsExist = async () => { |
||||
try { |
||||
const stateLogs = `${downloadsFolder}/MetaMask State Logs.json`; |
||||
await fs.access(stateLogs); |
||||
return true; |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
}; |
||||
|
||||
describe('State logs', function () { |
||||
const ganacheOptions = { |
||||
accounts: [ |
||||
{ |
||||
secretKey: |
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC', |
||||
balance: convertToHexValue(25000000000000000000), |
||||
}, |
||||
], |
||||
}; |
||||
it('should download state logs for the account', async function () { |
||||
await withFixtures( |
||||
{ |
||||
fixtures: 'imported-account', |
||||
ganacheOptions, |
||||
title: this.test.title, |
||||
failOnConsoleError: false, |
||||
}, |
||||
async ({ driver }) => { |
||||
await createDownloadFolder(); |
||||
await driver.navigate(); |
||||
await driver.fill('#password', 'correct horse battery staple'); |
||||
await driver.press('#password', driver.Key.ENTER); |
||||
|
||||
// Download State Logs
|
||||
await driver.clickElement('.account-menu__icon'); |
||||
await driver.clickElement({ text: 'Settings', tag: 'div' }); |
||||
await driver.clickElement({ text: 'Advanced', tag: 'div' }); |
||||
await driver.clickElement({ |
||||
text: 'Download State Logs', |
||||
tag: 'button', |
||||
}); |
||||
|
||||
// Verify download
|
||||
let fileExists; |
||||
await driver.wait(async () => { |
||||
fileExists = await stateLogsExist(); |
||||
return fileExists === true; |
||||
}, 10000); |
||||
assert.equal(fileExists, true); |
||||
}, |
||||
); |
||||
}); |
||||
}); |
@ -1,168 +1,286 @@ |
||||
import React, { useContext } from 'react'; |
||||
import { useSelector } from 'react-redux'; |
||||
import PropTypes from 'prop-types'; |
||||
import React, { useContext, useEffect, useState } from 'react'; |
||||
import { useDispatch, useSelector } from 'react-redux'; |
||||
import { useHistory } from 'react-router-dom'; |
||||
import { I18nContext } from '../../../contexts/i18n'; |
||||
import Box from '../../ui/box'; |
||||
import Typography from '../../ui/typography'; |
||||
import { |
||||
ALIGN_ITEMS, |
||||
BLOCK_SIZES, |
||||
COLORS, |
||||
DISPLAY, |
||||
FLEX_DIRECTION, |
||||
FONT_WEIGHT, |
||||
TYPOGRAPHY, |
||||
JUSTIFY_CONTENT, |
||||
SIZES, |
||||
} from '../../../helpers/constants/design-system'; |
||||
import Button from '../../ui/button'; |
||||
import IconCaretLeft from '../../ui/icon/icon-caret-left'; |
||||
import Tooltip from '../../ui/tooltip'; |
||||
import IconWithFallback from '../../ui/icon-with-fallback'; |
||||
import IconBorder from '../../ui/icon-border'; |
||||
import { getTheme } from '../../../selectors'; |
||||
import { THEME_TYPE } from '../../../pages/settings/experimental-tab/experimental-tab.constant'; |
||||
import { |
||||
getFrequentRpcListDetail, |
||||
getUnapprovedConfirmations, |
||||
} from '../../../selectors'; |
||||
|
||||
import { |
||||
ENVIRONMENT_TYPE_FULLSCREEN, |
||||
ENVIRONMENT_TYPE_POPUP, |
||||
MESSAGE_TYPE, |
||||
} from '../../../../shared/constants/app'; |
||||
import { requestUserApproval } from '../../../store/actions'; |
||||
import Popover from '../../ui/popover'; |
||||
import ConfirmationPage from '../../../pages/confirmation/confirmation'; |
||||
import { FEATURED_RPCS } from '../../../../shared/constants/network'; |
||||
import { ADD_NETWORK_ROUTE } from '../../../helpers/constants/routes'; |
||||
import { getEnvironmentType } from '../../../../app/scripts/lib/util'; |
||||
|
||||
const AddNetwork = ({ |
||||
onBackClick, |
||||
onAddNetworkClick, |
||||
onAddNetworkManuallyClick, |
||||
featuredRPCS, |
||||
}) => { |
||||
const AddNetwork = () => { |
||||
const t = useContext(I18nContext); |
||||
const theme = useSelector(getTheme); |
||||
const dispatch = useDispatch(); |
||||
const history = useHistory(); |
||||
const frequentRpcList = useSelector(getFrequentRpcListDetail); |
||||
|
||||
const frequentRpcListChainIds = Object.values(frequentRpcList).map( |
||||
(net) => net.chainId, |
||||
); |
||||
|
||||
const infuraRegex = /infura.io/u; |
||||
|
||||
const nets = featuredRPCS |
||||
.sort((a, b) => (a.ticker > b.ticker ? 1 : -1)) |
||||
.slice(0, 8); |
||||
const nets = FEATURED_RPCS.sort((a, b) => |
||||
a.ticker > b.ticker ? 1 : -1, |
||||
).slice(0, FEATURED_RPCS.length); |
||||
|
||||
const notFrequentRpcNetworks = nets.filter( |
||||
(net) => frequentRpcListChainIds.indexOf(net.chainId) === -1, |
||||
); |
||||
const unapprovedConfirmations = useSelector(getUnapprovedConfirmations); |
||||
const [showPopover, setShowPopover] = useState(false); |
||||
|
||||
useEffect(() => { |
||||
const anAddNetworkConfirmationFromMetaMaskExists = unapprovedConfirmations?.find( |
||||
(confirmation) => { |
||||
return ( |
||||
confirmation.origin === 'metamask' && |
||||
confirmation.type === MESSAGE_TYPE.ADD_ETHEREUM_CHAIN |
||||
); |
||||
}, |
||||
); |
||||
if (!showPopover && anAddNetworkConfirmationFromMetaMaskExists) { |
||||
setShowPopover(true); |
||||
} |
||||
|
||||
if (showPopover && !anAddNetworkConfirmationFromMetaMaskExists) { |
||||
setShowPopover(false); |
||||
} |
||||
}, [unapprovedConfirmations, showPopover]); |
||||
|
||||
return ( |
||||
<Box> |
||||
<Box |
||||
height={BLOCK_SIZES.TWO_TWELFTHS} |
||||
padding={[4, 0, 4, 0]} |
||||
display={DISPLAY.FLEX} |
||||
alignItems={ALIGN_ITEMS.CENTER} |
||||
flexDirection={FLEX_DIRECTION.ROW} |
||||
className="add-network__header" |
||||
> |
||||
<IconCaretLeft |
||||
aria-label={t('back')} |
||||
onClick={onBackClick} |
||||
className="add-network__header__back-icon" |
||||
/> |
||||
<Typography variant={TYPOGRAPHY.H3} color={COLORS.TEXT_DEFAULT}> |
||||
{t('addNetwork')} |
||||
</Typography> |
||||
</Box> |
||||
<Box |
||||
height={BLOCK_SIZES.FOUR_FIFTHS} |
||||
width={BLOCK_SIZES.TEN_TWELFTHS} |
||||
margin={[0, 6, 0, 6]} |
||||
> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H6} |
||||
color={COLORS.TEXT_ALTERNATIVE} |
||||
margin={[4, 0, 0, 0]} |
||||
<> |
||||
{Object.keys(notFrequentRpcNetworks).length === 0 ? ( |
||||
<Box |
||||
className="add-network__edge-case-box" |
||||
borderRadius={SIZES.MD} |
||||
padding={4} |
||||
margin={[4, 6, 0, 6]} |
||||
display={DISPLAY.FLEX} |
||||
flexDirection={FLEX_DIRECTION.ROW} |
||||
backgroundColor={COLORS.BACKGROUND_ALTERNATIVE} |
||||
> |
||||
{t('addFromAListOfPopularNetworks')} |
||||
</Typography> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
color={COLORS.TEXT_MUTED} |
||||
margin={[4, 0, 3, 0]} |
||||
> |
||||
{t('popularCustomNetworks')} |
||||
</Typography> |
||||
{nets.map((item, index) => ( |
||||
<Box marginRight={4}> |
||||
<img src="images/info-fox.svg" /> |
||||
</Box> |
||||
<Box> |
||||
<Typography variant={TYPOGRAPHY.H7}> |
||||
{t('youHaveAddedAll', [ |
||||
<a |
||||
key="link" |
||||
className="add-network__edge-case-box__link" |
||||
href="https://chainlist.wtf/" |
||||
target="_blank" |
||||
rel="noreferrer" |
||||
> |
||||
{t('here')}. |
||||
</a>, |
||||
<Button |
||||
key="button" |
||||
type="inline" |
||||
onClick={(event) => { |
||||
event.preventDefault(); |
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP |
||||
? global.platform.openExtensionInBrowser( |
||||
ADD_NETWORK_ROUTE, |
||||
) |
||||
: history.push(ADD_NETWORK_ROUTE); |
||||
}} |
||||
> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
color={COLORS.INFO_DEFAULT} |
||||
> |
||||
{t('addMoreNetworks')}. |
||||
</Typography> |
||||
</Button>, |
||||
])} |
||||
</Typography> |
||||
</Box> |
||||
</Box> |
||||
) : ( |
||||
<Box className="add-network__networks-container"> |
||||
{getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN && ( |
||||
<Box |
||||
display={DISPLAY.FLEX} |
||||
alignItems={ALIGN_ITEMS.CENTER} |
||||
flexDirection={FLEX_DIRECTION.ROW} |
||||
marginTop={7} |
||||
marginBottom={4} |
||||
paddingBottom={2} |
||||
className="add-network__header" |
||||
> |
||||
<Typography variant={TYPOGRAPHY.H4} color={COLORS.TEXT_MUTED}> |
||||
{t('networks')} |
||||
</Typography> |
||||
<span className="add-network__header__subtitle">{' > '}</span> |
||||
<Typography variant={TYPOGRAPHY.H4} color={COLORS.TEXT_DEFAULT}> |
||||
{t('addANetwork')} |
||||
</Typography> |
||||
</Box> |
||||
)} |
||||
<Box |
||||
key={index} |
||||
display={DISPLAY.FLEX} |
||||
alignItems={ALIGN_ITEMS.CENTER} |
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN} |
||||
marginBottom={6} |
||||
margin={ |
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP |
||||
? [0, 0, 1, 0] |
||||
: [4, 0, 1, 0] |
||||
} |
||||
className="add-network__main-container" |
||||
> |
||||
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}> |
||||
<IconBorder size={24}> |
||||
<IconWithFallback |
||||
icon={item.rpcPrefs.imageUrl} |
||||
name={item.nickname} |
||||
size={24} |
||||
/> |
||||
</IconBorder> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
color={COLORS.TEXT_DEFAULT} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
boxProps={{ marginLeft: 2 }} |
||||
<Typography |
||||
variant={TYPOGRAPHY.H6} |
||||
color={COLORS.TEXT_ALTERNATIVE} |
||||
margin={[4, 0, 0, 0]} |
||||
> |
||||
{t('addFromAListOfPopularNetworks')} |
||||
</Typography> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
color={COLORS.TEXT_MUTED} |
||||
margin={[4, 0, 3, 0]} |
||||
> |
||||
{t('popularCustomNetworks')} |
||||
</Typography> |
||||
{notFrequentRpcNetworks.map((item, index) => ( |
||||
<Box |
||||
key={index} |
||||
display={DISPLAY.FLEX} |
||||
alignItems={ALIGN_ITEMS.CENTER} |
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN} |
||||
marginBottom={6} |
||||
className="add-network__list-of-networks" |
||||
> |
||||
{item.nickname} |
||||
</Typography> |
||||
</Box> |
||||
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}> |
||||
{ |
||||
// Warning for the networks that doesn't use infura.io as the RPC
|
||||
!infuraRegex.test(item.rpcUrl) && ( |
||||
<Tooltip |
||||
className="add-network__warning-tooltip" |
||||
position="top" |
||||
interactive |
||||
html={ |
||||
<Box margin={3} className="add-network__warning-tooltip"> |
||||
{t('addNetworkTooltipWarning', [ |
||||
<a |
||||
key="zendesk_page_link" |
||||
href="https://metamask.zendesk.com/hc/en-us/articles/4417500466971" |
||||
rel="noreferrer" |
||||
target="_blank" |
||||
<Box display={DISPLAY.FLEX} alignItems={ALIGN_ITEMS.CENTER}> |
||||
<Box> |
||||
<IconBorder size={24}> |
||||
<IconWithFallback |
||||
icon={item.rpcPrefs.imageUrl} |
||||
name={item.nickname} |
||||
size={24} |
||||
/> |
||||
</IconBorder> |
||||
</Box> |
||||
<Box marginLeft={2}> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H7} |
||||
color={COLORS.TEXT_DEFAULT} |
||||
fontWeight={FONT_WEIGHT.BOLD} |
||||
> |
||||
{item.nickname} |
||||
</Typography> |
||||
</Box> |
||||
</Box> |
||||
<Box |
||||
display={DISPLAY.FLEX} |
||||
alignItems={ALIGN_ITEMS.CENTER} |
||||
marginLeft={1} |
||||
> |
||||
{ |
||||
// Warning for the networks that doesn't use infura.io as the RPC
|
||||
!infuraRegex.test(item.rpcUrl) && ( |
||||
<Tooltip |
||||
position="top" |
||||
interactive |
||||
html={ |
||||
<Box |
||||
margin={3} |
||||
className="add-network__warning-tooltip" |
||||
> |
||||
{t('learnMoreUpperCase')} |
||||
</a>, |
||||
])} |
||||
</Box> |
||||
} |
||||
trigger="mouseenter" |
||||
theme={theme === THEME_TYPE.DEFAULT ? 'light' : 'dark'} |
||||
{t('addNetworkTooltipWarning', [ |
||||
<a |
||||
key="zendesk_page_link" |
||||
href="https://metamask.zendesk.com/hc/en-us/articles/4417500466971" |
||||
rel="noreferrer" |
||||
target="_blank" |
||||
> |
||||
{t('learnMoreUpperCase')} |
||||
</a>, |
||||
])} |
||||
</Box> |
||||
} |
||||
trigger="mouseenter" |
||||
> |
||||
<i |
||||
className="fa fa-exclamation-triangle add-network__warning-icon" |
||||
title={t('warning')} |
||||
/> |
||||
</Tooltip> |
||||
) |
||||
} |
||||
<Button |
||||
type="inline" |
||||
className="add-network__add-button" |
||||
onClick={async () => { |
||||
await dispatch(requestUserApproval(item, true)); |
||||
}} |
||||
> |
||||
<i |
||||
className="fa fa-exclamation-triangle add-network__warning-icon" |
||||
title={t('warning')} |
||||
/> |
||||
</Tooltip> |
||||
) |
||||
} |
||||
<Button |
||||
type="inline" |
||||
className="add-network__add-button" |
||||
onClick={onAddNetworkClick} |
||||
{t('add')} |
||||
</Button> |
||||
</Box> |
||||
</Box> |
||||
))} |
||||
</Box> |
||||
<Box |
||||
padding={ |
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP |
||||
? [2, 0, 2, 6] |
||||
: [2, 0, 2, 0] |
||||
} |
||||
className="add-network__footer" |
||||
> |
||||
<Button |
||||
type="link" |
||||
onClick={(event) => { |
||||
event.preventDefault(); |
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP |
||||
? global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE) |
||||
: history.push(ADD_NETWORK_ROUTE); |
||||
}} |
||||
> |
||||
<Typography |
||||
variant={TYPOGRAPHY.H6} |
||||
color={COLORS.PRIMARY_DEFAULT} |
||||
> |
||||
{t('add')} |
||||
</Button> |
||||
</Box> |
||||
{t('addANetworkManually')} |
||||
</Typography> |
||||
</Button> |
||||
</Box> |
||||
))} |
||||
</Box> |
||||
<Box |
||||
height={BLOCK_SIZES.ONE_TWELFTH} |
||||
padding={[4, 4, 4, 4]} |
||||
className="add-network__footer" |
||||
> |
||||
<Button type="link" onClick={onAddNetworkManuallyClick}> |
||||
<Typography variant={TYPOGRAPHY.H6} color={COLORS.PRIMARY_DEFAULT}> |
||||
{t('addANetworkManually')} |
||||
</Typography> |
||||
</Button> |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
)} |
||||
{showPopover && ( |
||||
<Popover> |
||||
<ConfirmationPage /> |
||||
</Popover> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
AddNetwork.propTypes = { |
||||
onBackClick: PropTypes.func, |
||||
onAddNetworkClick: PropTypes.func, |
||||
onAddNetworkManuallyClick: PropTypes.func, |
||||
featuredRPCS: PropTypes.array, |
||||
}; |
||||
|
||||
export default AddNetwork; |
||||
|
@ -0,0 +1,60 @@ |
||||
import React from 'react'; |
||||
import { screen } from '@testing-library/react'; |
||||
import { renderWithProvider } from '../../../../test/jest'; |
||||
import configureStore from '../../../store/store'; |
||||
import mockState from '../../../../test/data/mock-state.json'; |
||||
import AddNetwork from './add-network'; |
||||
|
||||
jest.mock('../../../selectors', () => ({ |
||||
getFrequentRpcListDetail: () => ({ |
||||
frequentRpcList: [ |
||||
{ |
||||
chainId: '0x539', |
||||
nickname: 'Localhost 8545', |
||||
rpcPrefs: {}, |
||||
rpcUrl: 'http://localhost:8545', |
||||
ticker: 'ETH', |
||||
}, |
||||
{ |
||||
chainId: '0xA4B1', |
||||
nickname: 'Arbitrum One', |
||||
rpcPrefs: { blockExplorerUrl: 'https://explorer.arbitrum.io' }, |
||||
rpcUrl: |
||||
'https://arbitrum-mainnet.infura.io/v3/7e127583378c4732a858df2550aff333', |
||||
ticker: 'AETH', |
||||
}, |
||||
], |
||||
}), |
||||
getUnapprovedConfirmations: jest.fn(), |
||||
getTheme: () => 'light', |
||||
})); |
||||
|
||||
const render = () => { |
||||
const store = configureStore({ |
||||
metamask: { |
||||
...mockState.metamask, |
||||
}, |
||||
}); |
||||
return renderWithProvider(<AddNetwork />, store); |
||||
}; |
||||
|
||||
describe('AddNetwork', () => { |
||||
it('should show Add from a list.. text', () => { |
||||
render(); |
||||
expect( |
||||
screen.getByText( |
||||
'Add from a list of popular networks or add a network manually. Only interact with the entities you trust.', |
||||
), |
||||
).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should show Popular custom networks text', () => { |
||||
render(); |
||||
expect(screen.getByText('Popular custom networks')).toBeInTheDocument(); |
||||
}); |
||||
|
||||
it('should show Arbitrum One network nickname', () => { |
||||
render(); |
||||
expect(screen.getByText('Arbitrum One')).toBeInTheDocument(); |
||||
}); |
||||
}); |
@ -0,0 +1,41 @@ |
||||
import React from 'react'; |
||||
import PropTypes from 'prop-types'; |
||||
import classnames from 'classnames'; |
||||
import Typography from '../../ui/typography'; |
||||
import { TYPOGRAPHY } from '../../../helpers/constants/design-system'; |
||||
import { useI18nContext } from '../../../hooks/useI18nContext'; |
||||
|
||||
export default function CollectibleDefaultImage({ |
||||
name, |
||||
tokenId, |
||||
handleImageClick, |
||||
}) { |
||||
const t = useI18nContext(); |
||||
return ( |
||||
<div |
||||
className={classnames('collectible-default', { |
||||
'collectible-default--clickable': handleImageClick, |
||||
})} |
||||
onClick={handleImageClick} |
||||
> |
||||
<Typography variant={TYPOGRAPHY.H6} className="collectible-default__text"> |
||||
{name ?? t('unknownCollection')} <br /> #{tokenId} |
||||
</Typography> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
CollectibleDefaultImage.propTypes = { |
||||
/** |
||||
* The name of the collectible collection if not supplied will default to "Unnamed collection" |
||||
*/ |
||||
name: PropTypes.string, |
||||
/** |
||||
* The token id of the collectible |
||||
*/ |
||||
tokenId: PropTypes.string, |
||||
/** |
||||
* The click handler for the collectible default image |
||||
*/ |
||||
handleImageClick: PropTypes.func, |
||||
}; |
@ -0,0 +1,42 @@ |
||||
import React from 'react'; |
||||
import CollectibleDefaultImage from '.'; |
||||
|
||||
export default { |
||||
title: 'Components/App/CollectibleDefaultImage', |
||||
id: __filename, |
||||
argTypes: { |
||||
name: { |
||||
control: 'text', |
||||
}, |
||||
tokenId: { |
||||
control: 'text', |
||||
}, |
||||
handleImageClick: { |
||||
action: 'handleImageClick', |
||||
}, |
||||
}, |
||||
args: { |
||||
name: null, |
||||
tokenId: '12345', |
||||
handleImageClick: null, |
||||
}, |
||||
}; |
||||
|
||||
export const DefaultStory = (args) => ( |
||||
<div style={{ width: 200, height: 200 }}> |
||||
<CollectibleDefaultImage {...args} /> |
||||
</div> |
||||
); |
||||
|
||||
DefaultStory.storyName = 'Default'; |
||||
|
||||
export const handleImageClick = (args) => ( |
||||
<div style={{ width: 200, height: 200 }}> |
||||
<CollectibleDefaultImage {...args} /> |
||||
</div> |
||||
); |
||||
|
||||
handleImageClick.args = { |
||||
// eslint-disable-next-line no-alert
|
||||
handleImageClick: () => window.alert('CollectibleDefaultImage clicked!'), |
||||
}; |
@ -0,0 +1 @@ |
||||
export { default } from './collectible-default-image'; |
@ -0,0 +1,22 @@ |
||||
.collectible-default { |
||||
background-color: var(--color-background-alternative); |
||||
padding-top: 100%; // retains 1:1 aspect ratio |
||||
position: relative; |
||||
width: 100%; |
||||
|
||||
&__text { |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
text-align: center; |
||||
position: absolute; |
||||
white-space: nowrap; |
||||
top: 50%; |
||||
left: 50%; |
||||
transform: translate(-50%, -50%); |
||||
width: calc(100% - 32px); |
||||
} |
||||
|
||||
&--clickable { |
||||
cursor: pointer; |
||||
} |
||||
} |
@ -0,0 +1,295 @@ |
||||
import { addHexPrefix } from 'ethereumjs-util'; |
||||
import abi from 'human-standard-token-abi'; |
||||
import { GAS_LIMITS } from '../../../shared/constants/gas'; |
||||
import { CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP } from '../../../shared/constants/network'; |
||||
import { |
||||
ASSET_TYPES, |
||||
TRANSACTION_ENVELOPE_TYPES, |
||||
} from '../../../shared/constants/transaction'; |
||||
import { readAddressAsContract } from '../../../shared/modules/contract-utils'; |
||||
import { |
||||
conversionUtil, |
||||
multiplyCurrencies, |
||||
} from '../../../shared/modules/conversion.utils'; |
||||
import { ETH, GWEI } from '../../helpers/constants/common'; |
||||
import { calcTokenAmount } from '../../helpers/utils/token-util'; |
||||
import { MIN_GAS_LIMIT_HEX } from '../../pages/send/send.constants'; |
||||
import { |
||||
addGasBuffer, |
||||
generateERC20TransferData, |
||||
generateERC721TransferData, |
||||
getAssetTransferData, |
||||
} from '../../pages/send/send.utils'; |
||||
import { getGasPriceInHexWei } from '../../selectors'; |
||||
import { estimateGas } from '../../store/actions'; |
||||
|
||||
export async function estimateGasLimitForSend({ |
||||
selectedAddress, |
||||
value, |
||||
gasPrice, |
||||
sendToken, |
||||
to, |
||||
data, |
||||
isNonStandardEthChain, |
||||
chainId, |
||||
gasLimit, |
||||
...options |
||||
}) { |
||||
let isSimpleSendOnNonStandardNetwork = false; |
||||
|
||||
// blockGasLimit may be a falsy, but defined, value when we receive it from
|
||||
// state, so we use logical or to fall back to MIN_GAS_LIMIT_HEX. Some
|
||||
// network implementations check the gas parameter supplied to
|
||||
// eth_estimateGas for validity. For this reason, we set token sends
|
||||
// blockGasLimit default to a higher number. Note that the current gasLimit
|
||||
// on a BLOCK is 15,000,000 and will be 30,000,000 on mainnet after London.
|
||||
// Meanwhile, MIN_GAS_LIMIT_HEX is 0x5208.
|
||||
let blockGasLimit = MIN_GAS_LIMIT_HEX; |
||||
if (options.blockGasLimit) { |
||||
blockGasLimit = options.blockGasLimit; |
||||
} else if (sendToken) { |
||||
blockGasLimit = GAS_LIMITS.BASE_TOKEN_ESTIMATE; |
||||
} |
||||
|
||||
// The parameters below will be sent to our background process to estimate
|
||||
// how much gas will be used for a transaction. That background process is
|
||||
// located in tx-gas-utils.js in the transaction controller folder.
|
||||
const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }; |
||||
|
||||
if (sendToken) { |
||||
if (!to) { |
||||
// If no to address is provided, we cannot generate the token transfer
|
||||
// hexData. hexData in a transaction largely dictates how much gas will
|
||||
// be consumed by a transaction. We must use our best guess, which is
|
||||
// represented in the gas shared constants.
|
||||
return GAS_LIMITS.BASE_TOKEN_ESTIMATE; |
||||
} |
||||
paramsForGasEstimate.value = '0x0'; |
||||
|
||||
// We have to generate the erc20/erc721 contract call to transfer tokens in
|
||||
// order to get a proper estimate for gasLimit.
|
||||
paramsForGasEstimate.data = getAssetTransferData({ |
||||
sendToken, |
||||
fromAddress: selectedAddress, |
||||
toAddress: to, |
||||
amount: value, |
||||
}); |
||||
|
||||
paramsForGasEstimate.to = sendToken.address; |
||||
} else { |
||||
if (!data) { |
||||
// eth.getCode will return the compiled smart contract code at the
|
||||
// address. If this returns 0x, 0x0 or a nullish value then the address
|
||||
// is an externally owned account (NOT a contract account). For these
|
||||
// types of transactions the gasLimit will always be 21,000 or 0x5208
|
||||
const { isContractAddress } = to |
||||
? await readAddressAsContract(global.eth, to) |
||||
: {}; |
||||
if (!isContractAddress && !isNonStandardEthChain) { |
||||
return GAS_LIMITS.SIMPLE; |
||||
} else if (!isContractAddress && isNonStandardEthChain) { |
||||
isSimpleSendOnNonStandardNetwork = true; |
||||
} |
||||
} |
||||
|
||||
paramsForGasEstimate.data = data; |
||||
|
||||
if (to) { |
||||
paramsForGasEstimate.to = to; |
||||
} |
||||
|
||||
if (!value || value === '0') { |
||||
// TODO: Figure out what's going on here. According to eth_estimateGas
|
||||
// docs this value can be zero, or undefined, yet we are setting it to a
|
||||
// value here when the value is undefined or zero. For more context:
|
||||
// https://github.com/MetaMask/metamask-extension/pull/6195
|
||||
paramsForGasEstimate.value = '0xff'; |
||||
} |
||||
} |
||||
|
||||
if (!isSimpleSendOnNonStandardNetwork) { |
||||
// If we do not yet have a gasLimit, we must call into our background
|
||||
// process to get an estimate for gasLimit based on known parameters.
|
||||
|
||||
paramsForGasEstimate.gas = addHexPrefix( |
||||
multiplyCurrencies(blockGasLimit, 0.95, { |
||||
multiplicandBase: 16, |
||||
multiplierBase: 10, |
||||
roundDown: '0', |
||||
toNumericBase: 'hex', |
||||
}), |
||||
); |
||||
} |
||||
|
||||
// The buffer multipler reduces transaction failures by ensuring that the
|
||||
// estimated gas is always sufficient. Without the multiplier, estimates
|
||||
// for contract interactions can become inaccurate over time. This is because
|
||||
// gas estimation is non-deterministic. The gas required for the exact same
|
||||
// transaction call can change based on state of a contract or changes in the
|
||||
// contracts environment (blockchain data or contracts it interacts with).
|
||||
// Applying the 1.5 buffer has proven to be a useful guard against this non-
|
||||
// deterministic behaviour.
|
||||
//
|
||||
// Gas estimation of simple sends should, however, be deterministic. As such
|
||||
// no buffer is needed in those cases.
|
||||
let bufferMultiplier = 1.5; |
||||
if (isSimpleSendOnNonStandardNetwork) { |
||||
bufferMultiplier = 1; |
||||
} else if (CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId]) { |
||||
bufferMultiplier = CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId]; |
||||
} |
||||
|
||||
try { |
||||
// Call into the background process that will simulate transaction
|
||||
// execution on the node and return an estimate of gasLimit
|
||||
const estimatedGasLimit = await estimateGas(paramsForGasEstimate); |
||||
const estimateWithBuffer = addGasBuffer( |
||||
estimatedGasLimit, |
||||
blockGasLimit, |
||||
bufferMultiplier, |
||||
); |
||||
return addHexPrefix(estimateWithBuffer); |
||||
} catch (error) { |
||||
const simulationFailed = |
||||
error.message.includes('Transaction execution error.') || |
||||
error.message.includes( |
||||
'gas required exceeds allowance or always failing transaction', |
||||
) || |
||||
(CHAIN_ID_TO_GAS_LIMIT_BUFFER_MAP[chainId] && |
||||
error.message.includes('gas required exceeds allowance')); |
||||
if (simulationFailed) { |
||||
const estimateWithBuffer = addGasBuffer( |
||||
paramsForGasEstimate?.gas ?? gasLimit, |
||||
blockGasLimit, |
||||
bufferMultiplier, |
||||
); |
||||
return addHexPrefix(estimateWithBuffer); |
||||
} |
||||
throw error; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Generates a txParams from the send slice. |
||||
* |
||||
* @param {import('.').SendState} sendState - the state of the send slice |
||||
* @returns {import( |
||||
* '../../../shared/constants/transaction' |
||||
* ).TxParams} A txParams object that can be used to create a transaction or |
||||
* update an existing transaction. |
||||
*/ |
||||
export function generateTransactionParams(sendState) { |
||||
const draftTransaction = |
||||
sendState.draftTransactions[sendState.currentTransactionUUID]; |
||||
const txParams = { |
||||
// If the fromAccount has been specified we use that, if not we use the
|
||||
// selected account.
|
||||
from: |
||||
draftTransaction.fromAccount?.address || |
||||
sendState.selectedAccount.address, |
||||
// gasLimit always needs to be set regardless of the asset being sent
|
||||
// or the type of transaction.
|
||||
gas: draftTransaction.gas.gasLimit, |
||||
}; |
||||
switch (draftTransaction.asset.type) { |
||||
case ASSET_TYPES.TOKEN: |
||||
// When sending a token the to address is the contract address of
|
||||
// the token being sent. The value is set to '0x0' and the data
|
||||
// is generated from the recipient address, token being sent and
|
||||
// amount.
|
||||
txParams.to = draftTransaction.asset.details.address; |
||||
txParams.value = '0x0'; |
||||
txParams.data = generateERC20TransferData({ |
||||
toAddress: draftTransaction.recipient.address, |
||||
amount: draftTransaction.amount.value, |
||||
sendToken: draftTransaction.asset.details, |
||||
}); |
||||
break; |
||||
case ASSET_TYPES.COLLECTIBLE: |
||||
// When sending a token the to address is the contract address of
|
||||
// the token being sent. The value is set to '0x0' and the data
|
||||
// is generated from the recipient address, token being sent and
|
||||
// amount.
|
||||
txParams.to = draftTransaction.asset.details.address; |
||||
txParams.value = '0x0'; |
||||
txParams.data = generateERC721TransferData({ |
||||
toAddress: draftTransaction.recipient.address, |
||||
fromAddress: |
||||
draftTransaction.fromAccount?.address ?? |
||||
sendState.selectedAccount.address, |
||||
tokenId: draftTransaction.asset.details.tokenId, |
||||
}); |
||||
break; |
||||
case ASSET_TYPES.NATIVE: |
||||
default: |
||||
// When sending native currency the to and value fields use the
|
||||
// recipient and amount values and the data key is either null or
|
||||
// populated with the user input provided in hex field.
|
||||
txParams.to = draftTransaction.recipient.address; |
||||
txParams.value = draftTransaction.amount.value; |
||||
txParams.data = draftTransaction.userInputHexData ?? undefined; |
||||
} |
||||
|
||||
// We need to make sure that we only include the right gas fee fields
|
||||
// based on the type of transaction the network supports. We will also set
|
||||
// the type param here.
|
||||
if (sendState.eip1559support) { |
||||
txParams.type = TRANSACTION_ENVELOPE_TYPES.FEE_MARKET; |
||||
|
||||
txParams.maxFeePerGas = draftTransaction.gas.maxFeePerGas; |
||||
txParams.maxPriorityFeePerGas = draftTransaction.gas.maxPriorityFeePerGas; |
||||
|
||||
if (!txParams.maxFeePerGas || txParams.maxFeePerGas === '0x0') { |
||||
txParams.maxFeePerGas = draftTransaction.gas.gasPrice; |
||||
} |
||||
|
||||
if ( |
||||
!txParams.maxPriorityFeePerGas || |
||||
txParams.maxPriorityFeePerGas === '0x0' |
||||
) { |
||||
txParams.maxPriorityFeePerGas = txParams.maxFeePerGas; |
||||
} |
||||
} else { |
||||
txParams.gasPrice = draftTransaction.gas.gasPrice; |
||||
txParams.type = TRANSACTION_ENVELOPE_TYPES.LEGACY; |
||||
} |
||||
|
||||
return txParams; |
||||
} |
||||
|
||||
/** |
||||
* This method is used to keep the original logic from the gas.duck.js file |
||||
* after receiving a gasPrice from eth_gasPrice. First, the returned gasPrice |
||||
* was converted to GWEI, then it was converted to a Number, then in the send |
||||
* duck (here) we would use getGasPriceInHexWei to get back to hexWei. Now that |
||||
* we receive a GWEI estimate from the controller, we still need to do this |
||||
* weird conversion to get the proper rounding. |
||||
* |
||||
* @param {string} gasPriceEstimate |
||||
* @returns {string} |
||||
*/ |
||||
export function getRoundedGasPrice(gasPriceEstimate) { |
||||
const gasPriceInDecGwei = conversionUtil(gasPriceEstimate, { |
||||
numberOfDecimals: 9, |
||||
toDenomination: GWEI, |
||||
fromNumericBase: 'dec', |
||||
toNumericBase: 'dec', |
||||
fromCurrency: ETH, |
||||
fromDenomination: GWEI, |
||||
}); |
||||
const gasPriceAsNumber = Number(gasPriceInDecGwei); |
||||
return getGasPriceInHexWei(gasPriceAsNumber); |
||||
} |
||||
|
||||
export async function getERC20Balance(token, accountAddress) { |
||||
const contract = global.eth.contract(abi).at(token.address); |
||||
const usersToken = (await contract.balanceOf(accountAddress)) ?? null; |
||||
if (!usersToken) { |
||||
return '0x0'; |
||||
} |
||||
const amount = calcTokenAmount( |
||||
usersToken.balance.toString(), |
||||
token.decimals, |
||||
).toString(16); |
||||
return addHexPrefix(amount); |
||||
} |
@ -0,0 +1,163 @@ |
||||
import { ethers } from 'ethers'; |
||||
import { GAS_LIMITS } from '../../../shared/constants/gas'; |
||||
import { |
||||
ASSET_TYPES, |
||||
TRANSACTION_ENVELOPE_TYPES, |
||||
} from '../../../shared/constants/transaction'; |
||||
import { BURN_ADDRESS } from '../../../shared/modules/hexstring-utils'; |
||||
import { getInitialSendStateWithExistingTxState } from '../../../test/jest/mocks'; |
||||
import { TOKEN_STANDARDS } from '../../helpers/constants/common'; |
||||
import { |
||||
generateERC20TransferData, |
||||
generateERC721TransferData, |
||||
} from '../../pages/send/send.utils'; |
||||
import { generateTransactionParams } from './helpers'; |
||||
|
||||
describe('Send Slice Helpers', () => { |
||||
describe('generateTransactionParams', () => { |
||||
it('should generate a txParams for a token transfer', () => { |
||||
const tokenDetails = { |
||||
address: '0xToken', |
||||
symbol: 'SYMB', |
||||
decimals: 18, |
||||
}; |
||||
const txParams = generateTransactionParams( |
||||
getInitialSendStateWithExistingTxState({ |
||||
fromAccount: { |
||||
address: '0x00', |
||||
}, |
||||
amount: { |
||||
value: '0x1', |
||||
}, |
||||
asset: { |
||||
type: ASSET_TYPES.TOKEN, |
||||
balance: '0xaf', |
||||
details: tokenDetails, |
||||
}, |
||||
recipient: { |
||||
address: BURN_ADDRESS, |
||||
}, |
||||
}), |
||||
); |
||||
expect(txParams).toStrictEqual({ |
||||
from: '0x00', |
||||
data: generateERC20TransferData({ |
||||
toAddress: BURN_ADDRESS, |
||||
amount: '0x1', |
||||
sendToken: tokenDetails, |
||||
}), |
||||
to: '0xToken', |
||||
type: '0x0', |
||||
value: '0x0', |
||||
gas: '0x0', |
||||
gasPrice: '0x0', |
||||
}); |
||||
}); |
||||
|
||||
it('should generate a txParams for a collectible transfer', () => { |
||||
const txParams = generateTransactionParams( |
||||
getInitialSendStateWithExistingTxState({ |
||||
fromAccount: { |
||||
address: '0x00', |
||||
}, |
||||
amount: { |
||||
value: '0x1', |
||||
}, |
||||
asset: { |
||||
type: ASSET_TYPES.COLLECTIBLE, |
||||
balance: '0xaf', |
||||
details: { |
||||
address: '0xToken', |
||||
standard: TOKEN_STANDARDS.ERC721, |
||||
tokenId: ethers.BigNumber.from(15000).toString(), |
||||
}, |
||||
}, |
||||
recipient: { |
||||
address: BURN_ADDRESS, |
||||
}, |
||||
}), |
||||
); |
||||
expect(txParams).toStrictEqual({ |
||||
from: '0x00', |
||||
data: generateERC721TransferData({ |
||||
toAddress: BURN_ADDRESS, |
||||
fromAddress: '0x00', |
||||
tokenId: ethers.BigNumber.from(15000).toString(), |
||||
}), |
||||
to: '0xToken', |
||||
type: '0x0', |
||||
value: '0x0', |
||||
gas: '0x0', |
||||
gasPrice: '0x0', |
||||
}); |
||||
}); |
||||
|
||||
it('should generate a txParams for a native legacy transaction', () => { |
||||
const txParams = generateTransactionParams( |
||||
getInitialSendStateWithExistingTxState({ |
||||
fromAccount: { |
||||
address: '0x00', |
||||
}, |
||||
amount: { |
||||
value: '0x1', |
||||
}, |
||||
asset: { |
||||
type: ASSET_TYPES.NATIVE, |
||||
balance: '0xaf', |
||||
details: null, |
||||
}, |
||||
recipient: { |
||||
address: BURN_ADDRESS, |
||||
}, |
||||
}), |
||||
); |
||||
expect(txParams).toStrictEqual({ |
||||
from: '0x00', |
||||
data: undefined, |
||||
to: BURN_ADDRESS, |
||||
type: '0x0', |
||||
value: '0x1', |
||||
gas: '0x0', |
||||
gasPrice: '0x0', |
||||
}); |
||||
}); |
||||
|
||||
it('should generate a txParams for a native fee market transaction', () => { |
||||
const txParams = generateTransactionParams({ |
||||
...getInitialSendStateWithExistingTxState({ |
||||
fromAccount: { |
||||
address: '0x00', |
||||
}, |
||||
amount: { |
||||
value: '0x1', |
||||
}, |
||||
asset: { |
||||
type: ASSET_TYPES.NATIVE, |
||||
balance: '0xaf', |
||||
details: null, |
||||
}, |
||||
recipient: { |
||||
address: BURN_ADDRESS, |
||||
}, |
||||
gas: { |
||||
maxFeePerGas: '0x2', |
||||
maxPriorityFeePerGas: '0x1', |
||||
gasLimit: GAS_LIMITS.SIMPLE, |
||||
}, |
||||
transactionType: TRANSACTION_ENVELOPE_TYPES.FEE_MARKET, |
||||
}), |
||||
eip1559support: true, |
||||
}); |
||||
expect(txParams).toStrictEqual({ |
||||
from: '0x00', |
||||
data: undefined, |
||||
to: BURN_ADDRESS, |
||||
type: '0x2', |
||||
value: '0x1', |
||||
gas: GAS_LIMITS.SIMPLE, |
||||
maxFeePerGas: '0x2', |
||||
maxPriorityFeePerGas: '0x1', |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,79 @@ |
||||
import React from 'react'; |
||||
import { fireEvent } from '@testing-library/react'; |
||||
import configureMockStore from 'redux-mock-store'; |
||||
import { renderWithProvider } from '../../../test/jest/rendering'; |
||||
import * as Actions from '../../store/actions'; |
||||
import AddCollectible from '.'; |
||||
|
||||
const VALID_ADDRESS = '0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9'; |
||||
const INVALID_ADDRESS = 'aoinsafasdfa'; |
||||
const VALID_TOKENID = '1201'; |
||||
const INVALID_TOKENID = 'abcde'; |
||||
|
||||
describe('AddCollectible', () => { |
||||
const store = configureMockStore([])({ |
||||
metamask: { provider: { chainId: '0x1' } }, |
||||
}); |
||||
|
||||
it('should enable the "Add" button when valid entries are input into both Address and TokenId fields', () => { |
||||
const { getByTestId, getByText } = renderWithProvider( |
||||
<AddCollectible />, |
||||
store, |
||||
); |
||||
expect(getByText('Add')).not.toBeEnabled(); |
||||
fireEvent.change(getByTestId('address'), { |
||||
target: { value: VALID_ADDRESS }, |
||||
}); |
||||
fireEvent.change(getByTestId('token-id'), { |
||||
target: { value: VALID_TOKENID }, |
||||
}); |
||||
expect(getByText('Add')).toBeEnabled(); |
||||
}); |
||||
|
||||
it('should not enable the "Add" button when an invalid entry is input into one or both Address and TokenId fields', () => { |
||||
const { getByTestId, getByText } = renderWithProvider( |
||||
<AddCollectible />, |
||||
store, |
||||
); |
||||
expect(getByText('Add')).not.toBeEnabled(); |
||||
fireEvent.change(getByTestId('address'), { |
||||
target: { value: INVALID_ADDRESS }, |
||||
}); |
||||
fireEvent.change(getByTestId('token-id'), { |
||||
target: { value: VALID_TOKENID }, |
||||
}); |
||||
expect(getByText('Add')).not.toBeEnabled(); |
||||
fireEvent.change(getByTestId('address'), { |
||||
target: { value: VALID_ADDRESS }, |
||||
}); |
||||
expect(getByText('Add')).toBeEnabled(); |
||||
fireEvent.change(getByTestId('token-id'), { |
||||
target: { value: INVALID_TOKENID }, |
||||
}); |
||||
expect(getByText('Add')).not.toBeEnabled(); |
||||
}); |
||||
|
||||
it('should call addCollectibleVerifyOwnership action with correct values (tokenId should not be in scientific notation)', () => { |
||||
const { getByTestId, getByText } = renderWithProvider( |
||||
<AddCollectible />, |
||||
store, |
||||
); |
||||
fireEvent.change(getByTestId('address'), { |
||||
target: { value: VALID_ADDRESS }, |
||||
}); |
||||
const LARGE_TOKEN_ID = Number.MAX_SAFE_INTEGER + 1; |
||||
fireEvent.change(getByTestId('token-id'), { |
||||
target: { value: LARGE_TOKEN_ID }, |
||||
}); |
||||
const addCollectibleVerifyOwnershipSpy = jest.spyOn( |
||||
Actions, |
||||
'addCollectibleVerifyOwnership', |
||||
); |
||||
|
||||
fireEvent.click(getByText('Add')); |
||||
expect(addCollectibleVerifyOwnershipSpy).toHaveBeenCalledWith( |
||||
'0x312BE6a98441F9F6e3F6246B13CA19701e0AC3B9', |
||||
'9007199254740992', |
||||
); |
||||
}); |
||||
}); |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue