diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000..a9aa20d2b9 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +Jenkinsfile @morgsmccauley @andy-haynes diff --git a/Jenkinsfile b/Jenkinsfile index 3b325b4185..e57a00041c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -11,9 +11,9 @@ pipeline { AWS_CREDENTIALS = 'aws-credentials-password' AWS_REGION = 'us-west-2' TESTNET_AWS_ROLE = credentials('testnet-assumed-role') - TESTNET_AWS_ROLE_ACCOUNT = credentials('testnet-assumed-role-account') + TESTNET_AWS_ACCOUNT_ID = credentials('testnet-mnw-account-id') MAINNET_AWS_ROLE = credentials('mainnet-assumed-role') - MAINNET_AWS_ROLE_ACCOUNT = credentials('mainnet-assumed-role-account') + MAINNET_AWS_ACCOUNT_ID = credentials('mainnet-mnw-account-id') // s3 buckets TESTNET_PR_PREVIEW_STATIC_SITE_BUCKET = credentials('testnet-pr-previews-static-website') @@ -47,6 +47,7 @@ pipeline { steps { dir("$WORKSPACE/packages/frontend") { sh 'yarn install --frozen-lockfile' + sh 'yarn lint' } } } @@ -62,10 +63,13 @@ pipeline { stage('packages:test') { failFast true + when { + expression { env.BUILD_FRONTEND == 'true' } + } parallel { stage('frontend:prebuild:testnet-staging') { when { - expression { env.BUILD_FRONTEND == 'true' } + not { branch 'stable' } } environment { NEAR_WALLET_ENV = 'testnet_STAGING' @@ -79,7 +83,7 @@ pipeline { stage('frontend:prebuild:testnet') { when { - expression { env.BUILD_FRONTEND == 'true' } + not { branch 'stable' } } environment { NEAR_WALLET_ENV = 'testnet' @@ -93,7 +97,7 @@ pipeline { stage('frontend:prebuild:mainnet-staging') { when { - expression { env.BUILD_FRONTEND == 'true' } + not { branch 'stable' } } environment { NEAR_WALLET_ENV = 'mainnet_STAGING' @@ -106,9 +110,6 @@ pipeline { } stage('frontend:prebuild:mainnet') { - when { - expression { env.BUILD_FRONTEND == 'true' } - } environment { NEAR_WALLET_ENV = 'mainnet' } @@ -137,8 +138,7 @@ pipeline { // build end-to-end testing package stage('e2e-tests') { when { - expression { env.BUILD_E2E == 'true' }; - anyOf { branch 'master' ; branch 'stable' } + expression { env.BUILD_E2E == 'true' } } stages { stage('e2e-tests:build') { @@ -155,7 +155,8 @@ pipeline { // build frontend bundles stage('frontend:bundle:testnet-staging') { when { - expression { env.BUILD_FRONTEND == 'true' } + expression { env.BUILD_FRONTEND == 'true' }; + not { branch 'stable' } } environment { NEAR_WALLET_ENV = 'testnet_STAGING' @@ -170,7 +171,8 @@ pipeline { stage('frontend:bundle:testnet') { when { - expression { env.BUILD_FRONTEND == 'true' } + expression { env.BUILD_FRONTEND == 'true' }; + branch 'master' } environment { NEAR_WALLET_ENV = 'testnet' @@ -185,7 +187,8 @@ pipeline { stage('frontend:bundle:mainnet-staging') { when { - expression { env.BUILD_FRONTEND == 'true' } + expression { env.BUILD_FRONTEND == 'true' }; + not { branch 'stable' } } environment { NEAR_WALLET_ENV = 'mainnet_STAGING' @@ -200,7 +203,8 @@ pipeline { stage('frontend:bundle:mainnet') { when { - expression { env.BUILD_FRONTEND == 'true' } + expression { env.BUILD_FRONTEND == 'true' }; + branch 'stable' } environment { NEAR_WALLET_ENV = 'mainnet' @@ -243,13 +247,13 @@ pipeline { region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, role: env.TESTNET_AWS_ROLE, - roleAccount: env.TESTNET_AWS_ROLE_ACCOUNT + roleAccount: env.TESTNET_AWS_ACCOUNT_ID ) { s3Upload( bucket: "$TESTNET_PR_PREVIEW_STATIC_SITE_BUCKET/$CHANGE_ID", includePathPattern: "*", path: '', - workingDir: env.FRONTEND_TESTNET_BUNDLE_PATH + workingDir: env.FRONTEND_TESTNET_STAGING_BUNDLE_PATH ) } } @@ -265,13 +269,13 @@ pipeline { region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, role: env.MAINNET_AWS_ROLE, - roleAccount: env.MAINNET_AWS_ROLE_ACCOUNT + roleAccount: env.MAINNET_AWS_ACCOUNT_ID ) { s3Upload( bucket: "$MAINNET_PR_PREVIEW_STATIC_SITE_BUCKET/$CHANGE_ID", includePathPattern: "*", path: '', - workingDir: env.FRONTEND_MAINNET_BUNDLE_PATH + workingDir: env.FRONTEND_MAINNET_STAGING_BUNDLE_PATH ) } } @@ -295,7 +299,7 @@ pipeline { region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, role: env.TESTNET_AWS_ROLE, - roleAccount: env.TESTNET_AWS_ROLE_ACCOUNT + roleAccount: env.TESTNET_AWS_ACCOUNT_ID ) { s3Upload( bucket: env.TESTNET_STAGING_STATIC_SITE_BUCKET, @@ -313,13 +317,11 @@ pipeline { } steps { milestone(403) - input(message: 'Deploy to testnet?') - milestone(404) withAWS( region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, role: env.TESTNET_AWS_ROLE, - roleAccount: env.TESTNET_AWS_ROLE_ACCOUNT + roleAccount: env.TESTNET_AWS_ACCOUNT_ID ) { s3Upload( bucket: env.TESTNET_STATIC_SITE_BUCKET, @@ -333,15 +335,15 @@ pipeline { stage('frontend:deploy:mainnet-staging') { when { - branch 'stable' + branch 'master' } steps { - milestone(405) + milestone(404) withAWS( region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, role: env.MAINNET_AWS_ROLE, - roleAccount: env.MAINNET_AWS_ROLE_ACCOUNT + roleAccount: env.MAINNET_AWS_ACCOUNT_ID ) { s3Upload( bucket: env.MAINNET_STAGING_STATIC_SITE_BUCKET, @@ -358,14 +360,14 @@ pipeline { branch 'stable' } steps { - milestone(406) + milestone(405) input(message: 'Deploy to mainnet?') - milestone(407) + milestone(406) withAWS( region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, role: env.MAINNET_AWS_ROLE, - roleAccount: env.MAINNET_AWS_ROLE_ACCOUNT + roleAccount: env.MAINNET_AWS_ACCOUNT_ID ) { s3Upload( bucket: env.MAINNET_STATIC_SITE_BUCKET, diff --git a/packages/frontend/ci/ParcelBundler.js b/packages/frontend/ci/ParcelBundler.js index bd9788a091..98f411f73b 100644 --- a/packages/frontend/ci/ParcelBundler.js +++ b/packages/frontend/ci/ParcelBundler.js @@ -191,6 +191,7 @@ class ParcelBundler { this.bundler.on('bundled', (bundle) => { fs.copyFileSync(this.buildWasmSourcePath('multisig.wasm'), this.buildOutputPath('multisig.wasm')); fs.copyFileSync(this.buildWasmSourcePath('main.wasm'), this.buildOutputPath('main.wasm')); + fs.copyFileSync(this.buildWasmSourcePath('state_cleanup.wasm'), this.buildOutputPath('state_cleanup.wasm')); }); return this.bundler; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 30337d790e..e1d9b6c3b7 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -35,7 +35,7 @@ "lodash.unset": "^4.5.2", "lodash.update": "^4.10.2", "mixpanel-browser": "^2.41.0", - "near-api-js": "^0.43.1", + "near-api-js": "^0.45.1", "near-ledger-js": "^0.2.1", "near-seed-phrase": "^0.2.0", "node-cache": "^5.1.2", @@ -65,7 +65,7 @@ }, "scripts": { "predeploy": "yarn build", - "update:static": "cp src/wasm/multisig.wasm dist/multisig.wasm && cp src/wasm/main.wasm dist/main.wasm", + "update:static": "cp src/wasm/multisig.wasm dist/multisig.wasm && cp src/wasm/main.wasm dist/main.wasm && cp src/wasm/state_cleanup.wasm dist/state_cleanup.wasm", "start": "yarn update:static; parcel src/index.html --https --cert devServerCertificates/primary.crt --key devServerCertificates/private.pem", "prebuild": "rm -rf dist/", "build": "yarn run bundle && yarn run sentry", @@ -76,8 +76,8 @@ "fix": "eslint --ext .js --ext .jsx . --fix", "storybook": "start-storybook -p 6006", "build-storybook": "build-storybook", - "prepush": "yarn run test", - "precommit": "concurrently \"yarn run fix\" \".githooks/format-json\"", + "prepush": "concurrently \"yarn run test\" \"yarn lint\"", + "precommit": ".githooks/format-json", "preinstall": "npx only-allow yarn" }, "browserslist": [ diff --git a/packages/frontend/src/components/Routing.js b/packages/frontend/src/components/Routing.js index 6b81711995..054e7b2fd4 100644 --- a/packages/frontend/src/components/Routing.js +++ b/packages/frontend/src/components/Routing.js @@ -56,6 +56,7 @@ import { import AccessKeysWrapper from './access-keys/v2/AccessKeysWrapper'; import { AutoImportWrapper } from './accounts/auto_import/AutoImportWrapper'; import BatchImportAccounts from './accounts/batch_import_accounts'; +import BatchLedgerExport from './accounts/batch_ledger_export'; import { ExistingAccountWrapper } from './accounts/create/existing_account/ExistingAccountWrapper'; import { InitialDepositWrapper } from './accounts/create/initial_deposit/InitialDepositWrapper'; import { CreateAccountLanding } from './accounts/create/landing/CreateAccountLanding'; @@ -579,6 +580,11 @@ class Routing extends Component { }, {}); return this.props.history.replace('/')}/>; })} /> + { +const BatchImportAccountsSuccessScreen = ({ accounts = [], customTitleId }) => { const dispatch = useDispatch(); const accountUrlReferrer = useSelector(selectAccountUrlReferrer); @@ -36,7 +36,7 @@ const BatchImportAccountsSuccessScreen = ({ accounts = [] }) => {

- + {accountUrlReferrer || }


diff --git a/packages/frontend/src/components/accounts/batch_import_accounts/sequentialAccountImportReducer.js b/packages/frontend/src/components/accounts/batch_import_accounts/sequentialAccountImportReducer.js index 1968d4e224..b09f8e1e5c 100644 --- a/packages/frontend/src/components/accounts/batch_import_accounts/sequentialAccountImportReducer.js +++ b/packages/frontend/src/components/accounts/batch_import_accounts/sequentialAccountImportReducer.js @@ -1,4 +1,4 @@ -import { differenceBy } from 'lodash'; +import { differenceBy, uniqWith, isEqual } from 'lodash'; import { IMPORT_STATUS } from '.'; @@ -6,8 +6,9 @@ import { IMPORT_STATUS } from '.'; * @typedef {{ * accountId: string, * status: "pending" | "success" | "waiting" | "error" | null , - * key: string, - * ledgerHdPath: string + * key?: string, + * ledgerHdPath?: string, + * keyType?: string * }} account * * @typedef {{accounts: account[], urlConfirmed: boolean}} state @@ -46,6 +47,13 @@ const sequentialAccountImportReducer = (state = initialState, action) => { return; } + case ACTIONS.ADD_ACCOUNTS: { + if (state.accounts.every(({ status }) => status === null)) { + state.accounts = uniqWith(state.accounts.concat(action.accounts), isEqual); + } + + return; + } case ACTIONS.BEGIN_IMPORT: { if (state.accounts.every(({ status }) => status === null)) { const [firstAccount, ...remainingAccounts] = state.accounts; diff --git a/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js b/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js new file mode 100644 index 0000000000..493ffe9e76 --- /dev/null +++ b/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js @@ -0,0 +1,124 @@ +import BN from 'bn.js'; +import { KeyPair, transactions } from 'near-api-js'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Translate } from 'react-localize-redux'; +import { useDispatch, useSelector } from 'react-redux'; + +import { actions as ledgerActions, LEDGER_HD_PATH_PREFIX, selectLedgerConnectionAvailable } from '../../../redux/slices/ledger'; +import { getEstimatedFees } from '../../../redux/slices/sign'; +import { setLedgerHdPath } from '../../../utils/localStorage'; +import WalletClass, { wallet } from '../../../utils/wallet'; +import FormButton from '../../common/FormButton'; +import FormButtonGroup from '../../common/FormButtonGroup'; +import Modal from '../../common/modal/Modal'; +import SignTransaction from '../../sign/v2/SignTransaction'; +import SignTransactionDetails from '../../sign/v2/SignTransactionDetails'; +import { HDPathSelect } from '../ledger/LedgerHdPaths'; +import { ModalContainer } from './styles'; + +const { checkAndHideLedgerModal, handleShowConnectModal } = ledgerActions; + + +const AccountExportModal = ({ account, onSuccess, onFail }) => { + const [accountBalance, setAccountBalance] = useState(null); + const [showTxDetails, setShowTxDetails] = useState(false); + const [addingKey, setAddingKey] = useState(false); + const [error, setError] = useState(false); + const [path, setPath] = useState(1); + const ledgerConnectionAvailable = useSelector(selectLedgerConnectionAvailable); + + const addFAKTransaction = { + receiverId: account.accountId, + actions: [transactions.addKey(KeyPair.fromRandom('ed25519').getPublicKey(), transactions.fullAccessKey())] + }; + + const estimatedAddFAKTransactionFees = useMemo(() => addFAKTransaction ? getEstimatedFees([addFAKTransaction]) : new BN('0') ,[addFAKTransaction]); + const dispatch = useDispatch(); + + useEffect(() => { + setAccountBalance(null); + setShowTxDetails(false); + setAddingKey(false); + setError(false); + + wallet + .getBalance(account.accountId) + .then(({ available }) => setAccountBalance(available)); + },[account]); + + const addKeyToWalletKeyStore = useCallback(async () => { + setAddingKey(true); + setError(false); + + if (!ledgerConnectionAvailable) { + setAddingKey(false); + return dispatch(handleShowConnectModal()); + } + + try { + const ledgerHdPath = `${LEDGER_HD_PATH_PREFIX}${path}'`; + if (account.keyType === WalletClass.KEY_TYPES.MULTISIG) { + await wallet.exportToLedgerWallet(ledgerHdPath, account.accountId); + } else { + setLedgerHdPath({ accountId: account.accountId, path: ledgerHdPath }); + await wallet.addLedgerAccessKey(ledgerHdPath, account.accountId); + } + onSuccess(); + } catch (error) { + setError(true); + setAddingKey(false); + } + dispatch(checkAndHideLedgerModal()); + }, [path, account.accountId, ledgerConnectionAvailable]); + + return ( + {}} + disableClose + > + {showTxDetails ? ( + setShowTxDetails(false)} + transactions={[addFAKTransaction]} + signGasFee={estimatedAddFAKTransactionFees.toString()} + /> + ) : ( + +

+ +

+ + + setShowTxDetails(true)}> + + + {error ?
: null} + + + + + + + + +
+ )} +
+ ); + }; + + export default AccountExportModal; diff --git a/packages/frontend/src/components/accounts/batch_ledger_export/index.js b/packages/frontend/src/components/accounts/batch_ledger_export/index.js new file mode 100644 index 0000000000..6de441d265 --- /dev/null +++ b/packages/frontend/src/components/accounts/batch_ledger_export/index.js @@ -0,0 +1,140 @@ +import { partition } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Translate } from 'react-localize-redux'; +import { useSelector } from 'react-redux'; +import { withRouter } from 'react-router-dom'; +import styled from 'styled-components'; +import { useImmerReducer } from 'use-immer'; + +import { NETWORK_ID } from '../../../config'; +import { selectAvailableAccountsIsLoading } from '../../../redux/slices/availableAccounts'; +import WalletClass, { wallet } from '../../../utils/wallet'; +import FormButton from '../../common/FormButton'; +import FormButtonGroup from '../../common/FormButtonGroup'; +import Container from '../../common/styled/Container.css'; +import LedgerImageCircle from '../../svg/LedgerImageCircle'; +import AccountListImport from '../AccountListImport'; +import { IMPORT_STATUS } from '../batch_import_accounts'; +import BatchImportAccountsSuccessScreen from '../batch_import_accounts/BatchImportAccountsSuccessScreen'; +import reducer, { ACTIONS } from '../batch_import_accounts/sequentialAccountImportReducer'; +import AccountExportModal from './AccountExportModal'; + +const CustomContainer = styled.div` + width: 100%; + margin-top: 40px; + + .buttons-bottom-buttons { + margin-top: 40px; + } + + .title { + text-align: left; + font-size: 12px; + } + + .screen-descripton { + margin-top: 40px; + margin-bottom: 56px; + } +`; + +const BatchLedgerExport = ({ history }) => { + const availableAccountsIsLoading = useSelector(selectAvailableAccountsIsLoading); + const [, setLedgerAccounts] = useState([]); + + const [state, dispatch] = useImmerReducer(reducer, { + accounts: [] + }); + + useEffect(() => { + const addAccountsToList = async () => { + const accounts = await wallet.keyStore.getAccounts(NETWORK_ID); + const getAccountWithAccessKeysAndType = async (accountId) => { + const keyType = await wallet.getAccountKeyType(accountId); + const accessKeys = await wallet.getAccessKeys(accountId); + return {accountId, accessKeys, keyType}; + }; + const accountsWithKeys = await Promise.all( + accounts.map(getAccountWithAccessKeysAndType) + ); + const [ledgerAccounts, nonLedgerAccounts] = partition( + accountsWithKeys, + ({ keyType, accessKeys }) => + keyType === WalletClass.KEY_TYPES.LEDGER || + accessKeys.some( + (accessKey) => accessKey.meta.type === 'ledger' + ) + ); + setLedgerAccounts(ledgerAccounts); + dispatch({type: ACTIONS.ADD_ACCOUNTS, accounts: nonLedgerAccounts.map(({accountId, keyType}) => ({ + accountId, + status: null, + keyType + }))}); + }; + addAccountsToList(); + },[]); + + const currentAccount = useMemo(() => state.accounts.find((account) => account.status === IMPORT_STATUS.PENDING), [state.accounts]); + const accountsApproved = useMemo(() => state.accounts.filter((account) => account.status === IMPORT_STATUS.SUCCESS), [state.accounts]); + const completed = useMemo(() => state.accounts.every((account) => account.status === IMPORT_STATUS.SUCCESS || account.status === IMPORT_STATUS.FAILED), [state.accounts]); + const showSuccessScreen = useMemo(() => completed && state.accounts.some((account) => account.status === IMPORT_STATUS.SUCCESS), [completed, state.accounts]); + + if (showSuccessScreen) { + return ; + } + + return ( + <> + + + +
+

+ +

+
+
+ +
+
+ {accountsApproved.length}/{state.accounts.length}{' '} + +
+ +
+ + history.goBack()} + className="gray-blue" + disabled={availableAccountsIsLoading} + > + + + + dispatch({ type: ACTIONS.BEGIN_IMPORT }) + } + disabled={availableAccountsIsLoading || completed} + > + + + + + + {currentAccount ? ( + + dispatch({ type: ACTIONS.SET_CURRENT_DONE }) + } + onFail={() => + dispatch({ type: ACTIONS.SET_CURRENT_FAILED }) + } + /> + ) : null} + + ); +}; + +export default withRouter(BatchLedgerExport); diff --git a/packages/frontend/src/components/accounts/batch_ledger_export/styles.js b/packages/frontend/src/components/accounts/batch_ledger_export/styles.js new file mode 100644 index 0000000000..baedb10c90 --- /dev/null +++ b/packages/frontend/src/components/accounts/batch_ledger_export/styles.js @@ -0,0 +1,65 @@ +import styled from 'styled-components'; + +import Container from '../../common/styled/Container.css'; + +export const ModalContainer = styled(Container)` + display: flex; + flex-direction: column; + align-items: center; + padding: 24px; + + h3 { + text-align: center; + } + + .desc { + text-align: left !important; + padding: 0 !important; + } + + .top-icon { + height: 60px; + width: 60px; + margin-bottom: 40px; + } + + .desc { + padding: 0 45px; + text-align: center; + margin-top: 24px; + p { + margin: 0; + } + } + + button { + align-self: stretch; + } + + .link { + margin-top: 16px !important; + font-weight: normal !important; + } + + .button-group { + align-self: stretch; + } + + .connect-with-application { + margin: 20px auto 30px auto; + } + + .transfer-amount { + width: 100%; + } + + .error-label { + margin-top: 16px; + color: #fc5b5b; + } + + .wallet-url { + color: #000; + font-weight: bold; + } +`; diff --git a/packages/frontend/src/components/accounts/create/FundWithFtx.js b/packages/frontend/src/components/accounts/create/FundWithFtx.js new file mode 100644 index 0000000000..c73ece7abe --- /dev/null +++ b/packages/frontend/src/components/accounts/create/FundWithFtx.js @@ -0,0 +1,3 @@ +export function buildFtxPayLink(accountId) { + return `https://ftx.us/pay/request?coin=NEAR&address=${accountId}&tag=&wallet=near&memoIsRequired=false&memo=&allowTip=false&fixedWidth=true`; +} diff --git a/packages/frontend/src/components/accounts/ledger/LedgerHdPaths.js b/packages/frontend/src/components/accounts/ledger/LedgerHdPaths.js index ec9f709714..8fa2328fab 100644 --- a/packages/frontend/src/components/accounts/ledger/LedgerHdPaths.js +++ b/packages/frontend/src/components/accounts/ledger/LedgerHdPaths.js @@ -108,16 +108,16 @@ const Container = styled.div` } } `; - -export default function LedgerHdPaths({ - confirmedPath, - setConfirmedPath -}) { - const [path, setPath] = useState(confirmedPath); +/** + * @param {Object} props + * @param {"import" | "export"} props.type - curently only affects copy + */ +export const HDPathSelect = ({ handleConfirmHdPath, path, setPath, type = 'import' }) => { onKeyDown((e) => { - const dropdownOpen = document.getElementById('hd-paths-dropdown').classList.contains('open'); - if (dropdownOpen) { + const dropdownElement = document.getElementById('hd-paths-dropdown'); + const dropdownOpenIfRendered = dropdownElement === null || document.getElementById('hd-paths-dropdown').classList.contains('open'); + if (dropdownOpenIfRendered) { if (e.keyCode === 38) { increment(); e.preventDefault(); @@ -138,15 +138,11 @@ export default function LedgerHdPaths({ } }; - const handleConfirmHdPath = () => { - setConfirmedPath(path); - }; - - const dropDownContent = () => { - return ( + return ( +
-
+
44 / 397 / 0 / 0
@@ -162,12 +158,24 @@ export default function LedgerHdPaths({
- - - + {handleConfirmHdPath ? ( + handleConfirmHdPath(path)} + > + + + ) : null}
- ); - }; + + ); +}; + +export default function LedgerHdPaths({ + confirmedPath, + setConfirmedPath +}) { + const [path, setPath] = useState(confirmedPath || 1); return ( @@ -175,7 +183,7 @@ export default function LedgerHdPaths({ name='hd-paths-dropdown' icon={} title={} - content={dropDownContent()} + content={} maxHeight={false} /> diff --git a/packages/frontend/src/components/buy/BuyNear.js b/packages/frontend/src/components/buy/BuyNear.js index bf265af92e..4bd7aae568 100644 --- a/packages/frontend/src/components/buy/BuyNear.js +++ b/packages/frontend/src/components/buy/BuyNear.js @@ -8,6 +8,7 @@ import { getPayMethods } from '../../config/buyNearConfig'; import { Mixpanel } from '../../mixpanel'; import { selectAccountId } from '../../redux/slices/account'; import { isMoonpayAvailable, getSignedUrl } from '../../utils/moonpay'; +import { buildFtxPayLink } from '../accounts/create/FundWithFtx'; import { buildUtorgPayLink } from '../accounts/create/FundWithUtorg'; import FormButton from '../common/FormButton'; import ArrowIcon from '../svg/ArrowIcon'; @@ -180,6 +181,7 @@ export function BuyNear({ match, location, history }) { const [moonPayAvailable, setMoonPayAvailable] = useState(null); const [signedMoonPayUrl, setSignedMoonPayUrl] = useState(null); const [utorgPayUrl, setUtorgPayUrl] = useState(null); + const [ftxPayUrl, setFtxPayUrl] = useState(null); useEffect(() => { if (!accountId) { @@ -187,11 +189,12 @@ export function BuyNear({ match, location, history }) { } setUtorgPayUrl(buildUtorgPayLink(accountId)); + setFtxPayUrl(buildFtxPayLink(accountId)); checkMoonPay(); }, [accountId]); const PayMethods = useMemo( - () => getPayMethods({ accountId, moonPayAvailable, signedMoonPayUrl, utorgPayUrl }), + () => getPayMethods({ accountId, moonPayAvailable, signedMoonPayUrl, utorgPayUrl, ftxPayUrl }), [accountId, moonPayAvailable, signedMoonPayUrl, utorgPayUrl] ); @@ -223,7 +226,7 @@ export function BuyNear({ match, location, history }) { + + + + + + diff --git a/packages/frontend/src/components/common/Footer.js b/packages/frontend/src/components/common/Footer.js index 9529a79fab..a406209653 100644 --- a/packages/frontend/src/components/common/Footer.js +++ b/packages/frontend/src/components/common/Footer.js @@ -133,7 +133,7 @@ const Footer = () => {

Mixpanel.track('Footer Click Join Community')} diff --git a/packages/frontend/src/components/nft/NFTDetail.js b/packages/frontend/src/components/nft/NFTDetail.js index c65e6b7090..df558484af 100644 --- a/packages/frontend/src/components/nft/NFTDetail.js +++ b/packages/frontend/src/components/nft/NFTDetail.js @@ -3,8 +3,8 @@ import React, { useState } from 'react'; import { Translate } from 'react-localize-redux'; import styled from 'styled-components'; +import { TOKEN_TRANSFER_DEPOSIT, NFT_TRANSFER_GAS } from '../../config'; import UserIconGrey from '../../images/UserIconGrey'; -import { NFT_TRANSFER_DEPOSIT, NFT_TRANSFER_GAS } from '../../services/NonFungibleTokens'; import BackArrowButton from '../common/BackArrowButton'; import FormButton from '../common/FormButton'; import Container from '../common/styled/Container.css'; @@ -112,7 +112,7 @@ const UserIcon = styled.div` export function NFTDetail({ nft, accountId, nearBalance, ownerId, history }) { const [transferNftDetail, setTransferNftDetail] = useState(); - const transferMax = new BN((parseInt(NFT_TRANSFER_GAS, 10) + NFT_TRANSFER_DEPOSIT).toString()); + const transferMax = new BN((parseInt(NFT_TRANSFER_GAS, 10) + TOKEN_TRANSFER_DEPOSIT).toString()); const hasSufficientBalance = (new BN(nearBalance)).gte(transferMax); return ( diff --git a/packages/frontend/src/components/nft/NFTTransferModal.js b/packages/frontend/src/components/nft/NFTTransferModal.js index a78043058e..9ae36fb363 100644 --- a/packages/frontend/src/components/nft/NFTTransferModal.js +++ b/packages/frontend/src/components/nft/NFTTransferModal.js @@ -3,14 +3,14 @@ import { Translate } from 'react-localize-redux'; import { useDispatch, useSelector } from 'react-redux'; import styled from 'styled-components'; -import { EXPLORER_URL } from '../../config'; +import { EXPLORER_URL, NFT_TRANSFER_GAS } from '../../config'; import { checkAccountAvailable } from '../../redux/actions/account'; import { clearLocalAlert, showCustomAlert } from '../../redux/actions/status'; import { selectBalance } from '../../redux/slices/account'; import { actions as ledgerActions } from '../../redux/slices/ledger'; import { actions as nftActions } from '../../redux/slices/nft'; import { selectStatusLocalAlert } from '../../redux/slices/status'; -import NonFungibleTokens, { NFT_TRANSFER_GAS } from '../../services/NonFungibleTokens'; +import NonFungibleTokens from '../../services/NonFungibleTokens'; import isMobile from '../../utils/isMobile'; import Balance from '../common/balance/Balance'; import FormButton from '../common/FormButton'; diff --git a/packages/frontend/src/config/buyNearConfig.js b/packages/frontend/src/config/buyNearConfig.js index efe314810b..b6a9ec0cee 100644 --- a/packages/frontend/src/config/buyNearConfig.js +++ b/packages/frontend/src/config/buyNearConfig.js @@ -1,4 +1,5 @@ import binance from '../components/buy/assets/binance.svg'; +import ftx from '../components/buy/assets/ftx.svg'; import huobi from '../components/buy/assets/huobi.svg'; import monoPay from '../components/buy/assets/monoPay.svg'; import okex from '../components/buy/assets/okex.svg'; @@ -7,7 +8,7 @@ import rainbow from '../components/buy/assets/rainbow.svg'; import utorg from '../components/buy/assets/utorg.svg'; import { Mixpanel } from '../mixpanel'; -export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, utorgPayUrl }) => { +export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, utorgPayUrl, ftxPayUrl }) => { return { moonPay: { disabled: accountId && !moonPayAvailable, @@ -16,11 +17,48 @@ export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, u link: signedMoonPayUrl, track: () => Mixpanel.track('Wallet Click Buy with Moonpay'), }, - nearPay: { icon: payNear, name: 'NearPay', link: 'https://www.nearpay.co/' }, - utorg: { icon: utorg, name: 'UTORG', link: utorgPayUrl, provideReferrer: true }, - rainbow: { icon: rainbow, name: 'Rainbow Bridge', link: 'https://rainbowbridge.app/transfer' }, - binance: { icon: okex, name: 'Okex', link: 'https://www.okex.com/' }, - okex: { icon: binance, name: 'Binance', link: 'https://www.binance.com/' }, - huobi: { icon: huobi, name: 'Huobi', link: 'https://c2c.huobi.com/en-us/one-trade/buy' }, + nearPay: { + icon: payNear, + name: 'NearPay', + link: 'https://widget.nearpay.co/', + track: () => Mixpanel.track('Wallet Click Buy with Nearpay') + }, + utorg: { + icon: utorg, + name: 'UTORG', + link: utorgPayUrl, + provideReferrer: true, + track: () => Mixpanel.track('Wallet Click Buy with UTORG') + }, + ftx: { + icon: ftx, + name: 'FTX', + link: ftxPayUrl, + track: () => Mixpanel.track('Wallet Click Buy with FTX') + }, + rainbow: { + icon: rainbow, + name: 'Rainbow Bridge', + link: 'https://rainbowbridge.app/transfer', + track: () => Mixpanel.track('Wallet Click Bridge with Rainbow Bridge') + }, + okex: { + icon: okex, + name: 'Okex', + link: 'https://www.okex.com/', + track: () => Mixpanel.track('Wallet Click Exchange with Okex') + }, + binance: { + icon: binance, + name: 'Binance', + link: 'https://www.binance.com/', + track: () => Mixpanel.track('Wallet Click Exchange with Binance') + }, + huobi: { + icon: huobi, + name: 'Huobi', + link: 'https://c2c.huobi.com/en-us/one-trade/buy', + track: () => Mixpanel.track('Wallet Click Exchange with Huobi') + }, }; }; diff --git a/packages/frontend/src/config/configFromEnvironment.js b/packages/frontend/src/config/configFromEnvironment.js index f013687f4a..a43bf0cbef 100644 --- a/packages/frontend/src/config/configFromEnvironment.js +++ b/packages/frontend/src/config/configFromEnvironment.js @@ -44,6 +44,13 @@ module.exports = { Environments.MAINNET_NEARORG, ].some((env) => env === NEAR_WALLET_ENV), LINKDROP_GAS: process.env.LINKDROP_GAS, + TOKEN_TRANSFER_DEPOSIT: process.env.TOKEN_TRANSFER_DEPOSIT || '1', + FT_TRANSFER_GAS: process.env.FT_TRANSFER_GAS || '15000000000000', + FT_STORAGE_DEPOSIT_GAS: process.env.FT_STORAGE_DEPOSIT_GAS || '30000000000000', + FT_MINIMUM_STORAGE_BALANCE: process.env.FT_MINIMUM_STORAGE_BALANCE || '1250000000000000000000', + FT_MINIMUM_STORAGE_BALANCE_LARGE: process.env.FT_MINIMUM_STORAGE_BALANCE_LARGE || '12500000000000000000000', + SEND_NEAR_GAS: process.env.SEND_NEAR_GAS || '450000000000', + NFT_TRANSFER_GAS: process.env.NFT_TRANSFER_GAS || '30000000000000', LOCKUP_ACCOUNT_ID_SUFFIX: process.env.LOCKUP_ACCOUNT_ID_SUFFIX, MIGRATION_START_DATE: parseDateFromShell(process.env.MIGRATION_START_DATE || '2022-06-28'), MIGRATION_END_DATE: parseDateFromShell(process.env.MIGRATION_END_DATE || '2022-08-28'), diff --git a/packages/frontend/src/config/environmentDefaults/development.js b/packages/frontend/src/config/environmentDefaults/development.js index 27bc3aaad5..523d65a4a9 100644 --- a/packages/frontend/src/config/environmentDefaults/development.js +++ b/packages/frontend/src/config/environmentDefaults/development.js @@ -37,7 +37,7 @@ export default { MULTISIG_MIN_AMOUNT: '4', MULTISIG_MIN_PROMPT_AMOUNT: '200', NETWORK_ID: 'default', - NODE_URL: 'https://rpc.nearprotocol.com', + NODE_URL: 'https://rpc.testnet.near.org', REACT_APP_USE_TESTINGLOCKUP: true, SENTRY_DSN: 'https://75d1dabd0ab646329fad8a3e7d6c761d@o398573.ingest.sentry.io/5254526', SENTRY_RELEASE: 'development', diff --git a/packages/frontend/src/config/environmentDefaults/testnet.js b/packages/frontend/src/config/environmentDefaults/testnet.js index b3dbb43acb..567eed5485 100644 --- a/packages/frontend/src/config/environmentDefaults/testnet.js +++ b/packages/frontend/src/config/environmentDefaults/testnet.js @@ -38,7 +38,7 @@ export default { MULTISIG_MIN_AMOUNT: '4', MULTISIG_MIN_PROMPT_AMOUNT: '200', NETWORK_ID: 'default', - NODE_URL: 'https://rpc.nearprotocol.com', + NODE_URL: 'https://rpc.testnet.near.org', REACT_APP_USE_TESTINGLOCKUP: false, SENTRY_DSN: 'https://75d1dabd0ab646329fad8a3e7d6c761d@o398573.ingest.sentry.io/5254526', SHOW_PRERELEASE_WARNING: false, diff --git a/packages/frontend/src/config/environmentDefaults/testnet_STAGING.js b/packages/frontend/src/config/environmentDefaults/testnet_STAGING.js index f674d7e6fb..8776fd429f 100644 --- a/packages/frontend/src/config/environmentDefaults/testnet_STAGING.js +++ b/packages/frontend/src/config/environmentDefaults/testnet_STAGING.js @@ -16,6 +16,7 @@ export default { EXPLORE_DEFI_URL: 'https://awesomenear.com/categories/defi/', EXPLORER_URL: 'https://explorer.testnet.near.org', HIDE_SIGN_IN_WITH_LEDGER_ENTER_ACCOUNT_ID_MODAL: false, + INDEXER_SERVICE_URL: 'https://preflight-api.kitwallet.app', LINKDROP_GAS: '100000000000000', LOCKUP_ACCOUNT_ID_SUFFIX: 'lockup.m0', MIN_BALANCE_FOR_GAS: nearApiJs.utils.format.parseNearAmount('0.05'), @@ -37,7 +38,7 @@ export default { MULTISIG_MIN_AMOUNT: '4', MULTISIG_MIN_PROMPT_AMOUNT: '200', NETWORK_ID: 'default', - NODE_URL: 'https://rpc.nearprotocol.com', + NODE_URL: 'https://rpc.testnet.near.org', REACT_APP_USE_TESTINGLOCKUP: false, SENTRY_DSN: 'https://75d1dabd0ab646329fad8a3e7d6c761d@o398573.ingest.sentry.io/5254526', SHOW_PRERELEASE_WARNING: false, diff --git a/packages/frontend/src/redux/actions/staking.js b/packages/frontend/src/redux/actions/staking.js index bb4b8cd45a..0b592d294d 100644 --- a/packages/frontend/src/redux/actions/staking.js +++ b/packages/frontend/src/redux/actions/staking.js @@ -8,9 +8,10 @@ import { STAKING_GAS_BASE, FARMING_CLAIM_GAS, FARMING_CLAIM_YOCTO, - LOCKUP_ACCOUNT_ID_SUFFIX + LOCKUP_ACCOUNT_ID_SUFFIX, + FT_MINIMUM_STORAGE_BALANCE_LARGE } from '../../config'; -import { fungibleTokensService, FT_MINIMUM_STORAGE_BALANCE_LARGE } from '../../services/FungibleTokens'; +import { fungibleTokensService } from '../../services/FungibleTokens'; import { listStakingPools } from '../../services/indexer'; import StakingFarmContracts from '../../services/StakingFarmContracts'; import { getLockupAccountId, getLockupMinBalanceForStorage } from '../../utils/account-with-lockup'; diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 176dab2447..bf6b4aea2f 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -46,7 +46,7 @@ const collectFarmingData = (args) => { } }; -export default createSelector( +const selectCollectedAvailableForClaimData = createSelector( [ selectValidatorsFarmData, selectContractsMetadata, @@ -70,3 +70,36 @@ export default createSelector( }); } ); + +export const selectCollectedAvailableForClaimDataByAccountId = createSelector( + [ + selectValidatorsFarmData, + selectContractsMetadata, + selectTokensFiatValueUSD, + selectTokenWhiteList, + (state, accountId) => accountId + ], + ( + validatorsFarmData, + contractMetadataByContractId, + tokenFiatValues, + tokenWhitelist, + accountId + ) => { + return collectFarmingData({ + validatorsFarmData, + contractMetadataByContractId, + tokenFiatValues, + tokenWhitelist, + accountId + }); + } +); + +export const selectHasAvailableForClaimForAccountId = createSelector( + [selectCollectedAvailableForClaimDataByAccountId], + (farmData) => + farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 +); + +export default selectCollectedAvailableForClaimData; diff --git a/packages/frontend/src/services/FungibleTokens.js b/packages/frontend/src/services/FungibleTokens.js index 2e10691d09..59a8d879b0 100644 --- a/packages/frontend/src/services/FungibleTokens.js +++ b/packages/frontend/src/services/FungibleTokens.js @@ -1,12 +1,21 @@ import BN from 'bn.js'; import * as nearApiJs from 'near-api-js'; -import { NEAR_TOKEN_ID } from '../config'; +import { + NEAR_TOKEN_ID, + TOKEN_TRANSFER_DEPOSIT, + FT_TRANSFER_GAS, + FT_STORAGE_DEPOSIT_GAS, + FT_MINIMUM_STORAGE_BALANCE, + FT_MINIMUM_STORAGE_BALANCE_LARGE, + SEND_NEAR_GAS, +} from '../config'; import { parseTokenAmount, formatTokenAmount, removeTrailingZeros, } from '../utils/amounts'; +import { getTotalGasFee } from '../utils/gasPrice'; import { wallet } from '../utils/wallet'; import { listLikelyTokens } from './indexer'; @@ -17,21 +26,6 @@ const { }, } = nearApiJs; -// account creation costs 0.00125 NEAR for storage, 0.00000000003 NEAR for gas -// https://docs.near.org/docs/api/naj-cookbook#wrap-and-unwrap-near -const FT_MINIMUM_STORAGE_BALANCE = parseNearAmount('0.00125'); -// FT_MINIMUM_STORAGE_BALANCE: nUSDC, nUSDT require minimum 0.0125 NEAR. Came to this conclusion using trial and error. -export const FT_MINIMUM_STORAGE_BALANCE_LARGE = parseNearAmount('0.0125'); -const FT_STORAGE_DEPOSIT_GAS = parseNearAmount('0.00000000003'); - -// set this to the same value as we use for creating an account and the remainder is refunded -const FT_TRANSFER_GAS = parseNearAmount('0.00000000003'); - -// contract might require an attached depositof of at least 1 yoctoNear on transfer methods -// "This 1 yoctoNEAR is not enforced by this standard, but is encouraged to do. While ability to receive attached deposit is enforced by this token." -// from: https://github.com/near/NEPs/issues/141 -const FT_TRANSFER_DEPOSIT = '1'; - // Fungible Token Standard // https://github.com/near/NEPs/tree/master/specs/Standards/FungibleToken export default class FungibleTokens { @@ -93,12 +87,10 @@ export default class FungibleTokens { accountId && !(await this.isStorageBalanceAvailable({ contractName, accountId })) ) { - return new BN(FT_TRANSFER_GAS) - .add(new BN(FT_MINIMUM_STORAGE_BALANCE)) - .add(new BN(FT_STORAGE_DEPOSIT_GAS)) - .toString(); + const totalGasFees = await getTotalGasFee(new BN(FT_TRANSFER_GAS).add(new BN(FT_STORAGE_DEPOSIT_GAS))); + return new BN(totalGasFees).add(new BN(FT_MINIMUM_STORAGE_BALANCE)).toString(); } else { - return FT_TRANSFER_GAS; + return getTotalGasFee(contractName ? FT_TRANSFER_GAS : SEND_NEAR_GAS); } } @@ -137,6 +129,7 @@ export default class FungibleTokens { } catch (e) { // sic.typo in `mimimum` wording of responses, so we check substring // Original string was: 'attached deposit is less than the mimimum storage balance' + // TODO: Call storage_balance_bounds: https://github.com/near/near-wallet/issues/2522 if (e.message.includes('attached deposit is less than')) { await this.transferStorageDeposit({ account, @@ -160,7 +153,7 @@ export default class FungibleTokens { receiver_id: receiverId, }, FT_TRANSFER_GAS, - FT_TRANSFER_DEPOSIT + TOKEN_TRANSFER_DEPOSIT ), ], }); @@ -198,7 +191,7 @@ export default class FungibleTokens { toWNear ? 'near_deposit' : 'near_withdraw', toWNear ? {} : { amount: wrapAmount }, FT_STORAGE_DEPOSIT_GAS, - toWNear ? wrapAmount : FT_TRANSFER_DEPOSIT + toWNear ? wrapAmount : TOKEN_TRANSFER_DEPOSIT ), ]; diff --git a/packages/frontend/src/services/NonFungibleTokens.js b/packages/frontend/src/services/NonFungibleTokens.js index d1ed4a23b6..08605f89f9 100644 --- a/packages/frontend/src/services/NonFungibleTokens.js +++ b/packages/frontend/src/services/NonFungibleTokens.js @@ -1,12 +1,10 @@ import * as nearAPI from 'near-api-js'; +import { TOKEN_TRANSFER_DEPOSIT, NFT_TRANSFER_GAS } from '../config'; import { wallet } from '../utils/wallet'; import { listLikelyNfts } from './indexer'; export const TOKENS_PER_PAGE = 4; -export const NFT_TRANSFER_GAS = nearAPI.utils.format.parseNearAmount('0.00000000003'); - -export const NFT_TRANSFER_DEPOSIT = 1; // 1 yocto Near const functionCall = nearAPI.transactions.functionCall; @@ -129,7 +127,7 @@ export default class NonFungibleTokens { token_id: tokenId }, NFT_TRANSFER_GAS, - NFT_TRANSFER_DEPOSIT + TOKEN_TRANSFER_DEPOSIT ) ] }); diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index e72e9a8ae9..d8372c0e4b 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -1,12 +1,26 @@ +import { Account, Connection } from 'near-api-js'; + import { - wallet -} from '../utils/wallet'; + NETWORK_ID, + NODE_URL +} from '../config'; +import { FARMING_VALIDATOR_VERSION, getValidationVersion, MAINNET, TESTNET } from '../utils/constants'; // Staking Farm Contract // https://github.com/referencedev/staking-farm/ export default class StakingFarmContracts { // View functions are not signed, so do not require a real account! - static viewFunctionAccount = wallet.getAccountBasic('dontcare'); + static viewFunctionAccount = new Account( + Connection.fromConfig({ + networkId: NETWORK_ID, + provider: { + type: 'JsonRpcProvider', + args: { url: NODE_URL + '/' }, + }, + signer: {} + }), + 'dontcare' + ); static getFarms({ contractName, from_index, limit }) { return this.viewFunctionAccount.viewFunction( @@ -46,4 +60,34 @@ export default class StakingFarmContracts { ) ); } + + static isFarmingValidator(accountId) { + return ( + getValidationVersion( + NODE_URL.indexOf(MAINNET) > -1 ? MAINNET : TESTNET, + accountId + ) === FARMING_VALIDATOR_VERSION + ); + } + + static hasUnclaimedRewards = ({ + contractName, + account_id, + from_index = 0, + limit = 300, + }) => { + return ( + StakingFarmContracts.isFarmingValidator(contractName) && + StakingFarmContracts.getFarmListWithUnclaimedRewards({ + contractName, + account_id, + from_index, + limit, + }).then( + (farmListWithBalance) => + farmListWithBalance.filter(({ balance }) => +balance > 0) + .length > 0 + ) + ); + }; } diff --git a/packages/frontend/src/translations/en.global.json b/packages/frontend/src/translations/en.global.json index 07e9dc7979..a9cad86471 100644 --- a/packages/frontend/src/translations/en.global.json +++ b/packages/frontend/src/translations/en.global.json @@ -195,6 +195,20 @@ "available": "Available balance", "reserved": "Reserved for fees" }, + "batchExportAccounts": { + "confirmExportModal": { + "accountToExport": "Account to Export", + "title": "Approve Account Export", + "transactionDetails": "+ Transaction Details" + }, + "exportScreen": { + "desc": "Begin your export and confirm each account
when prompted.", + "weFound": "We found ${noOfAccounts} accounts to export to your device." + }, + "successScreen": { + "title": "${noOfAccounts} account(s) were successfully
exported to your device" + } + }, "batchImportAccounts": { "confirmImportModal": { "accountToImport": "Account to Import", @@ -225,6 +239,7 @@ "authorize": "Authorize", "authorizing": "Authorizing", "backToSwap": "Back to Swap", + "beginExport": "Begin Export", "beginImport": "Begin Import", "browseApps": "Browse Apps", "buy": "Buy", @@ -1368,6 +1383,7 @@ "signInLedger": { "advanced": { "desc": "Specify an HD path to import its linked accounts.", + "exportDesc": "Specify an HD path to export your account to, you’ll need to remember your choice to use this account in the future.
For additional security use different HD paths for each unique account.", "setPath": "Set HD Path", "subTitle": "HD Path", "title": "Advanced Options" diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 251c915419..330ef4d297 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -11,6 +11,7 @@ import { REACT_APP_USE_TESTINGLOCKUP, } from '../config'; import { listStakingDeposits } from '../services/indexer'; +import StakingFarmContracts from '../services/StakingFarmContracts'; import { WalletError } from './walletError'; // TODO: Should gas allowance be dynamically calculated @@ -122,7 +123,13 @@ export async function transferAllFromLockup(missingAmount) { const lockedBalance = new BN(await this.wrappedAccount.viewFunction(lockupAccountId, 'get_locked_amount')); if (lockedBalance.eq(new BN(0))) { const stakingPoolBalance = await this.wrappedAccount.viewFunction(lockupAccountId, 'get_known_deposited_balance'); - if (!new BN(stakingPoolBalance).eq(new BN(0))) { + const hasUnclaimedTokenRewards = + poolAccountId && + (await StakingFarmContracts.hasUnclaimedRewards({ + contractName: poolAccountId, + account_id: lockupAccountId + })); + if (!new BN(stakingPoolBalance).eq(new BN(0)) || hasUnclaimedTokenRewards) { throw new WalletError('Staking pool balance detected.', 'lockup.transferAllWithStakingPoolBalance'); } @@ -259,6 +266,10 @@ async function getAccountBalance(limitedAccountData = false) { let totalBalance = new BN(lockupBalance.total); let stakedBalanceLockup = new BN(0); const stakingPoolLockupAccountId = await this.wrappedAccount.viewFunction(lockupAccountId, 'get_staking_pool_account_id'); + const hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ + contractName: stakingPoolLockupAccountId, + account_id: lockupAccountId + }); if (stakingPoolLockupAccountId) { stakedBalanceLockup = new BN(await this.wrappedAccount.viewFunction(stakingPoolLockupAccountId, 'get_account_total_balance', { account_id: lockupAccountId })); @@ -268,7 +279,7 @@ async function getAccountBalance(limitedAccountData = false) { const ownersBalance = totalBalance.sub(lockedAmount); // if acc is deletable (nothing locked && nothing stake) you can transfer the whole amount ohterwise get_liquid_owners_balance - const isAccDeletable = lockedAmount.isZero() && stakedBalanceLockup.isZero(); + const isAccDeletable = lockedAmount.isZero() && stakedBalanceLockup.isZero() && !hasUnclaimedTokenBalance; const MIN_BALANCE_FOR_STORAGE = getLockupMinBalanceForStorage(lockupContractCodeHash); const liquidOwnersBalanceTransfersEnabled = isAccDeletable ? new BN(lockupBalance.total) diff --git a/packages/frontend/src/utils/gasPrice.js b/packages/frontend/src/utils/gasPrice.js new file mode 100644 index 0000000000..bfe75087b3 --- /dev/null +++ b/packages/frontend/src/utils/gasPrice.js @@ -0,0 +1,18 @@ +import BN from 'bn.js'; + +import { wallet } from './wallet'; + +export const getLatestBlock = () => wallet.connection.provider.block({ finality: 'final' }); + +export const getLatestGasPrice = async () => { + const latestBlock = await getLatestBlock(); + return latestBlock.header.gas_price; +}; + +export const getTotalGasFee = async (gas) => { + const latestGasPrice = await getLatestGasPrice(); + return new BN(latestGasPrice).mul(new BN(gas)).toString(); +}; + +export const formatTGasToYoctoNEAR = (tGas) => new BN(tGas * 10 ** 12).toString(); + diff --git a/packages/frontend/src/utils/twoFactor.js b/packages/frontend/src/utils/twoFactor.js index 92372b65b1..07bc87a282 100644 --- a/packages/frontend/src/utils/twoFactor.js +++ b/packages/frontend/src/utils/twoFactor.js @@ -65,7 +65,8 @@ export class TwoFactor extends Account2FA { async disableMultisig() { const contractBytes = new Uint8Array(await (await fetch('/main.wasm')).arrayBuffer()); - const result = await this.disable(contractBytes); + const stateCleanupContractBytes = new Uint8Array(await (await fetch('/state_cleanup.wasm')).arrayBuffer()); + const result = await this.disable(contractBytes, stateCleanupContractBytes); await store.dispatch(refreshAccount()); this.has2fa = false; return result; diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index ccfb91ea17..dffef7ac5c 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -9,7 +9,8 @@ import { store } from '..'; import * as Config from '../config'; import { makeAccountActive, - redirectTo + redirectTo, + switchAccount } from '../redux/actions/account'; import { actions as ledgerActions } from '../redux/slices/ledger'; import sendJson from '../tmp_fetch_send_json'; @@ -201,7 +202,8 @@ export default class Wallet { if (accessKeys) { const localKey = await this.getLocalAccessKey(accountId, accessKeys); const ledgerKey = accessKeys.find((accessKey) => accessKey.meta.type === 'ledger'); - if (ledgerKey && (!localKey || localKey.permission !== 'FullAccess')) { + const localKeyIsNullOrNonMultisigLAK = !localKey || (localKey.permission !== 'FullAccess' && !this.isMultisigKeyInfoView(accountId, localKey)); + if (ledgerKey && localKeyIsNullOrNonMultisigLAK) { return PublicKey.from(ledgerKey.public_key); } } @@ -295,6 +297,11 @@ export default class Wallet { throw new Error('No matching key pair for public key'); } + async getAccountKeyType(accountId) { + const keypair = await wallet.keyStore.getKey(NETWORK_ID, accountId); + return this.getPublicKeyType(accountId, keypair.getPublicKey().toString()); + } + isFullAccessKeyInfoView(keyInfoView) { return keyInfoView?.access_key?.permission === 'FullAccess'; } @@ -625,20 +632,36 @@ export default class Wallet { } } - async addLedgerAccessKey(path) { - const accountId = this.accountId; + async addLedgerAccessKey(path, accountIdOverride) { + const accountId = accountIdOverride || this.accountId; const ledgerPublicKey = await this.getLedgerPublicKey(path); - const accessKeys = await this.getAccessKeys(); - const accountHasLedgerKey = accessKeys.map((key) => key.public_key).includes(ledgerPublicKey.toString()); + const accessKeys = await this.getAccessKeys(accountId); + const accountHasLedgerKey = accessKeys.some((key) => key.public_key === ledgerPublicKey.toString()); await setKeyMeta(ledgerPublicKey, { type: 'ledger' }); - const account = await this.getAccount(accountId); if (!accountHasLedgerKey) { + const account = await this.getAccount(accountId); await account.addKey(ledgerPublicKey); await this.postSignedJson('/account/ledgerKeyAdded', { accountId, publicKey: ledgerPublicKey.toString() }); } } + async exportToLedgerWallet(path, accountId) { + const ledgerPublicKey = await this.getLedgerPublicKey(path); + const accessKeys = await this.getAccessKeys(accountId); + const accountHasLedgerKey = accessKeys.some((key) => key.public_key === ledgerPublicKey.toString()); + + if (!accountHasLedgerKey) { + const account = await this.getAccount(accountId); + const has2fa = await TwoFactor.has2faEnabled(account); + + if (has2fa) { + await store.dispatch(switchAccount({accountId: account.accountId})); + } + await account.addKey(ledgerPublicKey); + } + } + async disableLedger() { const account = await this.getAccount(this.accountId); const keyPair = nearApiJs.KeyPair.fromRandom('ed25519'); diff --git a/packages/frontend/src/wasm/state_cleanup.wasm b/packages/frontend/src/wasm/state_cleanup.wasm new file mode 100644 index 0000000000..e3561bfcee Binary files /dev/null and b/packages/frontend/src/wasm/state_cleanup.wasm differ diff --git a/packages/frontend/tools/disable-2fa-with-seedphrase/README.md b/packages/frontend/tools/disable-2fa-with-seedphrase/README.md deleted file mode 100644 index f2e6e3193e..0000000000 --- a/packages/frontend/tools/disable-2fa-with-seedphrase/README.md +++ /dev/null @@ -1,13 +0,0 @@ -## Disable 2fa with seedphrase - -This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase - -## Prerequisites - -* [near-cli](https://github.com/near/near-cli) needs to be installed - -## Usage - -```shell= -NEAR_WALLET_ENV=mainnet ./disable-2fa-with-seedphrase.sh --accountId="testAccount.near" --seedPhrase='lorem ipsum dolor sit amet consectetur adipiscing elit nunc efficitur est' -``` \ No newline at end of file diff --git a/packages/frontend/tools/disable-2fa-with-seedphrase/disable-2fa-with-seedphrase.sh b/packages/frontend/tools/disable-2fa-with-seedphrase/disable-2fa-with-seedphrase.sh deleted file mode 100644 index 15e8646654..0000000000 --- a/packages/frontend/tools/disable-2fa-with-seedphrase/disable-2fa-with-seedphrase.sh +++ /dev/null @@ -1,24 +0,0 @@ -#/bin/bash -for i in "$@"; do - case $i in - --accountId=*) - ACCOUNTID="${i#*=}" - shift - ;; - --seedPhrase=*) - SEEDPHRASE="${i#*=}" - shift - ;; - esac -done - -if [ -z "$SEEDPHRASE" ] || [ -z "$ACCOUNTID" ] -then - echo "Both accountId and seedphrase are required" - exit 1 -fi - -near generate-key $ACCOUNTID --seedPhrase="$SEEDPHRASE" -curl "https://raw.githubusercontent.com/near/near-wallet/master/packages/frontend/src/wasm/main.wasm" --output ./main.wasm -near deploy --accountId $ACCOUNTID --wasmFile main.wasm -rm -rf main.wasm \ No newline at end of file diff --git a/packages/utilities/.gitignore b/packages/utilities/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/utilities/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/utilities/README.md b/packages/utilities/README.md new file mode 100644 index 0000000000..34758d3f2c --- /dev/null +++ b/packages/utilities/README.md @@ -0,0 +1,36 @@ +# Utilities CLI (command line interface) + +The uitilites package provides various wallet related command line utilities. + +## Overview + +_Click on a command for more information and examples._ + +| Command | Description | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | +| [`disable-2fa`](#disable-2fa) | disable 2fa using the provided seedphrase | +| [`restore-account-contract`](#restore-account-contract)| restore account contract to a given `blockHash` | + + +## Disable 2fa + +This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase. It converts 2fa LAKs back to FAKs, deploys the empty contract and optionally cleans up state +* arguments: `accountId` `seedPhrase` +* options: `cleanupState` `helperUrl` + +### Usage + +```shell= +NEAR_WALLET_ENV=mainnet node index.js disable-2fa --accountId="testAccount.near" --seedPhrase='lorem ipsum dolor sit amet consectetur adipiscing elit nunc efficitur est' +``` +--- +## Restore account contract + +This script restores account code back to a given `blockHash` by deploying a contract. It pulls contract code from the given archival node +* arguments: `accountId` `seedPhrase` `nodeUrl` `blockHash` + +### Usage + +```shell= +NEAR_WALLET_ENV=testnet node index.js restore-account-contract --accountId='testAccount.testnet' --seedPhrase='lorem ipsum dolor sit amet consectetur adipiscing elit nunc efficitur est' --blockHash='9c5tk9kdTzgAgejgmnLCRu9yJ1Jp1LjFqtgB4SHEzzeB' --nodeUrl='https://sample-archival-testnet-node-url.com/' +``` \ No newline at end of file diff --git a/packages/utilities/commands/disable2fa.js b/packages/utilities/commands/disable2fa.js new file mode 100644 index 0000000000..11ccf42a19 --- /dev/null +++ b/packages/utilities/commands/disable2fa.js @@ -0,0 +1,78 @@ +const { InMemoryKeyStore } = require("near-api-js/lib/key_stores"); +const { parseSeedPhrase } = require("near-seed-phrase"); +const { Connection, KeyPair } = require("near-api-js"); + +const Environments = require("../../../features/environments.json"); +const { Account2FA } = require("near-api-js/lib/account_multisig"); + +module.exports = { + command: `disable-2fa`, + builder: (yargs) => + yargs + .option("accountId", { + desc: "accountId to disable the 2fa on", + type: "string", + required: true, + }) + .option("seedPhrase", { + desc: "seedPhrase for the accountId", + type: "string", + required: true, + }) + .option("cleanupState", { + desc: `cleanup account state on disable`, + type: "boolean", + required: false, + }) + .option("helperUrl", { + desc: `helperUrl to use`, + type: "string", + required: false, + }), + handler: disable2fa, +}; + +async function disable2fa({ accountId, seedPhrase, helperUrl, cleanupState }) { + const networkId = process.env.NEAR_WALLET_ENV || "mainnet"; + const walletEnvConfigMap = { + [Environments.TESTNET]: { + helperUrl: "https://near-contract-helper.onrender.com", + rpcUrl: "https://rpc.nearprotocol.com", + }, + [Environments.MAINNET]: { + helperUrl: "https://helper.mainnet.near.org", + rpcUrl: "https://rpc.mainnet.near.org", + }, + }; + const walletEnvConfig = walletEnvConfigMap[networkId]; + const keyStore = new InMemoryKeyStore(); + await keyStore.setKey(networkId, accountId, KeyPair.fromString(parseSeedPhrase(seedPhrase).secretKey)); + const connection = Connection.fromConfig({ + networkId, + provider: { + type: "JsonRpcProvider", + args: { url: walletEnvConfig.rpcUrl }, + }, + signer: { type: "InMemorySigner", keyStore }, + }); + + const emptyContractBytes = new Uint8Array( + await ( + await fetch("https://github.com/near/near-wallet/blob/master/packages/frontend/src/wasm/main.wasm?raw=true") + ).arrayBuffer() + ); + let cleanupContractBytes; + if (cleanupState) { + cleanupContractBytes = new Uint8Array( + await ( + await fetch("https://github.com/near/core-contracts/blob/master/state-cleanup/res/state_cleanup.wasm?raw=true") + ).arrayBuffer() + ); + } + + const account = new Account2FA(connection, accountId, { + helperUrl: helperUrl || walletEnvConfig.helperUrl, + }); + + await account.disableWithFAK({ contractBytes: emptyContractBytes, cleanupContractBytes }); +} diff --git a/packages/utilities/commands/restoreAccountContract.js b/packages/utilities/commands/restoreAccountContract.js new file mode 100644 index 0000000000..407cee633d --- /dev/null +++ b/packages/utilities/commands/restoreAccountContract.js @@ -0,0 +1,57 @@ +const { InMemoryKeyStore } = require("near-api-js/lib/key_stores"); +const { parseSeedPhrase } = require("near-seed-phrase"); +const { Connection, KeyPair } = require("near-api-js"); +const { Account } = require("near-api-js/lib/account"); + +module.exports = { + command: `restore-account-contract`, + builder: (yargs) => + yargs + .option("accountId", { + desc: "accountId to disable the 2fa on", + type: "string", + required: true, + }) + .option("seedPhrase", { + desc: "seedPhrase for the accountId", + type: "string", + required: true, + }) + .option("nodeUrl", { + desc: "Url for the archival rpc node to pull the code from", + type: "string", + required: true, + }) + .option("blockHash", { + desc: `block hash to pull code from and deploy to the account`, + type: "string", + required: true, + }), + handler: restoreAccountContract, +}; + +async function restoreAccountContract({ accountId, seedPhrase, nodeUrl, blockHash }) { + const networkId = process.env.NEAR_WALLET_ENV || "mainnet"; + + const keyStore = new InMemoryKeyStore(); + await keyStore.setKey(networkId, accountId, KeyPair.fromString(parseSeedPhrase(seedPhrase).secretKey)); + + const connection = Connection.fromConfig({ + networkId, + provider: { + type: "JsonRpcProvider", + args: { url: nodeUrl }, + }, + signer: { type: "InMemorySigner", keyStore }, + }); + + const { code_base64 } = await connection.provider.query({ + request_type: "view_code", + account_id: accountId, + blockId: blockHash, + finality: 'final' + }); + + const account = new Account(connection, accountId); + await account.deployContract(new Uint8Array(Buffer.from(code_base64, 'base64'))); +} \ No newline at end of file diff --git a/packages/utilities/index.js b/packages/utilities/index.js new file mode 100644 index 0000000000..52e997ed27 --- /dev/null +++ b/packages/utilities/index.js @@ -0,0 +1,9 @@ +const yargs = require("yargs/yargs"); +const { hideBin } = require("yargs/helpers"); +const disable2fa = require("./commands/disable2fa"); +const restoreAccountContract = require("./commands/restoreAccountContract"); + +yargs(hideBin(process.argv)) + .command(disable2fa) + .command(restoreAccountContract) + .parse(); diff --git a/packages/utilities/package.json b/packages/utilities/package.json new file mode 100644 index 0000000000..883e4c6a40 --- /dev/null +++ b/packages/utilities/package.json @@ -0,0 +1,11 @@ +{ + "name": "@near-wallet/utilities", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "near-api-js": "^0.45.1", + "near-seed-phrase": "^0.2.0", + "yargs": "^17.3.1" + } +} diff --git a/yarn.lock b/yarn.lock index e5b1ca129a..3ef3dadcb6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3636,6 +3636,15 @@ borsh@^0.6.0: bs58 "^4.0.0" text-encoding-utf-8 "^1.0.2" +borsh@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/borsh/-/borsh-0.7.0.tgz#6e9560d719d86d90dc589bca60ffc8a6c51fec2a" + integrity sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA== + dependencies: + bn.js "^5.2.0" + bs58 "^4.0.0" + text-encoding-utf-8 "^1.0.2" + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -9389,6 +9398,23 @@ near-api-js@^0.43.1: text-encoding-utf-8 "^1.0.2" tweetnacl "^1.0.1" +near-api-js@^0.45.1: + version "0.45.1" + resolved "https://registry.yarnpkg.com/near-api-js/-/near-api-js-0.45.1.tgz#0f0a4b378758a2f1b32555399d7356da73d0ef27" + integrity sha512-QyPO/vjvMFlcMO1DCpsqzmnSqPIyHsjK1Qi4B5ZR1cJCIWMkqugDF/TDf8FVQ85pmlcYeYwfiTqKanKz+3IG0A== + dependencies: + bn.js "5.2.0" + borsh "^0.7.0" + bs58 "^4.0.0" + depd "^2.0.0" + error-polyfill "^0.1.3" + http-errors "^1.7.2" + js-sha256 "^0.9.0" + mustache "^4.0.0" + node-fetch "^2.6.1" + text-encoding-utf-8 "^1.0.2" + tweetnacl "^1.0.1" + near-hd-key@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/near-hd-key/-/near-hd-key-1.2.1.tgz#f508ff15436cf8a439b543220f3cc72188a46756"