1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge branch 'develop' of github.com:MetaMask/metamask-extension into minimal

This commit is contained in:
Matthias Kretschmann 2023-04-21 15:28:36 +01:00
commit 4e7ee233e6
Signed by: m
GPG Key ID: 606EEEF3C479A91F
46 changed files with 1349 additions and 217 deletions

View File

@ -951,6 +951,9 @@
"custodyRefreshTokenModalTitle": {
"message": "Your custodian session has expired"
},
"custodySessionExpired": {
"message": "Custodian session expired."
},
"custom": {
"message": "Advanced"
},
@ -1987,16 +1990,16 @@
"message": "Prior to clicking confirm:"
},
"ledgerConnectionInstructionStepFour": {
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device"
"message": "Enable \"smart contract data\" or \"blind signing\" on your Ledger device."
},
"ledgerConnectionInstructionStepOne": {
"message": "Enable Use Ledger Live under Settings > Advanced"
"message": "Enable Use Ledger Live under Settings > Advanced."
},
"ledgerConnectionInstructionStepThree": {
"message": "Plug in your Ledger device and select the Ethereum app"
"message": "Be sure your Ledger is plugged in and to select the Ethereum app."
},
"ledgerConnectionInstructionStepTwo": {
"message": "Open and unlock Ledger Live App"
"message": "Open and unlock Ledger Live App."
},
"ledgerConnectionPreferenceDescription": {
"message": "Customize how you connect your Ledger to MetaMask. $1 is recommended, but other options are available. Read more here: $2",
@ -2795,7 +2798,7 @@
"message": "Open Codefi Compliance"
},
"openFullScreenForLedgerWebHid": {
"message": "Open MetaMask in full screen to connect your ledger via WebHID.",
"message": "Go to full screen to connect your Ledger.",
"description": "Shown to the user on the confirm screen when they are viewing MetaMask in a popup window but need to connect their ledger via webhid."
},
"openInBlockExplorer": {
@ -2807,6 +2810,9 @@
"openSeaNew": {
"message": "OpenSea"
},
"operationFailed": {
"message": "Operation Failed"
},
"optional": {
"message": "Optional"
},
@ -3790,6 +3796,9 @@
"stableLowercase": {
"message": "stable"
},
"stake": {
"message": "Stake"
},
"stateLogError": {
"message": "Error in retrieving state logs."
},
@ -4472,6 +4481,10 @@
"transactionErrored": {
"message": "Transaction encountered an error."
},
"transactionFailed": {
"message": "Transaction Failed"
},
"transactionFee": {
"message": "Transaction fee"
},

View File

@ -0,0 +1,14 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clipPath="url(#clip0_735_24127)">
<path
d="M3.99902 19C7.24796 17.5 8.87242 16.75 10.4969 16.75M16.9948 19C13.7458 17.5 12.1214 16.75 10.4969 16.75M10.4969 16.75V11.5M10.4969 11.5L10 10.4091M10.4969 11.5V9.5L10.9967 8.5M10 10.4091C10 10.4091 5.00889 11.0985 2.99935 9.5C1.29118 8.14126 1 4.5 1 4.5C1 4.5 5.55008 3.95155 7.54545 5.90909C8.91802 7.25563 10 10.4091 10 10.4091ZM10.9967 8.5C10.9967 8.5 11.5374 4.11404 13.4959 2.5C15.2137 1.08439 18.9941 1 18.9941 1C18.9941 1 19.1777 5.2683 17.4946 7C15.6792 8.86783 10.9967 8.5 10.9967 8.5Z"
strokeWidth="1.5"
strokeLinecap="round"
/>
</g>
<defs>
<clipPath id="clip0_735_24127">
<rect width="20" height="20" fill="white" />
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 873 B

View File

@ -22,7 +22,6 @@ import {
MESSAGE_TYPE,
///: END:ONLY_INCLUDE_IN
} from '../../shared/constants/app';
import { SECOND } from '../../shared/constants/time';
import {
REJECT_NOTIFICATION_CLOSE,
REJECT_NOTIFICATION_CLOSE_SIG,
@ -442,7 +441,6 @@ export function setupController(
infuraProjectId: process.env.INFURA_PROJECT_ID,
// User confirmation callbacks:
showUserConfirmation: triggerUi,
openPopup,
// initial state
initState,
// initial locale code
@ -839,22 +837,6 @@ async function triggerUi() {
}
}
/**
* Opens the browser popup for user confirmation of watchAsset
* then it waits until user interact with the UI
*/
async function openPopup() {
await triggerUi();
await new Promise((resolve) => {
const interval = setInterval(() => {
if (!notificationIsOpen) {
clearInterval(interval);
resolve();
}
}, SECOND);
});
}
// It adds the "App Installed" event into a queue of events, which will be tracked only after a user opts into metrics.
const addAppInstalledEvent = () => {
if (controller) {

View File

@ -802,8 +802,8 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.getTransactionWithActionId(actionId);
if (existingTxMeta) {
this.emit('newUnapprovedTx', existingTxMeta);
this._requestApproval(existingTxMeta);
existingTxMeta = await this.addTransactionGasDefaults(existingTxMeta);
this._requestApproval(existingTxMeta);
return existingTxMeta;
}
}
@ -875,9 +875,9 @@ export default class TransactionController extends EventEmitter {
this.addTransaction(txMeta);
this.emit('newUnapprovedTx', txMeta);
this._requestApproval(txMeta);
txMeta = await this.addTransactionGasDefaults(txMeta);
this._requestApproval(txMeta);
return txMeta;
}

View File

@ -339,6 +339,14 @@ export default class MetamaskController extends EventEmitter {
}),
config: { provider: this.provider },
state: initState.TokensController,
messenger: this.controllerMessenger.getRestricted({
name: 'TokensController',
allowedActions: [
`${this.approvalController.name}:addRequest`,
`${this.approvalController.name}:acceptRequest`,
`${this.approvalController.name}:rejectRequest`,
],
}),
});
this.assetsContractController = new AssetsContractController(
@ -699,10 +707,6 @@ export default class MetamaskController extends EventEmitter {
initState: initState.CachedBalancesController,
});
this.tokensController.hub.on('pendingSuggestedAsset', async () => {
await opts.openPopup();
});
let additionalKeyrings = [keyringBuilderFactory(QRHardwareKeyring)];
if (this.canUseHardwareWallets()) {

View File

@ -889,17 +889,6 @@ function setupBundlerDefaults(
// Run TypeScript files through Babel
{ extensions },
],
// Transpile libraries that use ES2020 unsupported by Chrome v78
[
babelify,
{
only: [
'./**/node_modules/@ethereumjs/util',
'./**/node_modules/superstruct',
],
global: true,
},
],
// Inline `fs.readFileSync` files
brfs,
],

View File

@ -80,7 +80,6 @@
"app/scripts/lib/createStreamSink.js",
"app/scripts/lib/createTabIdMiddleware.js",
"app/scripts/lib/decrypt-message-manager.js",
"app/scripts/lib/encryption-public-key-manager.js",
"app/scripts/lib/ens-ipfs/contracts/registry.js",
"app/scripts/lib/ens-ipfs/contracts/resolver.js",
"app/scripts/lib/ens-ipfs/resolver.js",

View File

