Skip to content

Commit

Permalink
Add JSON Transaction submitter (#339)
Browse files Browse the repository at this point in the history
* Add Advanced Mode permission

* Add BuildTransaction view

* Improve JSON editor UI

* Add Beautify button

* Add cypress tests

* Handle offline mode

* Fix error display

* Rename into 'Submit Raw Transaction'
  • Loading branch information
ThibautBremand authored Dec 7, 2023
1 parent a463286 commit 57a9933
Show file tree
Hide file tree
Showing 17 changed files with 566 additions and 67 deletions.
124 changes: 124 additions & 0 deletions packages/extension/cypress/e2e/submit_raw_json_tx.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { Chain, XRPLNetwork } from '@gemwallet/constants';

import { navigate } from '../utils/navigation';

// deepcode ignore NoHardcodedPasswords: password used for testing purposes
const SUBMIT_RAW_TRANSACTION_PATH = 'http://localhost:3000/build-transaction?transaction=buildRaw';
const PASSWORD = 'SECRET_PASSWORD';

beforeEach(() => {
// Mock the localStorage with a wallet already loaded
cy.window().then((win) => {
win.localStorage.setItem(
'wallets',
'U2FsdGVkX19VA07d7tVhAAtUbt+YVbw0xQY7OZMykOW4YI4nRZK9iZ7LT3+xHvrj4kwlPKEcRg0S1GjbIWSFaMzg3Mw8fklZrZLL9QZvnbF821SeDB5lBBj/F9PBg8A07uZhYz1p4sTDsWAOFvrnKJjmlWIqXzN5MFFbWBb3os2xGtAGTslFVUXuTp6eM9X9'
);
win.localStorage.setItem(
'network',
JSON.stringify({
chain: Chain.XRPL,
name: XRPLNetwork.TESTNET
})
);
});
});

describe('JSON Transaction', () => {
it('Sign JSON Transaction', () => {
navigate(SUBMIT_RAW_TRANSACTION_PATH, PASSWORD);

const rawTx = `{
"TransactionType": "Payment",
"Destination": "rhikRdkFw28csKw9z7fVoBjWncz1HSoQij",
"Amount": "100000",
"Memos": [
{
"Memo": {
"MemoData": "54657374206D656D6F",
"MemoType": "4465736372697074696F6E"
}
}
]
}`;

cy.get('.json-editor').type(rawTx);

// Type extra character to trigger validation
cy.get('.json-editor').type('a');

// Click on 'Sign' button
cy.get('button').contains('Sign').click();

// 'Invalid JSON' error should be displayed
cy.contains('Invalid JSON');

// Remove extra character
cy.get('.json-editor').type('{backspace}');

// Click on 'Sign' button
cy.get('button').contains('Sign').click();

// 'Sign Transaction' modal should be displayed
cy.contains('Sign Transaction');
cy.contains('Amount').next().should('have.text', '0.1 XRP');

// Click on 'Sign' button
cy.get('button').contains('Sign').click();

cy.get('h1[data-testid="transaction-title"]').contains('Transaction accepted', {
timeout: 10000
});
});

it('Submit JSON Transaction', () => {
navigate(SUBMIT_RAW_TRANSACTION_PATH, PASSWORD);

const rawTx = `{
"TransactionType": "Payment",
"Destination": "rhikRdkFw28csKw9z7fVoBjWncz1HSoQij",
"Amount": "100000",
"Memos": [
{
"Memo": {
"MemoData": "54657374206D656D6F",
"MemoType": "4465736372697074696F6E"
}
}
]
}`;

cy.get('.json-editor').type(rawTx);

// Type extra character to trigger validation
cy.get('.json-editor').type('a');

// Click on 'Sign' button
cy.get('button').contains('Sign').click();

// 'Invalid JSON' error should be displayed
cy.contains('Invalid JSON');

// Remove extra character
cy.get('.json-editor').type('{backspace}');

// Click on 'Submit' button
cy.get('button').contains('Submit').click();

// 'Submit Transaction' modal should be displayed
cy.contains('Submit Transaction');
cy.contains('Amount').next().should('have.text', '0.1 XRP');

// Click on 'Submit' button
cy.get('button').contains('Submit').click();

cy.get('h1[data-testid="transaction-title"]').should('have.text', 'Transaction in progress');
cy.get('p[data-testid="transaction-subtitle"]').should(
'have.text',
'We are processing your transactionPlease wait'
);

cy.get('h1[data-testid="transaction-title"]').contains('Transaction accepted', {
timeout: 10000
});
});
});
3 changes: 3 additions & 0 deletions packages/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"crypto-js": "^4.1.1",
"lottie-react": "^2.1.0",
"moment": "^2.29.4",
"prismjs": "^1.29.0",
"qrcode.react": "^3.1.0",
"react": "^17.0.2",
"react-content-loader": "^6.2.0",
Expand All @@ -25,6 +26,7 @@
"react-lazy-load-image-component": "^1.6.0",
"react-router-dom": "6",
"react-scripts": "4.0.3",
"react-simple-code-editor": "^0.13.1",
"ripple-keypairs": "^1.3.1",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1",
Expand All @@ -42,6 +44,7 @@
"@types/crypto-js": "^4.1.0",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/prismjs": "^1.26.3",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-lazy-load-image-component": "^1.5.2",
Expand Down
4 changes: 4 additions & 0 deletions packages/extension/src/components/pages/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
PARAMETER_SUBMIT_TRANSACTION,
PARAMETER_SUBMIT_TRANSACTIONS_BULK,
PARAMETER_TRANSACTION_ACCEPT_NFT_OFFER,
PARAMETER_TRANSACTION_BUILD_RAW,
PARAMETER_TRANSACTION_BURN_NFT,
PARAMETER_TRANSACTION_CANCEL_NFT_OFFER,
PARAMETER_TRANSACTION_CANCEL_OFFER,
Expand All @@ -49,6 +50,7 @@ import {
SHARE_NFT_PATH,
SHARE_PUBLIC_ADDRESS_PATH,
SHARE_PUBLIC_KEY_PATH,
SUBMIT_RAW_TRANSACTION_PATH,
SIGN_MESSAGE_PATH,
SIGN_TRANSACTION_PATH,
STORAGE_WALLETS,
Expand Down Expand Up @@ -116,6 +118,8 @@ export const Login: FC = () => {
navigate(`${CREATE_OFFER_PATH}${search}`);
} else if (search.includes(PARAMETER_TRANSACTION_CANCEL_OFFER)) {
navigate(`${CANCEL_OFFER_PATH}${search}`);
} else if (search.includes(PARAMETER_TRANSACTION_BUILD_RAW)) {
navigate(`${SUBMIT_RAW_TRANSACTION_PATH}${search}`);
} else {
navigate(`${HOME_PATH}${search}`);
}
Expand Down
54 changes: 34 additions & 20 deletions packages/extension/src/components/pages/Permissions/Permissions.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { FC, useCallback, useState, useEffect } from 'react';

import { Switch, Typography } from '@mui/material';
import { Typography } from '@mui/material';
import { useNavigate } from 'react-router-dom';

import { SETTINGS_PATH, STORAGE_PERMISSION_SUBMIT_BULK } from '../../../constants';
import {
SETTINGS_PATH,
STORAGE_PERMISSION_ADVANCED_MODE,
STORAGE_PERMISSION_SUBMIT_BULK
} from '../../../constants';
import { loadFromChromeLocalStorage, saveInChromeLocalStorage } from '../../../utils';
import { PageWithReturn } from '../../templates';
import { PermissionSwitch } from './PermissionsSwitch';

export const Permissions: FC = () => {
const navigate = useNavigate();
const [isSubmitBulkTransactionsEnabled, setIsSubmitBulkTransactionsEnabled] =
useState<boolean>(false);
const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useState<boolean>(false);

useEffect(() => {
const loadInitialData = async () => {
const storedData = await loadFromChromeLocalStorage(STORAGE_PERMISSION_SUBMIT_BULK);
if (!storedData) return;
const storedDataSubmitBulk = await loadFromChromeLocalStorage(STORAGE_PERMISSION_SUBMIT_BULK);
setIsSubmitBulkTransactionsEnabled(storedDataSubmitBulk === 'true');

setIsSubmitBulkTransactionsEnabled(storedData === 'true');
const storedDataAdvancedMode = await loadFromChromeLocalStorage(
STORAGE_PERMISSION_ADVANCED_MODE
);
setIsAdvancedModeEnabled(storedDataAdvancedMode === 'true');
};

loadInitialData();
Expand All @@ -30,6 +39,10 @@ export const Permissions: FC = () => {
);
}, [isSubmitBulkTransactionsEnabled]);

useEffect(() => {
saveInChromeLocalStorage(STORAGE_PERMISSION_ADVANCED_MODE, isAdvancedModeEnabled.toString());
}, [isAdvancedModeEnabled]);

const handleBack = useCallback(() => {
navigate(SETTINGS_PATH);
}, [navigate]);
Expand All @@ -38,25 +51,26 @@ export const Permissions: FC = () => {
setIsSubmitBulkTransactionsEnabled(!isSubmitBulkTransactionsEnabled);
};

const toggleAdvancedMode = () => {
setIsAdvancedModeEnabled(!isAdvancedModeEnabled);
};

return (
<PageWithReturn title="Permissions" onBackClick={handleBack}>
<div style={{ marginTop: '1rem' }}>
<Typography variant="subtitle2">Permissions</Typography>
<div style={{ marginTop: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Switch
checked={isSubmitBulkTransactionsEnabled}
onChange={toggleSubmitBulkTransactions}
color="primary"
inputProps={{ 'aria-label': 'Enable or disable bulk transactions' }}
/>
<Typography style={{ marginLeft: '1rem' }}>Submit Bulk Transactions</Typography>
</div>
<Typography variant="body2" color="textSecondary" style={{ marginLeft: '0.7rem' }}>
Enabling this will allow to submit multiple transactions at once. Enable at your own
risk.
</Typography>
</div>
<PermissionSwitch
isEnabled={isAdvancedModeEnabled}
toggleSwitch={toggleAdvancedMode}
name="Advanced Mode"
description="Unlocks the advanced features."
/>
<PermissionSwitch
isEnabled={isSubmitBulkTransactionsEnabled}
toggleSwitch={toggleSubmitBulkTransactions}
name="Bulk Transactions"
description="Enabling this will allow to submit multiple transactions at once. Enable at your own risk."
/>
</div>
</PageWithReturn>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { FC } from 'react';

import { Switch, Typography } from '@mui/material';

export interface PermissionSwitchProps {
isEnabled: boolean;
toggleSwitch: () => void;
name: string;
description: string;
}

export const PermissionSwitch: FC<PermissionSwitchProps> = ({
isEnabled,
toggleSwitch,
name,
description
}) => {
return (
<div style={{ marginTop: '0.5rem' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Switch
checked={isEnabled}
onChange={toggleSwitch}
color="primary"
inputProps={{ 'aria-label': `Enable or disable ${name.toLowerCase()}` }}
/>
<Typography style={{ marginLeft: '1rem' }}>{name}</Typography>
</div>
<Typography variant="body2" color="textSecondary" style={{ marginLeft: '0.7rem' }}>
{description}
</Typography>
</div>
);
};
33 changes: 31 additions & 2 deletions packages/extension/src/components/pages/Settings/Settings.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, useCallback, useMemo } from 'react';
import { FC, useCallback, useEffect, useMemo, useState } from 'react';

import LockIcon from '@mui/icons-material/Lock';
import { Button } from '@mui/material';
Expand All @@ -12,17 +12,32 @@ import {
NAV_MENU_HEIGHT,
PERMISSIONS_PATH,
RESET_PASSWORD_PATH,
STORAGE_PERMISSION_ADVANCED_MODE,
SUBMIT_RAW_TRANSACTION_PATH,
TRUSTED_APPS_PATH
} from '../../../constants';
import { useWallet } from '../../../contexts';
import { openExternalLink } from '../../../utils';
import { loadFromChromeLocalStorage, openExternalLink } from '../../../utils';
import { PageWithHeader } from '../../templates';
import { ItemMenuGroup, MenuGroup } from './MenuGroup';

export const Settings: FC = () => {
const navigate = useNavigate();
const { signOut } = useWallet();

const [advancedModeEnabled, setAdvancedModeEnabled] = useState<boolean>(false);

useEffect(() => {
const loadInitialData = async () => {
const storedData = await loadFromChromeLocalStorage(STORAGE_PERMISSION_ADVANCED_MODE);
if (!storedData) return;

setAdvancedModeEnabled(storedData === 'true');
};

loadInitialData();
}, []);

const handleLock = useCallback(() => {
signOut();
}, [signOut]);
Expand Down Expand Up @@ -64,6 +79,17 @@ export const Settings: FC = () => {
[navigate]
);

const advancedItems = useMemo<ItemMenuGroup[]>(
() => [
{
name: 'Submit Raw Transaction',
type: 'button',
onClick: () => navigate(SUBMIT_RAW_TRANSACTION_PATH)
}
],
[navigate]
);

const dangerZoneItems = useMemo<ItemMenuGroup[]>(
() => [
{
Expand Down Expand Up @@ -93,6 +119,9 @@ export const Settings: FC = () => {
<div style={{ paddingBottom: '0.75rem' }}>
<MenuGroup sectionName={'Account settings'} items={accountParamsItems} />
<MenuGroup sectionName={'Informations'} items={infoItems} />
{advancedModeEnabled ? (
<MenuGroup sectionName={'Advanced'} items={advancedItems} />
) : null}
<MenuGroup sectionName={'Danger zone'} items={dangerZoneItems} />
<Button
variant="contained"
Expand Down
Loading

0 comments on commit 57a9933

Please sign in to comment.