mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
Merge pull request #15173 from MetaMask/Version-v10.18.0
Version v10.18.0 RC
This commit is contained in:
commit
cea02f8fe0
.circleci
.metamaskrc.dist.nvmrcCHANGELOG.mdapp
development/build
package.jsonshared
test
e2e
fixtures/special-settings
mock-e2e.jssnaps
enums.jstest-snap-bip-44.spec.jstest-snap-confirm.spec.jstest-snap-error.spec.jstest-snap-managestate.spec.jstest-snap-notification.spec.js
tests
webdriver
jest
ui
components
app
add-network
app-components.scssasset-list-item
collectible-default-image
collectible-details
collectibles-items
confirm-page-container/confirm-page-container-content/confirm-page-container-summary
dropdowns
metamask-template-renderer
srp-input
transaction-list-item-details
wallet-overview
ui
ducks
helpers
constants
utils
hooks
pages
add-collectible
confirm-approve
confirm-send-ether
confirm-send-token
confirm-transaction-base
confirm-transaction-switch
confirm-transaction
confirmation
home
send/send-content
@ -3,10 +3,10 @@ version: 2.1
|
||||
executors:
|
||||
node-browsers:
|
||||
docker:
|
||||
- image: circleci/node:14-browsers
|
||||
- image: circleci/node:16-browsers
|
||||
node-browsers-medium-plus:
|
||||
docker:
|
||||
- image: circleci/node:14-browsers
|
||||
- image: circleci/node:16-browsers
|
||||
resource_class: medium+
|
||||
environment:
|
||||
NODE_OPTIONS: --max_old_space_size=2048
|
||||
|
@ -5,12 +5,12 @@ set -u
|
||||
set -o pipefail
|
||||
|
||||
# To get the latest version, see <https://www.ubuntuupdates.org/ppa/google_chrome?dist=stable>
|
||||
CHROME_VERSION='102.0.5005.61-1'
|
||||
CHROME_VERSION='103.0.5060.53-1'
|
||||
CHROME_BINARY="google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||
CHROME_BINARY_URL="https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/${CHROME_BINARY}"
|
||||
|
||||
# To retrieve this checksum, run the `wget` and `shasum` commands below
|
||||
CHROME_BINARY_SHA512SUM='dd701b99febf7d927657f38716d90f3a0b967ae75dac5f6e8fbf9df632c8a531ccb9f37ee09340ad730b4fe40d0564c1b64201121d2d3e4e503f3f167ca632cd'
|
||||
CHROME_BINARY_SHA512SUM='36f4e79f46cb71c1431dccf1489f5f8e89d35204a717a4618c7f6f638123ddc2b37bd5cbd00498be8f84c7713149f2faa447cb6da3518be1cb9703e99d110e1a'
|
||||
|
||||
wget -O "${CHROME_BINARY}" -t 5 "${CHROME_BINARY_URL}"
|
||||
|
||||
|
@ -4,7 +4,7 @@ set -e
|
||||
set -u
|
||||
set -o pipefail
|
||||
|
||||
FIREFOX_VERSION='83.0'
|
||||
FIREFOX_VERSION='102.0'
|
||||
FIREFOX_BINARY="firefox-${FIREFOX_VERSION}.tar.bz2"
|
||||
FIREFOX_BINARY_URL="https://ftp.mozilla.org/pub/firefox/releases/${FIREFOX_VERSION}/linux-x86_64/en-US/${FIREFOX_BINARY}"
|
||||
FIREFOX_PATH='/opt/firefox'
|
||||
|
@ -6,6 +6,7 @@ ONBOARDING_V2=
|
||||
SWAPS_USE_DEV_APIS=
|
||||
COLLECTIBLES_V1=
|
||||
TOKEN_DETECTION_V2=
|
||||
ADD_POPULAR_NETWORKS=
|
||||
|
||||
; Set this to test changes to the phishing warning page.
|
||||
PHISHING_WARNING_PAGE_URL=
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [10.18.0]
|
||||
### Added
|
||||
- Add setApprovalForAll confirmation view so granted permissions are displayed in a digested manner, instead of a simple contract interaction([#15010](https://github.com/MetaMask/metamask-extension/pull/15010))
|
||||
- Add warning when performing a Send directly to a token contract([#13588](https://github.com/MetaMask/metamask-extension/pull/13588))
|
||||
|
||||
### Changed
|
||||
- Update Optimism ChainID from Kovan to Goerli ([#15119](https://github.com/MetaMask/metamask-extension/pull/15119))
|
||||
|
||||
### Fixed
|
||||
- Fix one of the possible causes for "Sending to a random cached address", by removing the global transaction state from the Send flow ([#14777](https://github.com/MetaMask/metamask-extension/pull/14777))
|
||||
- Fix Chinese translation for the message of Importing repeated tokens ([#14994](https://github.com/MetaMask/metamask-extension/pull/14994))
|
||||
- Fix Japanese translation for the word Sign ([#15078](https://github.com/MetaMask/metamask-extension/pull/15078))
|
||||
- Fix partially the error "Seedphrase is invalid" by disabling Seedphrase Import button after switching the Seedphrase length ([#15139](https://github.com/MetaMask/metamask-extension/pull/15139))
|
||||
- Fix Edit Transaction flow by ensuring that changing a tx from a Transfer to a Send resets data and updates tx type ([#15248](https://github.com/MetaMask/metamask-extension/pull/15248))
|
||||
- Fix UI on Import Seedphrase page by disabling Import button, if any of the characters of the Seedphrase is in uppercase ([#15186](https://github.com/MetaMask/metamask-extension/pull/15186))
|
||||
|
||||
## [10.17.0]
|
||||
### Added
|
||||
- Add cost estimation for canceling a Smart Transaction on Awaiting Swap page ([#15011](https://github.com/MetaMask/metamask-extension/pull/15011))
|
||||
@ -3068,7 +3084,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Uncategorized
|
||||
- Added the ability to restore accounts from seed words.
|
||||
|
||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.17.0...HEAD
|
||||
[Unreleased]: https://github.com/MetaMask/metamask-extension/compare/v10.18.0...HEAD
|
||||
[10.18.0]: https://github.com/MetaMask/metamask-extension/compare/v10.17.0...v10.18.0
|
||||
[10.17.0]: https://github.com/MetaMask/metamask-extension/compare/v10.16.2...v10.17.0
|
||||
[10.16.2]: https://github.com/MetaMask/metamask-extension/compare/v10.16.1...v10.16.2
|
||||
[10.16.1]: https://github.com/MetaMask/metamask-extension/compare/v10.16.0...v10.16.1
|
||||
|
59
app/_locales/en/messages.json
generated
59
app/_locales/en/messages.json
generated
@ -157,6 +157,9 @@
|
||||
"addMemo": {
|
||||
"message": "Add memo"
|
||||
},
|
||||
"addMoreNetworks": {
|
||||
"message": "add more networks manually"
|
||||
},
|
||||
"addNetwork": {
|
||||
"message": "Add Network"
|
||||
},
|
||||
@ -227,6 +230,10 @@
|
||||
"alerts": {
|
||||
"message": "Alerts"
|
||||
},
|
||||
"allOfYour": {
|
||||
"message": "All of your $1",
|
||||
"description": "$1 is the symbol or name of the token that the user is approving spending"
|
||||
},
|
||||
"allowExternalExtensionTo": {
|
||||
"message": "Allow this external extension to:"
|
||||
},
|
||||
@ -263,6 +270,10 @@
|
||||
"approve": {
|
||||
"message": "Approve spend limit"
|
||||
},
|
||||
"approveAllTokensTitle": {
|
||||
"message": "Give permission to access all of your $1?",
|
||||
"description": "$1 is the symbol of the token for which the user is granting approval"
|
||||
},
|
||||
"approveAndInstall": {
|
||||
"message": "Approve & Install"
|
||||
},
|
||||
@ -1284,6 +1295,9 @@
|
||||
"functionApprove": {
|
||||
"message": "Function: Approve"
|
||||
},
|
||||
"functionSetApprovalForAll": {
|
||||
"message": "Function: SetApprovalForAll"
|
||||
},
|
||||
"functionType": {
|
||||
"message": "Function Type"
|
||||
},
|
||||
@ -1954,6 +1968,9 @@
|
||||
"network": {
|
||||
"message": "Network:"
|
||||
},
|
||||
"networkAddedSuccessfully": {
|
||||
"message": "Network added successfully!"
|
||||
},
|
||||
"networkDetails": {
|
||||
"message": "Network Details"
|
||||
},
|
||||
@ -2690,6 +2707,14 @@
|
||||
"revealTheSeedPhrase": {
|
||||
"message": "Reveal seed phrase"
|
||||
},
|
||||
"revokeAllTokensTitle": {
|
||||
"message": "Revoke permission to access all of your $1?",
|
||||
"description": "$1 is the symbol of the token for which the user is revoking approval"
|
||||
},
|
||||
"revokeApproveForAllDescription": {
|
||||
"message": "By revoking permission, the following $1 will no longer be able to access your $2",
|
||||
"description": "$1 is either key 'account' or 'contract', and $2 is either a string or link of a given token symbol or name"
|
||||
},
|
||||
"rinkeby": {
|
||||
"message": "Rinkeby Test Network"
|
||||
},
|
||||
@ -2866,12 +2891,23 @@
|
||||
"message": "Sending $1",
|
||||
"description": "$1 represents the native currency symbol for the current network (e.g. ETH or BNB)"
|
||||
},
|
||||
"sendingToTokenContractWarning": {
|
||||
"message": "Warning: you are about to send to a token contract which could result in a loss of funds. $1",
|
||||
"description": "$1 is a clickable link with text defined by the 'learnMoreUpperCase' key. The link will open to a support article regarding the known contract address warning"
|
||||
},
|
||||
"setAdvancedPrivacySettings": {
|
||||
"message": "Set advanced privacy settings"
|
||||
},
|
||||
"setAdvancedPrivacySettingsDetails": {
|
||||
"message": "MetaMask uses these trusted third-party services to enhance product usability and safety."
|
||||
},
|
||||
"setApprovalForAll": {
|
||||
"message": "Set Approval for All"
|
||||
},
|
||||
"setApprovalForAllTitle": {
|
||||
"message": "Approve $1 with no spend limit",
|
||||
"description": "The token symbol that is being approved"
|
||||
},
|
||||
"settings": {
|
||||
"message": "Settings"
|
||||
},
|
||||
@ -2891,6 +2927,12 @@
|
||||
"showAdvancedGasInlineDescription": {
|
||||
"message": "Select this to show gas price and limit controls directly on the send and confirm screens."
|
||||
},
|
||||
"showCustomNetworkList": {
|
||||
"message": "Show Custom Network List"
|
||||
},
|
||||
"showCustomNetworkListDescription": {
|
||||
"message": "Select this to show a list of networks with prefilled details when adding a new network."
|
||||
},
|
||||
"showFiatConversionInTestnets": {
|
||||
"message": "Show Conversion on test networks"
|
||||
},
|
||||
@ -3000,6 +3042,9 @@
|
||||
"snapsToggle": {
|
||||
"message": "A snap will only run if it is enabled"
|
||||
},
|
||||
"someNetworksMayPoseSecurity": {
|
||||
"message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network."
|
||||
},
|
||||
"somethingWentWrong": {
|
||||
"message": "Oops! Something went wrong."
|
||||
},
|
||||
@ -3562,6 +3607,10 @@
|
||||
"switchNetworks": {
|
||||
"message": "Switch Networks"
|
||||
},
|
||||
"switchToNetwork": {
|
||||
"message": "Switch to $1",
|
||||
"description": "$1 represents the custom network that has previously been added"
|
||||
},
|
||||
"switchToThisAccount": {
|
||||
"message": "Switch to this account"
|
||||
},
|
||||
@ -3865,6 +3914,9 @@
|
||||
"unknownCameraErrorTitle": {
|
||||
"message": "Ooops! Something went wrong...."
|
||||
},
|
||||
"unknownCollection": {
|
||||
"message": "Unnamed collection"
|
||||
},
|
||||
"unknownNetwork": {
|
||||
"message": "Unknown Private Network"
|
||||
},
|
||||
@ -4005,6 +4057,9 @@
|
||||
"walletCreationSuccessTitle": {
|
||||
"message": "Wallet creation successful"
|
||||
},
|
||||
"wantToAddThisNetwork": {
|
||||
"message": "Want to add this network?"
|
||||
},
|
||||
"warning": {
|
||||
"message": "Warning"
|
||||
},
|
||||
@ -4067,6 +4122,10 @@
|
||||
"yesLetsTry": {
|
||||
"message": "Yes, let's try"
|
||||
},
|
||||
"youHaveAddedAll": {
|
||||
"message": "You've added all the popular networks. You can discover more networks $1 Or you can $2",
|
||||
"description": "$1 is a link with the text 'here' and $2 is a button with the text 'add more networks manually'"
|
||||
},
|
||||
"youNeedToAllowCameraAccess": {
|
||||
"message": "You need to allow camera access to use this feature."
|
||||
},
|
||||
|
2
app/_locales/ja/messages.json
generated
2
app/_locales/ja/messages.json
generated
@ -4061,7 +4061,7 @@
|
||||
"message": "この機能を使用するには、カメラへのアクセスを許可する必要があります。"
|
||||
},
|
||||
"youSign": {
|
||||
"message": "著名しています"
|
||||
"message": "署名しています"
|
||||
},
|
||||
"yourPrivateSeedPhrase": {
|
||||
"message": "秘密のシークレットリカバリーフレーズ"
|
||||
|
2
app/_locales/zh/messages.json
generated
2
app/_locales/zh/messages.json
generated
@ -1646,7 +1646,7 @@
|
||||
"message": "已知合约地址。"
|
||||
},
|
||||
"knownTokenWarning": {
|
||||
"message": "此操作将编辑已经在您的钱包中列出的代币,有肯能被用来欺骗您。只有确定要更改这些代币的内容时,才通过此操作。了解更多关于 $1"
|
||||
"message": "此操作将编辑已经在您的钱包中列出的代币,有可能被用来欺骗您。只有确定要更改这些代币的内容时,才通过此操作。了解更多关于 $1"
|
||||
},
|
||||
"kovan": {
|
||||
"message": "Kovan 测试网络"
|
||||
|
2
app/_locales/zh_CN/messages.json
generated
2
app/_locales/zh_CN/messages.json
generated
@ -1377,7 +1377,7 @@
|
||||
"message": "已知接收方地址。"
|
||||
},
|
||||
"knownTokenWarning": {
|
||||
"message": "此操作将编辑已经在您的钱包中列出的代币,有肯能被用来欺骗您。只有确定要更改这些代币的内容时,才通过此操作。了解更多关于 $1"
|
||||
"message": "此操作将编辑已经在您的钱包中列出的代币,有可能被用来欺骗您。只有确定要更改这些代币的内容时,才通过此操作。了解更多关于 $1"
|
||||
},
|
||||
"kovan": {
|
||||
"message": "Kovan 测试网络"
|
||||
|
1
app/images/fantom-opera.svg
Normal file
1
app/images/fantom-opera.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="1024" height="1024" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle fill="#1969FF" cx="512" cy="512" r="512"/><path d="M480.953 162.82c17.25-9.093 43.496-9.093 60.746 0l176.016 92.795c10.39 5.477 16.095 13.638 17.117 22.063H735V744.11c-.23 9.19-5.988 18.32-17.285 24.275L541.7 861.18c-17.25 9.093-43.497 9.093-60.746 0l-176.017-92.795c-11.249-5.93-16.647-15.123-16.914-24.275a32.372 32.372 0 0 1-.001-2.35V280.82a24 24 0 0 1 0-1.937v-1.204h.08c.781-8.518 6.228-16.47 16.835-22.063l176.017-92.795ZM707 537l-165.355 87.46c-17.225 9.111-43.433 9.111-60.658 0L316 537.195v205.474l164.987 86.802c9.75 5.217 19.888 10.3 29.76 10.521l.569.008c9.852.032 19.418-4.978 29.117-9.72L707 741.92V537ZM260.424 734c0 17.88 2.06 29.633 6.15 37.912 3.389 6.863 8.475 12.107 17.761 18.489l.53.362c2.038 1.387 4.283 2.839 7.016 4.545l3.223 1.992 9.896 6.025L290.806 827l-11.076-6.75-1.862-1.153c-3.202-1.995-5.857-3.707-8.333-5.392-26.467-18.003-36.337-37.63-36.532-78.461L233 734h27.424ZM498 413c-1.28.44-2.481.951-3.575 1.53l-175.748 93.094c-.185.097-.36.194-.528.29L318 508l.276.159.4.217 175.749 93.094c1.094.579 2.294 1.09 3.575 1.53V413Zm28 0v190a25.085 25.085 0 0 0 3.576-1.53l175.747-93.094c.184-.097.36-.194.528-.29L706 508l-.276-.159-.401-.217-175.747-93.094A25.085 25.085 0 0 0 526 413Zm181-102-158 83 158 83V311Zm-391 0v166l158-83-158-83Zm213.422-123.373c-9.147-4.836-25.697-4.836-34.844 0l-175.9 92.997c-.185.098-.362.194-.529.29L318 281l.276.158.401.218 175.9 92.996c9.148 4.837 25.698 4.837 34.845 0l175.9-92.996c.185-.098.361-.194.528-.29L706 281l-.276-.158-.402-.218-175.9-92.997ZM733.194 197l11.076 6.75 1.862 1.152c3.202 1.995 5.857 3.709 8.333 5.393 26.467 18.003 36.337 37.63 36.532 78.461L791 290h-27.424c0-17.882-2.06-29.633-6.15-37.913-3.388-6.862-8.474-12.107-17.76-18.488l-.531-.362a212.559 212.559 0 0 0-7.016-4.545l-3.223-1.992-9.896-6.025L733.194 197Z" fill="#FFF" fill-rule="nonzero"/></g></svg>
|
After (image error) Size: 1.9 KiB |
1
app/images/harmony-one.svg
Normal file
1
app/images/harmony-one.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"><defs><linearGradient id="a" x1="71.37" y1="228.63" x2="228.63" y2="71.37" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#00aee9"/><stop offset="1" stop-color="#69fabd"/></linearGradient></defs><path d="M201.17 60a38.81 38.81 0 0 0-38.84 38.71v42.92c-4 .27-8.09.44-12.33.44s-8.31.17-12.33.41V98.71a38.84 38.84 0 0 0-77.67 0v102.58a38.84 38.84 0 0 0 77.67 0v-42.92c4-.27 8.09-.44 12.33-.44s8.31-.17 12.33-.41v43.77a38.84 38.84 0 0 0 77.67 0V98.71A38.81 38.81 0 0 0 201.17 60ZM98.83 75.86a22.91 22.91 0 0 1 22.92 22.85v45.45a130.64 130.64 0 0 0-33 9.33 60 60 0 0 0-12.8 7.64V98.71a22.91 22.91 0 0 1 22.88-22.85Zm22.92 125.43a22.92 22.92 0 0 1-45.84 0V191c0-9.09 7.2-17.7 19.27-23.06a113 113 0 0 1 26.57-7.77Zm79.42 22.85a22.91 22.91 0 0 1-22.92-22.85v-45.45a130.64 130.64 0 0 0 33-9.33 60 60 0 0 0 12.8-7.64v62.42a22.91 22.91 0 0 1-22.88 22.85Zm3.65-92.14a113 113 0 0 1-26.57 7.77V98.71a22.92 22.92 0 0 1 45.84 0V109c0 9.05-7.2 17.66-19.27 23Z" style="fill:url(#a)"/><path style="fill:none" d="M0 0h300v300H0z"/></svg>
|
After (image error) Size: 1.1 KiB |
1
app/images/info-fox.svg
Normal file
1
app/images/info-fox.svg
Normal file
File diff suppressed because one or more lines are too long
After (image error) Size: 78 KiB |
@ -69,6 +69,7 @@ export default class PreferencesController {
|
||||
? LEDGER_TRANSPORT_TYPES.WEBHID
|
||||
: LEDGER_TRANSPORT_TYPES.U2F,
|
||||
theme: 'light',
|
||||
customNetworkListEnabled: false,
|
||||
...opts.initState,
|
||||
};
|
||||
|
||||
@ -179,6 +180,17 @@ export default class PreferencesController {
|
||||
this.store.updateState({ theme: val });
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the `customNetworkListEnabled` property
|
||||
*
|
||||
* @param customNetworkListEnabled
|
||||
*/
|
||||
setCustomNetworkListEnabled(customNetworkListEnabled) {
|
||||
this.store.updateState({
|
||||
customNetworkListEnabled,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add new methodData to state, to avoid requesting this information again through Infura
|
||||
*
|
||||
|
@ -462,7 +462,10 @@ export default class TransactionController extends EventEmitter {
|
||||
};
|
||||
|
||||
// only update what is defined
|
||||
editableParams.txParams = pickBy(editableParams.txParams);
|
||||
editableParams.txParams = pickBy(
|
||||
editableParams.txParams,
|
||||
(prop) => prop !== undefined,
|
||||
);
|
||||
|
||||
// update transaction type in case it has changes
|
||||
const transactionBeforeEdit = this._getTransaction(txId);
|
||||
|
@ -2274,6 +2274,7 @@ describe('Transaction Controller', function () {
|
||||
});
|
||||
|
||||
it('updates editible params when type changes from simple send to token transfer', async function () {
|
||||
providerResultStub.eth_getCode = '0xab';
|
||||
// test update gasFees
|
||||
await txController.updateEditableParams('1', {
|
||||
data:
|
||||
|
@ -165,7 +165,7 @@ describe('ComposableObservableStore', () => {
|
||||
Example: exampleController,
|
||||
},
|
||||
}),
|
||||
).toThrow(`Cannot read property 'subscribe' of undefined`);
|
||||
).toThrow(`Cannot read properties of undefined (reading 'subscribe')`);
|
||||
});
|
||||
|
||||
it('should throw if the controller messenger is omitted and updateStructure called with a BaseControllerV2 controller', () => {
|
||||
@ -175,7 +175,7 @@ describe('ComposableObservableStore', () => {
|
||||
});
|
||||
const store = new ComposableObservableStore({});
|
||||
expect(() => store.updateStructure({ Example: exampleController })).toThrow(
|
||||
`Cannot read property 'subscribe' of undefined`,
|
||||
`Cannot read properties of undefined (reading 'subscribe')`,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -1572,7 +1572,8 @@ export default class MetamaskController extends EventEmitter {
|
||||
setCustomRpc: this.setCustomRpc.bind(this),
|
||||
updateAndSetCustomRpc: this.updateAndSetCustomRpc.bind(this),
|
||||
delCustomRpc: this.delCustomRpc.bind(this),
|
||||
|
||||
addCustomNetwork: this.addCustomNetwork.bind(this),
|
||||
requestUserApproval: this.requestUserApproval.bind(this),
|
||||
// PreferencesController
|
||||
setSelectedAddress: preferencesController.setSelectedAddress.bind(
|
||||
preferencesController,
|
||||
@ -1609,7 +1610,9 @@ export default class MetamaskController extends EventEmitter {
|
||||
preferencesController,
|
||||
),
|
||||
setTheme: preferencesController.setTheme.bind(preferencesController),
|
||||
|
||||
setCustomNetworkListEnabled: preferencesController.setCustomNetworkListEnabled.bind(
|
||||
preferencesController,
|
||||
),
|
||||
// AssetsContractController
|
||||
getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this),
|
||||
|
||||
@ -2026,6 +2029,43 @@ export default class MetamaskController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async requestUserApproval(customRpc, originIsMetaMask) {
|
||||
try {
|
||||
await this.approvalController.addAndShowApprovalRequest({
|
||||
origin: 'metamask',
|
||||
type: 'wallet_addEthereumChain',
|
||||
requestData: {
|
||||
chainId: customRpc.chainId,
|
||||
blockExplorerUrl: customRpc.rpcPrefs.blockExplorerUrl,
|
||||
chainName: customRpc.nickname,
|
||||
rpcUrl: customRpc.rpcUrl,
|
||||
ticker: customRpc.ticker,
|
||||
imageUrl: customRpc.rpcPrefs.imageUrl,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
if (
|
||||
!(originIsMetaMask && error.message === 'User rejected the request.')
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async addCustomNetwork(customRpc) {
|
||||
const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc;
|
||||
|
||||
await this.preferencesController.addToFrequentRpcList(
|
||||
rpcUrl,
|
||||
chainId,
|
||||
ticker,
|
||||
chainName,
|
||||
{
|
||||
blockExplorerUrl,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Vault and restore an existent keyring.
|
||||
*
|
||||
|
@ -36,6 +36,7 @@ const metamaskrc = require('rc')('metamask', {
|
||||
COLLECTIBLES_V1: process.env.COLLECTIBLES_V1,
|
||||
PHISHING_WARNING_PAGE_URL: process.env.PHISHING_WARNING_PAGE_URL,
|
||||
TOKEN_DETECTION_V2: process.env.TOKEN_DETECTION_V2,
|
||||
ADD_POPULAR_NETWORKS: process.env.ADD_POPULAR_NETWORKS,
|
||||
SEGMENT_HOST: process.env.SEGMENT_HOST,
|
||||
SEGMENT_WRITE_KEY: process.env.SEGMENT_WRITE_KEY,
|
||||
SEGMENT_BETA_WRITE_KEY: process.env.SEGMENT_BETA_WRITE_KEY,
|
||||
@ -940,6 +941,7 @@ function getEnvironmentVariables({ buildType, devMode, testing, version }) {
|
||||
ONBOARDING_V2: metamaskrc.ONBOARDING_V2 === '1',
|
||||
COLLECTIBLES_V1: metamaskrc.COLLECTIBLES_V1 === '1',
|
||||
TOKEN_DETECTION_V2: metamaskrc.TOKEN_DETECTION_V2 === '1',
|
||||
ADD_POPULAR_NETWORKS: metamaskrc.ADD_POPULAR_NETWORKS === '1',
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -26,7 +26,7 @@ describe('transform utils', () => {
|
||||
// This error is an artifact of how we're mocking the ESLint singleton,
|
||||
// and won't actually occur in production.
|
||||
await expect(() => lintTransformedFile()).rejects.toThrow(
|
||||
`Cannot read property '0' of undefined`,
|
||||
`Cannot read properties of undefined (reading '0')`,
|
||||
);
|
||||
expect(mockESLint).toBeDefined();
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "metamask-crx",
|
||||
"version": "10.17.0",
|
||||
"version": "10.18.0",
|
||||
"private": true,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -282,7 +282,7 @@
|
||||
"browser-util-inspect": "^0.2.0",
|
||||
"browserify": "^16.5.1",
|
||||
"chalk": "^3.0.0",
|
||||
"chromedriver": "^102.0.0",
|
||||
"chromedriver": "^103.0.0",
|
||||
"concurrently": "^5.2.0",
|
||||
"copy-webpack-plugin": "^6.0.3",
|
||||
"cross-spawn": "^7.0.3",
|
||||
@ -381,7 +381,7 @@
|
||||
"yarn-deduplicate": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.1",
|
||||
"node": "^16.0.0",
|
||||
"yarn": "^1.16.0"
|
||||
},
|
||||
"lavamoat": {
|
||||
|
@ -23,11 +23,14 @@ export const KOVAN_CHAIN_ID = '0x2a';
|
||||
export const LOCALHOST_CHAIN_ID = '0x539';
|
||||
export const BSC_CHAIN_ID = '0x38';
|
||||
export const OPTIMISM_CHAIN_ID = '0xa';
|
||||
export const OPTIMISM_TESTNET_CHAIN_ID = '0x45';
|
||||
export const OPTIMISM_TESTNET_CHAIN_ID = '0x1a4';
|
||||
export const POLYGON_CHAIN_ID = '0x89';
|
||||
export const AVALANCHE_CHAIN_ID = '0xa86a';
|
||||
export const FANTOM_CHAIN_ID = '0xfa';
|
||||
export const CELO_CHAIN_ID = '0xa4ec';
|
||||
export const ARBITRUM_CHAIN_ID = '0xa4b1';
|
||||
export const HARMONY_CHAIN_ID = '0x63564c40';
|
||||
export const PALM_CHAIN_ID = '0x2a15c308d';
|
||||
|
||||
/**
|
||||
* The largest possible chain ID we can handle.
|
||||
@ -43,7 +46,14 @@ export const GOERLI_DISPLAY_NAME = 'Goerli';
|
||||
export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545';
|
||||
export const BSC_DISPLAY_NAME = 'Binance Smart Chain';
|
||||
export const POLYGON_DISPLAY_NAME = 'Polygon';
|
||||
export const AVALANCHE_DISPLAY_NAME = 'Avalanche';
|
||||
export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain';
|
||||
export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One';
|
||||
export const BNB_DISPLAY_NAME =
|
||||
'BNB Smart Chain (previously Binance Smart Chain Mainnet)';
|
||||
export const OPTIMISM_DISPLAY_NAME = 'Optimism';
|
||||
export const FANTOM_DISPLAY_NAME = 'Fantom Opera';
|
||||
export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0';
|
||||
export const PALM_DISPLAY_NAME = 'Palm';
|
||||
|
||||
const infuraProjectId = process.env.INFURA_PROJECT_ID;
|
||||
export const getRpcUrl = ({ network, excludeProjectId = false }) =>
|
||||
@ -64,12 +74,20 @@ export const MATIC_SYMBOL = 'MATIC';
|
||||
export const AVALANCHE_SYMBOL = 'AVAX';
|
||||
export const FANTOM_SYMBOL = 'FTM';
|
||||
export const CELO_SYMBOL = 'CELO';
|
||||
export const ARBITRUM_SYMBOL = 'AETH';
|
||||
export const HARMONY_SYMBOL = 'ONE';
|
||||
export const PALM_SYMBOL = 'PALM';
|
||||
|
||||
export const ETH_TOKEN_IMAGE_URL = './images/eth_logo.svg';
|
||||
export const TEST_ETH_TOKEN_IMAGE_URL = './images/black-eth-logo.svg';
|
||||
export const BNB_TOKEN_IMAGE_URL = './images/bnb.png';
|
||||
export const MATIC_TOKEN_IMAGE_URL = './images/matic-token.png';
|
||||
export const AVAX_TOKEN_IMAGE_URL = './images/avax-token.png';
|
||||
export const AETH_TOKEN_IMAGE_URL = './images/arbitrum.svg';
|
||||
export const FTM_TOKEN_IMAGE_URL = './images/fantom-opera.svg';
|
||||
export const HARMONY_ONE_TOKEN_IMAGE_URL = './images/harmony-one.svg';
|
||||
export const OPTIMISM_TOKEN_IMAGE_URL = './images/optimism.svg';
|
||||
export const PALM_TOKEN_IMAGE_URL = './images/palm.svg';
|
||||
|
||||
export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI];
|
||||
|
||||
@ -166,6 +184,12 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = {
|
||||
[AVALANCHE_CHAIN_ID]: AVAX_TOKEN_IMAGE_URL,
|
||||
[BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL,
|
||||
[POLYGON_CHAIN_ID]: MATIC_TOKEN_IMAGE_URL,
|
||||
[ARBITRUM_CHAIN_ID]: AETH_TOKEN_IMAGE_URL,
|
||||
[BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL,
|
||||
[FANTOM_CHAIN_ID]: FTM_TOKEN_IMAGE_URL,
|
||||
[HARMONY_CHAIN_ID]: HARMONY_ONE_TOKEN_IMAGE_URL,
|
||||
[OPTIMISM_CHAIN_ID]: OPTIMISM_TOKEN_IMAGE_URL,
|
||||
[PALM_CHAIN_ID]: PALM_TOKEN_IMAGE_URL,
|
||||
};
|
||||
|
||||
export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values(
|
||||
@ -309,3 +333,86 @@ export const BUYABLE_CHAINS_MAP = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FEATURED_RPCS = [
|
||||
{
|
||||
chainId: ARBITRUM_CHAIN_ID,
|
||||
nickname: ARBITRUM_DISPLAY_NAME,
|
||||
rpcUrl: `https://arbitrum-mainnet.infura.io/v3/${infuraProjectId}`,
|
||||
ticker: ARBITRUM_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://explorer.arbitrum.io',
|
||||
imageUrl: AETH_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: AVALANCHE_CHAIN_ID,
|
||||
nickname: AVALANCHE_DISPLAY_NAME,
|
||||
rpcUrl: 'https://api.avax.network/ext/bc/C/rpc',
|
||||
ticker: AVALANCHE_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://snowtrace.io/',
|
||||
imageUrl: AVAX_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: BSC_CHAIN_ID,
|
||||
nickname: BNB_DISPLAY_NAME,
|
||||
rpcUrl: 'https://bsc-dataseed.binance.org/',
|
||||
ticker: BNB_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://bscscan.com/',
|
||||
imageUrl: BNB_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: FANTOM_CHAIN_ID,
|
||||
nickname: FANTOM_DISPLAY_NAME,
|
||||
rpcUrl: 'https://rpc.ftm.tools/',
|
||||
ticker: FANTOM_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://ftmscan.com/',
|
||||
imageUrl: FTM_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: HARMONY_CHAIN_ID,
|
||||
nickname: HARMONY_DISPLAY_NAME,
|
||||
rpcUrl: 'https://api.harmony.one/',
|
||||
ticker: HARMONY_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://explorer.harmony.one/',
|
||||
imageUrl: HARMONY_ONE_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: OPTIMISM_CHAIN_ID,
|
||||
nickname: OPTIMISM_DISPLAY_NAME,
|
||||
rpcUrl: `https://optimism-mainnet.infura.io/v3/${infuraProjectId}`,
|
||||
ticker: ETH_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://optimistic.etherscan.io/',
|
||||
imageUrl: OPTIMISM_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: PALM_CHAIN_ID,
|
||||
nickname: PALM_DISPLAY_NAME,
|
||||
rpcUrl: `https://palm-mainnet.infura.io/v3/${infuraProjectId}`,
|
||||
ticker: PALM_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://explorer.palm.io/',
|
||||
imageUrl: PALM_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
{
|
||||
chainId: POLYGON_CHAIN_ID,
|
||||
nickname: `${POLYGON_DISPLAY_NAME} ${capitalize(MAINNET)}`,
|
||||
rpcUrl: `https://polygon-mainnet.infura.io/v3/${infuraProjectId}`,
|
||||
ticker: MATIC_SYMBOL,
|
||||
rpcPrefs: {
|
||||
blockExplorerUrl: 'https://polygonscan.com/',
|
||||
imageUrl: MATIC_TOKEN_IMAGE_URL,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
@ -8,3 +8,16 @@ import contractMap from '@metamask/contract-metadata';
|
||||
export const LISTED_CONTRACT_ADDRESSES = Object.keys(
|
||||
contractMap,
|
||||
).map((address) => address.toLowerCase());
|
||||
|
||||
/**
|
||||
* @typedef {Object} TokenDetails
|
||||
* @property {string} address - The address of the selected 'TOKEN' or
|
||||
* 'COLLECTIBLE' contract.
|
||||
* @property {string} [symbol] - The symbol of the token.
|
||||
* @property {number} [decimals] - The number of decimals of the selected
|
||||
* 'ERC20' asset.
|
||||
* @property {number} [tokenId] - The id of the selected 'COLLECTIBLE' asset.
|
||||
* @property {TokenStandardStrings} [standard] - The standard of the selected
|
||||
* asset.
|
||||
* @property {boolean} [isERC721] - True when the asset is a ERC721 token.
|
||||
*/
|
||||
|
@ -15,6 +15,8 @@ import { MESSAGE_TYPE } from './app';
|
||||
* to ensure that the receiver is an address capable of handling with the token being sent.
|
||||
* @property {'approve'} TOKEN_METHOD_APPROVE - A token transaction requesting an
|
||||
* allowance of the token to spend on behalf of the user
|
||||
* @property {'setapprovalforall'} TOKEN_METHOD_SET_APPROVAL_FOR_ALL - A token transaction requesting an
|
||||
* allowance of all of a user's token to spend on behalf of the user
|
||||
* @property {'incoming'} INCOMING - An incoming (deposit) transaction
|
||||
* @property {'simpleSend'} SIMPLE_SEND - A transaction sending a network's native asset to a recipient
|
||||
* @property {'contractInteraction'} CONTRACT_INTERACTION - A transaction that is
|
||||
@ -66,6 +68,7 @@ export const TRANSACTION_TYPES = {
|
||||
TOKEN_METHOD_SAFE_TRANSFER_FROM: 'safetransferfrom',
|
||||
TOKEN_METHOD_TRANSFER: 'transfer',
|
||||
TOKEN_METHOD_TRANSFER_FROM: 'transferfrom',
|
||||
TOKEN_METHOD_SET_APPROVAL_FOR_ALL: 'setapprovalforall',
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -8,7 +8,7 @@ import { readAddressAsContract } from './contract-utils';
|
||||
import { isEqualCaseInsensitive } from './string-utils';
|
||||
|
||||
/**
|
||||
* @typedef { 'transfer' | 'approve' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes
|
||||
* @typedef { 'transfer' | 'approve' | 'setapprovalforall' | 'transferfrom' | 'contractInteraction'| 'simpleSend' } InferrableTransactionTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -148,32 +148,35 @@ export async function determineTransactionType(txParams, query) {
|
||||
log.debug('Failed to parse transaction data.', error, data);
|
||||
}
|
||||
|
||||
const tokenMethodName = [
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM,
|
||||
].find((methodName) => isEqualCaseInsensitive(methodName, name));
|
||||
|
||||
let result;
|
||||
if (data && tokenMethodName) {
|
||||
result = tokenMethodName;
|
||||
} else if (data && !to) {
|
||||
result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
|
||||
}
|
||||
|
||||
let contractCode;
|
||||
|
||||
if (!result) {
|
||||
if (data && !to) {
|
||||
result = TRANSACTION_TYPES.DEPLOY_CONTRACT;
|
||||
} else {
|
||||
const {
|
||||
contractCode: resultCode,
|
||||
isContractAddress,
|
||||
} = await readAddressAsContract(query, to);
|
||||
|
||||
contractCode = resultCode;
|
||||
result = isContractAddress
|
||||
? TRANSACTION_TYPES.CONTRACT_INTERACTION
|
||||
: TRANSACTION_TYPES.SIMPLE_SEND;
|
||||
|
||||
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 };
|
||||
@ -181,6 +184,7 @@ export async function determineTransactionType(txParams, query) {
|
||||
|
||||
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,
|
||||
@ -220,6 +224,7 @@ export async function determineTransactionAssetType(
|
||||
// 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);
|
||||
|
@ -111,13 +111,23 @@ describe('Transaction.utils', function () {
|
||||
const genericProvider = createTestProviderTools().provider;
|
||||
const query = new EthQuery(genericProvider);
|
||||
|
||||
it('should return a simple send type when to is truthy but data is falsy', async function () {
|
||||
it('should return a simple send type when to is truthy and is not a contract address', async function () {
|
||||
const _providerResultStub = {
|
||||
// 1 gwei
|
||||
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||
// by default, all accounts are external accounts (not contracts)
|
||||
eth_getCode: '0x',
|
||||
};
|
||||
const _provider = createTestProviderTools({
|
||||
scaffold: _providerResultStub,
|
||||
}).provider;
|
||||
|
||||
const result = await determineTransactionType(
|
||||
{
|
||||
to: '0xabc',
|
||||
data: '',
|
||||
},
|
||||
query,
|
||||
new EthQuery(_provider),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
type: TRANSACTION_TYPES.SIMPLE_SEND,
|
||||
@ -125,33 +135,78 @@ describe('Transaction.utils', function () {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a token transfer type when data is for the respective method call', async function () {
|
||||
it('should return a token transfer type when the recipient is a contract and data is for the respective method call', async function () {
|
||||
const _providerResultStub = {
|
||||
// 1 gwei
|
||||
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||
// by default, all accounts are external accounts (not contracts)
|
||||
eth_getCode: '0xab',
|
||||
};
|
||||
const _provider = createTestProviderTools({
|
||||
scaffold: _providerResultStub,
|
||||
}).provider;
|
||||
|
||||
const result = await determineTransactionType(
|
||||
{
|
||||
to: '0xabc',
|
||||
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
|
||||
data:
|
||||
'0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
|
||||
},
|
||||
query,
|
||||
new EthQuery(_provider),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
type: TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
|
||||
getCodeResponse: undefined,
|
||||
getCodeResponse: '0xab',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a token approve type when data is for the respective method call', async function () {
|
||||
it('should NOT return a token transfer type when the recipient is not a contract but the data matches the respective method call', async function () {
|
||||
const _providerResultStub = {
|
||||
// 1 gwei
|
||||
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||
// by default, all accounts are external accounts (not contracts)
|
||||
eth_getCode: '0x',
|
||||
};
|
||||
const _provider = createTestProviderTools({
|
||||
scaffold: _providerResultStub,
|
||||
}).provider;
|
||||
|
||||
const result = await determineTransactionType(
|
||||
{
|
||||
to: '0xabc',
|
||||
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
|
||||
data:
|
||||
'0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
|
||||
},
|
||||
new EthQuery(_provider),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
type: TRANSACTION_TYPES.SIMPLE_SEND,
|
||||
getCodeResponse: '0x',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return a token approve type when when the recipient is a contract and data is for the respective method call', async function () {
|
||||
const _providerResultStub = {
|
||||
// 1 gwei
|
||||
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||
// by default, all accounts are external accounts (not contracts)
|
||||
eth_getCode: '0xab',
|
||||
};
|
||||
const _provider = createTestProviderTools({
|
||||
scaffold: _providerResultStub,
|
||||
}).provider;
|
||||
|
||||
const result = await determineTransactionType(
|
||||
{
|
||||
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
|
||||
data:
|
||||
'0x095ea7b30000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C9700000000000000000000000000000000000000000000000000000000000000005',
|
||||
},
|
||||
query,
|
||||
new EthQuery(_provider),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
type: TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
|
||||
getCodeResponse: undefined,
|
||||
getCodeResponse: '0xab',
|
||||
});
|
||||
});
|
||||
|
||||
@ -184,12 +239,22 @@ describe('Transaction.utils', function () {
|
||||
});
|
||||
|
||||
it('should return a simple send type with a null getCodeResponse when to is truthy and there is data and but getCode returns an error', async function () {
|
||||
const _providerResultStub = {
|
||||
// 1 gwei
|
||||
eth_gasPrice: '0x0de0b6b3a7640000',
|
||||
// by default, all accounts are external accounts (not contracts)
|
||||
eth_getCode: null,
|
||||
};
|
||||
const _provider = createTestProviderTools({
|
||||
scaffold: _providerResultStub,
|
||||
}).provider;
|
||||
|
||||
const result = await determineTransactionType(
|
||||
{
|
||||
to: '0xabc',
|
||||
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
|
||||
data: '0xabd',
|
||||
},
|
||||
query,
|
||||
new EthQuery(_provider),
|
||||
);
|
||||
expect(result).toMatchObject({
|
||||
type: TRANSACTION_TYPES.SIMPLE_SEND,
|
||||
|
146
test/e2e/fixtures/special-settings/state.json
Normal file
146
test/e2e/fixtures/special-settings/state.json
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -25,6 +25,28 @@ async function setupMocking(server, testSpecificMock) {
|
||||
};
|
||||
});
|
||||
|
||||
await server
|
||||
.forGet('https://www.4byte.directory/api/v1/signatures/')
|
||||
.thenCallback(() => {
|
||||
return {
|
||||
statusCode: 200,
|
||||
json: {
|
||||
count: 1,
|
||||
next: null,
|
||||
previous: null,
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
created_at: null,
|
||||
text_signature: 'deposit()',
|
||||
hex_signature: null,
|
||||
bytes_signature: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
await server
|
||||
.forGet('https://gas-api.metaswap.codefi.network/networks/1/gasPrices')
|
||||
.thenCallback(() => {
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -31,11 +31,16 @@ describe('Test Snap bip-44', function () {
|
||||
|
||||
// navigate to test snaps page and connect
|
||||
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
|
||||
await driver.fill('.snapId3', 'npm:@metamask/test-snap-bip44');
|
||||
await driver.clickElement({
|
||||
text: 'Connect BIP-44 Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.delay(1000);
|
||||
await driver.fill('#snapId3', 'npm:@metamask/test-snap-bip44');
|
||||
|
||||
// reveal snapId3 by finding and scrolling to #snapId4
|
||||
const snapButton = await driver.findElement('#snapId4');
|
||||
await driver.scrollToElement(snapButton);
|
||||
await driver.delay(500);
|
||||
|
||||
// connect the snap
|
||||
await driver.clickElement('#connectBip44');
|
||||
|
||||
// switch to metamask extension and click connect
|
||||
await driver.waitUntilXWindowHandles(2, 5000, 10000);
|
||||
@ -77,14 +82,11 @@ describe('Test Snap bip-44', function () {
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement({
|
||||
text: 'Send Test to BIP-44 Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.clickElement('#sendBip44');
|
||||
|
||||
// check the results of the public key test
|
||||
await driver.delay(2000);
|
||||
const bip44Result = await driver.findElement('.bip44Result');
|
||||
const bip44Result = await driver.findElement('#bip44Result');
|
||||
assert.equal(
|
||||
await bip44Result.getText(),
|
||||
'Public key: "0x86debb44fb3a984d93f326131d4c1db0bc39644f1a67b673b3ab45941a1cea6a385981755185ac4594b6521e4d1e8d1"',
|
||||
|
@ -31,11 +31,8 @@ describe('Test Snap Confirm', function () {
|
||||
|
||||
// navigate to test snaps page and connect
|
||||
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
|
||||
await driver.fill('.snapId1', 'npm:@metamask/test-snap-confirm');
|
||||
await driver.clickElement({
|
||||
text: 'Connect To Confirm Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.fill('#snapId1', 'npm:@metamask/test-snap-confirm');
|
||||
await driver.clickElement('#connectHello');
|
||||
|
||||
// switch to metamask extension and click connect
|
||||
await driver.waitUntilXWindowHandles(2, 5000, 10000);
|
||||
@ -70,7 +67,7 @@ describe('Test Snap Confirm', function () {
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement('.sendConfirmButton');
|
||||
await driver.clickElement('#sendConfirmButton');
|
||||
|
||||
// hit 'approve' on the custom confirm
|
||||
await driver.waitUntilXWindowHandles(2, 5000, 10000);
|
||||
@ -88,7 +85,7 @@ describe('Test Snap Confirm', function () {
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
const confirmResult = await driver.findElement('.confirmResult');
|
||||
const confirmResult = await driver.findElement('#confirmResult');
|
||||
assert.equal(await confirmResult.getText(), 'true');
|
||||
},
|
||||
);
|
||||
|
@ -30,11 +30,8 @@ describe('Test Snap Error', function () {
|
||||
|
||||
// navigate to test snaps page and connect
|
||||
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
|
||||
await driver.fill('.snapId2', 'npm:@metamask/test-snap-error');
|
||||
await driver.clickElement({
|
||||
text: 'Connect Error Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.fill('#snapId2', 'npm:@metamask/test-snap-error');
|
||||
await driver.clickElement('#connectError');
|
||||
|
||||
// switch to metamask extension and click connect
|
||||
await driver.waitUntilXWindowHandles(2, 5000, 10000);
|
||||
@ -68,10 +65,7 @@ describe('Test Snap Error', function () {
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement({
|
||||
text: 'Send Test to Error Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.clickElement('#sendError');
|
||||
|
||||
await driver.navigate(PAGES.HOME);
|
||||
|
||||
|
@ -13,6 +13,7 @@ describe('Test Snap manageState', function () {
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'imported-account',
|
||||
@ -29,13 +30,18 @@ describe('Test Snap manageState', function () {
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
// navigate to test snaps page and connect
|
||||
// navigate to test snaps page, then fill in the snapId
|
||||
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
|
||||
await driver.fill('.snapId3', 'npm:@metamask/test-snap-managestate');
|
||||
await driver.clickElement({
|
||||
text: 'Connect manageState Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.delay(1000);
|
||||
await driver.fill('#snapId4', 'npm:@metamask/test-snap-managestate');
|
||||
|
||||
// find and scroll to the rest of the card
|
||||
const snapButton = await driver.findElement('#snapId4');
|
||||
await driver.scrollToElement(snapButton);
|
||||
await driver.delay(500);
|
||||
|
||||
// connect the snap
|
||||
await driver.clickElement('#connectManageState');
|
||||
|
||||
// switch to metamask extension and click connect
|
||||
await driver.waitUntilXWindowHandles(2, 5000, 10000);
|
||||
@ -51,7 +57,6 @@ describe('Test Snap manageState', function () {
|
||||
},
|
||||
10000,
|
||||
);
|
||||
|
||||
await driver.delay(2000);
|
||||
|
||||
// approve install of snap
|
||||
@ -70,32 +75,23 @@ describe('Test Snap manageState', function () {
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.fill('.dataManageState', '23');
|
||||
await driver.clickElement({
|
||||
text: 'Send data to manageState Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.fill('#dataManageState', '23');
|
||||
await driver.clickElement('#sendManageState');
|
||||
|
||||
// check the results of the public key test
|
||||
await driver.delay(2000);
|
||||
await driver.delay(500);
|
||||
const manageStateResult = await driver.findElement(
|
||||
'.sendManageStateResult',
|
||||
'#sendManageStateResult',
|
||||
);
|
||||
assert.equal(await manageStateResult.getText(), 'true');
|
||||
|
||||
// click get results
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement({
|
||||
text: 'Get data from manageState Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.clickElement('#retrieveManageState');
|
||||
|
||||
// check the results
|
||||
await driver.delay(2000);
|
||||
await driver.delay(500);
|
||||
const retrieveManageStateResult = await driver.findElement(
|
||||
'.retrieveManageStateResult',
|
||||
'#retrieveManageStateResult',
|
||||
);
|
||||
assert.equal(
|
||||
await retrieveManageStateResult.getText(),
|
||||
@ -103,34 +99,22 @@ describe('Test Snap manageState', function () {
|
||||
);
|
||||
|
||||
// click clear results
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement({
|
||||
text: 'Clear data of manageState Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.clickElement('#clearManageState');
|
||||
|
||||
// check if true
|
||||
await driver.delay(2000);
|
||||
await driver.delay(500);
|
||||
const clearManageStateResult = await driver.findElement(
|
||||
'.clearManageStateResult',
|
||||
'#clearManageStateResult',
|
||||
);
|
||||
assert.equal(await clearManageStateResult.getText(), 'true');
|
||||
|
||||
// click get results again
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement({
|
||||
text: 'Get data from manageState Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.clickElement('#retrieveManageState');
|
||||
|
||||
// check result array is empty
|
||||
await driver.delay(2000);
|
||||
await driver.delay(500);
|
||||
const retrieveManageStateResult2 = await driver.findElement(
|
||||
'.retrieveManageStateResult',
|
||||
'#retrieveManageStateResult',
|
||||
);
|
||||
assert.equal(
|
||||
await retrieveManageStateResult2.getText(),
|
||||
|
@ -30,13 +30,18 @@ describe('Test Snap Notification', function () {
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
// navigate to test snaps page and connect
|
||||
// navigate to test snaps page
|
||||
await driver.driver.get(TEST_SNAPS_WEBSITE_URL);
|
||||
await driver.fill('.snapId5', 'npm:@metamask/test-snap-notification');
|
||||
await driver.clickElement({
|
||||
text: 'Connect Notification Snap',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.delay(1000);
|
||||
|
||||
// find and scroll down to snapId5
|
||||
const snapButton = await driver.findElement('#snapId5');
|
||||
await driver.scrollToElement(snapButton);
|
||||
await driver.delay(500);
|
||||
await driver.fill('#snapId5', 'npm:@metamask/test-snap-notification');
|
||||
|
||||
// connect the snap
|
||||
await driver.clickElement('#connectNotification');
|
||||
|
||||
// switch to metamask extension and click connect
|
||||
await driver.waitUntilXWindowHandles(2, 5000, 10000);
|
||||
@ -70,10 +75,7 @@ describe('Test Snap Notification', function () {
|
||||
await driver.waitUntilXWindowHandles(1, 5000, 10000);
|
||||
windowHandles = await driver.getAllWindowHandles();
|
||||
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
|
||||
await driver.clickElement({
|
||||
text: 'Send InApp Notification',
|
||||
tag: 'button',
|
||||
});
|
||||
await driver.clickElement('#sendInAppNotification');
|
||||
|
||||
// try to go to the MM pages
|
||||
await driver.navigate(PAGES.HOME);
|
||||
|
@ -103,7 +103,7 @@ describe('Deploy contract and call contract methods', function () {
|
||||
);
|
||||
await driver.waitForSelector({
|
||||
css: '.confirm-page-container-summary__action__name',
|
||||
text: 'Withdraw',
|
||||
text: 'Deposit',
|
||||
});
|
||||
await driver.clickElement({ text: 'Confirm', tag: 'button' });
|
||||
await driver.waitUntilXWindowHandles(2);
|
||||
|
@ -92,6 +92,54 @@ describe('Send ETH from inside MetaMask using default gas', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Send ETH non-contract address with data that matches ERC20 transfer data signature', function () {
|
||||
const ganacheOptions = {
|
||||
accounts: [
|
||||
{
|
||||
secretKey:
|
||||
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
|
||||
balance: convertToHexValue(25000000000000000000),
|
||||
},
|
||||
],
|
||||
};
|
||||
it('renders the correct recipient on the confirmation screen', async function () {
|
||||
await withFixtures(
|
||||
{
|
||||
fixtures: 'special-settings',
|
||||
ganacheOptions,
|
||||
title: this.test.title,
|
||||
},
|
||||
async ({ driver }) => {
|
||||
await driver.navigate();
|
||||
await driver.fill('#password', 'correct horse battery staple');
|
||||
await driver.press('#password', driver.Key.ENTER);
|
||||
|
||||
await driver.clickElement('[data-testid="eth-overview-send"]');
|
||||
|
||||
await driver.fill(
|
||||
'input[placeholder="Search, public address (0x), or ENS"]',
|
||||
'0xc427D562164062a23a5cFf596A4a3208e72Acd28',
|
||||
);
|
||||
|
||||
await driver.fill(
|
||||
'textarea[placeholder="Optional',
|
||||
'0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
|
||||
);
|
||||
|
||||
await driver.clickElement({ text: 'Next', tag: 'button' });
|
||||
|
||||
await driver.clickElement({ text: '0xc42...cd28' });
|
||||
|
||||
const recipientAddress = await driver.findElements({
|
||||
text: '0xc427D562164062a23a5cFf596A4a3208e72Acd28',
|
||||
});
|
||||
|
||||
assert.equal(recipientAddress.length, 1);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
/* eslint-disable-next-line mocha/max-top-level-suites */
|
||||
describe('Send ETH from inside MetaMask using advanced gas modal', function () {
|
||||
const ganacheOptions = {
|
||||
|
329
test/e2e/tests/send-hex-address.spec.js
Normal file
329
test/e2e/tests/send-hex-address.spec.js
Normal file
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
65
test/e2e/tests/state-logs.spec.js
Normal file
65
test/e2e/tests/state-logs.spec.js
Normal file
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
@ -22,6 +22,9 @@ class ChromeDriver {
|
||||
const options = new chrome.Options().addArguments(args);
|
||||
options.setProxy(proxy.manual({ https: HTTPS_PROXY_HOST }));
|
||||
options.setAcceptInsecureCerts(true);
|
||||
options.setUserPreferences({
|
||||
'download.default_directory': `${process.cwd()}/test-artifacts/downloads`,
|
||||
});
|
||||
const builder = new Builder()
|
||||
.forBrowser('chrome')
|
||||
.setChromeOptions(options);
|
||||
|
@ -266,21 +266,21 @@ class Driver {
|
||||
/**
|
||||
* Paste a string into a field.
|
||||
*
|
||||
* @param {string} element - The element locator.
|
||||
* @param {string} rawLocator - The element locator.
|
||||
* @param {string} contentToPaste - The content to paste.
|
||||
*/
|
||||
async pasteIntoField(element, contentToPaste) {
|
||||
async pasteIntoField(rawLocator, contentToPaste) {
|
||||
// Throw if double-quote is present in content to paste
|
||||
// so that we don't have to worry about escaping double-quotes
|
||||
if (contentToPaste.includes('"')) {
|
||||
throw new Error('Cannot paste content with double-quote');
|
||||
}
|
||||
// Click to focus the field
|
||||
await this.clickElement(element);
|
||||
await this.clickElement(rawLocator);
|
||||
await this.executeScript(
|
||||
`navigator.clipboard.writeText("${contentToPaste}")`,
|
||||
);
|
||||
await this.fill(element, Key.chord(this.Key.MODIFIER, 'v'));
|
||||
await this.fill(rawLocator, Key.chord(this.Key.MODIFIER, 'v'));
|
||||
}
|
||||
|
||||
// Navigation
|
||||
|
@ -40,6 +40,11 @@ class FirefoxDriver {
|
||||
const options = new firefox.Options().setProfile(templateProfile);
|
||||
options.setProxy(proxy.manual({ https: HTTPS_PROXY_HOST }));
|
||||
options.setAcceptInsecureCerts(true);
|
||||
options.setPreference('browser.download.folderList', 2);
|
||||
options.setPreference(
|
||||
'browser.download.dir',
|
||||
`${process.cwd()}/test-artifacts/downloads`,
|
||||
);
|
||||
const builder = new Builder()
|
||||
.forBrowser('firefox')
|
||||
.setFirefoxOptions(options);
|
||||
|
@ -1,3 +1,8 @@
|
||||
import {
|
||||
draftTransactionInitialState,
|
||||
initialState,
|
||||
} from '../../ui/ducks/send';
|
||||
|
||||
export const TOP_ASSETS_GET_RESPONSE = [
|
||||
{
|
||||
symbol: 'LINK',
|
||||
@ -103,3 +108,42 @@ export const createGasFeeEstimatesForFeeMarket = () => {
|
||||
estimatedBaseFee: '50',
|
||||
};
|
||||
};
|
||||
|
||||
export const INITIAL_SEND_STATE_FOR_EXISTING_DRAFT = {
|
||||
...initialState,
|
||||
currentTransactionUUID: 'test-uuid',
|
||||
draftTransactions: {
|
||||
'test-uuid': {
|
||||
...draftTransactionInitialState,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getInitialSendStateWithExistingTxState = (draftTxState) => ({
|
||||
...INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||
draftTransactions: {
|
||||
'test-uuid': {
|
||||
...draftTransactionInitialState,
|
||||
...draftTxState,
|
||||
amount: {
|
||||
...draftTransactionInitialState.amount,
|
||||
...draftTxState.amount,
|
||||
},
|
||||
asset: {
|
||||
...draftTransactionInitialState.asset,
|
||||
...draftTxState.asset,
|
||||
},
|
||||
gas: {
|
||||
...draftTransactionInitialState.gas,
|
||||
...draftTxState.gas,
|
||||
},
|
||||
recipient: {
|
||||
...draftTransactionInitialState.recipient,
|
||||
...draftTxState.recipient,
|
||||
},
|
||||
history: draftTxState.history ?? [],
|
||||
// Use this key if you want to console.log inside the send.js file.
|
||||
test: draftTxState.test ?? 'yo',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -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';
|
||||
|
||||
const AddNetwork = ({
|
||||
onBackClick,
|
||||
onAddNetworkClick,
|
||||
onAddNetworkManuallyClick,
|
||||
featuredRPCS,
|
||||
}) => {
|
||||
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 = () => {
|
||||
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
|
||||
key={index}
|
||||
display={DISPLAY.FLEX}
|
||||
alignItems={ALIGN_ITEMS.CENTER}
|
||||
justifyContent={JUSTIFY_CONTENT.SPACE_BETWEEN}
|
||||
marginBottom={6}
|
||||
>
|
||||
<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 }}
|
||||
>
|
||||
{item.nickname}
|
||||
<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 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"
|
||||
>
|
||||
{t('learnMoreUpperCase')}
|
||||
</a>,
|
||||
])}
|
||||
</Box>
|
||||
}
|
||||
trigger="mouseenter"
|
||||
theme={theme === THEME_TYPE.DEFAULT ? 'light' : 'dark'}
|
||||
>
|
||||
<i
|
||||
className="fa fa-exclamation-triangle add-network__warning-icon"
|
||||
title={t('warning')}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
type="inline"
|
||||
className="add-network__add-button"
|
||||
onClick={onAddNetworkClick}
|
||||
)}
|
||||
<Box
|
||||
margin={
|
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
? [0, 0, 1, 0]
|
||||
: [4, 0, 1, 0]
|
||||
}
|
||||
className="add-network__main-container"
|
||||
>
|
||||
<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"
|
||||
>
|
||||
{t('add')}
|
||||
</Button>
|
||||
</Box>
|
||||
<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('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));
|
||||
}}
|
||||
>
|
||||
{t('add')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</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
|
||||
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('addANetworkManually')}
|
||||
</Typography>
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{showPopover && (
|
||||
<Popover>
|
||||
<ConfirmationPage />
|
||||
</Popover>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AddNetwork.propTypes = {
|
||||
onBackClick: PropTypes.func,
|
||||
onAddNetworkClick: PropTypes.func,
|
||||
onAddNetworkManuallyClick: PropTypes.func,
|
||||
featuredRPCS: PropTypes.array,
|
||||
};
|
||||
|
||||
export default AddNetwork;
|
||||
|
60
ui/components/app/add-network/add-network.test.js
Normal file
60
ui/components/app/add-network/add-network.test.js
Normal file
@ -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();
|
||||
});
|
||||
});
|
@ -1,10 +1,36 @@
|
||||
.add-network {
|
||||
&__networks-container {
|
||||
padding-inline-end: 24px;
|
||||
|
||||
@media screen and (max-width: $break-small) {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__header {
|
||||
border-bottom: 1px solid var(--color-border-default);
|
||||
|
||||
&__back-icon {
|
||||
margin-left: 24px;
|
||||
margin-right: 16px;
|
||||
@media screen and (max-width: 575px) {
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: 24px;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__main-container {
|
||||
@media screen and (max-width: 575px) {
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&__list-of-networks {
|
||||
@media screen and (min-width: $break-large) {
|
||||
width: 75%;
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,19 +49,25 @@
|
||||
|
||||
&__add-icon {
|
||||
color: var(--color-text-alternative);
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&__add-button.button {
|
||||
color: var(--color-primary-default);
|
||||
font-size: $font-size-h7;
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
border-top: 1px solid var(--color-border-muted);
|
||||
width: 100%;
|
||||
padding-bottom: 8px;
|
||||
|
||||
@media screen and (max-width: 575px) {
|
||||
padding-inline-start: 24px !important;
|
||||
}
|
||||
|
||||
& .btn-link {
|
||||
display: initial;
|
||||
@ -51,6 +83,14 @@
|
||||
color: var(--color-text-alternative);
|
||||
}
|
||||
}
|
||||
|
||||
&__edge-case-box {
|
||||
border: 1px solid var(--color-border-muted);
|
||||
|
||||
&__link {
|
||||
color: var(--color-info-default);
|
||||
display: inline;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -14,6 +14,7 @@
|
||||
@import 'collectibles-items/index';
|
||||
@import 'collectibles-tab/index';
|
||||
@import 'collectible-details/index';
|
||||
@import 'collectible-default-image/index';
|
||||
@import 'collectible-options/index';
|
||||
@import 'collectibles-detection-notice/index';
|
||||
@import 'connected-accounts-list/index';
|
||||
|
@ -9,7 +9,7 @@ import Tooltip from '../../ui/tooltip';
|
||||
import InfoIcon from '../../ui/icon/info-icon.component';
|
||||
import Button from '../../ui/button';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import { updateSendAsset } from '../../../ducks/send';
|
||||
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||
import { SEND_ROUTE } from '../../../helpers/constants/routes';
|
||||
import { SEVERITIES } from '../../../helpers/constants/design-system';
|
||||
import { INVALID_ASSET_TYPE } from '../../../helpers/constants/error-keys';
|
||||
@ -74,7 +74,7 @@ const AssetListItem = ({
|
||||
});
|
||||
try {
|
||||
await dispatch(
|
||||
updateSendAsset({
|
||||
startNewDraftTransaction({
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
details: {
|
||||
address: tokenAddress,
|
||||
|
@ -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!'),
|
||||
};
|
1
ui/components/app/collectible-default-image/index.js
Normal file
1
ui/components/app/collectible-default-image/index.js
Normal file
@ -0,0 +1 @@
|
||||
export { default } from './collectible-default-image';
|
22
ui/components/app/collectible-default-image/index.scss
Normal file
22
ui/components/app/collectible-default-image/index.scss
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -45,13 +45,14 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import CollectibleOptions from '../collectible-options/collectible-options';
|
||||
import Button from '../../ui/button';
|
||||
import { updateSendAsset } from '../../../ducks/send';
|
||||
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||
import InfoTooltip from '../../ui/info-tooltip';
|
||||
import { ERC721 } from '../../../helpers/constants/common';
|
||||
import { usePrevious } from '../../../hooks/usePrevious';
|
||||
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
||||
import { isEqualCaseInsensitive } from '../../../../shared/modules/string-utils';
|
||||
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||
import CollectibleDefaultImage from '../collectible-default-image';
|
||||
|
||||
export default function CollectibleDetails({ collectible }) {
|
||||
const {
|
||||
@ -119,7 +120,7 @@ export default function CollectibleDetails({ collectible }) {
|
||||
|
||||
const onSend = async () => {
|
||||
await dispatch(
|
||||
updateSendAsset({
|
||||
startNewDraftTransaction({
|
||||
type: ASSET_TYPES.COLLECTIBLE,
|
||||
details: collectible,
|
||||
}),
|
||||
@ -176,7 +177,11 @@ export default function CollectibleDetails({ collectible }) {
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
className="collectible-details__card"
|
||||
>
|
||||
<img className="collectible-details__image" src={image} />
|
||||
{image ? (
|
||||
<img className="collectible-details__image" src={image} />
|
||||
) : (
|
||||
<CollectibleDefaultImage name={name} tokenId={tokenId} />
|
||||
)}
|
||||
</Card>
|
||||
<Box
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
@ -215,6 +220,7 @@ export default function CollectibleDetails({ collectible }) {
|
||||
<Typography
|
||||
color={COLORS.TEXT_ALTERNATIVE}
|
||||
variant={TYPOGRAPHY.H6}
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
boxProps={{ margin: 0, marginBottom: 4 }}
|
||||
>
|
||||
{description}
|
||||
|
@ -1,16 +1,6 @@
|
||||
import React from 'react';
|
||||
import CollectibleDetails from './collectible-details';
|
||||
|
||||
export default {
|
||||
title: 'Components/App/CollectiblesDetail',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
collectible: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const collectible = {
|
||||
name: 'Catnip Spicywright',
|
||||
tokenId: '1124157',
|
||||
@ -20,12 +10,32 @@ const collectible = {
|
||||
"Good day. My name is Catnip Spicywight, which got me teased a lot in high school. If I want to put low fat mayo all over my hamburgers, I shouldn't have to answer to anyone about it, am I right? One time I beat Arlene in an arm wrestle.",
|
||||
};
|
||||
|
||||
export const DefaultStory = () => {
|
||||
return <CollectibleDetails collectible={collectible} />;
|
||||
export default {
|
||||
title: 'Components/App/CollectiblesDetail',
|
||||
id: __filename,
|
||||
argTypes: {
|
||||
collectible: {
|
||||
control: 'object',
|
||||
},
|
||||
},
|
||||
args: {
|
||||
collectible,
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultStory = (args) => {
|
||||
return <CollectibleDetails {...args} />;
|
||||
};
|
||||
|
||||
DefaultStory.storyName = 'Default';
|
||||
|
||||
DefaultStory.args = {
|
||||
collectible,
|
||||
export const NoImage = (args) => {
|
||||
return <CollectibleDetails {...args} />;
|
||||
};
|
||||
|
||||
NoImage.args = {
|
||||
collectible: {
|
||||
...collectible,
|
||||
image: undefined,
|
||||
},
|
||||
};
|
||||
|
@ -28,6 +28,8 @@ import { getAssetImageURL } from '../../../helpers/utils/util';
|
||||
import { updateCollectibleDropDownState } from '../../../store/actions';
|
||||
import { usePrevious } from '../../../hooks/usePrevious';
|
||||
import { getCollectiblesDropdownState } from '../../../ducks/metamask/metamask';
|
||||
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||
import CollectibleDefaultImage from '../collectible-default-image';
|
||||
|
||||
const width =
|
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
@ -46,6 +48,7 @@ export default function CollectiblesItems({
|
||||
const previousCollectionKeys = usePrevious(collectionsKeys);
|
||||
const selectedAddress = useSelector(getSelectedAddress);
|
||||
const chainId = useSelector(getCurrentChainId);
|
||||
const t = useI18nContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@ -101,7 +104,7 @@ export default function CollectiblesItems({
|
||||
}
|
||||
return (
|
||||
<div className="collectibles-items__collection-image-alt">
|
||||
{collectionName[0]}
|
||||
{collectionName?.[0]?.toUpperCase() ?? null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -164,7 +167,9 @@ export default function CollectiblesItems({
|
||||
variant={TYPOGRAPHY.H5}
|
||||
margin={[0, 0, 0, 2]}
|
||||
>
|
||||
{`${collectionName} (${collectibles.length})`}
|
||||
{`${collectionName ?? t('unknownCollection')} (${
|
||||
collectibles.length
|
||||
})`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box alignItems={ALIGN_ITEMS.FLEX_END}>
|
||||
@ -180,29 +185,48 @@ export default function CollectiblesItems({
|
||||
{isExpanded ? (
|
||||
<Box display={DISPLAY.FLEX} flexWrap={FLEX_WRAP.WRAP} gap={4}>
|
||||
{collectibles.map((collectible, i) => {
|
||||
const { image, address, tokenId, backgroundColor } = collectible;
|
||||
const {
|
||||
image,
|
||||
address,
|
||||
tokenId,
|
||||
backgroundColor,
|
||||
name,
|
||||
} = collectible;
|
||||
const collectibleImage = getAssetImageURL(image, ipfsGateway);
|
||||
const handleImageClick = () =>
|
||||
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`);
|
||||
|
||||
return (
|
||||
<Box
|
||||
width={width}
|
||||
key={`collectible-${i}`}
|
||||
className="collectibles-items__collection-item-wrapper"
|
||||
className="collectibles-items__item-wrapper"
|
||||
>
|
||||
<Card padding={0} justifyContent={JUSTIFY_CONTENT.CENTER}>
|
||||
<div
|
||||
className="collectibles-items__collection-item"
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
onClick={() =>
|
||||
history.push(`${ASSET_ROUTE}/${address}/${tokenId}`)
|
||||
}
|
||||
className="collectibles-items__collection-item-image"
|
||||
src={collectibleImage}
|
||||
<Card
|
||||
padding={0}
|
||||
justifyContent={JUSTIFY_CONTENT.CENTER}
|
||||
className="collectibles-items__item-wrapper__card"
|
||||
>
|
||||
{collectibleImage ? (
|
||||
<div
|
||||
className="collectibles-items__item"
|
||||
style={{
|
||||
backgroundColor,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
onClick={handleImageClick}
|
||||
className="collectibles-items__item-image"
|
||||
src={collectibleImage}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CollectibleDefaultImage
|
||||
name={name}
|
||||
tokenId={tokenId}
|
||||
handleImageClick={handleImageClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
|
@ -27,29 +27,33 @@
|
||||
color: var(--color-overlay-inverse);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-item-wrapper {
|
||||
align-self: center;
|
||||
&__item-wrapper {
|
||||
align-self: center;
|
||||
|
||||
&__card {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
}
|
||||
&__item {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
align-self: center;
|
||||
|
||||
&-item-image {
|
||||
&-image {
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon-chevron {
|
||||
color: var(--color-icon-default);
|
||||
}
|
||||
&__icon-chevron {
|
||||
color: var(--color-icon-default);
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,8 @@ const ConfirmPageContainerSummary = (props) => {
|
||||
contractAddress =
|
||||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER ||
|
||||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM ||
|
||||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM
|
||||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM ||
|
||||
transactionType === TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL
|
||||
? tokenAddress
|
||||
: toAddress;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
|
||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||
import {
|
||||
ADD_NETWORK_ROUTE,
|
||||
ADD_POPULAR_CUSTOM_NETWORK,
|
||||
ADVANCED_ROUTE,
|
||||
} from '../../../helpers/constants/routes';
|
||||
import IconCheck from '../../ui/icon/icon-check';
|
||||
@ -49,6 +50,7 @@ function mapStateToProps(state) {
|
||||
frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
|
||||
networkDropdownOpen: state.appState.networkDropdownOpen,
|
||||
showTestnetMessageInDropdown: state.metamask.showTestnetMessageInDropdown,
|
||||
addPopularNetworkFeatureToggledOn: state.metamask.customNetworkListEnabled,
|
||||
};
|
||||
}
|
||||
|
||||
@ -101,6 +103,7 @@ class NetworkDropdown extends Component {
|
||||
showTestnetMessageInDropdown: PropTypes.bool.isRequired,
|
||||
hideTestNetMessage: PropTypes.func.isRequired,
|
||||
history: PropTypes.object,
|
||||
addPopularNetworkFeatureToggledOn: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleClick(newProviderType) {
|
||||
@ -129,10 +132,12 @@ class NetworkDropdown extends Component {
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
|
||||
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
|
||||
if (this.props.addPopularNetworkFeatureToggledOn) {
|
||||
this.props.history.push(ADD_POPULAR_CUSTOM_NETWORK);
|
||||
} else {
|
||||
this.props.history.push(ADD_NETWORK_ROUTE);
|
||||
getEnvironmentType() === ENVIRONMENT_TYPE_POPUP
|
||||
? global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE)
|
||||
: this.props.history.push(ADD_NETWORK_ROUTE);
|
||||
}
|
||||
this.props.hideNetworkDropdown();
|
||||
}}
|
||||
|
@ -43,6 +43,9 @@ const MetaMaskTemplateRenderer = ({ sections }) => {
|
||||
return (
|
||||
<>
|
||||
{sections.reduce((allChildren, child) => {
|
||||
if (child?.hide === true) {
|
||||
return allChildren;
|
||||
}
|
||||
if (typeof child === 'string') {
|
||||
// React can render strings directly, so push them into the accumulator
|
||||
allChildren.push(child);
|
||||
|
@ -9,6 +9,8 @@ import MetaMaskTranslation from '../metamask-translation';
|
||||
import NetworkDisplay from '../network-display';
|
||||
import TextArea from '../../ui/textarea/textarea';
|
||||
import ConfirmationNetworkSwitch from '../../../pages/confirmation/components/confirmation-network-switch';
|
||||
import UrlIcon from '../../ui/url-icon';
|
||||
import Tooltip from '../../ui/tooltip/tooltip';
|
||||
|
||||
export const safeComponentList = {
|
||||
MetaMaskTranslation,
|
||||
@ -27,4 +29,7 @@ export const safeComponentList = {
|
||||
NetworkDisplay,
|
||||
TextArea,
|
||||
ConfirmationNetworkSwitch,
|
||||
UrlIcon,
|
||||
Tooltip,
|
||||
i: 'i',
|
||||
};
|
||||
|
@ -35,7 +35,7 @@ export default function SrpInput({ onChange, srpText }) {
|
||||
const onSrpChange = useCallback(
|
||||
(newDraftSrp) => {
|
||||
let newSrpError = '';
|
||||
const joinedDraftSrp = newDraftSrp.join(' ');
|
||||
const joinedDraftSrp = newDraftSrp.join(' ').trim();
|
||||
|
||||
if (newDraftSrp.some((word) => word !== '')) {
|
||||
if (newDraftSrp.some((word) => word === '')) {
|
||||
|
@ -251,7 +251,10 @@ export default class TransactionListItemDetails extends PureComponent {
|
||||
<div className="transaction-list-item-details__cards-container">
|
||||
<TransactionBreakdown
|
||||
nonce={transactionGroup.initialTransaction.txParams.nonce}
|
||||
isTokenApprove={type === TRANSACTION_TYPES.TOKEN_METHOD_APPROVE}
|
||||
isTokenApprove={
|
||||
type === TRANSACTION_TYPES.TOKEN_METHOD_APPROVE ||
|
||||
type === TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL
|
||||
}
|
||||
transaction={transaction}
|
||||
primaryCurrency={primaryCurrency}
|
||||
className="transaction-list-item-details__transaction-breakdown"
|
||||
|
@ -33,6 +33,8 @@ import { isHardwareKeyring } from '../../../helpers/utils/hardware';
|
||||
import { MetaMetricsContext } from '../../../contexts/metametrics';
|
||||
import { EVENT } from '../../../../shared/constants/metametrics';
|
||||
import Spinner from '../../ui/spinner';
|
||||
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||
import WalletOverview from './wallet-overview';
|
||||
|
||||
const EthOverview = ({ className }) => {
|
||||
@ -131,7 +133,11 @@ const EthOverview = ({ className }) => {
|
||||
legacy_event: true,
|
||||
},
|
||||
});
|
||||
history.push(SEND_ROUTE);
|
||||
dispatch(
|
||||
startNewDraftTransaction({ type: ASSET_TYPES.NATIVE }),
|
||||
).then(() => {
|
||||
history.push(SEND_ROUTE);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from '../../../helpers/constants/routes';
|
||||
import { useTokenTracker } from '../../../hooks/useTokenTracker';
|
||||
import { useTokenFiatAmount } from '../../../hooks/useTokenFiatAmount';
|
||||
import { updateSendAsset } from '../../../ducks/send';
|
||||
import { startNewDraftTransaction } from '../../../ducks/send';
|
||||
import { setSwapsFromToken } from '../../../ducks/swaps/swaps';
|
||||
import {
|
||||
getCurrentKeyring,
|
||||
@ -93,7 +93,7 @@ const TokenOverview = ({ className, token }) => {
|
||||
});
|
||||
try {
|
||||
await dispatch(
|
||||
updateSendAsset({
|
||||
startNewDraftTransaction({
|
||||
type: ASSET_TYPES.TOKEN,
|
||||
details: token,
|
||||
}),
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import classnames from 'classnames';
|
||||
import { omit } from 'lodash';
|
||||
import Typography from '../typography';
|
||||
import UrlIcon from '../url-icon';
|
||||
import { COLORS, TYPOGRAPHY } from '../../../helpers/constants/design-system';
|
||||
|
||||
export default function Chip({
|
||||
@ -14,9 +15,11 @@ export default function Chip({
|
||||
label,
|
||||
labelProps = {},
|
||||
leftIcon,
|
||||
leftIconUrl = '',
|
||||
rightIcon,
|
||||
onClick,
|
||||
maxContent = true,
|
||||
displayInlineBlock = false,
|
||||
}) {
|
||||
const onKeyPress = (event) => {
|
||||
if (event.key === 'Enter' && onClick) {
|
||||
@ -37,11 +40,17 @@ export default function Chip({
|
||||
[`chip--border-color-${borderColor}`]: true,
|
||||
[`chip--background-color-${backgroundColor}`]: true,
|
||||
'chip--max-content': maxContent,
|
||||
'chip--display-inline-block': displayInlineBlock,
|
||||
})}
|
||||
role={isInteractive ? 'button' : undefined}
|
||||
tabIndex={isInteractive ? 0 : undefined}
|
||||
>
|
||||
{leftIcon ? <div className="chip__left-icon">{leftIcon}</div> : null}
|
||||
{leftIcon && !leftIconUrl ? (
|
||||
<div className="chip__left-icon">{leftIcon}</div>
|
||||
) : null}
|
||||
{leftIconUrl ? (
|
||||
<UrlIcon className="chip__left-url-icon" url={leftIconUrl} />
|
||||
) : null}
|
||||
{children ?? (
|
||||
<Typography
|
||||
className="chip__label"
|
||||
@ -106,4 +115,12 @@ Chip.propTypes = {
|
||||
* max-content can overflow the parent's width and break designs
|
||||
*/
|
||||
maxContent: PropTypes.bool,
|
||||
/**
|
||||
* Icon location
|
||||
*/
|
||||
leftIconUrl: PropTypes.string,
|
||||
/**
|
||||
* Display or not the inline block
|
||||
*/
|
||||
displayInlineBlock: PropTypes.bool,
|
||||
};
|
||||
|
@ -16,6 +16,10 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__left-url-icon {
|
||||
margin-right: 8px;
|
||||
};
|
||||
|
||||
@each $variant, $color in design-system.$color-map {
|
||||
&--border-color-#{$variant} {
|
||||
border-color: var($color);
|
||||
@ -67,4 +71,8 @@
|
||||
&--max-content {
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
&--display-inline-block {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import {
|
||||
SIZES,
|
||||
TYPOGRAPHY,
|
||||
FONT_WEIGHT,
|
||||
OVERFLOW_WRAP,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import Tooltip from '../tooltip';
|
||||
|
||||
@ -60,6 +61,7 @@ export default function DefinitionList({
|
||||
marginBottom: MARGIN_MAP[gapSize],
|
||||
}}
|
||||
className="definition-list__definition"
|
||||
overflowWrap={OVERFLOW_WRAP.BREAK_WORD}
|
||||
tag="dd"
|
||||
>
|
||||
{definition}
|
||||
|
@ -118,7 +118,7 @@ export default class TokenInput extends PureComponent {
|
||||
isEqualCaseInsensitive(address, token.address),
|
||||
);
|
||||
|
||||
const tokenExchangeRate = tokenExchangeRates?.[existingToken.address] || 0;
|
||||
const tokenExchangeRate = tokenExchangeRates?.[existingToken?.address] ?? 0;
|
||||
let currency, numberOfDecimals;
|
||||
|
||||
if (hideConversion) {
|
||||
|
@ -60,6 +60,7 @@ export default function reduceApp(state = {}, action) {
|
||||
newCollectibleAddedMessage: '',
|
||||
sendInputCurrencySwitched: false,
|
||||
newTokensImported: '',
|
||||
newCustomNetworkAdded: {},
|
||||
...state,
|
||||
};
|
||||
|
||||
@ -393,6 +394,11 @@ export default function reduceApp(state = {}, action) {
|
||||
...appState,
|
||||
sendInputCurrencySwitched: !appState.sendInputCurrencySwitched,
|
||||
};
|
||||
case actionConstants.SET_NEW_CUSTOM_NETWORK_ADDED:
|
||||
return {
|
||||
...appState,
|
||||
newCustomNetworkAdded: action.value,
|
||||
};
|
||||
default:
|
||||
return appState;
|
||||
}
|
||||
@ -444,3 +450,7 @@ export function getLedgerTransportStatus(state) {
|
||||
export function toggleCurrencySwitch() {
|
||||
return { type: actionConstants.TOGGLE_CURRENCY_INPUT_SWITCH };
|
||||
}
|
||||
|
||||
export function setNewCustomNetworkAdded(value) {
|
||||
return { type: actionConstants.SET_NEW_CUSTOM_NETWORK_ADDED, value };
|
||||
}
|
||||
|
295
ui/ducks/send/helpers.js
Normal file
295
ui/ducks/send/helpers.js
Normal file
@ -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);
|
||||
}
|
163
ui/ducks/send/helpers.test.js
Normal file
163
ui/ducks/send/helpers.test.js
Normal file
@ -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
@ -47,6 +47,8 @@ export const GAS_ESTIMATE_TYPES = {
|
||||
|
||||
let _supportLink = 'https://support.metamask.io';
|
||||
let _supportRequestLink = 'https://metamask.zendesk.com/hc/en-us/requests/new';
|
||||
const _contractAddressLink =
|
||||
'https://metamask.zendesk.com/hc/en-us/articles/360020028092-What-is-the-known-contract-address-warning-';
|
||||
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
_supportLink = 'https://metamask-flask.zendesk.com/hc';
|
||||
@ -56,3 +58,4 @@ _supportRequestLink =
|
||||
|
||||
export const SUPPORT_LINK = _supportLink;
|
||||
export const SUPPORT_REQUEST_LINK = _supportRequestLink;
|
||||
export const CONTRACT_ADDRESS_LINK = _contractAddressLink;
|
||||
|
@ -12,6 +12,8 @@ const ALERTS_ROUTE = '/settings/alerts';
|
||||
const NETWORKS_ROUTE = '/settings/networks';
|
||||
const NETWORKS_FORM_ROUTE = '/settings/networks/form';
|
||||
const ADD_NETWORK_ROUTE = '/settings/networks/add-network';
|
||||
const ADD_POPULAR_CUSTOM_NETWORK =
|
||||
'/settings/networks/add-popular-custom-network';
|
||||
const SNAPS_LIST_ROUTE = '/settings/snaps-list';
|
||||
const SNAPS_VIEW_ROUTE = '/settings/snaps-view';
|
||||
const CONTACT_LIST_ROUTE = '/settings/contact-list';
|
||||
@ -88,6 +90,7 @@ const CONFIRM_SEND_ETHER_PATH = '/send-ether';
|
||||
const CONFIRM_SEND_TOKEN_PATH = '/send-token';
|
||||
const CONFIRM_DEPLOY_CONTRACT_PATH = '/deploy-contract';
|
||||
const CONFIRM_APPROVE_PATH = '/approve';
|
||||
const CONFIRM_SET_APPROVAL_FOR_ALL_PATH = '/set-approval-for-all';
|
||||
const CONFIRM_TRANSFER_FROM_PATH = '/transfer-from';
|
||||
const CONFIRM_SAFE_TRANSFER_FROM_PATH = '/safe-transfer-from';
|
||||
const CONFIRM_TOKEN_METHOD_PATH = '/token-method';
|
||||
@ -113,6 +116,8 @@ const PATH_NAME_MAP = {
|
||||
[NETWORKS_ROUTE]: 'Network Settings Page',
|
||||
[NETWORKS_FORM_ROUTE]: 'Network Settings Page Form',
|
||||
[ADD_NETWORK_ROUTE]: 'Add Network From Settings Page Form',
|
||||
[ADD_POPULAR_CUSTOM_NETWORK]:
|
||||
'Add Network From A List Of Popular Custom Networks',
|
||||
[CONTACT_LIST_ROUTE]: 'Contact List Settings Page',
|
||||
[`${CONTACT_EDIT_ROUTE}/:address`]: 'Edit Contact Settings Page',
|
||||
[CONTACT_ADD_ROUTE]: 'Add Contact Settings Page',
|
||||
@ -141,6 +146,7 @@ const PATH_NAME_MAP = {
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SEND_TOKEN_PATH}`]: 'Confirm Send Token Transaction Page',
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_DEPLOY_CONTRACT_PATH}`]: 'Confirm Deploy Contract Transaction Page',
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_APPROVE_PATH}`]: 'Confirm Approve Transaction Page',
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`]: 'Confirm Set Approval For All Transaction Page',
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_TRANSFER_FROM_PATH}`]: 'Confirm Transfer From Transaction Page',
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${CONFIRM_SAFE_TRANSFER_FROM_PATH}`]: 'Confirm Safe Transfer From Transaction Page',
|
||||
[`${CONFIRM_TRANSACTION_ROUTE}/:id${SIGNATURE_REQUEST_PATH}`]: 'Signature Request Page',
|
||||
@ -202,6 +208,7 @@ export {
|
||||
CONFIRM_SEND_TOKEN_PATH,
|
||||
CONFIRM_DEPLOY_CONTRACT_PATH,
|
||||
CONFIRM_APPROVE_PATH,
|
||||
CONFIRM_SET_APPROVAL_FOR_ALL_PATH,
|
||||
CONFIRM_TRANSFER_FROM_PATH,
|
||||
CONFIRM_SAFE_TRANSFER_FROM_PATH,
|
||||
CONFIRM_TOKEN_METHOD_PATH,
|
||||
@ -224,6 +231,7 @@ export {
|
||||
NETWORKS_ROUTE,
|
||||
NETWORKS_FORM_ROUTE,
|
||||
ADD_NETWORK_ROUTE,
|
||||
ADD_POPULAR_CUSTOM_NETWORK,
|
||||
INITIALIZE_BACKUP_SEED_PHRASE_ROUTE,
|
||||
INITIALIZE_SEED_PHRASE_INTRO_ROUTE,
|
||||
CONNECT_ROUTE,
|
||||
|
@ -346,4 +346,11 @@ export const SETTINGS_CONSTANTS = [
|
||||
icon: 'fa fa-flask',
|
||||
featureFlag: 'COLLECTIBLES_V1',
|
||||
},
|
||||
{
|
||||
tabMessage: (t) => t('experimental'),
|
||||
sectionMessage: (t) => t('showCustomNetworkList'),
|
||||
descriptionMessage: (t) => t('showCustomNetworkListDescription'),
|
||||
route: `${EXPERIMENTAL_ROUTE}#show-custom-network`,
|
||||
icon: 'fa fa-flask',
|
||||
},
|
||||
];
|
||||
|
@ -17,6 +17,7 @@ export const PRIORITY_STATUS_HASH = {
|
||||
|
||||
export const TOKEN_CATEGORY_HASH = {
|
||||
[TRANSACTION_TYPES.TOKEN_METHOD_APPROVE]: true,
|
||||
[TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL]: true,
|
||||
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER]: true,
|
||||
[TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM]: true,
|
||||
};
|
||||
|
@ -195,7 +195,7 @@ describe('Settings Search Utils', () => {
|
||||
|
||||
it('should get good experimental section number', () => {
|
||||
expect(getNumberOfSettingsInSection(t, t('experimental'))).toStrictEqual(
|
||||
3,
|
||||
4,
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -43,7 +43,7 @@ async function getDecimalsFromContract(tokenAddress) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTokenMetadata(tokenAddress, tokenList) {
|
||||
export function getTokenMetadata(tokenAddress, tokenList) {
|
||||
const casedTokenList = Object.keys(tokenList).reduce((acc, base) => {
|
||||
return {
|
||||
...acc,
|
||||
@ -151,6 +151,10 @@ export function getTokenValueParam(tokenData = {}) {
|
||||
return tokenData?.args?._value?.toString();
|
||||
}
|
||||
|
||||
export function getTokenApprovedParam(tokenData = {}) {
|
||||
return tokenData?.args?._approved;
|
||||
}
|
||||
|
||||
export function getTokenValue(tokenParams = []) {
|
||||
const valueData = tokenParams.find((param) => param.name === '_value');
|
||||
return valueData && valueData.value;
|
||||
|
@ -116,6 +116,7 @@ export function isTokenMethodAction(type) {
|
||||
return [
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_APPROVE,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM,
|
||||
TRANSACTION_TYPES.TOKEN_METHOD_SAFE_TRANSFER_FROM,
|
||||
].includes(type);
|
||||
@ -217,6 +218,9 @@ export function getTransactionTypeTitle(t, type, nativeCurrency = 'ETH') {
|
||||
case TRANSACTION_TYPES.TOKEN_METHOD_APPROVE: {
|
||||
return t('approve');
|
||||
}
|
||||
case TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL: {
|
||||
return t('setApprovalForAll');
|
||||
}
|
||||
case TRANSACTION_TYPES.SIMPLE_SEND: {
|
||||
return t('sendingNativeAsset', [nativeCurrency]);
|
||||
}
|
||||
|
@ -222,6 +222,12 @@ export function useTransactionDisplayData(transactionGroup) {
|
||||
title = t('approveSpendLimit', [token?.symbol || t('token')]);
|
||||
subtitle = origin;
|
||||
subtitleContainsOrigin = true;
|
||||
} else if (type === TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL) {
|
||||
category = TRANSACTION_GROUP_CATEGORIES.APPROVAL;
|
||||
prefix = '';
|
||||
title = t('setApprovalForAllTitle', [token?.symbol || t('token')]);
|
||||
subtitle = origin;
|
||||
subtitleContainsOrigin = true;
|
||||
} else if (type === TRANSACTION_TYPES.CONTRACT_INTERACTION) {
|
||||
category = TRANSACTION_GROUP_CATEGORIES.INTERACTION;
|
||||
const transactionTypeTitle = getTransactionTypeTitle(t, type);
|
||||
|
@ -4,7 +4,6 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { util } from '@metamask/controllers';
|
||||
import { useI18nContext } from '../../hooks/useI18nContext';
|
||||
import { DEFAULT_ROUTE } from '../../helpers/constants/routes';
|
||||
|
||||
import {
|
||||
DISPLAY,
|
||||
FONT_WEIGHT,
|
||||
@ -55,9 +54,7 @@ export default function AddCollectible() {
|
||||
|
||||
const handleAddCollectible = async () => {
|
||||
try {
|
||||
await dispatch(
|
||||
addCollectibleVerifyOwnership(address, tokenId.toString()),
|
||||
);
|
||||
await dispatch(addCollectibleVerifyOwnership(address, tokenId));
|
||||
} catch (error) {
|
||||
const { message } = error;
|
||||
dispatch(setNewCollectibleAddedMessage(message));
|
||||
@ -99,7 +96,7 @@ export default function AddCollectible() {
|
||||
};
|
||||
|
||||
const validateAndSetTokenId = (val) => {
|
||||
setDisabled(!util.isValidHexAddress(address) || !val);
|
||||
setDisabled(!util.isValidHexAddress(address) || !val || isNaN(Number(val)));
|
||||
setTokenId(val);
|
||||
};
|
||||
|
||||
@ -149,7 +146,7 @@ export default function AddCollectible() {
|
||||
)}
|
||||
<Box margin={4}>
|
||||
<FormField
|
||||
id="address"
|
||||
dataTestId="address"
|
||||
titleText={t('address')}
|
||||
placeholder="0x..."
|
||||
value={address}
|
||||
@ -161,7 +158,7 @@ export default function AddCollectible() {
|
||||
autoFocus
|
||||
/>
|
||||
<FormField
|
||||
id="token-id"
|
||||
dataTestId="token-id"
|
||||
titleText={t('tokenId')}
|
||||
placeholder={t('nftTokenIdPlaceholder')}
|
||||
value={tokenId}
|
||||
@ -170,7 +167,6 @@ export default function AddCollectible() {
|
||||
setCollectibleAddFailed(false);
|
||||
}}
|
||||
tooltipText={t('importNFTTokenIdToolTip')}
|
||||
numeric
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
|
79
ui/pages/add-collectible/add-collectible.test.js
Normal file
79
ui/pages/add-collectible/add-collectible.test.js
Normal file
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
@ -71,6 +71,9 @@ export default class ConfirmApproveContent extends Component {
|
||||
assetName: PropTypes.string,
|
||||
tokenId: PropTypes.string,
|
||||
assetStandard: PropTypes.string,
|
||||
isSetApproveForAll: PropTypes.bool,
|
||||
setApproveForAllArg: PropTypes.bool,
|
||||
userAddress: PropTypes.string,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -100,7 +103,7 @@ export default class ConfirmApproveContent extends Component {
|
||||
>
|
||||
{showHeader && (
|
||||
<div className="confirm-approve-content__card-header">
|
||||
{!supportsEIP1559V2 && (
|
||||
{supportsEIP1559V2 && title === t('transactionFee') ? null : (
|
||||
<>
|
||||
<div className="confirm-approve-content__card-header__symbol">
|
||||
{symbol}
|
||||
@ -184,7 +187,7 @@ export default class ConfirmApproveContent extends Component {
|
||||
|
||||
renderERC721OrERC1155PermissionContent() {
|
||||
const { t } = this.context;
|
||||
const { origin, toAddress, isContract } = this.props;
|
||||
const { origin, toAddress, isContract, isSetApproveForAll } = this.props;
|
||||
|
||||
const titleTokenDescription = this.getTitleTokenDescription();
|
||||
|
||||
@ -201,7 +204,9 @@ export default class ConfirmApproveContent extends Component {
|
||||
{t('approvedAsset')}:
|
||||
</div>
|
||||
<div className="confirm-approve-content__medium-text">
|
||||
{titleTokenDescription}
|
||||
{isSetApproveForAll
|
||||
? t('allOfYour', [titleTokenDescription])
|
||||
: titleTokenDescription}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-row">
|
||||
@ -299,12 +304,19 @@ export default class ConfirmApproveContent extends Component {
|
||||
|
||||
renderDataContent() {
|
||||
const { t } = this.context;
|
||||
const { data } = this.props;
|
||||
const { data, isSetApproveForAll, setApproveForAllArg } = this.props;
|
||||
return (
|
||||
<div className="flex-column">
|
||||
<div className="confirm-approve-content__small-text">
|
||||
{t('functionApprove')}
|
||||
{isSetApproveForAll
|
||||
? t('functionSetApprovalForAll')
|
||||
: t('functionApprove')}
|
||||
</div>
|
||||
{isSetApproveForAll && setApproveForAllArg !== undefined ? (
|
||||
<div className="confirm-approve-content__small-text">
|
||||
{`${t('parameters')}: ${setApproveForAllArg}`}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="confirm-approve-content__small-text confirm-approve-content__data__data-block">
|
||||
{data}
|
||||
</div>
|
||||
@ -442,6 +454,8 @@ export default class ConfirmApproveContent extends Component {
|
||||
chainId,
|
||||
assetStandard,
|
||||
tokenSymbol,
|
||||
isSetApproveForAll,
|
||||
userAddress,
|
||||
} = this.props;
|
||||
const { t } = this.context;
|
||||
let titleTokenDescription = t('token');
|
||||
@ -450,6 +464,7 @@ export default class ConfirmApproveContent extends Component {
|
||||
tokenAddress,
|
||||
chainId,
|
||||
null,
|
||||
userAddress,
|
||||
{
|
||||
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
|
||||
},
|
||||
@ -468,7 +483,10 @@ export default class ConfirmApproveContent extends Component {
|
||||
titleTokenDescription = unknownTokenLink;
|
||||
}
|
||||
|
||||
if (assetStandard === ERC20 || (tokenSymbol && !tokenId)) {
|
||||
if (
|
||||
assetStandard === ERC20 ||
|
||||
(tokenSymbol && !tokenId && !isSetApproveForAll)
|
||||
) {
|
||||
titleTokenDescription = tokenSymbol;
|
||||
} else if (
|
||||
assetStandard === ERC721 ||
|
||||
@ -477,14 +495,15 @@ export default class ConfirmApproveContent extends Component {
|
||||
(assetName && tokenId) ||
|
||||
(tokenSymbol && tokenId)
|
||||
) {
|
||||
const tokenIdWrapped = tokenId ? ` (#${tokenId})` : null;
|
||||
const tokenIdWrapped = tokenId ? ` (#${tokenId})` : '';
|
||||
if (assetName || tokenSymbol) {
|
||||
titleTokenDescription = `${assetName ?? tokenSymbol} ${tokenIdWrapped}`;
|
||||
titleTokenDescription = `${assetName ?? tokenSymbol}${tokenIdWrapped}`;
|
||||
} else {
|
||||
const unknownNFTBlockExplorerLink = getTokenTrackerLink(
|
||||
tokenAddress,
|
||||
chainId,
|
||||
null,
|
||||
userAddress,
|
||||
{
|
||||
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
|
||||
},
|
||||
@ -509,6 +528,41 @@ export default class ConfirmApproveContent extends Component {
|
||||
return titleTokenDescription;
|
||||
}
|
||||
|
||||
renderTitle() {
|
||||
const { t } = this.context;
|
||||
const { isSetApproveForAll, setApproveForAllArg } = this.props;
|
||||
const titleTokenDescription = this.getTitleTokenDescription();
|
||||
|
||||
let title;
|
||||
|
||||
if (isSetApproveForAll) {
|
||||
title = t('approveAllTokensTitle', [titleTokenDescription]);
|
||||
if (setApproveForAllArg === false) {
|
||||
title = t('revokeAllTokensTitle', [titleTokenDescription]);
|
||||
}
|
||||
}
|
||||
return title || t('allowSpendToken', [titleTokenDescription]);
|
||||
}
|
||||
|
||||
renderDescription() {
|
||||
const { t } = this.context;
|
||||
const { isContract, isSetApproveForAll, setApproveForAllArg } = this.props;
|
||||
const grantee = isContract
|
||||
? t('contract').toLowerCase()
|
||||
: t('account').toLowerCase();
|
||||
|
||||
let description = t('trustSiteApprovePermission', [grantee]);
|
||||
|
||||
if (isSetApproveForAll && setApproveForAllArg === false) {
|
||||
description = t('revokeApproveForAllDescription', [
|
||||
grantee,
|
||||
this.getTitleTokenDescription(),
|
||||
]);
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.context;
|
||||
const {
|
||||
@ -531,11 +585,10 @@ export default class ConfirmApproveContent extends Component {
|
||||
rpcPrefs,
|
||||
isContract,
|
||||
assetStandard,
|
||||
userAddress,
|
||||
} = this.props;
|
||||
const { showFullTxDetails } = this.state;
|
||||
|
||||
const titleTokenDescription = this.getTitleTokenDescription();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('confirm-approve-content', {
|
||||
@ -575,14 +628,10 @@ export default class ConfirmApproveContent extends Component {
|
||||
</Box>
|
||||
</Box>
|
||||
<div className="confirm-approve-content__title">
|
||||
{t('allowSpendToken', [titleTokenDescription])}
|
||||
{this.renderTitle()}
|
||||
</div>
|
||||
<div className="confirm-approve-content__description">
|
||||
{t('trustSiteApprovePermission', [
|
||||
isContract
|
||||
? t('contract').toLowerCase()
|
||||
: t('account').toLowerCase(),
|
||||
])}
|
||||
{this.renderDescription()}
|
||||
</div>
|
||||
<Box className="confirm-approve-content__address-display-content">
|
||||
<Box display={DISPLAY.FLEX}>
|
||||
@ -623,7 +672,7 @@ export default class ConfirmApproveContent extends Component {
|
||||
className="confirm-approve-content__etherscan-link"
|
||||
onClick={() => {
|
||||
const blockExplorerTokenLink = isContract
|
||||
? getTokenTrackerLink(toAddress, chainId, null, null, {
|
||||
? getTokenTrackerLink(toAddress, chainId, null, userAddress, {
|
||||
blockExplorerUrl: rpcPrefs?.blockExplorerUrl ?? null,
|
||||
})
|
||||
: getAccountLink(
|
||||
|
@ -8,7 +8,10 @@ import {
|
||||
updateCustomNonce,
|
||||
getNextNonce,
|
||||
} from '../../store/actions';
|
||||
import { calcTokenAmount } from '../../helpers/utils/token-util';
|
||||
import {
|
||||
calcTokenAmount,
|
||||
getTokenApprovedParam,
|
||||
} from '../../helpers/utils/token-util';
|
||||
import { readAddressAsContract } from '../../../shared/modules/contract-utils';
|
||||
import { GasFeeContextProvider } from '../../contexts/gasFee';
|
||||
import { TransactionModalContextProvider } from '../../contexts/transaction-modal';
|
||||
@ -34,6 +37,7 @@ import EditGasFeePopover from '../../components/app/edit-gas-fee-popover';
|
||||
import EditGasPopover from '../../components/app/edit-gas-popover/edit-gas-popover.component';
|
||||
import Loading from '../../components/ui/loading-screen';
|
||||
import { ERC20, ERC1155, ERC721 } from '../../helpers/constants/common';
|
||||
import { parseStandardTokenTransactionData } from '../../../shared/modules/transaction.utils';
|
||||
import { getCustomTxParamsData } from './confirm-approve.util';
|
||||
import ConfirmApproveContent from './confirm-approve-content';
|
||||
|
||||
@ -57,6 +61,7 @@ export default function ConfirmApprove({
|
||||
ethTransactionTotal,
|
||||
fiatTransactionTotal,
|
||||
hexTransactionTotal,
|
||||
isSetApproveForAll,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const { txParams: { data: transactionData } = {} } = transaction;
|
||||
@ -150,6 +155,11 @@ export default function ConfirmApprove({
|
||||
})
|
||||
: null;
|
||||
|
||||
const parsedTransactionData = parseStandardTokenTransactionData(
|
||||
transactionData,
|
||||
);
|
||||
const setApproveForAllArg = getTokenApprovedParam(parsedTransactionData);
|
||||
|
||||
return tokenSymbol === undefined && assetName === undefined ? (
|
||||
<Loading />
|
||||
) : (
|
||||
@ -162,6 +172,9 @@ export default function ConfirmApprove({
|
||||
contentComponent={
|
||||
<TransactionModalContextProvider>
|
||||
<ConfirmApproveContent
|
||||
userAddress={userAddress}
|
||||
isSetApproveForAll={isSetApproveForAll}
|
||||
setApproveForAllArg={setApproveForAllArg}
|
||||
decimals={decimals}
|
||||
siteImage={siteImage}
|
||||
setCustomAmount={setCustomPermissionAmount}
|
||||
@ -290,4 +303,5 @@ ConfirmApprove.propTypes = {
|
||||
ethTransactionTotal: PropTypes.string,
|
||||
fiatTransactionTotal: PropTypes.string,
|
||||
hexTransactionTotal: PropTypes.string,
|
||||
isSetApproveForAll: PropTypes.bool,
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { editTransaction } from '../../ducks/send';
|
||||
import { editExistingTransaction } from '../../ducks/send';
|
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import { ASSET_TYPES } from '../../../shared/constants/transaction';
|
||||
import ConfirmSendEther from './confirm-send-ether.component';
|
||||
@ -20,7 +20,9 @@ const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
editTransaction: async (txData) => {
|
||||
const { id } = txData;
|
||||
await dispatch(editTransaction(ASSET_TYPES.NATIVE, id.toString()));
|
||||
await dispatch(
|
||||
editExistingTransaction(ASSET_TYPES.NATIVE, id.toString()),
|
||||
);
|
||||
dispatch(clearConfirmTransaction());
|
||||
},
|
||||
};
|
||||
|
@ -6,14 +6,15 @@ import { SEND_ROUTE } from '../../helpers/constants/routes';
|
||||
export default class ConfirmSendToken extends Component {
|
||||
static propTypes = {
|
||||
history: PropTypes.object,
|
||||
editTransaction: PropTypes.func,
|
||||
editExistingTransaction: PropTypes.func,
|
||||
tokenAmount: PropTypes.string,
|
||||
};
|
||||
|
||||
handleEdit(confirmTransactionData) {
|
||||
const { editTransaction, history } = this.props;
|
||||
editTransaction(confirmTransactionData);
|
||||
history.push(SEND_ROUTE);
|
||||
const { editExistingTransaction, history } = this.props;
|
||||
editExistingTransaction(confirmTransactionData).then(() => {
|
||||
history.push(SEND_ROUTE);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -3,7 +3,7 @@ import { compose } from 'redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { clearConfirmTransaction } from '../../ducks/confirm-transaction/confirm-transaction.duck';
|
||||
import { showSendTokenPage } from '../../store/actions';
|
||||
import { editTransaction } from '../../ducks/send';
|
||||
import { editExistingTransaction } from '../../ducks/send';
|
||||
import { sendTokenTokenAmountAndToAddressSelector } from '../../selectors';
|
||||
import { ASSET_TYPES } from '../../../shared/constants/transaction';
|
||||
import ConfirmSendToken from './confirm-send-token.component';
|
||||
@ -18,18 +18,11 @@ const mapStateToProps = (state) => {
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
editTransaction: ({ txData, tokenData, tokenProps: assetDetails }) => {
|
||||
editExistingTransaction: async ({ txData }) => {
|
||||
const { id } = txData;
|
||||
dispatch(
|
||||
editTransaction(
|
||||
ASSET_TYPES.TOKEN,
|
||||
id.toString(),
|
||||
tokenData,
|
||||
assetDetails,
|
||||
),
|
||||
);
|
||||
dispatch(clearConfirmTransaction());
|
||||
dispatch(showSendTokenPage());
|
||||
await dispatch(editExistingTransaction(ASSET_TYPES.TOKEN, id.toString()));
|
||||
await dispatch(clearConfirmTransaction());
|
||||
await dispatch(showSendTokenPage());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import ConfirmTokenTransactionBase from '../confirm-token-transaction-base/confirm-token-transaction-base';
|
||||
import { SEND_ROUTE } from '../../helpers/constants/routes';
|
||||
import { editTransaction } from '../../ducks/send';
|
||||
import { editExistingTransaction } from '../../ducks/send';
|
||||
import {
|
||||
contractExchangeRateSelector,
|
||||
getCurrentCurrency,
|
||||
@ -35,27 +35,17 @@ export default function ConfirmSendToken({
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
|
||||
const handleEditTransaction = ({
|
||||
txData,
|
||||
tokenData,
|
||||
tokenProps: assetDetails,
|
||||
}) => {
|
||||
const handleEditTransaction = async ({ txData }) => {
|
||||
const { id } = txData;
|
||||
dispatch(
|
||||
editTransaction(
|
||||
ASSET_TYPES.TOKEN,
|
||||
id.toString(),
|
||||
tokenData,
|
||||
assetDetails,
|
||||
),
|
||||
);
|
||||
await dispatch(editExistingTransaction(ASSET_TYPES.TOKEN, id.toString()));
|
||||
dispatch(clearConfirmTransaction());
|
||||
dispatch(showSendTokenPage());
|
||||
};
|
||||
|
||||
const handleEdit = (confirmTransactionData) => {
|
||||
handleEditTransaction(confirmTransactionData);
|
||||
history.push(SEND_ROUTE);
|
||||
handleEditTransaction(confirmTransactionData).then(() => {
|
||||
history.push(SEND_ROUTE);
|
||||
});
|
||||
};
|
||||
const conversionRate = useSelector(getConversionRate);
|
||||
const nativeCurrency = useSelector(getNativeCurrency);
|
||||
|
@ -338,6 +338,7 @@ export default class ConfirmTransactionBase extends Component {
|
||||
};
|
||||
|
||||
const hasSimulationError = Boolean(txData.simulationFails);
|
||||
|
||||
const renderSimulationFailureWarning =
|
||||
hasSimulationError && !userAcknowledgedGasMissing;
|
||||
const networkName = NETWORK_TO_NAME_MAP[txData.chainId];
|
||||
|
@ -54,6 +54,7 @@ import { toChecksumHexAddress } from '../../../shared/modules/hexstring-utils';
|
||||
import { getGasLoadingAnimationIsShowing } from '../../ducks/app/app';
|
||||
import { isLegacyTransaction } from '../../helpers/utils/transactions.util';
|
||||
import { CUSTOM_GAS_ESTIMATE } from '../../../shared/constants/gas';
|
||||
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
|
||||
import { isEqualCaseInsensitive } from '../../../shared/modules/string-utils';
|
||||
import { getTokenAddressParam } from '../../helpers/utils/token-util';
|
||||
import ConfirmTransactionBase from './confirm-transaction-base.component';
|
||||
@ -112,7 +113,10 @@ const mapStateToProps = (state, ownProps) => {
|
||||
|
||||
const { balance } = accounts[fromAddress];
|
||||
const { name: fromName } = identities[fromAddress];
|
||||
const toAddress = propsToAddress || tokenToAddress || txParamsToAddress;
|
||||
let toAddress = txParamsToAddress;
|
||||
if (type !== TRANSACTION_TYPES.SIMPLE_SEND) {
|
||||
toAddress = propsToAddress || tokenToAddress || txParamsToAddress;
|
||||
}
|
||||
|
||||
const tokenList = getTokenList(state);
|
||||
const useTokenDetection = getUseTokenDetection(state);
|
||||
|
@ -14,6 +14,7 @@ import {
|
||||
DECRYPT_MESSAGE_REQUEST_PATH,
|
||||
ENCRYPTION_PUBLIC_KEY_REQUEST_PATH,
|
||||
CONFIRM_SAFE_TRANSFER_FROM_PATH,
|
||||
CONFIRM_SET_APPROVAL_FOR_ALL_PATH,
|
||||
} from '../../helpers/constants/routes';
|
||||
import { MESSAGE_TYPE } from '../../../shared/constants/app';
|
||||
import { TRANSACTION_TYPES } from '../../../shared/constants/transaction';
|
||||
@ -47,6 +48,10 @@ export default class ConfirmTransactionSwitch extends Component {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_APPROVE_PATH}`;
|
||||
return <Redirect to={{ pathname }} />;
|
||||
}
|
||||
case TRANSACTION_TYPES.TOKEN_METHOD_SET_APPROVAL_FOR_ALL: {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`;
|
||||
return <Redirect to={{ pathname }} />;
|
||||
}
|
||||
case TRANSACTION_TYPES.TOKEN_METHOD_TRANSFER_FROM: {
|
||||
const pathname = `${CONFIRM_TRANSACTION_ROUTE}/${id}${CONFIRM_TRANSFER_FROM_PATH}`;
|
||||
return <Redirect to={{ pathname }} />;
|
||||
|
@ -6,6 +6,7 @@ import {
|
||||
CONFIRM_APPROVE_PATH,
|
||||
CONFIRM_SAFE_TRANSFER_FROM_PATH,
|
||||
CONFIRM_SEND_TOKEN_PATH,
|
||||
CONFIRM_SET_APPROVAL_FOR_ALL_PATH,
|
||||
CONFIRM_TRANSACTION_ROUTE,
|
||||
CONFIRM_TRANSFER_FROM_PATH,
|
||||
} from '../../helpers/constants/routes';
|
||||
@ -66,6 +67,30 @@ export default function ConfirmTokenTransactionSwitch({ transaction }) {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_SET_APPROVAL_FOR_ALL_PATH}`}
|
||||
render={() => (
|
||||
<ConfirmApprove
|
||||
isSetApproveForAll
|
||||
assetStandard={assetStandard}
|
||||
assetName={assetName}
|
||||
userBalance={userBalance}
|
||||
tokenSymbol={tokenSymbol}
|
||||
decimals={decimals}
|
||||
tokenImage={tokenImage}
|
||||
tokenAmount={tokenAmount}
|
||||
tokenId={tokenId}
|
||||
userAddress={userAddress}
|
||||
tokenAddress={tokenAddress}
|
||||
toAddress={toAddress}
|
||||
transaction={transaction}
|
||||
ethTransactionTotal={ethTransactionTotal}
|
||||
fiatTransactionTotal={fiatTransactionTotal}
|
||||
hexTransactionTotal={hexTransactionTotal}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path={`${CONFIRM_TRANSACTION_ROUTE}/:id?${CONFIRM_TRANSFER_FROM_PATH}`}
|
||||
|
@ -25,6 +25,7 @@ import { getUnapprovedTemplatedConfirmations } from '../../selectors';
|
||||
import NetworkDisplay from '../../components/app/network-display/network-display';
|
||||
import Callout from '../../components/ui/callout';
|
||||
import SiteOrigin from '../../components/ui/site-origin';
|
||||
import { addCustomNetwork } from '../../store/actions';
|
||||
import ConfirmationFooter from './components/confirmation-footer';
|
||||
import { getTemplateValues, getTemplateAlerts } from './templates';
|
||||
|
||||
@ -130,6 +131,7 @@ export default function ConfirmationPage() {
|
||||
const pendingConfirmation = pendingConfirmations[currentPendingConfirmation];
|
||||
const originMetadata = useOriginMetadata(pendingConfirmation?.origin) || {};
|
||||
const [alertState, dismissAlert] = useAlertState(pendingConfirmation);
|
||||
const [stayOnPage, setStayOnPage] = useState(false);
|
||||
|
||||
// Generating templatedValues is potentially expensive, and if done on every render
|
||||
// will result in a new object. Avoiding calling this generation unnecessarily will
|
||||
@ -146,11 +148,11 @@ export default function ConfirmationPage() {
|
||||
// confirmations reduces to a number that is less than the currently
|
||||
// viewed index, reset the index.
|
||||
if (pendingConfirmations.length === 0) {
|
||||
history.push(DEFAULT_ROUTE);
|
||||
!stayOnPage && history.push(DEFAULT_ROUTE);
|
||||
} else if (pendingConfirmations.length <= currentPendingConfirmation) {
|
||||
setCurrentPendingConfirmation(pendingConfirmations.length - 1);
|
||||
}
|
||||
}, [pendingConfirmations, history, currentPendingConfirmation]);
|
||||
}, [pendingConfirmations, history, currentPendingConfirmation, stayOnPage]);
|
||||
if (!pendingConfirmation) {
|
||||
return null;
|
||||
}
|
||||
@ -197,23 +199,25 @@ export default function ConfirmationPage() {
|
||||
/>
|
||||
</Box>
|
||||
) : null}
|
||||
<Box
|
||||
alignItems="center"
|
||||
marginTop={1}
|
||||
padding={[1, 4, 4]}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<SiteIcon
|
||||
icon={originMetadata.iconUrl}
|
||||
name={originMetadata.hostname}
|
||||
size={36}
|
||||
/>
|
||||
<SiteOrigin
|
||||
chip
|
||||
siteOrigin={stripHttpsScheme(originMetadata.origin)}
|
||||
title={stripHttpsScheme(originMetadata.origin)}
|
||||
/>
|
||||
</Box>
|
||||
{pendingConfirmation.origin === 'metamask' ? null : (
|
||||
<Box
|
||||
alignItems="center"
|
||||
marginTop={1}
|
||||
padding={[1, 4, 4]}
|
||||
flexDirection={FLEX_DIRECTION.COLUMN}
|
||||
>
|
||||
<SiteIcon
|
||||
icon={originMetadata.iconUrl}
|
||||
name={originMetadata.hostname}
|
||||
size={36}
|
||||
/>
|
||||
<SiteOrigin
|
||||
chip
|
||||
siteOrigin={stripHttpsScheme(originMetadata.origin)}
|
||||
title={stripHttpsScheme(originMetadata.origin)}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<MetaMaskTemplateRenderer sections={templatedValues.content} />
|
||||
</div>
|
||||
<ConfirmationFooter
|
||||
@ -234,8 +238,15 @@ export default function ConfirmationPage() {
|
||||
</Callout>
|
||||
))
|
||||
}
|
||||
onApprove={templatedValues.onApprove}
|
||||
onCancel={templatedValues.onCancel}
|
||||
onApprove={() => {
|
||||
templatedValues.onApprove.apply();
|
||||
pendingConfirmation.origin === 'metamask' &&
|
||||
dispatch(addCustomNetwork(pendingConfirmation.requestData));
|
||||
}}
|
||||
onCancel={() => {
|
||||
templatedValues.onCancel.apply();
|
||||
pendingConfirmation.origin === 'metamask' && setStayOnPage(true);
|
||||
}}
|
||||
approveText={templatedValues.approvalText}
|
||||
cancelText={templatedValues.cancelText}
|
||||
/>
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
import React from 'react';
|
||||
import {
|
||||
SEVERITIES,
|
||||
TYPOGRAPHY,
|
||||
TEXT_ALIGN,
|
||||
JUSTIFY_CONTENT,
|
||||
DISPLAY,
|
||||
COLORS,
|
||||
} from '../../../helpers/constants/design-system';
|
||||
import fetchWithCache from '../../../helpers/utils/fetch-with-cache';
|
||||
|
||||
@ -79,6 +84,11 @@ async function getAlerts(pendingApproval) {
|
||||
);
|
||||
let validated = Boolean(matchedChain);
|
||||
|
||||
const originIsMetaMask = pendingApproval.origin === 'metamask';
|
||||
if (originIsMetaMask && validated) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (matchedChain) {
|
||||
if (
|
||||
matchedChain.nativeCurrency?.decimals !== 18 ||
|
||||
@ -104,12 +114,39 @@ async function getAlerts(pendingApproval) {
|
||||
}
|
||||
|
||||
function getValues(pendingApproval, t, actions) {
|
||||
const originIsMetaMask = pendingApproval.origin === 'metamask';
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
hide: !originIsMetaMask,
|
||||
element: 'Box',
|
||||
key: 'network-box',
|
||||
props: {
|
||||
textAlign: TEXT_ALIGN.CENTER,
|
||||
display: DISPLAY.FLEX,
|
||||
justifyContent: JUSTIFY_CONTENT.CENTER,
|
||||
marginTop: 4,
|
||||
marginBottom: 2,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
element: 'Chip',
|
||||
key: 'network-chip',
|
||||
props: {
|
||||
label: pendingApproval.requestData.chainName,
|
||||
backgroundColor: COLORS.BACKGROUND_ALTERNATIVE,
|
||||
leftIconUrl: pendingApproval.requestData.imageUrl,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: 'Typography',
|
||||
key: 'title',
|
||||
children: t('addEthereumChainConfirmationTitle'),
|
||||
children: originIsMetaMask
|
||||
? t('wantToAddThisNetwork')
|
||||
: t('addEthereumChainConfirmationTitle'),
|
||||
props: {
|
||||
variant: TYPOGRAPHY.H3,
|
||||
align: 'center',
|
||||
@ -127,7 +164,7 @@ function getValues(pendingApproval, t, actions) {
|
||||
variant: TYPOGRAPHY.H7,
|
||||
align: 'center',
|
||||
boxProps: {
|
||||
margin: [0, 0, 4],
|
||||
margin: originIsMetaMask ? [0, 8, 4] : [0, 0, 4],
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -138,7 +175,55 @@ function getValues(pendingApproval, t, actions) {
|
||||
{
|
||||
element: 'b',
|
||||
key: 'bolded-text',
|
||||
children: `${t('addEthereumChainConfirmationRisks')} `,
|
||||
props: {
|
||||
style: { display: originIsMetaMask && '-webkit-box' },
|
||||
},
|
||||
children: [
|
||||
`${t('addEthereumChainConfirmationRisks')} `,
|
||||
{
|
||||
hide: !originIsMetaMask,
|
||||
element: 'Tooltip',
|
||||
key: 'tooltip-info',
|
||||
props: {
|
||||
position: 'bottom',
|
||||
interactive: true,
|
||||
trigger: 'mouseenter',
|
||||
html: (
|
||||
<div
|
||||
style={{
|
||||
width: '180px',
|
||||
margin: '16px',
|
||||
textAlign: 'left',
|
||||
}}
|
||||
>
|
||||
{t('someNetworksMayPoseSecurity')}{' '}
|
||||
<a
|
||||
key="zendesk_page_link"
|
||||
href="https://metamask.zendesk.com/hc/en-us/articles/4417500466971"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
style={{ color: 'var(--color-primary-default)' }}
|
||||
>
|
||||
{t('learnMoreUpperCase')}
|
||||
</a>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
children: [
|
||||
{
|
||||
element: 'i',
|
||||
key: 'info-circle',
|
||||
props: {
|
||||
className: 'fas fa-info-circle',
|
||||
style: {
|
||||
marginLeft: '4px',
|
||||
color: 'var(--color-icon-default)',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
element: 'MetaMaskTranslation',
|
||||
@ -164,7 +249,7 @@ function getValues(pendingApproval, t, actions) {
|
||||
variant: TYPOGRAPHY.H7,
|
||||
align: 'center',
|
||||
boxProps: {
|
||||
margin: 0,
|
||||
margin: originIsMetaMask ? [0, 8] : 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -205,7 +290,7 @@ function getValues(pendingApproval, t, actions) {
|
||||
pendingApproval.id,
|
||||
ethErrors.provider.userRejectedRequest().serialize(),
|
||||
),
|
||||
networkDisplay: true,
|
||||
networkDisplay: !originIsMetaMask,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -26,9 +26,7 @@ import {
|
||||
TYPOGRAPHY,
|
||||
FONT_WEIGHT,
|
||||
DISPLAY,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
COLORS,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
} from '../../helpers/constants/design-system';
|
||||
|
||||
import {
|
||||
@ -143,6 +141,9 @@ export default class Home extends PureComponent {
|
||||
closeNotificationPopup: PropTypes.func.isRequired,
|
||||
newTokensImported: PropTypes.string,
|
||||
setNewTokensImported: PropTypes.func.isRequired,
|
||||
newCustomNetworkAdded: PropTypes.object,
|
||||
setNewCustomNetworkAdded: PropTypes.func,
|
||||
setRpcTarget: PropTypes.func,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -280,6 +281,9 @@ export default class Home extends PureComponent {
|
||||
setNewCollectibleAddedMessage,
|
||||
newTokensImported,
|
||||
setNewTokensImported,
|
||||
newCustomNetworkAdded,
|
||||
setNewCustomNetworkAdded,
|
||||
setRpcTarget,
|
||||
} = this.props;
|
||||
return (
|
||||
<MultipleNotifications>
|
||||
@ -479,6 +483,53 @@ export default class Home extends PureComponent {
|
||||
key="home-infuraBlockedNotification"
|
||||
/>
|
||||
) : null}
|
||||
{Object.keys(newCustomNetworkAdded).length !== 0 && (
|
||||
<Popover className="home__new-network-added">
|
||||
<i className="fa fa-check-circle fa-2x home__new-network-added__check-circle" />
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H4}
|
||||
margin={[5, 9, 0, 9]}
|
||||
fontWeight={FONT_WEIGHT.BOLD}
|
||||
>
|
||||
{t('networkAddedSuccessfully')}
|
||||
</Typography>
|
||||
<Box margin={[8, 8, 5, 8]}>
|
||||
<Button
|
||||
type="primary"
|
||||
className="home__new-network-added__switch-to-button"
|
||||
onClick={() => {
|
||||
setRpcTarget(
|
||||
newCustomNetworkAdded.rpcUrl,
|
||||
newCustomNetworkAdded.chainId,
|
||||
newCustomNetworkAdded.ticker,
|
||||
newCustomNetworkAdded.chainName,
|
||||
);
|
||||
setNewCustomNetworkAdded();
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.NORMAL}
|
||||
color={COLORS.PRIMARY_INVERSE}
|
||||
>
|
||||
{t('switchToNetwork', [newCustomNetworkAdded.chainName])}
|
||||
</Typography>
|
||||
</Button>
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={() => setNewCustomNetworkAdded()}
|
||||
>
|
||||
<Typography
|
||||
variant={TYPOGRAPHY.H6}
|
||||
fontWeight={FONT_WEIGHT.NORMAL}
|
||||
color={COLORS.PRIMARY_DEFAULT}
|
||||
>
|
||||
{t('dismiss')}
|
||||
</Typography>
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
)}
|
||||
</MultipleNotifications>
|
||||
);
|
||||
}
|
||||
|
@ -37,11 +37,16 @@ import {
|
||||
setNewNetworkAdded,
|
||||
setNewCollectibleAddedMessage,
|
||||
setNewTokensImported,
|
||||
setRpcTarget,
|
||||
///: BEGIN:ONLY_INCLUDE_IN(flask)
|
||||
removeSnapError,
|
||||
///: END:ONLY_INCLUDE_IN
|
||||
} from '../../store/actions';
|
||||
import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app';
|
||||
import {
|
||||
setThreeBoxLastUpdated,
|
||||
hideWhatsNewPopup,
|
||||
setNewCustomNetworkAdded,
|
||||
} from '../../ducks/app/app';
|
||||
import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask';
|
||||
import { getSwapsFeatureIsLive } from '../../ducks/swaps/swaps';
|
||||
import { getEnvironmentType } from '../../../app/scripts/lib/util';
|
||||
@ -138,6 +143,7 @@ const mapStateToProps = (state) => {
|
||||
isSigningQRHardwareTransaction,
|
||||
newCollectibleAddedMessage: getNewCollectibleAddedMessage(state),
|
||||
newTokensImported: getNewTokensImported(state),
|
||||
newCustomNetworkAdded: appState.newCustomNetworkAdded,
|
||||
};
|
||||
};
|
||||
|
||||
@ -180,6 +186,12 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
setNewTokensImported: (newTokens) => {
|
||||
dispatch(setNewTokensImported(newTokens));
|
||||
},
|
||||
setNewCustomNetworkAdded: () => {
|
||||
dispatch(setNewCustomNetworkAdded({}));
|
||||
},
|
||||
setRpcTarget: (rpcUrl, chainId, ticker, nickname) => {
|
||||
dispatch(setRpcTarget(rpcUrl, chainId, ticker, nickname));
|
||||
},
|
||||
});
|
||||
|
||||
export default compose(
|
||||
|
@ -207,4 +207,18 @@
|
||||
margin-inline-start: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
&__new-network-added {
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
|
||||
&__check-circle {
|
||||
color: var(--color-success-default);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
&__switch-to-button {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ export default class AddRecipient extends Component {
|
||||
error: PropTypes.string,
|
||||
warning: PropTypes.string,
|
||||
}),
|
||||
updateRecipientUserInput: PropTypes.func,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
@ -70,6 +71,7 @@ export default class AddRecipient extends Component {
|
||||
`sendFlow - User clicked recipient from ${type}. address: ${address}, nickname ${nickname}`,
|
||||
);
|
||||
this.props.updateRecipient({ address, nickname });
|
||||
this.props.updateRecipientUserInput(address);
|
||||
};
|
||||
|
||||
searchForContacts = () => {
|
||||
|
@ -3,9 +3,13 @@ import configureMockStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { initialState, SEND_STATUSES } from '../../../../../ducks/send';
|
||||
import { AMOUNT_MODES, SEND_STATUSES } from '../../../../../ducks/send';
|
||||
import { renderWithProvider } from '../../../../../../test/jest';
|
||||
import { GAS_ESTIMATE_TYPES } from '../../../../../../shared/constants/gas';
|
||||
import {
|
||||
getInitialSendStateWithExistingTxState,
|
||||
INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||
} from '../../../../../../test/jest/mocks';
|
||||
import AmountMaxButton from './amount-max-button';
|
||||
|
||||
const middleware = [thunk];
|
||||
@ -22,7 +26,7 @@ describe('AmountMaxButton Component', () => {
|
||||
EIPS: {},
|
||||
},
|
||||
},
|
||||
send: initialState,
|
||||
send: INITIAL_SEND_STATE_FOR_EXISTING_DRAFT,
|
||||
}),
|
||||
);
|
||||
expect(getByText('Max')).toBeTruthy();
|
||||
@ -36,12 +40,14 @@ describe('AmountMaxButton Component', () => {
|
||||
EIPS: {},
|
||||
},
|
||||
},
|
||||
send: { ...initialState, status: SEND_STATUSES.VALID },
|
||||
send: getInitialSendStateWithExistingTxState({
|
||||
status: SEND_STATUSES.VALID,
|
||||
}),
|
||||
});
|
||||
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: 'send/updateAmountMode', payload: 'MAX' },
|
||||
{ type: 'send/updateAmountMode', payload: AMOUNT_MODES.MAX },
|
||||
];
|
||||
|
||||
fireEvent.click(getByText('Max'), { bubbles: true });
|
||||
@ -58,9 +64,10 @@ describe('AmountMaxButton Component', () => {
|
||||
},
|
||||
},
|
||||
send: {
|
||||
...initialState,
|
||||
status: SEND_STATUSES.VALID,
|
||||
amount: { ...initialState.amount, mode: 'MAX' },
|
||||
...getInitialSendStateWithExistingTxState({
|
||||
status: SEND_STATUSES.VALID,
|
||||
}),
|
||||
amountMode: AMOUNT_MODES.MAX,
|
||||
},
|
||||
});
|
||||
const { getByText } = renderWithProvider(<AmountMaxButton />, store);
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PageContainerContent from '../../../components/ui/page-container/page-container-content.component';
|
||||
import Dialog from '../../../components/ui/dialog';
|
||||
import ActionableMessage from '../../../components/ui/actionable-message';
|
||||
import NicknamePopovers from '../../../components/app/modals/nickname-popovers';
|
||||
import {
|
||||
ETH_GAS_PRICE_FETCH_WARNING_KEY,
|
||||
@ -10,6 +11,7 @@ import {
|
||||
INSUFFICIENT_FUNDS_FOR_GAS_ERROR_KEY,
|
||||
} from '../../../helpers/constants/error-keys';
|
||||
import { ASSET_TYPES } from '../../../../shared/constants/transaction';
|
||||
import { CONTRACT_ADDRESS_LINK } from '../../../helpers/constants/common';
|
||||
import SendAmountRow from './send-amount-row';
|
||||
import SendHexDataRow from './send-hex-data-row';
|
||||
import SendAssetRow from './send-asset-row';
|
||||
@ -38,6 +40,9 @@ export default class SendContent extends Component {
|
||||
asset: PropTypes.object,
|
||||
to: PropTypes.string,
|
||||
assetError: PropTypes.string,
|
||||
recipient: PropTypes.object,
|
||||
acknowledgeRecipientWarning: PropTypes.func,
|
||||
recipientWarningAcknowledged: PropTypes.bool,
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -51,6 +56,8 @@ export default class SendContent extends Component {
|
||||
getIsBalanceInsufficient,
|
||||
asset,
|
||||
assetError,
|
||||
recipient,
|
||||
recipientWarningAcknowledged,
|
||||
} = this.props;
|
||||
|
||||
let gasError;
|
||||
@ -66,6 +73,10 @@ export default class SendContent extends Component {
|
||||
asset.type !== ASSET_TYPES.TOKEN &&
|
||||
asset.type !== ASSET_TYPES.COLLECTIBLE;
|
||||
|
||||
const showKnownRecipientWarning =
|
||||
recipient.warning === 'knownAddressRecipient';
|
||||
const hideAddContactDialog = recipient.warning === 'loading';
|
||||
|
||||
return (
|
||||
<PageContainerContent>
|
||||
<div className="send-v2__form">
|
||||
@ -76,7 +87,12 @@ export default class SendContent extends Component {
|
||||
: null}
|
||||
{error ? this.renderError(error) : null}
|
||||
{warning ? this.renderWarning() : null}
|
||||
{this.maybeRenderAddContact()}
|
||||
{showKnownRecipientWarning && !recipientWarningAcknowledged
|
||||
? this.renderRecipientWarning()
|
||||
: null}
|
||||
{showKnownRecipientWarning || hideAddContactDialog
|
||||
? null
|
||||
: this.maybeRenderAddContact()}
|
||||
<SendAssetRow />
|
||||
<SendAmountRow />
|
||||
{networkOrAccountNotSupports1559 ? <SendGasRow /> : null}
|
||||
@ -104,6 +120,7 @@ export default class SendContent extends Component {
|
||||
>
|
||||
{t('newAccountDetectedDialogMessage')}
|
||||
</Dialog>
|
||||
|
||||
{showNicknamePopovers ? (
|
||||
<NicknamePopovers
|
||||
onClose={() => this.setState({ showNicknamePopovers: false })}
|
||||
@ -124,6 +141,36 @@ export default class SendContent extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderRecipientWarning() {
|
||||
const { acknowledgeRecipientWarning } = this.props;
|
||||
const { t } = this.context;
|
||||
return (
|
||||
<div className="send__warning-container">
|
||||
<ActionableMessage
|
||||
type="danger"
|
||||
useIcon
|
||||
iconFillColor="#d73a49"
|
||||
primaryActionV2={{
|
||||
label: t('tooltipApproveButton'),
|
||||
onClick: acknowledgeRecipientWarning,
|
||||
}}
|
||||
message={t('sendingToTokenContractWarning', [
|
||||
<a
|
||||
key="contractWarningSupport"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="send__warning-container__link"
|
||||
href={CONTRACT_ADDRESS_LINK}
|
||||
>
|
||||
{t('learnMoreUpperCase')}
|
||||
</a>,
|
||||
])}
|
||||
roundedButtons
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderError(error) {
|
||||
const { t } = this.context;
|
||||
return (
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user