@ -687,6 +687,7 @@
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.info": true,
"console.log": true,
"setInterval": true,
@ -696,13 +697,13 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"@metamask/utils": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
"eth-query": true,
@ -730,18 +731,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/assets-controllers>abort-controller": {
"globals": {
"AbortController": true
@ -766,8 +755,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>@metamask/utils": true,
"@metamask/controller-utils>@spruceid/siwe-parser": true,
"@metamask/utils": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -775,18 +764,6 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/controller-utils>@spruceid/siwe-parser": {
"globals": {
"console.error": true,

View File

@ -687,6 +687,7 @@
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.info": true,
"console.log": true,
"setInterval": true,
@ -696,13 +697,13 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"@metamask/utils": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
"eth-query": true,
@ -730,18 +731,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/assets-controllers>abort-controller": {
"globals": {
"AbortController": true
@ -766,8 +755,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>@metamask/utils": true,
"@metamask/controller-utils>@spruceid/siwe-parser": true,
"@metamask/utils": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -775,18 +764,6 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/controller-utils>@spruceid/siwe-parser": {
"globals": {
"console.error": true,

View File

@ -687,6 +687,7 @@
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.info": true,
"console.log": true,
"setInterval": true,
@ -696,13 +697,13 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"@metamask/utils": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
"eth-query": true,
@ -730,18 +731,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/assets-controllers>abort-controller": {
"globals": {
"AbortController": true
@ -766,8 +755,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>@metamask/utils": true,
"@metamask/controller-utils>@spruceid/siwe-parser": true,
"@metamask/utils": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -775,18 +764,6 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/controller-utils>@spruceid/siwe-parser": {
"globals": {
"console.error": true,

View File

@ -687,6 +687,7 @@
"URL": true,
"clearInterval": true,
"clearTimeout": true,
"console.error": true,
"console.info": true,
"console.log": true,
"setInterval": true,
@ -696,13 +697,13 @@
"@ethersproject/contracts": true,
"@ethersproject/providers": true,
"@metamask/assets-controllers>@metamask/abi-utils": true,
"@metamask/assets-controllers>@metamask/utils": true,
"@metamask/assets-controllers>abort-controller": true,
"@metamask/assets-controllers>multiformats": true,
"@metamask/base-controller": true,
"@metamask/contract-metadata": true,
"@metamask/controller-utils": true,
"@metamask/metamask-eth-abis": true,
"@metamask/utils": true,
"browserify>events": true,
"eth-json-rpc-filters>async-mutex": true,
"eth-query": true,
@ -730,18 +731,6 @@
"semver": true
}
},
"@metamask/assets-controllers>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/assets-controllers>abort-controller": {
"globals": {
"AbortController": true
@ -766,8 +755,8 @@
"setTimeout": true
},
"packages": {
"@metamask/controller-utils>@metamask/utils": true,
"@metamask/controller-utils>@spruceid/siwe-parser": true,
"@metamask/utils": true,
"browserify>buffer": true,
"eslint>fast-deep-equal": true,
"eth-ens-namehash": true,
@ -775,18 +764,6 @@
"ethjs>ethjs-unit": true
}
},
"@metamask/controller-utils>@metamask/utils": {
"globals": {
"TextDecoder": true,
"TextEncoder": true
},
"packages": {
"@metamask/utils>superstruct": true,
"browserify>buffer": true,
"nock>debug": true,
"semver": true
}
},
"@metamask/controller-utils>@spruceid/siwe-parser": {
"globals": {
"console.error": true,

View File

@ -226,10 +226,10 @@
"@metamask/address-book-controller": "^2.0.0",
"@metamask/announcement-controller": "^3.0.0",
"@metamask/approval-controller": "^2.1.0",
"@metamask/assets-controllers": "^5.0.0",
"@metamask/assets-controllers": "^6.0.0",
"@metamask/base-controller": "^2.0.0",
"@metamask/contract-metadata": "^2.3.1",
"@metamask/controller-utils": "^3.1.0",
"@metamask/controller-utils": "^3.2.0",
"@metamask/design-tokens": "^1.9.0",
"@metamask/desktop": "^0.3.0",
"@metamask/eth-json-rpc-infura": "^8.0.0",

View File

@ -8,3 +8,46 @@ export const ALLOWED_BRIDGE_CHAIN_IDS = [
CHAIN_IDS.OPTIMISM,
CHAIN_IDS.ARBITRUM,
];
export const ALLOWED_BRIDGE_TOKEN_ADDRESSES = {
[CHAIN_IDS.MAINNET]: [
'0xdac17f958d2ee523a2206206994597c13d831ec7',
'0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
'0x6b175474e89094c44da98b954eedeac495271d0f',
'0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0',
'0x8965349fb649a33a30cbfda057d8ec2c48abe2a2',
],
[CHAIN_IDS.BSC]: [
'0x55d398326f99059ff775485246999027b3197955',
'0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d',
'0x1af3f329e8be154074d8769d1ffa4ee058b1dbc3',
'0x2170ed0880ac9a755fd29b2688956bd959f933f8',
'0xcc42724c6683b7e57334c4e856f4c9965ed682bd',
'0x1ce0c2827e2ef14d5c4f29a091d735a204794041',
],
[CHAIN_IDS.POLYGON]: [
'0xc2132d05d31c914a87c6611c10748aeb04b58e8f',
'0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
'0x8f3cf7ad23cd3cadbd9735aff958023239c6a063',
'0x7ceb23fd6bc0add59e62ac25578270cff1b9f619',
'0x2c89bbc92bd86f8075d1decc58c7f4e0107f286b',
],
[CHAIN_IDS.AVALANCHE]: [
'0xc7198437980c041c805a1edcba50c1ce5db95118',
'0x9702230a8ea53601f5cd2dc00fdbc13d4df4a8c7',
'0xa7d7079b0fead91f3e65f86e8915cb59c1a4c664',
'0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e',
'0xd586e7f844cea2f87f50152665bcbc2c279d8d70',
'0x49d5c2bdffac6ce2bfdb6640f4f80f226bc10bab',
],
[CHAIN_IDS.OPTIMISM]: [
'0x94b008aa00579c1307b0ef2c499ad98a8ce58e58',
'0x7f5c764cbc14f9669b88837ca1490cca17c31607',
'0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
],
[CHAIN_IDS.ARBITRUM]: [
'0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9',
'0xff970a61a04b1ca14834a43f5de4533ebddb5cc8',
'0xda10009cbd5d07dd0cecc66161fc93d7c9000da1',
],
};

View File

@ -0,0 +1,11 @@
/* eslint-disable no-undef */
export async function sha256(str: string): Promise<string> {
const buf = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(str),
);
return Array.prototype.map
.call(new Uint8Array(buf), (x: number) => `00${x.toString(16)}`.slice(-2))
.join('');
}

View File

@ -176,6 +176,8 @@ export async function determineTransactionType(txParams, query) {
contractCode = resultCode;
if (isContractAddress) {
const hasValue = txParams.value && txParams.value !== '0x0';
const tokenMethodName = [
TransactionType.tokenMethodApprove,
TransactionType.tokenMethodSetApprovalForAll,
@ -184,13 +186,8 @@ export async function determineTransactionType(txParams, query) {
TransactionType.tokenMethodSafeTransferFrom,
].find((methodName) => isEqualCaseInsensitive(methodName, name));
const isSendWithApprove =
txParams.value &&
txParams.value !== '0x0' &&
tokenMethodName === TransactionType.tokenMethodApprove;
result =
data && tokenMethodName && !isSendWithApprove
data && tokenMethodName && !hasValue
? tokenMethodName
: TransactionType.contractInteraction;
} else {

View File

@ -135,7 +135,7 @@ describe('Transaction.utils', function () {
});
});
it('should return a token transfer type when the recipient is a contract and data is for the respective method call', async function () {
it('should return a token transfer type when the recipient is a contract, there is no value passed, and data is for the respective method call', async function () {
const _providerResultStub = {
// 1 gwei
eth_gasPrice: '0x0de0b6b3a7640000',
@ -159,6 +159,48 @@ describe('Transaction.utils', function () {
});
});
it(
'should NOT return a token transfer type and instead return contract interaction' +
' when the recipient is a contract, the data matches the respective method call, but there is a value passed',
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 resultWithEmptyValue = await determineTransactionType(
{
value: '0x0',
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
},
new EthQuery(_provider),
);
expect(resultWithEmptyValue).toMatchObject({
type: TransactionType.tokenMethodTransfer,
getCodeResponse: '0xab',
});
const resultWithValue = await determineTransactionType(
{
value: '0x12345',
to: '0x9e673399f795D01116e9A8B2dD2F156705131ee9',
data: '0xa9059cbb0000000000000000000000002f318C334780961FB129D2a6c30D0763d9a5C970000000000000000000000000000000000000000000000000000000000000000a',
},
new EthQuery(_provider),
);
expect(resultWithValue).toMatchObject({
type: TransactionType.contractInteraction,
getCodeResponse: '0xab',
});
},
);
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

View File

@ -1,3 +1,3 @@
module.exports = {
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/5.1.2/',
TEST_SNAPS_WEBSITE_URL: 'https://metamask.github.io/test-snaps/5.2.0/',
};

View File

@ -0,0 +1,92 @@
const { withFixtures } = require('../helpers');
const FixtureBuilder = require('../fixture-builder');
const { TEST_SNAPS_WEBSITE_URL } = require('./enums');
describe('Test Snap WASM', function () {
it('can use webassembly inside a snap', async function () {
const ganacheOptions = {
accounts: [
{
secretKey:
'0x7C9529A67102755B7E6102D6D950AC5D5863C98713805CEC576B945B15B71EAC',
balance: 25000000000000000000,
},
],
};
await withFixtures(
{
fixtures: new FixtureBuilder().build(),
ganacheOptions,
failOnConsoleError: false,
title: this.test.title,
},
async ({ driver }) => {
await driver.navigate();
// enter pw into extension
await driver.fill('#password', 'correct horse battery staple');
await driver.press('#password', driver.Key.ENTER);
// navigate to test snaps page and connect
await driver.openNewPage(TEST_SNAPS_WEBSITE_URL);
await driver.delay(1000);
const snapButton = await driver.findElement('#connectWasmSnap');
await driver.scrollToElement(snapButton);
await driver.delay(1000);
await driver.clickElement('#connectWasmSnap');
await driver.delay(1000);
// switch to metamask extension and click connect
const windowHandles = await driver.waitUntilXWindowHandles(
3,
1000,
10000,
);
// const extensionPage = windowHandles[0];
await driver.switchToWindowWithTitle(
'MetaMask Notification',
windowHandles,
);
await driver.clickElement({
text: 'Connect',
tag: 'button',
});
await driver.waitForSelector({ text: 'Approve & install' });
await driver.clickElement({
text: 'Approve & install',
tag: 'button',
});
await driver.waitForSelector({ text: 'Ok' });
await driver.clickElement({
text: 'Ok',
tag: 'button',
});
// click send inputs on test snap page
await driver.switchToWindowWithTitle('Test Snaps', windowHandles);
// wait for npm installation success
await driver.waitForSelector({
css: '#connectWasmSnap',
text: 'Reconnect to WebAssembly Snap',
});
// enter number for test to input field
await driver.pasteIntoField('#wasmInput', '23');
// find and click on send error
await driver.clickElement('#sendWasmMessage');
// wait for the correct output
await driver.waitForSelector({
css: '#wasmResult',
text: '28657',
});
},
);
});
});

View File

@ -103,3 +103,6 @@
@import 'network-account-balance-header/index';
@import 'approve-content-card/index';
@import 'transaction-alerts/transaction-alerts';
///: BEGIN:ONLY_INCLUDE_IN(mmi)
@import '../institutional/transaction-failed-modal/index';
///: END:ONLY_INCLUDE_IN

View File

@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LedgerInstructionField Component rendering should render properly with data instruction 1`] = `
<div>
<div>
<div
class="confirm-detail-row"
>
<div
class="box mm-banner-base mm-banner-alert mm-banner-alert--severity-info box--padding-3 box--padding-left-2 box--display-flex box--gap-2 box--flex-direction-row box--background-color-primary-muted box--rounded-sm"
>
<span
class="box mm-icon mm-icon--size-lg box--display-inline-block box--flex-direction-row box--color-primary-default"
style="mask-image: url('./images/icons/info.svg');"
/>
<div>
<div
class="ledger-live-dialog"
>
<h6
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
Prior to clicking confirm:
</h6>
<h6
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
• Be sure your Ledger is plugged in and to select the Ethereum app.
</h6>
<h6
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
• Enable "smart contract data" or "blind signing" on your Ledger device.
</h6>
<h6
class="box mm-text mm-text--body-md box--flex-direction-row box--color-text-default"
>
<span>
<button
class="box mm-text mm-button-base mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--text-align-left box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-primary-default box--background-color-transparent"
>
Go to full screen to connect your Ledger.
</button>
</span>
</h6>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -19,15 +19,13 @@ import {
getLedgerTransportStatus,
} from '../../../ducks/app/app';
import Typography from '../../ui/typography/typography';
import Button from '../../ui/button';
import { BannerAlert, ButtonLink, Text } from '../../component-library';
import { useI18nContext } from '../../../hooks/useI18nContext';
import {
FONT_WEIGHT,
SEVERITIES,
TEXT_ALIGN,
TextColor,
TypographyVariant,
} from '../../../helpers/constants/design-system';
import Dialog from '../../ui/dialog';
import {
getPlatform,
getEnvironmentType,
@ -42,14 +40,9 @@ const renderInstructionStep = (
) => {
return (
show && (
<Typography
boxProps={{ margin: 0 }}
color={color}
fontWeight={FONT_WEIGHT.BOLD}
variant={TypographyVariant.H7}
>
<Text color={color} as="h6">
{text}
</Typography>
</Text>
)
);
};
@ -71,8 +64,8 @@ export default function LedgerInstructionField({ showDataInstruction }) {
ledgerTransportType === LedgerTransportTypes.webhid &&
webHidConnectedStatus !== WebHIDConnectedStatuses.connected
) {
const devices = await window.navigator.hid.getDevices();
const webHidIsConnected = devices.some(
const devices = await window.navigator?.hid?.getDevices();
const webHidIsConnected = devices?.some(
(device) => device.vendorId === Number(LEDGER_USB_VENDOR_ID),
);
dispatch(
@ -136,28 +129,28 @@ export default function LedgerInstructionField({ showDataInstruction }) {
return (
<div>
<div className="confirm-detail-row">
<Dialog type="message">
<BannerAlert severity={SEVERITIES.INFO}>
<div className="ledger-live-dialog">
{renderInstructionStep(t('ledgerConnectionInstructionHeader'))}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepOne')}`,
` ${t('ledgerConnectionInstructionStepOne')}`,
!isFirefox && usingLedgerLive,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepTwo')}`,
` ${t('ledgerConnectionInstructionStepTwo')}`,
!isFirefox && usingLedgerLive,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepThree')}`,
` ${t('ledgerConnectionInstructionStepThree')}`,
)}
{renderInstructionStep(
`- ${t('ledgerConnectionInstructionStepFour')}`,
` ${t('ledgerConnectionInstructionStepFour')}`,
showDataInstruction,
)}
{renderInstructionStep(
<span>
<Button
type="link"
<ButtonLink
textAlign={TEXT_ALIGN.LEFT}
onClick={async () => {
if (environmentTypeIsFullScreen) {
window.location.reload();
@ -167,14 +160,14 @@ export default function LedgerInstructionField({ showDataInstruction }) {
}}
>
{t('ledgerConnectionInstructionCloseOtherApps')}
</Button>
</ButtonLink>
</span>,
transportStatus === HardwareTransportStates.deviceOpenFailure,
)}
{renderInstructionStep(
<span>
<Button
type="link"
<ButtonLink
textAlign={TEXT_ALIGN.LEFT}
onClick={async () => {
if (environmentTypeIsFullScreen) {
const connectedDevices =
@ -200,19 +193,20 @@ export default function LedgerInstructionField({ showDataInstruction }) {
{environmentTypeIsFullScreen
? t('clickToConnectLedgerViaWebHID')
: t('openFullScreenForLedgerWebHid')}
</Button>
</ButtonLink>
</span>,
usingWebHID &&
webHidConnectedStatus === WebHIDConnectedStatuses.notConnected,
TextColor.WARNING_DEFAULT,
)}
</div>
</Dialog>
</BannerAlert>
</div>
</div>
);
}
LedgerInstructionField.propTypes = {
// whether or not to show the data instruction
showDataInstruction: PropTypes.bool,
};

View File

@ -0,0 +1,17 @@
import React from 'react';
import LedgerInstructionField from '.';
export default {
title: 'Components/App/LedgerInstructionField',
argTypes: {
showDataInstruction: {
control: {
type: 'boolean',
},
},
},
};
export const DefaultStory = (args) => <LedgerInstructionField {...args} />;
DefaultStory.storyName = 'Default';

View File

@ -0,0 +1,28 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import LedgerInstructionField from '.';
describe('LedgerInstructionField Component', () => {
const mockStore = {
appState: {
ledgerWebHidConnectedStatus: 'notConnected',
},
metamask: {
ledgerTransportType: 'webhid',
},
};
describe('rendering', () => {
it('should render properly with data instruction', () => {
const store = configureMockStore()(mockStore);
const { container } = renderWithProvider(
<LedgerInstructionField showDataInstruction />,
store,
);
expect(container).toMatchSnapshot();
});
});
});

View File

@ -181,7 +181,9 @@ describe('EthOverview', () => {
await waitFor(() =>
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringContaining(`/bridge?metamaskEntry=ext`),
url: expect.stringContaining(
'/bridge?metamaskEntry=ext_bridge_button',
),
}),
);
});

View File

@ -112,7 +112,14 @@
align-items: center;
margin: 16px 0 4px 0;
padding: 0 16px;
max-width: 100%;
max-width: 326px;
}
&__primary-container {
display: flex;
max-width: inherit;
justify-content: center;
flex-wrap: wrap;
}
&__primary-balance {
@ -129,6 +136,11 @@
color: var(--color-text-alternative);
}
&__portfolio-button {
height: inherit;
padding-inline-start: 16px;
}
&__button:last-of-type {
margin-right: 0;
}

View File

@ -44,11 +44,14 @@ const TokenOverview = ({ className, token }) => {
<WalletOverview
balance={
<div className="token-overview__balance">
<CurrencyDisplay
className="token-overview__primary-balance"
displayValue={balanceToRender}
suffix={token.symbol}
/>
<div className="token-overview__primary-container">
<CurrencyDisplay
style={{ display: 'contents' }}
className="token-overview__primary-balance"
displayValue={balanceToRender}
suffix={token.symbol}
/>
</div>
{formattedFiatBalance ? (
<CurrencyDisplay
className="token-overview__secondary-balance"

View File

@ -23,6 +23,7 @@ jest.mock('../../../../shared/constants/network', () => ({
},
},
}));
let openTabSpy;
describe('TokenOverview', () => {
const mockStore = {
@ -68,6 +69,11 @@ describe('TokenOverview', () => {
openTab: jest.fn(),
},
});
openTabSpy = jest.spyOn(global.platform, 'openTab');
});
beforeEach(() => {
openTabSpy.mockClear();
});
const token = {
@ -209,8 +215,6 @@ describe('TokenOverview', () => {
mockedStoreWithBuyableChainId,
);
const openTabSpy = jest.spyOn(global.platform, 'openTab');
const { queryByTestId } = renderWithProvider(
<TokenOverview token={token} />,
mockedStore,
@ -228,5 +232,137 @@ describe('TokenOverview', () => {
}),
);
});
it('should always show the Portfolio button', () => {
const mockToken = {
name: 'test',
isERC721: false,
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
symbol: 'test',
};
const { queryByTestId } = renderWithProvider(
<TokenOverview token={mockToken} />,
store,
);
const portfolioButton = queryByTestId('home__portfolio-site');
expect(portfolioButton).toBeInTheDocument();
});
it('should open the Portfolio URI when clicking on Portfolio button', async () => {
const mockToken = {
name: 'test',
isERC721: false,
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
symbol: 'test',
};
const { queryByTestId } = renderWithProvider(
<TokenOverview token={mockToken} />,
store,
);
const portfolioButton = queryByTestId('home__portfolio-site');
expect(portfolioButton).toBeInTheDocument();
expect(portfolioButton).not.toBeDisabled();
fireEvent.click(portfolioButton);
expect(openTabSpy).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringContaining(`?metamaskEntry=ext`),
}),
);
});
it('should show the Bridge button if chain id and token are supported', async () => {
const mockToken = {
name: 'test',
isERC721: false,
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
symbol: 'test',
};
const mockedStoreWithBridgeableChainId = {
metamask: {
...mockStore.metamask,
provider: { type: 'test', chainId: CHAIN_IDS.POLYGON },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithBridgeableChainId,
);
const { queryByTestId } = renderWithProvider(
<TokenOverview token={mockToken} />,
mockedStore,
);
const bridgeButton = queryByTestId('token-overview-bridge');
expect(bridgeButton).toBeInTheDocument();
expect(bridgeButton).not.toBeDisabled();
fireEvent.click(bridgeButton);
expect(openTabSpy).toHaveBeenCalledTimes(1);
await waitFor(() =>
expect(openTabSpy).toHaveBeenCalledWith({
url: expect.stringContaining(
'/bridge?metamaskEntry=ext_bridge_button&token=0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
),
}),
);
});
it('should not show the Bridge button if chain id is not supported', async () => {
const mockToken = {
name: 'test',
isERC721: false,
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f619',
symbol: 'test',
};
const mockedStoreWithBridgeableChainId = {
metamask: {
...mockStore.metamask,
provider: { type: 'test', chainId: CHAIN_IDS.FANTOM },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithBridgeableChainId,
);
const { queryByTestId } = renderWithProvider(
<TokenOverview token={mockToken} />,
mockedStore,
);
const bridgeButton = queryByTestId('token-overview-bridge');
expect(bridgeButton).not.toBeInTheDocument();
});
it('should not show the Bridge button if token is not supported', async () => {
const mockToken = {
name: 'test',
isERC721: false,
address: '0x7ceb23fd6bc0add59e62ac25578270cff1B9f620',
symbol: 'test',
};
const mockedStoreWithBridgeableChainId = {
metamask: {
...mockStore.metamask,
provider: { type: 'test', chainId: CHAIN_IDS.POLYGON },
},
};
const mockedStore = configureMockStore([thunk])(
mockedStoreWithBridgeableChainId,
);
const { queryByTestId } = renderWithProvider(
<TokenOverview token={mockToken} />,
mockedStore,
);
const bridgeButton = queryByTestId('token-overview-bridge');
expect(bridgeButton).not.toBeInTheDocument();
});
});
});

View File

@ -132,6 +132,7 @@ export enum IconName {
Snaps = 'snaps',
Speedometer = 'speedometer',
Star = 'star',
Stake = 'stake',
Student = 'student',
SwapHorizontal = 'swap-horizontal',
SwapVertical = 'swap-vertical',

View File

@ -0,0 +1 @@
export { default } from './interactive-replacement-token-notification';

View File

@ -0,0 +1,143 @@
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { getCurrentKeyring, getSelectedAddress } from '../../../selectors';
import { getInteractiveReplacementToken } from '../../../selectors/institutional/selectors';
import { getIsUnlocked } from '../../../ducks/metamask/metamask';
import { useI18nContext } from '../../../hooks/useI18nContext';
import { mmiActionsFactory } from '../../../store/institutional/institution-background';
import { sha256 } from '../../../../shared/modules/hash.utils';
import {
Size,
IconColor,
AlignItems,
DISPLAY,
BLOCK_SIZES,
JustifyContent,
TextColor,
TextVariant,
BackgroundColor,
} from '../../../helpers/constants/design-system';
import {
Icon,
IconName,
IconSize,
ButtonLink,
Text,
} from '../../component-library';
import Box from '../../ui/box';
const InteractiveReplacementTokenNotification = ({ isVisible }) => {
const t = useI18nContext();
const dispatch = useDispatch();
const mmiActions = mmiActionsFactory();
const keyring = useSelector(getCurrentKeyring);
const address = useSelector(getSelectedAddress);
const isUnlocked = useSelector(getIsUnlocked);
const interactiveReplacementToken = useSelector(
getInteractiveReplacementToken,
);
const [showNotification, setShowNotification] = useState(isVisible);
useEffect(() => {
const handleShowNotification = async () => {
const hasInteractiveReplacementToken =
interactiveReplacementToken &&
Boolean(Object.keys(interactiveReplacementToken).length);
if (!/^Custody/u.test(keyring.type)) {
setShowNotification(false);
return;
} else if (!hasInteractiveReplacementToken) {
setShowNotification(false);
return;
}
const token = await dispatch(mmiActions.getCustodianToken());
const custodyAccountDetails = await dispatch(
mmiActions.getAllCustodianAccountsWithToken(
keyring.type.split(' - ')[1],
token,
),
);
const showNotificationValue =
isUnlocked &&
interactiveReplacementToken.oldRefreshToken &&
custodyAccountDetails &&
Boolean(Object.keys(custodyAccountDetails).length);
let tokenAccount;
if (Array.isArray(custodyAccountDetails)) {
tokenAccount = custodyAccountDetails
.filter(
(item) => item.address.toLowerCase() === address.toLowerCase(),
)
.map((item) => ({
token: item.authDetails?.refreshToken,
}))[0];
}
const refreshTokenAccount = await sha256(
tokenAccount?.token + interactiveReplacementToken.url,
);
setShowNotification(
showNotificationValue &&
refreshTokenAccount === interactiveReplacementToken.oldRefreshToken,
);
};
handleShowNotification();
}, [
dispatch,
address,
interactiveReplacementToken,
isUnlocked,
keyring,
mmiActions,
]);
return showNotification ? (
<Box
width={BLOCK_SIZES.FULL}
display={DISPLAY.FLEX}
justifyContent={JustifyContent.center}
alignItems={AlignItems.center}
padding={[1, 2]}
backgroundColor={BackgroundColor.backgroundAlternative}
marginBottom={1}
className="interactive-replacement-token-notification"
data-testid="interactive-replacement-token-notification"
>
<Icon
name={IconName.Danger}
color={IconColor.errorDefault}
size={IconSize.Xl}
/>
<Text variant={TextVariant.bodyXs} gap={2} color={TextColor.errorDefault}>
{t('custodySessionExpired')}
</Text>
<ButtonLink
data-testid="show-modal"
size={Size.auto}
marginLeft={1}
onClick={() => {
dispatch(mmiActions.showInteractiveReplacementTokenModal());
}}
>
{t('learnMore')}
</ButtonLink>
</Box>
) : null;
};
export default InteractiveReplacementTokenNotification;
InteractiveReplacementTokenNotification.propTypes = {
isVisible: PropTypes.bool,
};

View File

@ -0,0 +1,15 @@
.interactive-replacement-token-notification {
height: 24px;
@media screen and (min-width: $break-large) {
width: 85vw;
}
@media screen and (min-width: 768px) {
width: 80vw;
}
@media screen and (min-width: 1280px) {
width: 62vw;
}
}

View File

@ -0,0 +1,59 @@
import React from 'react';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
import testData from '../../../../.storybook/test-data';
import InteractiveReplacementTokenNotification from '.';
const customData = {
...testData,
metamask: {
...testData.metamask,
provider: {
type: 'test',
},
selectedAddress: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
identities: {
'0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281': {
address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
name: 'Custodian A',
},
},
isUnlocked: true,
interactiveReplacementToken: {
oldRefreshToken:
'81f96a88b6cbc5f50d3864122349fa9a9755833ee82a7e3cf6f268c78aab51ab',
url: 'url',
},
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
keyrings: [
{
type: 'Custody - Saturn',
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281'],
},
],
},
};
const store = configureStore(customData);
export default {
title: 'Components/Institutional/InteractiveReplacementToken-Notification',
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
component: InteractiveReplacementTokenNotification,
args: {
isVisible: true,
},
argTypes: {
onClick: {
action: 'onClick',
},
},
};
export const DefaultStory = (args) => (
<InteractiveReplacementTokenNotification {...args} />
);
DefaultStory.storyName = 'InteractiveReplacementTokenNotification';

View File

@ -0,0 +1,156 @@
import React from 'react';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import { screen, fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { sha256 } from '../../../../shared/modules/hash.utils';
import { KeyringType } from '../../../../shared/constants/keyring';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import InteractiveReplacementTokenNotification from './interactive-replacement-token-notification';
jest.mock('../../../../shared/modules/hash.utils');
const mockedShowInteractiveReplacementTokenModal = jest
.fn()
.mockReturnValue({ type: 'TYPE' });
const mockedGetCustodianToken = jest
.fn()
.mockReturnValue({ type: 'Custody', payload: 'token' });
const mockedGetAllCustodianAccountsWithToken = jest.fn().mockReturnValue({
type: 'TYPE',
payload: [
{
address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
authDetails: { refreshToken: 'def' },
},
],
});
jest.mock('../../../store/institutional/institution-background', () => ({
mmiActionsFactory: () => ({
getCustodianToken: mockedGetCustodianToken,
getAllCustodianAccountsWithToken: mockedGetAllCustodianAccountsWithToken,
showInteractiveReplacementTokenModal:
mockedShowInteractiveReplacementTokenModal,
}),
}));
describe('Interactive Replacement Token Notification', () => {
const selectedAddress = '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281';
const identities = {
'0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281': {
address: '0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281',
name: 'Custodian A',
},
};
const mockStore = {
metamask: {
provider: {
type: 'test',
},
selectedAddress,
identities,
isUnlocked: false,
interactiveReplacementToken: { oldRefreshToken: 'abc' },
preferences: {
useNativeCurrencyAsPrimaryCurrency: true,
},
keyrings: [
{
type: KeyringType.imported,
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281', '0x2'],
},
{
type: KeyringType.ledger,
accounts: [],
},
],
},
};
it('should not render if show notification is false', () => {
const store = configureMockStore([thunk])(mockStore);
renderWithProvider(<InteractiveReplacementTokenNotification />, store);
expect(
screen.queryByTestId('interactive-replacement-token-notification'),
).not.toBeInTheDocument();
});
it('should render if show notification is true and click on learn more', async () => {
const customMockStore = {
...mockStore,
metamask: {
...mockStore.metamask,
isUnlocked: true,
interactiveReplacementToken: { oldRefreshToken: 'def', url: 'url' },
keyrings: [
{
type: 'Custody - Saturn',
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281'],
},
],
},
};
const store = configureMockStore([thunk])(customMockStore);
sha256.mockReturnValue('def');
await act(async () => {
renderWithProvider(<InteractiveReplacementTokenNotification />, store);
});
expect(
screen.getByTestId('interactive-replacement-token-notification'),
).toBeInTheDocument();
expect(screen.getByTestId('show-modal')).toBeInTheDocument();
await act(async () => {
fireEvent.click(screen.getByTestId('show-modal'));
});
expect(mockedShowInteractiveReplacementTokenModal).toHaveBeenCalled();
});
it('should render and call showNotification when component starts', async () => {
const customMockStore = {
...mockStore,
metamask: {
...mockStore.metamask,
isUnlocked: true,
interactiveReplacementToken: { oldRefreshToken: 'def', url: 'url' },
keyrings: [
{
type: 'Custody - Saturn',
accounts: ['0xca8f1F0245530118D0cf14a06b01Daf8f76Cf281', '0x2'],
},
{
type: KeyringType.ledger,
accounts: [],
},
],
},
};
const store = configureMockStore([thunk])(customMockStore);
sha256.mockReturnValue('def');
await act(async () => {
renderWithProvider(<InteractiveReplacementTokenNotification />, store);
});
expect(mockedGetCustodianToken).toHaveBeenCalled();
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 0));
});
expect(
screen.getByTestId('interactive-replacement-token-notification'),
).toBeInTheDocument();
});
});

View File

@ -0,0 +1 @@
export { default } from './transaction-failed';

View File

@ -0,0 +1,7 @@
.transaction-failed {
&__description {
border: 1px solid var(--color-border-muted);
max-width: 100%;
overflow-wrap: anywhere;
}
}

View File

@ -0,0 +1,79 @@
import React from 'react';
import PropTypes from 'prop-types';
import withModalProps from '../../../helpers/higher-order-components/with-modal-props';
import { useI18nContext } from '../../../hooks/useI18nContext';
import Modal from '../../app/modal';
import Box from '../../ui/box/box';
import {
AlignItems,
BorderRadius,
DISPLAY,
FLEX_DIRECTION,
FONT_WEIGHT,
TEXT_ALIGN,
TextVariant,
} from '../../../helpers/constants/design-system';
import { Text, Icon, IconName, IconSize } from '../../component-library';
const TransactionFailedModal = ({
hideModal,
closeNotification,
operationFailed,
errorMessage,
}) => {
const t = useI18nContext();
const handleSubmit = () => {
if (closeNotification) {
global.platform.closeCurrentWindow();
}
hideModal();
};
return (
<Modal onSubmit={handleSubmit} submitText={t('ok')}>
<Box
display={DISPLAY.FLEX}
flexDirection={FLEX_DIRECTION.COLUMN}
alignItems={AlignItems.center}
paddingLeft={4}
paddingRight={4}
style={{ flex: 1, overflowY: 'auto' }}
>
<Icon name={IconName.Warning} size={IconSize.Xl} />
<Text
as="h1"
variant={TextVariant.displayMd}
textAlign={TEXT_ALIGN.CENTER}
fontWeight={FONT_WEIGHT.BOLD}
paddingTop={4}
paddingBottom={4}
>
{operationFailed
? `${t('operationFailed')}!`
: `${t('transactionFailed')}!`}
</Text>
<Text
textAlign={TEXT_ALIGN.CENTER}
variant={TextVariant.bodySm}
paddingTop={4}
paddingBottom={4}
paddingLeft={4}
paddingRight={4}
borderRadius={BorderRadius.MD}
className="transaction-failed__description"
>
{errorMessage}
</Text>
</Box>
</Modal>
);
};
TransactionFailedModal.propTypes = {
hideModal: PropTypes.func,
errorMessage: PropTypes.string,
closeNotification: PropTypes.bool,
operationFailed: PropTypes.bool,
};
export default withModalProps(TransactionFailedModal);

View File

@ -0,0 +1,17 @@
import React from 'react';
import TransactionFailedModal from '.';
export default {
title: 'Components/Institutional/TransactionFailedModal',
argTypes: {},
args: {
errorMessage: 'test',
operationFailed: false,
},
};
export const DefaultStory = (args) => {
return <TransactionFailedModal {...args} />;
};
DefaultStory.storyName = 'TransactionFailedModal';

View File

@ -0,0 +1,63 @@
import React from 'react';
import { screen, fireEvent } from '@testing-library/react';
import configureMockStore from 'redux-mock-store';
import { renderWithProvider } from '../../../../test/lib/render-helpers';
import testData from '../../../../.storybook/test-data';
import TransactionFailed from '.';
const mockErrorMessage = 'Something went wrong';
describe('Transaction Failed', () => {
const mockStore = {
...testData,
};
const store = configureMockStore()(mockStore);
it('renders the error message', () => {
renderWithProvider(
<TransactionFailed errorMessage={mockErrorMessage} />,
store,
);
const errorMessageElement = screen.getByText(mockErrorMessage);
expect(errorMessageElement).toBeInTheDocument();
});
it('renders the correct title when operation fails', () => {
const operationFailed = true;
const title = 'Operation Failed!';
renderWithProvider(
<TransactionFailed
operationFailed={operationFailed}
errorMessage={mockErrorMessage}
/>,
store,
);
const titleElement = screen.getByText(title);
expect(titleElement).toBeInTheDocument();
});
it('renders the correct title when transaction fails', () => {
const operationFailed = false;
const title = 'Transaction Failed!';
renderWithProvider(
<TransactionFailed
operationFailed={operationFailed}
errorMessage={mockErrorMessage}
/>,
store,
);
const titleElement = screen.getByText(title);
expect(titleElement).toBeInTheDocument();
});
it('closes window when closeNotification is true', () => {
global.platform = {
closeCurrentWindow: jest.fn(),
};
renderWithProvider(<TransactionFailed closeNotification />, store);
const okButton = screen.getByText('Ok');
fireEvent.click(okButton);
expect(global.platform.closeCurrentWindow).toHaveBeenCalledTimes(1);
});
});

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useRef } from 'react';
import classnames from 'classnames';
import PropTypes from 'prop-types';
import Box from '../../ui/box/box';
@ -31,6 +31,14 @@ export const NetworkListItem = ({
onDeleteClick,
}) => {
const t = useI18nContext();
const networkRef = useRef();
useEffect(() => {
if (networkRef.current && selected) {
networkRef.current.querySelector('.mm-button-link').focus();
}
}, [networkRef, selected]);
return (
<Box
onClick={onClick}
@ -43,6 +51,7 @@ export const NetworkListItem = ({
alignItems={AlignItems.center}
justifyContent={JustifyContent.spaceBetween}
width={BLOCK_SIZES.FULL}
ref={networkRef}
>
{selected && (
<Box
@ -53,7 +62,14 @@ export const NetworkListItem = ({
)}
<AvatarNetwork name={name} src={iconSrc} />
<Box className="multichain-network-list-item__network-name">
<ButtonLink onClick={onClick} color={TextColor.textDefault} ellipsis>
<ButtonLink
onClick={(e) => {
e.stopPropagation();
onClick();
}}
color={TextColor.textDefault}
ellipsis
>
{name.length > MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP ? (
<Tooltip
title={name}

View File

@ -0,0 +1 @@
export { default as institutionalFeature } from './institutional';

View File

@ -0,0 +1,106 @@
import { createSelector } from 'reselect';
import { createSlice } from '@reduxjs/toolkit';
import { captureException } from '@sentry/browser';
import { mmiActionsFactory } from '../../store/institutional/institution-background';
const name = 'institutionalFeatures';
const initialState = {
historicalReports: {},
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
};
const slice = createSlice({
name,
initialState,
reducers: {
setHistoricalReports(state, action) {
state.historicalReports[action.payload.address] = [
...action.payload.reports,
];
},
},
});
const { actions, reducer } = slice;
export default reducer;
export const getComplianceProjectId = (state) =>
state.metamask[name].complianceProjectId;
export const getComplianceClientId = (state) =>
state.metamask[name].complianceClientId;
export const getComplianceTenantSubdomain = (state) =>
state.metamask[name].complianceTenantSubdomain;
export const getComplianceHistoricalReports = (state) =>
state.metamask[name].historicalReports;
export const getComplianceReportsInProgress = (state) =>
state.metamask[name].reportsInProgress;
export const getInstitutionalConnectRequests = (state) =>
state.metamask[name].connectRequests;
export const complianceActivated = (state) =>
Boolean(state.metamask[name].complianceProjectId);
export const getComplianceHistoricalReportsByAddress = (address) =>
createSelector(getComplianceHistoricalReports, (reports) =>
reports ? reports[address] : [],
);
export const getComplianceReportsInProgressByAddress = (address) =>
createSelector(getComplianceReportsInProgress, (reports) =>
reports ? reports[address.toLowerCase()] : undefined,
);
export const fetchHistoricalReports = (address, testProjectId = undefined) => {
return async (dispatch, getState) => {
const state = getState();
const mmiActions = mmiActionsFactory();
let projectId;
// testProjectId is provided to make a test request, which checks if projectId is correct
if (!testProjectId) {
projectId = getComplianceProjectId(state);
if (!projectId) {
return;
}
}
try {
const result = await dispatch(
mmiActions.getComplianceHistoricalReportsByAddress(address, projectId),
);
dispatch(
mmiActions.syncReportsInProgress({
address,
historicalReports: result.items ? result.items : [],
}),
);
dispatch(
actions.setHistoricalReports({
address,
reports: result.items
? result.items.filter((report) => report.status !== 'inProgress')
: [],
}),
);
} catch (error) {
console.error(error);
captureException(error);
}
};
};
export function generateComplianceReport(address) {
return (dispatch, _getState) => {
const mmiActions = mmiActionsFactory();
dispatch(mmiActions.generateComplianceReport(address));
};
}
const { setHistoricalReports } = actions;
export { setHistoricalReports };

View File

@ -0,0 +1,70 @@
import InstitutionalReducer, {
fetchHistoricalReports,
getComplianceClientId,
getComplianceProjectId,
getComplianceTenantSubdomain,
getComplianceHistoricalReports,
getComplianceReportsInProgress,
getInstitutionalConnectRequests,
complianceActivated,
getComplianceReportsInProgressByAddress,
generateComplianceReport,
} from './institutional';
const mockSyncReportsInProgress = jest.fn();
const mockGenerateComplianceReport = jest.fn();
jest.mock('../../store/institutional/institution-background', () => ({
mmiActionsFactory: () => ({
generateComplianceReport: mockGenerateComplianceReport,
getComplianceHistoricalReportsByAddress: jest.fn(),
syncReportsInProgress: mockSyncReportsInProgress,
}),
}));
describe('Institutional Duck', () => {
const initState = {
historicalReports: {},
complianceProjectId: '',
complianceClientId: '',
reportsInProgress: {},
};
describe('InstitutionalReducer', () => {
it('should initialize state', () => {
expect(InstitutionalReducer(undefined, {})).toStrictEqual(initState);
});
it('should correctly return all getters values', async () => {
const state = {
metamask: {
institutionalFeatures: {
complianceProjectId: 'complianceProjectId',
complianceClientId: 'complianceClientId',
complianceTenantSubdomain: 'subdomain',
reportsInProgress: { id: [{ reportId: 'id' }] },
connectRequests: [{ id: 'id' }],
historicalReports: { id: [{ reportId: 'id' }] },
},
},
};
expect(getComplianceProjectId(state)).toBe('complianceProjectId');
expect(getComplianceClientId(state)).toBe('complianceClientId');
expect(getComplianceTenantSubdomain(state)).toBe('subdomain');
expect(getComplianceHistoricalReports(state).id[0].reportId).toBe('id');
expect(getComplianceReportsInProgress(state).id).toHaveLength(1);
expect(getInstitutionalConnectRequests(state)).toHaveLength(1);
expect(complianceActivated(state)).toBe(true);
expect(getComplianceReportsInProgressByAddress('id')(state)).toHaveLength(
1,
);
await fetchHistoricalReports('0xAddress', 'projectId')(
jest.fn().mockReturnValue({ items: [{ status: 'test' }] }),
() => state,
);
expect(mockSyncReportsInProgress).toHaveBeenCalled();
await generateComplianceReport('0xAddress')(jest.fn(), () => state);
expect(mockGenerateComplianceReport).toHaveBeenCalled();
});
});
});

View File

@ -62,3 +62,7 @@ export function getIsCustodianSupportedChain(state) {
)
: true;
}
export function getInteractiveReplacementToken(state) {
return state.metamask.interactiveReplacementToken || {};
}

View File

@ -46,7 +46,10 @@ import {
ALLOWED_DEV_SWAPS_CHAIN_IDS,
} from '../../shared/constants/swaps';
import { ALLOWED_BRIDGE_CHAIN_IDS } from '../../shared/constants/bridge';
import {
ALLOWED_BRIDGE_CHAIN_IDS,
ALLOWED_BRIDGE_TOKEN_ADDRESSES,
} from '../../shared/constants/bridge';
import {
shortenAddress,
@ -747,6 +750,15 @@ export function getIsBridgeChain(state) {
return ALLOWED_BRIDGE_CHAIN_IDS.includes(chainId);
}
export const getIsBridgeToken = (tokenAddress) => (state) => {
const chainId = getCurrentChainId(state);
const isBridgeChain = getIsBridgeChain(state);
return (
isBridgeChain &&
ALLOWED_BRIDGE_TOKEN_ADDRESSES[chainId].includes(tokenAddress.toLowerCase())
);
};
export function getIsBuyableChain(state) {
const chainId = getCurrentChainId(state);
return Object.keys(BUYABLE_CHAINS_MAP).includes(chainId);

View File

@ -450,4 +450,23 @@ describe('Selectors', () => {
const isFantomSupported = selectors.getIsBridgeChain(mockState);
expect(isFantomSupported).toBeFalsy();
});
it('#getIsBridgeToken', () => {
mockState.metamask.provider.chainId = '0xa';
const isOptimismTokenSupported = selectors.getIsBridgeToken(
'0x94B008aa00579c1307b0ef2c499ad98a8ce58e58',
)(mockState);
expect(isOptimismTokenSupported).toBeTruthy();
const isOptimismUnknownTokenSupported = selectors.getIsBridgeToken(
'0x94B008aa00579c1307b0ef2c499ad98a8ce58e60',
)(mockState);
expect(isOptimismUnknownTokenSupported).toBeFalsy();
mockState.metamask.provider.chainId = '0xfa';
const isFantomTokenSupported = selectors.getIsBridgeToken(
'0x94B008aa00579c1307b0ef2c499ad98a8ce58e58',
)(mockState);
expect(isFantomTokenSupported).toBeFalsy();
});
});

View File

@ -3615,35 +3615,37 @@ __metadata:
languageName: node
linkType: hard
"@metamask/assets-controllers@npm:^5.0.0":
version: 5.0.1
resolution: "@metamask/assets-controllers@npm:5.0.1"
"@metamask/assets-controllers@npm:^6.0.0":
version: 6.0.0
resolution: "@metamask/assets-controllers@npm:6.0.0"
dependencies:
"@ethersproject/bignumber": ^5.7.0
"@ethersproject/contracts": ^5.7.0
"@ethersproject/providers": ^5.7.0
"@metamask/abi-utils": ^1.1.0
"@metamask/approval-controller": ^2.1.0
"@metamask/base-controller": ^2.0.0
"@metamask/contract-metadata": ^2.3.1
"@metamask/controller-utils": ^3.1.0
"@metamask/controller-utils": ^3.2.0
"@metamask/metamask-eth-abis": 3.0.0
"@metamask/network-controller": ^6.0.0
"@metamask/network-controller": ^7.0.0
"@metamask/preferences-controller": ^3.0.0
"@metamask/utils": ^3.3.1
"@metamask/utils": ^5.0.1
"@types/uuid": ^8.3.0
abort-controller: ^3.0.0
async-mutex: ^0.2.6
babel-runtime: ^6.26.0
eth-query: ^2.1.2
eth-rpc-errors: ^4.0.0
eth-rpc-errors: ^4.0.2
ethereumjs-util: ^7.0.10
immer: ^9.0.6
multiformats: ^9.5.2
single-call-balance-checker-abi: ^1.0.0
uuid: ^8.3.2
peerDependencies:
"@metamask/network-controller": ^6.0.0
checksum: aa2ab83752c121fe410f191660c4b57be9cc74cbe462e8c35b86077160c8d6640ccf19d0fa423c8123803e00ba6d7c9112d68b9e058c0fbac1df6210ad3be2b7
"@metamask/approval-controller": ^2.1.0
"@metamask/network-controller": ^7.0.0
checksum: 79c56421567b48b24deab2410645ae60022954ad590e6d91c3770fbeaf24ea3e92608935ae15696349a51bfc5d4337b22cc21f48b0d09bbf4e2f1a0c4aa96904
languageName: node
linkType: hard
@ -3723,18 +3725,18 @@ __metadata:
languageName: node
linkType: hard
"@metamask/controller-utils@npm:^3.0.0, @metamask/controller-utils@npm:^3.1.0":
version: 3.1.0
resolution: "@metamask/controller-utils@npm:3.1.0"
"@metamask/controller-utils@npm:^3.0.0, @metamask/controller-utils@npm:^3.1.0, @metamask/controller-utils@npm:^3.2.0":
version: 3.2.0
resolution: "@metamask/controller-utils@npm:3.2.0"
dependencies:
"@metamask/utils": ^3.3.1
"@metamask/utils": ^5.0.1
"@spruceid/siwe-parser": 1.1.3
eth-ens-namehash: ^2.0.8
eth-rpc-errors: ^4.0.0
eth-rpc-errors: ^4.0.2
ethereumjs-util: ^7.0.10
ethjs-unit: ^0.1.6
fast-deep-equal: ^3.1.3
checksum: 811fc4b9da98ca406a0e002c87933687e745d20f802305bb2af0affcdad454189c705caae9389444da1f5f88f2d10269b3ae8354aa1f600a11ddb9315cfa5718
checksum: 06b27f9273719ca6eb556c032b77e9066c8d38ad4ff081896a68046e1e4764482f244bf849d51fc622f425e54c9063cc697abdb0cb2f2aaab9a0d8807f2310f3
languageName: node
linkType: hard
@ -4079,6 +4081,25 @@ __metadata:
languageName: node
linkType: hard
"@metamask/network-controller@npm:^7.0.0":
version: 7.0.0
resolution: "@metamask/network-controller@npm:7.0.0"
dependencies:
"@metamask/base-controller": ^2.0.0
"@metamask/controller-utils": ^3.1.0
"@metamask/swappable-obj-proxy": ^2.1.0
"@metamask/utils": ^3.3.1
async-mutex: ^0.2.6
babel-runtime: ^6.26.0
eth-json-rpc-infura: ^5.1.0
eth-query: ^2.1.2
immer: ^9.0.6
uuid: ^8.3.2
web3-provider-engine: ^16.0.3
checksum: 62eeb223f164db1cb5914070b9ec799ee980c0723e9e154afaab3a3032d5ac133c7d1e1cbc506fd9083254f1888a7f7e99137278a107c9b9d854cb9c629b1713
languageName: node
linkType: hard
"@metamask/notification-controller@npm:^2.0.0":
version: 2.0.0
resolution: "@metamask/notification-controller@npm:2.0.0"
@ -4452,16 +4473,16 @@ __metadata:
languageName: node
linkType: hard
"@metamask/utils@npm:^5.0.0":
version: 5.0.0
resolution: "@metamask/utils@npm:5.0.0"
"@metamask/utils@npm:^5.0.0, @metamask/utils@npm:^5.0.1":
version: 5.0.1
resolution: "@metamask/utils@npm:5.0.1"
dependencies:
"@ethereumjs/tx": ^4.1.1
"@types/debug": ^4.1.7
debug: ^4.3.4
semver: ^7.3.8
superstruct: ^1.0.3
checksum: 34e39fc0bf28db5fe92676753de3291b05a517f8c81dbe332a4b6002739a58450a89fb2bddd85922a4f420affb1674604e6ad4627cdf052459e5371361ef7dd2
checksum: 29745bd3d2db06bf66263bdec04e93a8f417c46c69d8054149c0046ed54b5f13d26d94a998fff1a31b5a8e7a2200935bfc8392a5fa3c4261e3cecd3ccdd9ddc0
languageName: node
linkType: hard
@ -24148,11 +24169,11 @@ __metadata:
"@metamask/address-book-controller": ^2.0.0
"@metamask/announcement-controller": ^3.0.0
"@metamask/approval-controller": ^2.1.0
"@metamask/assets-controllers": ^5.0.0
"@metamask/assets-controllers": ^6.0.0
"@metamask/auto-changelog": ^2.1.0
"@metamask/base-controller": ^2.0.0
"@metamask/contract-metadata": ^2.3.1
"@metamask/controller-utils": ^3.1.0
"@metamask/controller-utils": ^3.2.0
"@metamask/design-tokens": ^1.9.0
"@metamask/desktop": ^0.3.0
"@metamask/eslint-config": ^9.0.0
@ -34395,14 +34416,14 @@ __metadata:
linkType: hard
"vm2@npm:^3.9.3":
version: 3.9.16
resolution: "vm2@npm:3.9.16"
version: 3.9.17
resolution: "vm2@npm:3.9.17"
dependencies:
acorn: ^8.7.0
acorn-walk: ^8.2.0
bin:
vm2: bin/vm2
checksum: 646b45dca721acb3c8e4ae0742129f13612972387911c2475f3c06ac2b4232000cab0bdaaa65d97d6ea8dc70880e039542618b1b3d04adea79cd94803cbc4ab3
checksum: 9a03740a40ab2be5e3348a95fb31512da1a3c85318febb07e5299fa103ff05bcd7b6f458211fa38a1281dc27beccd04ff90355fc1d34fe2ee6ca10d0bb8c6f35
languageName: node
linkType: hard