From 49216e713c74f0c1fda6fefcd6a7d035dad04502 Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 29 Apr 2022 00:31:15 +0400 Subject: [PATCH 01/63] fix: add check for unclaimed rewards in lockup isAccDeletable --- .../selectCollectedAvailableForClaimData.js | 35 ++++++++++++- .../src/services/StakingFarmContracts.js | 50 +++++++++++++++++-- .../frontend/src/utils/account-with-lockup.js | 19 ++++++- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index ed8367d239..3d75e8ccc6 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, selectAllContractMetadata, @@ -70,3 +70,36 @@ export default createSelector( }); } ); + +export const selectCollectedAvailableForClaimDataForAccountId = createSelector( + [ + selectValidatorsFarmData, + selectAllContractMetadata, + selectTokensFiatValueUSD, + selectTokenWhiteList, + (state, accountId) => accountId + ], + ( + validatorsFarmData, + contractMetadataByContractId, + tokenFiatValues, + tokenWhitelist, + accountId + ) => { + return collectFarmingData({ + validatorsFarmData, + contractMetadataByContractId, + tokenFiatValues, + tokenWhitelist, + accountId + }); + } +); + +export const selectHasAvailableForClaimForAccountId = createSelector( + [selectCollectedAvailableForClaimDataForAccountId], + (farmData) => + farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 +); + +export default selectCollectedAvailableForClaimData diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index e72e9a8ae9..b118917832 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 { FARMING_VALIDATOR_VERSION, getValidationVersion, MAINNET, TESTNET } from '../utils/constants'; import { - wallet -} from '../utils/wallet'; + NETWORK_ID, + NODE_URL +} from '../config'; // 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 = async ({ + contractName, + account_id, + from_index, + limit, + }) => { + return ( + StakingFarmContracts.isFarmingValidator(contractName) && + (await StakingFarmContracts.getFarmListWithUnclaimedRewards({ + contractName, + account_id, + from_index, + limit, + }).then( + (farmListWithBalance) => + farmListWithBalance.filter(({ balance }) => +balance > 0) + .length > 0 + )) + ); + }; } diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index b2e8a93308..6ce15f5ce8 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -13,6 +13,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 @@ -124,7 +125,15 @@ 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, + from_index: 0, + limit: 300, + })); + if (!new BN(stakingPoolBalance).eq(new BN(0)) || hasUnclaimedTokenRewards) { throw new WalletError('Staking pool balance detected.', 'lockup.transferAllWithStakingPoolBalance'); } @@ -266,6 +275,12 @@ 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'); + let hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ + contractName: stakingPoolLockupAccountId, + account_id: lockupAccountId, + from_index: 0, + limit: 300, + }); if (stakingPoolLockupAccountId) { stakedBalanceLockup = new BN(await this.wrappedAccount.viewFunction(stakingPoolLockupAccountId, 'get_account_total_balance', { account_id: lockupAccountId })); @@ -275,7 +290,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) From fb815504273cf3467c3b93e32357551fc129a6ff Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 29 Apr 2022 00:46:08 +0400 Subject: [PATCH 02/63] fix: lint issues --- .../selectCollectedAvailableForClaimData.js | 2 +- packages/frontend/src/services/StakingFarmContracts.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 3d75e8ccc6..8a87ce0b6c 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -102,4 +102,4 @@ export const selectHasAvailableForClaimForAccountId = createSelector( farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 ); -export default selectCollectedAvailableForClaimData +export default selectCollectedAvailableForClaimData; diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index b118917832..e8b96c3dac 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -1,10 +1,10 @@ import { Account, Connection } from 'near-api-js'; -import { FARMING_VALIDATOR_VERSION, getValidationVersion, MAINNET, TESTNET } from '../utils/constants'; import { 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/ @@ -14,12 +14,12 @@ export default class StakingFarmContracts { Connection.fromConfig({ networkId: NETWORK_ID, provider: { - type: "JsonRpcProvider", - args: { url: NODE_URL + "/" }, + type: 'JsonRpcProvider', + args: { url: NODE_URL + '/' }, }, signer: {} }), - "dontcare" + 'dontcare' ); static getFarms({ contractName, from_index, limit }) { From 4e4728957d1543387d6472b8a8f23deb068fe6fd Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 16:58:33 +0400 Subject: [PATCH 03/63] refactor: remove await Co-authored-by: Morgan McCauley --- packages/frontend/src/services/StakingFarmContracts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index e8b96c3dac..18d41ae321 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -78,12 +78,12 @@ export default class StakingFarmContracts { }) => { return ( StakingFarmContracts.isFarmingValidator(contractName) && - (await StakingFarmContracts.getFarmListWithUnclaimedRewards({ + StakingFarmContracts.getFarmListWithUnclaimedRewards({ contractName, account_id, from_index, limit, - }).then( + }.then( (farmListWithBalance) => farmListWithBalance.filter(({ balance }) => +balance > 0) .length > 0 From 5e04f71c28ba9635397d258d87d6ac191f18f18c Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 17:10:03 +0400 Subject: [PATCH 04/63] refactor: add default from_index and limit Co-authored-by: Morgan McCauley --- packages/frontend/src/services/StakingFarmContracts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index 18d41ae321..9999a8801b 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -73,8 +73,8 @@ export default class StakingFarmContracts { static hasUnclaimedRewards = async ({ contractName, account_id, - from_index, - limit, + from_index = 0, + limit = 300, }) => { return ( StakingFarmContracts.isFarmingValidator(contractName) && From 18144f32b4aec2baf2ffd3f2e687239520ea3624 Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 17:11:22 +0400 Subject: [PATCH 05/63] refactor: let to const Co-authored-by: Morgan McCauley --- packages/frontend/src/utils/account-with-lockup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 6ce15f5ce8..2be71d1b47 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -275,7 +275,7 @@ 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'); - let hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ + const hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ contractName: stakingPoolLockupAccountId, account_id: lockupAccountId, from_index: 0, From 227d210584747b8b81f1cb01cf0ef2defac9c244 Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 17:12:30 +0400 Subject: [PATCH 06/63] refactor: function naming Co-authored-by: Morgan McCauley --- .../crossStateSelectors/selectCollectedAvailableForClaimData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 8a87ce0b6c..c2efc569f0 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -71,7 +71,7 @@ const selectCollectedAvailableForClaimData = createSelector( } ); -export const selectCollectedAvailableForClaimDataForAccountId = createSelector( +export const selectCollectedAvailableForClaimDataByAccountId = createSelector( [ selectValidatorsFarmData, selectAllContractMetadata, From 23302de9ca2dbc4c879e9fdda101c923e7a24921 Mon Sep 17 00:00:00 2001 From: esaminu Date: Mon, 13 Jun 2022 17:13:36 +0400 Subject: [PATCH 07/63] refactor: minor cleanups --- .../selectCollectedAvailableForClaimData.js | 2 +- packages/frontend/src/services/StakingFarmContracts.js | 2 +- packages/frontend/src/utils/account-with-lockup.js | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index c2efc569f0..6488e07a09 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -97,7 +97,7 @@ export const selectCollectedAvailableForClaimDataByAccountId = createSelector( ); export const selectHasAvailableForClaimForAccountId = createSelector( - [selectCollectedAvailableForClaimDataForAccountId], + [selectCollectedAvailableForClaimDataByAccountId], (farmData) => farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 ); diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index 9999a8801b..940ee27da8 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -70,7 +70,7 @@ export default class StakingFarmContracts { ); } - static hasUnclaimedRewards = async ({ + static hasUnclaimedRewards = ({ contractName, account_id, from_index = 0, diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 2be71d1b47..9a698e7ebb 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -129,9 +129,7 @@ export async function transferAllFromLockup(missingAmount) { poolAccountId && (await StakingFarmContracts.hasUnclaimedRewards({ contractName: poolAccountId, - account_id: lockupAccountId, - from_index: 0, - limit: 300, + account_id: lockupAccountId })); if (!new BN(stakingPoolBalance).eq(new BN(0)) || hasUnclaimedTokenRewards) { throw new WalletError('Staking pool balance detected.', 'lockup.transferAllWithStakingPoolBalance'); @@ -277,9 +275,7 @@ async function getAccountBalance(limitedAccountData = false) { const stakingPoolLockupAccountId = await this.wrappedAccount.viewFunction(lockupAccountId, 'get_staking_pool_account_id'); const hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ contractName: stakingPoolLockupAccountId, - account_id: lockupAccountId, - from_index: 0, - limit: 300, + account_id: lockupAccountId }); if (stakingPoolLockupAccountId) { stakedBalanceLockup = new BN(await this.wrappedAccount.viewFunction(stakingPoolLockupAccountId, From c1548cccc1980958d88584140b89bb8e2416677c Mon Sep 17 00:00:00 2001 From: no Date: Fri, 17 Jun 2022 20:00:33 +0100 Subject: [PATCH 08/63] feat(analytics): add MixPanel events to top-up options --- packages/frontend/src/config/buyNearConfig.js | 43 ++++++++++++++++--- 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/config/buyNearConfig.js b/packages/frontend/src/config/buyNearConfig.js index efe314810b..a8dc03cf8c 100644 --- a/packages/frontend/src/config/buyNearConfig.js +++ b/packages/frontend/src/config/buyNearConfig.js @@ -16,11 +16,42 @@ 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://www.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') + }, + rainbow: { + icon: rainbow, + name: 'Rainbow Bridge', + link: 'https://rainbowbridge.app/transfer', + track: () => Mixpanel.track('Wallet Click Bridge with Rainbow Bridge') + }, + binance: { + icon: okex, + name: 'Okex', + link: 'https://www.okex.com/', + track: () => Mixpanel.track('Wallet Click Exchange with Okex') + }, + okex: { + 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') + }, }; }; From 3732334f2d1ddd4297b2868976121b81024e3fa5 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Mon, 20 Jun 2022 15:56:11 -0700 Subject: [PATCH 09/63] fix: swap okex and binance keys to match values --- packages/frontend/src/config/buyNearConfig.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/config/buyNearConfig.js b/packages/frontend/src/config/buyNearConfig.js index a8dc03cf8c..921c9dd8a8 100644 --- a/packages/frontend/src/config/buyNearConfig.js +++ b/packages/frontend/src/config/buyNearConfig.js @@ -35,13 +35,13 @@ export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, u link: 'https://rainbowbridge.app/transfer', track: () => Mixpanel.track('Wallet Click Bridge with Rainbow Bridge') }, - binance: { + okex: { icon: okex, name: 'Okex', link: 'https://www.okex.com/', track: () => Mixpanel.track('Wallet Click Exchange with Okex') }, - okex: { + binance: { icon: binance, name: 'Binance', link: 'https://www.binance.com/', From daca0fabb20bbfbd010cf5f2d8917f91cb79e139 Mon Sep 17 00:00:00 2001 From: Den Ilin Date: Tue, 21 Jun 2022 17:12:31 +0300 Subject: [PATCH 10/63] feat: fTX Pay Integration --- packages/frontend/src/components/buy/BuyNear.js | 2 +- packages/frontend/src/components/buy/assets/ftx.svg | 7 +++++++ packages/frontend/src/config/buyNearConfig.js | 7 +++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 packages/frontend/src/components/buy/assets/ftx.svg diff --git a/packages/frontend/src/components/buy/BuyNear.js b/packages/frontend/src/components/buy/BuyNear.js index bf265af92e..84fa77afce 100644 --- a/packages/frontend/src/components/buy/BuyNear.js +++ b/packages/frontend/src/components/buy/BuyNear.js @@ -223,7 +223,7 @@ export function BuyNear({ match, location, history }) { + + + + + + diff --git a/packages/frontend/src/config/buyNearConfig.js b/packages/frontend/src/config/buyNearConfig.js index 921c9dd8a8..613e920997 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'; @@ -29,6 +30,12 @@ export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, u provideReferrer: true, track: () => Mixpanel.track('Wallet Click Buy with UTORG') }, + ftx: { + icon: ftx, + name: 'FTX', + link: 'https://ftx.com/trade/NEAR/USD', + track: () => Mixpanel.track('Wallet Click Buy with FTX') + }, rainbow: { icon: rainbow, name: 'Rainbow Bridge', From 126481ca383f9a5a8dc07e5cb7e9cfa252c3ff08 Mon Sep 17 00:00:00 2001 From: Den Ilin Date: Tue, 21 Jun 2022 17:55:03 +0300 Subject: [PATCH 11/63] feat: tune url --- .../frontend/src/components/accounts/create/FundWithFtx.js | 3 +++ packages/frontend/src/components/buy/BuyNear.js | 5 ++++- packages/frontend/src/config/buyNearConfig.js | 4 ++-- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 packages/frontend/src/components/accounts/create/FundWithFtx.js 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/buy/BuyNear.js b/packages/frontend/src/components/buy/BuyNear.js index 84fa77afce..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] ); diff --git a/packages/frontend/src/config/buyNearConfig.js b/packages/frontend/src/config/buyNearConfig.js index 613e920997..f94585d18e 100644 --- a/packages/frontend/src/config/buyNearConfig.js +++ b/packages/frontend/src/config/buyNearConfig.js @@ -8,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, @@ -33,7 +33,7 @@ export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, u ftx: { icon: ftx, name: 'FTX', - link: 'https://ftx.com/trade/NEAR/USD', + link: ftxPayUrl, track: () => Mixpanel.track('Wallet Click Buy with FTX') }, rainbow: { From 630bf095c0671ebdda7d980783a99870fcb15263 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Tue, 21 Jun 2022 16:18:42 -0700 Subject: [PATCH 12/63] feat(ci): Jenkinsfile updates & protection --- .github/CODEOWNERS | 1 + Jenkinsfile | 37 ++++++++++++++----------------------- 2 files changed, 15 insertions(+), 23 deletions(-) create mode 100644 .github/CODEOWNERS 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..8a9a437861 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -62,11 +62,11 @@ pipeline { stage('packages:test') { failFast true + when { + expression { env.BUILD_FRONTEND == 'true' } + } parallel { stage('frontend:prebuild:testnet-staging') { - when { - expression { env.BUILD_FRONTEND == 'true' } - } environment { NEAR_WALLET_ENV = 'testnet_STAGING' } @@ -78,9 +78,6 @@ pipeline { } stage('frontend:prebuild:testnet') { - when { - expression { env.BUILD_FRONTEND == 'true' } - } environment { NEAR_WALLET_ENV = 'testnet' } @@ -92,9 +89,6 @@ pipeline { } stage('frontend:prebuild:mainnet-staging') { - when { - expression { env.BUILD_FRONTEND == 'true' } - } environment { NEAR_WALLET_ENV = 'mainnet_STAGING' } @@ -106,9 +100,6 @@ pipeline { } stage('frontend:prebuild:mainnet') { - when { - expression { env.BUILD_FRONTEND == 'true' } - } environment { NEAR_WALLET_ENV = 'mainnet' } @@ -137,8 +128,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') { @@ -170,7 +160,9 @@ pipeline { stage('frontend:bundle:testnet') { when { - expression { env.BUILD_FRONTEND == 'true' } + expression { env.BUILD_FRONTEND == 'true' }; + anyOf { branch 'master' ; branch 'stable' } + } environment { NEAR_WALLET_ENV = 'testnet' @@ -200,7 +192,8 @@ pipeline { stage('frontend:bundle:mainnet') { when { - expression { env.BUILD_FRONTEND == 'true' } + expression { env.BUILD_FRONTEND == 'true' }; + anyOf { branch 'master' ; branch 'stable' } } environment { NEAR_WALLET_ENV = 'mainnet' @@ -249,7 +242,7 @@ pipeline { bucket: "$TESTNET_PR_PREVIEW_STATIC_SITE_BUCKET/$CHANGE_ID", includePathPattern: "*", path: '', - workingDir: env.FRONTEND_TESTNET_BUNDLE_PATH + workingDir: env.FRONTEND_TESTNET_STAGING_BUNDLE_PATH ) } } @@ -271,7 +264,7 @@ pipeline { bucket: "$MAINNET_PR_PREVIEW_STATIC_SITE_BUCKET/$CHANGE_ID", includePathPattern: "*", path: '', - workingDir: env.FRONTEND_MAINNET_BUNDLE_PATH + workingDir: env.FRONTEND_MAINNET_STAGING_BUNDLE_PATH ) } } @@ -313,8 +306,6 @@ pipeline { } steps { milestone(403) - input(message: 'Deploy to testnet?') - milestone(404) withAWS( region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, @@ -336,7 +327,7 @@ pipeline { branch 'stable' } steps { - milestone(405) + milestone(404) withAWS( region: env.AWS_REGION, credentials: env.AWS_CREDENTIALS, @@ -358,9 +349,9 @@ 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, From 00ac7c46b5242684d7109d11fddf6aa7f321b466 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Tue, 21 Jun 2022 16:22:56 -0700 Subject: [PATCH 13/63] feat(ci): configure MNW accounts --- Jenkinsfile | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8a9a437861..1956af5557 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') @@ -236,7 +236,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: "$TESTNET_PR_PREVIEW_STATIC_SITE_BUCKET/$CHANGE_ID", @@ -258,7 +258,7 @@ 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", @@ -288,7 +288,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, @@ -310,7 +310,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_STATIC_SITE_BUCKET, @@ -332,7 +332,7 @@ 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: env.MAINNET_STAGING_STATIC_SITE_BUCKET, @@ -356,7 +356,7 @@ 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: env.MAINNET_STATIC_SITE_BUCKET, From 8a95117799d5e61a4728ed31f378bb35b2bcc9c9 Mon Sep 17 00:00:00 2001 From: Den Ilin Date: Wed, 22 Jun 2022 14:25:38 +0300 Subject: [PATCH 14/63] feat: tune ftx icon --- packages/frontend/src/components/buy/assets/ftx.svg | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/components/buy/assets/ftx.svg b/packages/frontend/src/components/buy/assets/ftx.svg index 4361c86597..af229e08ac 100644 --- a/packages/frontend/src/components/buy/assets/ftx.svg +++ b/packages/frontend/src/components/buy/assets/ftx.svg @@ -1,7 +1,7 @@ - - - - - - + + + + + + From 984f2093c942301a859ba4dce75b712e683d38e2 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Wed, 22 Jun 2022 10:34:11 -0700 Subject: [PATCH 15/63] fix: use correct values in testnet/staging --- .../frontend/src/config/environmentDefaults/development.js | 2 +- packages/frontend/src/config/environmentDefaults/testnet.js | 2 +- .../frontend/src/config/environmentDefaults/testnet_STAGING.js | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) 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, From 7b24611caa04513a30e9f1f7a5d688dbecc461f4 Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Thu, 23 Jun 2022 16:05:57 +0400 Subject: [PATCH 16/63] Revert "Wal 277 withdrawing lockup funds issue" --- .../selectCollectedAvailableForClaimData.js | 35 +------------ .../src/services/StakingFarmContracts.js | 50 ++----------------- .../frontend/src/utils/account-with-lockup.js | 15 +----- 3 files changed, 6 insertions(+), 94 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 252416182b..176dab2447 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -46,7 +46,7 @@ const collectFarmingData = (args) => { } }; -const selectCollectedAvailableForClaimData = createSelector( +export default createSelector( [ selectValidatorsFarmData, selectContractsMetadata, @@ -70,36 +70,3 @@ const selectCollectedAvailableForClaimData = createSelector( }); } ); - -export const selectCollectedAvailableForClaimDataByAccountId = createSelector( - [ - selectValidatorsFarmData, - selectAllContractMetadata, - 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/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index 940ee27da8..e72e9a8ae9 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -1,26 +1,12 @@ -import { Account, Connection } from 'near-api-js'; - import { - NETWORK_ID, - NODE_URL -} from '../config'; -import { FARMING_VALIDATOR_VERSION, getValidationVersion, MAINNET, TESTNET } from '../utils/constants'; + wallet +} from '../utils/wallet'; // 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 = new Account( - Connection.fromConfig({ - networkId: NETWORK_ID, - provider: { - type: 'JsonRpcProvider', - args: { url: NODE_URL + '/' }, - }, - signer: {} - }), - 'dontcare' - ); + static viewFunctionAccount = wallet.getAccountBasic('dontcare'); static getFarms({ contractName, from_index, limit }) { return this.viewFunctionAccount.viewFunction( @@ -60,34 +46,4 @@ 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/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 330ef4d297..251c915419 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -11,7 +11,6 @@ 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 @@ -123,13 +122,7 @@ 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'); - const hasUnclaimedTokenRewards = - poolAccountId && - (await StakingFarmContracts.hasUnclaimedRewards({ - contractName: poolAccountId, - account_id: lockupAccountId - })); - if (!new BN(stakingPoolBalance).eq(new BN(0)) || hasUnclaimedTokenRewards) { + if (!new BN(stakingPoolBalance).eq(new BN(0))) { throw new WalletError('Staking pool balance detected.', 'lockup.transferAllWithStakingPoolBalance'); } @@ -266,10 +259,6 @@ 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 })); @@ -279,7 +268,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() && !hasUnclaimedTokenBalance; + const isAccDeletable = lockedAmount.isZero() && stakedBalanceLockup.isZero(); const MIN_BALANCE_FOR_STORAGE = getLockupMinBalanceForStorage(lockupContractCodeHash); const liquidOwnersBalanceTransfersEnabled = isAccDeletable ? new BN(lockupBalance.total) From 465ffa8d6fab60b7a80acbf4dbed60cd5ca9571c Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 29 Apr 2022 00:31:15 +0400 Subject: [PATCH 17/63] fix: add check for unclaimed rewards in lockup isAccDeletable --- .../selectCollectedAvailableForClaimData.js | 35 ++++++++++++- .../src/services/StakingFarmContracts.js | 50 +++++++++++++++++-- .../frontend/src/utils/account-with-lockup.js | 19 ++++++- 3 files changed, 98 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 176dab2447..1972fab0da 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 selectCollectedAvailableForClaimDataForAccountId = createSelector( + [ + selectValidatorsFarmData, + selectAllContractMetadata, + selectTokensFiatValueUSD, + selectTokenWhiteList, + (state, accountId) => accountId + ], + ( + validatorsFarmData, + contractMetadataByContractId, + tokenFiatValues, + tokenWhitelist, + accountId + ) => { + return collectFarmingData({ + validatorsFarmData, + contractMetadataByContractId, + tokenFiatValues, + tokenWhitelist, + accountId + }); + } +); + +export const selectHasAvailableForClaimForAccountId = createSelector( + [selectCollectedAvailableForClaimDataForAccountId], + (farmData) => + farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 +); + +export default selectCollectedAvailableForClaimData diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index e72e9a8ae9..b118917832 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 { FARMING_VALIDATOR_VERSION, getValidationVersion, MAINNET, TESTNET } from '../utils/constants'; import { - wallet -} from '../utils/wallet'; + NETWORK_ID, + NODE_URL +} from '../config'; // 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 = async ({ + contractName, + account_id, + from_index, + limit, + }) => { + return ( + StakingFarmContracts.isFarmingValidator(contractName) && + (await StakingFarmContracts.getFarmListWithUnclaimedRewards({ + contractName, + account_id, + from_index, + limit, + }).then( + (farmListWithBalance) => + farmListWithBalance.filter(({ balance }) => +balance > 0) + .length > 0 + )) + ); + }; } diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 251c915419..9bb8404dc3 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,15 @@ 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, + from_index: 0, + limit: 300, + })); + if (!new BN(stakingPoolBalance).eq(new BN(0)) || hasUnclaimedTokenRewards) { throw new WalletError('Staking pool balance detected.', 'lockup.transferAllWithStakingPoolBalance'); } @@ -259,6 +268,12 @@ 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'); + let hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ + contractName: stakingPoolLockupAccountId, + account_id: lockupAccountId, + from_index: 0, + limit: 300, + }); if (stakingPoolLockupAccountId) { stakedBalanceLockup = new BN(await this.wrappedAccount.viewFunction(stakingPoolLockupAccountId, 'get_account_total_balance', { account_id: lockupAccountId })); @@ -268,7 +283,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) From 919c0dc11672e8745133fb50cf3c8bda23cd7804 Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 29 Apr 2022 00:46:08 +0400 Subject: [PATCH 18/63] fix: lint issues --- .../selectCollectedAvailableForClaimData.js | 2 +- packages/frontend/src/services/StakingFarmContracts.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 1972fab0da..174775dbb3 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -102,4 +102,4 @@ export const selectHasAvailableForClaimForAccountId = createSelector( farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 ); -export default selectCollectedAvailableForClaimData +export default selectCollectedAvailableForClaimData; diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index b118917832..e8b96c3dac 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -1,10 +1,10 @@ import { Account, Connection } from 'near-api-js'; -import { FARMING_VALIDATOR_VERSION, getValidationVersion, MAINNET, TESTNET } from '../utils/constants'; import { 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/ @@ -14,12 +14,12 @@ export default class StakingFarmContracts { Connection.fromConfig({ networkId: NETWORK_ID, provider: { - type: "JsonRpcProvider", - args: { url: NODE_URL + "/" }, + type: 'JsonRpcProvider', + args: { url: NODE_URL + '/' }, }, signer: {} }), - "dontcare" + 'dontcare' ); static getFarms({ contractName, from_index, limit }) { From ec6cd6d37b9708d4ef750186784e304aa53c8ea5 Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 16:58:33 +0400 Subject: [PATCH 19/63] refactor: remove await Co-authored-by: Morgan McCauley --- packages/frontend/src/services/StakingFarmContracts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index e8b96c3dac..18d41ae321 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -78,12 +78,12 @@ export default class StakingFarmContracts { }) => { return ( StakingFarmContracts.isFarmingValidator(contractName) && - (await StakingFarmContracts.getFarmListWithUnclaimedRewards({ + StakingFarmContracts.getFarmListWithUnclaimedRewards({ contractName, account_id, from_index, limit, - }).then( + }.then( (farmListWithBalance) => farmListWithBalance.filter(({ balance }) => +balance > 0) .length > 0 From 7f4040d4eac20b0b8aa8d5e7a85fdb0774ec31d1 Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 17:10:03 +0400 Subject: [PATCH 20/63] refactor: add default from_index and limit Co-authored-by: Morgan McCauley --- packages/frontend/src/services/StakingFarmContracts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index 18d41ae321..9999a8801b 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -73,8 +73,8 @@ export default class StakingFarmContracts { static hasUnclaimedRewards = async ({ contractName, account_id, - from_index, - limit, + from_index = 0, + limit = 300, }) => { return ( StakingFarmContracts.isFarmingValidator(contractName) && From 39e12eac6c1494f5a8a8c4c0d2b5dff0ffa5b0eb Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 17:11:22 +0400 Subject: [PATCH 21/63] refactor: let to const Co-authored-by: Morgan McCauley --- packages/frontend/src/utils/account-with-lockup.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 9bb8404dc3..6cfd356211 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -268,7 +268,7 @@ 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'); - let hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ + const hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ contractName: stakingPoolLockupAccountId, account_id: lockupAccountId, from_index: 0, From 794c33046e4a994e093760300af82ead834531c1 Mon Sep 17 00:00:00 2001 From: Osman Abdelnasir Date: Mon, 13 Jun 2022 17:12:30 +0400 Subject: [PATCH 22/63] refactor: function naming Co-authored-by: Morgan McCauley --- .../crossStateSelectors/selectCollectedAvailableForClaimData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 174775dbb3..1289a9ab17 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -71,7 +71,7 @@ const selectCollectedAvailableForClaimData = createSelector( } ); -export const selectCollectedAvailableForClaimDataForAccountId = createSelector( +export const selectCollectedAvailableForClaimDataByAccountId = createSelector( [ selectValidatorsFarmData, selectAllContractMetadata, From 208b233da9d0f4f675d3676cb04f45ba2462f609 Mon Sep 17 00:00:00 2001 From: esaminu Date: Mon, 13 Jun 2022 17:13:36 +0400 Subject: [PATCH 23/63] refactor: minor cleanups --- .../selectCollectedAvailableForClaimData.js | 2 +- packages/frontend/src/services/StakingFarmContracts.js | 2 +- packages/frontend/src/utils/account-with-lockup.js | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 1289a9ab17..252416182b 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -97,7 +97,7 @@ export const selectCollectedAvailableForClaimDataByAccountId = createSelector( ); export const selectHasAvailableForClaimForAccountId = createSelector( - [selectCollectedAvailableForClaimDataForAccountId], + [selectCollectedAvailableForClaimDataByAccountId], (farmData) => farmData.filter((tokenData) => +tokenData.balance > 0).length > 0 ); diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index 9999a8801b..940ee27da8 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -70,7 +70,7 @@ export default class StakingFarmContracts { ); } - static hasUnclaimedRewards = async ({ + static hasUnclaimedRewards = ({ contractName, account_id, from_index = 0, diff --git a/packages/frontend/src/utils/account-with-lockup.js b/packages/frontend/src/utils/account-with-lockup.js index 6cfd356211..330ef4d297 100644 --- a/packages/frontend/src/utils/account-with-lockup.js +++ b/packages/frontend/src/utils/account-with-lockup.js @@ -127,9 +127,7 @@ export async function transferAllFromLockup(missingAmount) { poolAccountId && (await StakingFarmContracts.hasUnclaimedRewards({ contractName: poolAccountId, - account_id: lockupAccountId, - from_index: 0, - limit: 300, + account_id: lockupAccountId })); if (!new BN(stakingPoolBalance).eq(new BN(0)) || hasUnclaimedTokenRewards) { throw new WalletError('Staking pool balance detected.', 'lockup.transferAllWithStakingPoolBalance'); @@ -270,9 +268,7 @@ async function getAccountBalance(limitedAccountData = false) { const stakingPoolLockupAccountId = await this.wrappedAccount.viewFunction(lockupAccountId, 'get_staking_pool_account_id'); const hasUnclaimedTokenBalance = stakingPoolLockupAccountId && await StakingFarmContracts.hasUnclaimedRewards({ contractName: stakingPoolLockupAccountId, - account_id: lockupAccountId, - from_index: 0, - limit: 300, + account_id: lockupAccountId }); if (stakingPoolLockupAccountId) { stakedBalanceLockup = new BN(await this.wrappedAccount.viewFunction(stakingPoolLockupAccountId, From 38d0ef57aba1cb5473074d47292618e39b6b6091 Mon Sep 17 00:00:00 2001 From: esaminu Date: Thu, 23 Jun 2022 16:58:21 +0400 Subject: [PATCH 24/63] fix: add missing import --- .../crossStateSelectors/selectCollectedAvailableForClaimData.js | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 252416182b..7a6249c207 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -2,6 +2,7 @@ import BN from 'bn.js'; import { isEmpty, some } from 'lodash'; import { createSelector } from 'reselect'; +import { selectAllContractMetadata } from '../slices/nft'; import { selectStakingCurrentAccountAccountId, selectValidatorsFarmData } from '../slices/staking'; import { selectTokensFiatValueUSD, From 679483af32eb1936200d128bc25e5b3b7dd05e0f Mon Sep 17 00:00:00 2001 From: esaminu Date: Thu, 23 Jun 2022 18:02:06 +0400 Subject: [PATCH 25/63] fix: typo --- .../selectCollectedAvailableForClaimData.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js index 7a6249c207..bf6b4aea2f 100644 --- a/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js +++ b/packages/frontend/src/redux/crossStateSelectors/selectCollectedAvailableForClaimData.js @@ -2,7 +2,6 @@ import BN from 'bn.js'; import { isEmpty, some } from 'lodash'; import { createSelector } from 'reselect'; -import { selectAllContractMetadata } from '../slices/nft'; import { selectStakingCurrentAccountAccountId, selectValidatorsFarmData } from '../slices/staking'; import { selectTokensFiatValueUSD, @@ -75,7 +74,7 @@ const selectCollectedAvailableForClaimData = createSelector( export const selectCollectedAvailableForClaimDataByAccountId = createSelector( [ selectValidatorsFarmData, - selectAllContractMetadata, + selectContractsMetadata, selectTokensFiatValueUSD, selectTokenWhiteList, (state, accountId) => accountId From 07d109eaa59c7ab97d7ebae9d1666c854da560c9 Mon Sep 17 00:00:00 2001 From: Patrick Tajima Date: Thu, 23 Jun 2022 14:53:55 -0400 Subject: [PATCH 26/63] feat: query RPC 'get_price' to estimate fees for token transfer --- .../frontend/src/services/FungibleTokens.js | 18 +++++++++++------- packages/frontend/src/utils/gasPrice.js | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 7 deletions(-) create mode 100644 packages/frontend/src/utils/gasPrice.js diff --git a/packages/frontend/src/services/FungibleTokens.js b/packages/frontend/src/services/FungibleTokens.js index 2e10691d09..3f70bc4ff6 100644 --- a/packages/frontend/src/services/FungibleTokens.js +++ b/packages/frontend/src/services/FungibleTokens.js @@ -7,6 +7,7 @@ import { formatTokenAmount, removeTrailingZeros, } from '../utils/amounts'; +import { getTotalGasFee } from '../utils/gasPrice'; import { wallet } from '../utils/wallet'; import { listLikelyTokens } from './indexer'; @@ -24,8 +25,13 @@ const FT_MINIMUM_STORAGE_BALANCE = parseNearAmount('0.00125'); 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'); +// TODO: Convert all above constants into yoctoNEAR and add to config + +// Estimated gas required to call ft_transfer function call (remaining gas is refunded) +const FT_TRANSFER_GAS = '15000000000000'; // 15 TGAS + +// https://docs.near.org/docs/concepts/gas#the-cost-of-common-actions +const SEND_NEAR_GAS = '450000000000'; // 0.45 TGAS // 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." @@ -93,12 +99,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 await getTotalGasFee(contractName ? FT_TRANSFER_GAS : SEND_NEAR_GAS); } } 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(); + From c0cf33cded93fdb46fe07e2bdd28b5afdb72cf7d Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 24 Jun 2022 01:37:18 +0400 Subject: [PATCH 27/63] feat: add batch export to ledger wallet route --- packages/frontend/src/components/Routing.js | 6 + .../BatchImportAccountsSuccessScreen.js | 4 +- .../sequentialAccountImportReducer.js | 14 +- .../batch_ledger_export/AccountExportModal.js | 124 ++++++++++++++++ .../accounts/batch_ledger_export/index.js | 139 ++++++++++++++++++ .../accounts/batch_ledger_export/styles.js | 65 ++++++++ .../accounts/ledger/LedgerHdPaths.js | 50 ++++--- .../frontend/src/translations/en.global.json | 16 ++ packages/frontend/src/utils/wallet.js | 33 ++++- 9 files changed, 420 insertions(+), 31 deletions(-) create mode 100644 packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js create mode 100644 packages/frontend/src/components/accounts/batch_ledger_export/index.js create mode 100644 packages/frontend/src/components/accounts/batch_ledger_export/styles.js 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..49a2e332a3 --- /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()); + }, [onSuccess, 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..5232d5d101 --- /dev/null +++ b/packages/frontend/src/components/accounts/batch_ledger_export/index.js @@ -0,0 +1,139 @@ +import { partition } from 'lodash'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Translate } from 'react-localize-redux'; +import { useSelector } from 'react-redux'; +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 = ({ onCancel }) => { + 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}{' '} + +
+ +
+ + + + + + 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 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/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/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/wallet.js b/packages/frontend/src/utils/wallet.js index ccfb91ea17..02f8d88d01 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,7 @@ 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')) { + if (ledgerKey && (!localKey || localKey.access_key.permission !== 'FullAccess')) { return PublicKey.from(ledgerKey.public_key); } } @@ -295,6 +296,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,10 +631,10 @@ 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 accessKeys = await this.getAccessKeys(accountId); const accountHasLedgerKey = accessKeys.map((key) => key.public_key).includes(ledgerPublicKey.toString()); await setKeyMeta(ledgerPublicKey, { type: 'ledger' }); @@ -639,6 +645,23 @@ export default class Wallet { } } + async exportToLedgerWallet(path, accountId) { + const ledgerPublicKey = await this.getLedgerPublicKey(path); + const accessKeys = await this.getAccessKeys(accountId); + const accountHasLedgerKey = accessKeys.map((key) => key.public_key).includes(ledgerPublicKey.toString()); + + const account = await this.getAccount(accountId); + const has2fa = await TwoFactor.has2faEnabled(account); + + if (!accountHasLedgerKey) { + if (has2fa) { + await store.dispatch(switchAccount({accountId: account.accountId})); + } + await account.addKey(ledgerPublicKey); + await setKeyMeta(ledgerPublicKey, {}); + } + } + async disableLedger() { const account = await this.getAccount(this.accountId); const keyPair = nearApiJs.KeyPair.fromRandom('ed25519'); From 010dd1883bfb7fd601401303324fd535289526aa Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 24 Jun 2022 02:03:31 +0400 Subject: [PATCH 28/63] feat: add custom success title --- .../src/components/accounts/batch_ledger_export/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/accounts/batch_ledger_export/index.js b/packages/frontend/src/components/accounts/batch_ledger_export/index.js index 5232d5d101..aca44c3f2a 100644 --- a/packages/frontend/src/components/accounts/batch_ledger_export/index.js +++ b/packages/frontend/src/components/accounts/batch_ledger_export/index.js @@ -80,7 +80,7 @@ const BatchLedgerExport = ({ onCancel }) => { const showSuccessScreen = useMemo(() => completed && state.accounts.some((account) => account.status === IMPORT_STATUS.SUCCESS), [completed, state.accounts]); if (showSuccessScreen) { - return ; + return ; } return ( From 4448cc2827b6f9a831c09d67f1ce76047c916ee3 Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 24 Jun 2022 03:03:30 +0400 Subject: [PATCH 29/63] fix: return null ledger key if local multisig key --- packages/frontend/src/utils/wallet.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index 02f8d88d01..859f224474 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -202,7 +202,7 @@ 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.access_key.permission !== 'FullAccess')) { + if (ledgerKey && (!localKey || localKey.access_key.permission !== 'FullAccess') && !this.isMultisigKeyInfoView(accountId, localKey)) { return PublicKey.from(ledgerKey.public_key); } } @@ -658,7 +658,6 @@ export default class Wallet { await store.dispatch(switchAccount({accountId: account.accountId})); } await account.addKey(ledgerPublicKey); - await setKeyMeta(ledgerPublicKey, {}); } } From 39d817db9401ec4e0fc901570efebd5dd0276952 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Thu, 23 Jun 2022 16:10:16 -0700 Subject: [PATCH 30/63] feat(ci): don't build bundles that won't be deployed --- Jenkinsfile | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 1956af5557..2f6eb35659 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -145,7 +145,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' @@ -161,8 +162,7 @@ pipeline { stage('frontend:bundle:testnet') { when { expression { env.BUILD_FRONTEND == 'true' }; - anyOf { branch 'master' ; branch 'stable' } - + branch 'master' } environment { NEAR_WALLET_ENV = 'testnet' @@ -177,7 +177,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' From 6d69fe3da72467c22a22da4f14f3fefcb559e38e Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Thu, 23 Jun 2022 16:12:57 -0700 Subject: [PATCH 31/63] feat(ci): deploy mainnet staging when merging master --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2f6eb35659..213a2864ba 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -325,7 +325,7 @@ pipeline { stage('frontend:deploy:mainnet-staging') { when { - branch 'stable' + branch 'master' } steps { milestone(404) From a46b56fd838b099adddd89a2dd5890423132f2f6 Mon Sep 17 00:00:00 2001 From: esaminu Date: Sat, 19 Mar 2022 01:54:30 +0400 Subject: [PATCH 32/63] feat: pass in state_cleanup contract --- packages/frontend/package.json | 2 +- packages/frontend/src/utils/twoFactor.js | 3 ++- packages/frontend/src/wasm/state_cleanup.wasm | Bin 0 -> 96755 bytes 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 packages/frontend/src/wasm/state_cleanup.wasm diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 30337d790e..d3cf062a5f 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -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", 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/wasm/state_cleanup.wasm b/packages/frontend/src/wasm/state_cleanup.wasm new file mode 100644 index 0000000000000000000000000000000000000000..e3561bfcee90f4eaa8e2a675843d06b86bed6d7d GIT binary patch literal 96755 zcmd?S54>H~Rqwn0?0@I%eNJ{l{v2`wS$j9q@Fyiz8YrO6+)rpJt!;h1y?uT6b7_QF zI7xvdhqS#OayBHTX~h;56)U#sA8$}W+lq=x+q7ckeoA}Mi`S~$`>4grRn%NB`q0wy zzP~Z&T6^uY_kr-o`|k7^u|pqJXLDhgSetv=`+?JS*Rd`HBRm{h@-%;>FtLjl@Qt zs5(_LDwm7iUi0~stf^*0M|!}1tSOqD@EP{4@Fh?e1XqP0{nmYf#VGliiLL3*tM^as z-MQn6%l7Vk$5s30ckYcQl-9~$FWbHI>S$6)t1iE8@7|qP&tJCV^2@Kej^C@Uh?>e+ zx$mlXT)lJev!-9W%a>n!?cSYN?7V#EzJ1q>epJ2c>T9o?_XV3jwELRNcU^YX)mQ9% zchpi-D}2iQRqx!nZ+^!+ul1>|o!4G-`IVPlx$~-bTsiNPruNNWvv0K z=Yapvj@>)1zIhfq>pHJHJW!KIB@OeJ7vHO~LlwK#o>&v(K?CERwUh~eY z_U*jvawt8&Gg_faT6yC9l~A~?q-LH3@X<=8tj<$@XvaJl4&PhWs^KiFWtF~p$#BMw z%jd7U=IZci&LbZ6o5q#|)4Q*E&t;cibM^dQKK2;tjxGAb*IgZI-?4jlbdp*>Wo$ue zx_aLa=ghr+Y#A78*Uo6QD%mo&z>X`fxNO(X_gwa_tLCq~?1!)0y<5G3i~ToepU5|5 zaM!4%jbp#MV(0Fi^E=g;@BMg`hEjoleaSU>eS3HAyKeXVKG4We^*mazfzR_4E+rbX z*@!Mi&%5?s1;4Bnq^*2JNm@ik>FGhzJFnTl(WUo<%F8^S zZ5*r_q|RS6zhigEYo<@ce;=po*2Qr;b25L{ub*8v6UXtobp84yP1D5x*^sW^kfs}= z_0cKQ>(@_j*pRH7UAI1(j@GT?|5~-8KMnraPvz88Ppu`jT5Wb?*6GwMr*>;EdC8_z zPi=H6-&L=7POYR-nygDtduf`+>yu<%67xV)jm>F1OQY-80};Pze7gOsT>E=Qnx1)< zk_1(nL`h`-liuTqb%jenH?s%adpteu=L z#;)H#7Bv?mcjP;z-};tH?VUT{dClJUBvJM9-8*+&%{+10W#D(mWjn9FB8{7uAq{t2 zi!j@_GfgJG6~~c_n;(e(@3;KV`2T!5{zL<@nFzo03Q42jahsAB+Dg{_FVf;;+Zwil2(V9e*eO=lF2)W61}T z4<$dH{7iCNa%=Ka$%m8Mlb=uCm;7up`RHSnzmGowFD<^L&!5O;ySm9HdrY=x(dMMl zO+n?4KQ8PSqNe>Nx4wuGE4K0S%fP1}~KO4`4sFZGej<~%G})O>Rsmtagodg@lsIjbji zpZo`a+LpAlv=^V70C5&?OE&OB;tW5jo0(IKX_mDA#)JL`>UpxAuErkpx0R{MA=mqR35NJVm9#QZ-92nM$)NU&YO=;*%;Q=~I#`R7GMZ zrfZ9H-Q=RFN-JtM3$%W%jMgIcv2rT-O5OdI^49_VG`@}QLI8T#{P*A~+3r@i|5VxC zl%A}1^kj2#`**+(2<+43SYMY0-1?8r@n}eZy1?m1}dLu2To<8W`D@Gz=vjxIyW)98a~Z z4t4?1P0%@EHFZ^WlMmDOud4$c2$aHCfg_UUwQ-U|IN)ZgHzpMnV^|XeJS%I0mcW{# zU%{Gj^OMN7#pFO3L13lgVexp#{}mTCU=eW|W678*s|pR>s(abRV5RDQFS>Xt;rB1} z+w8?40w#fVJ9VM3lF>4qjoQCyG^~Ji7r&ZMAwJa_8#rhf4VS)WsB|=1I;HN@(?+M> z1NUHAKgsqUIKIAH@5TSuRMbl>BTIk#vps6_e-_`{sY3}r03#5X)z3|-UZ}3|q%n{a zrg>kQG*Gz*SgPh&@|Y1Hbn97-5lA=dutb5mx*@IG$kEoB$ZA<-E^F-4XqxncG1;C? zWOc)5)4g%7TXRpxd%7(*sR}EG-WHgl?rQr}z;E1V$O0cUS83H?q~2}6_RR-=mcJ*z zbtYr1SNgnKpQ8sr-0MGSgm{j+XS>*Rs% zR93sI_jG)40g*ZNHpXfH#$$a+*Sl}vw~($Ci3+;=15=C|##ke(zgk0LQsSfnOjR2Z z&(JSM%>bg`7Q9V9$_cZg>26VjuH$YO0C~d&I5+=jDfPcr(mGTuWHH=c^>lP_tEZT! zr}Y@ufcrE6=rP|{kly3_VJ&wjecQ;PQk#HPhk=ZJ>Yjb7u3#N6bCxy}H~9*f=vTTu z3)SY8G~J|x-qI$%3|B?3v-zg(#A5JH1RFKzSV6HjX0`TbMY2|^ChB*Jb6UOZ@X@Sw zv^(WCUWa^i4+a9>TP7gIn=X1yHkCCsu%=$KkLo9~2J!;54Dhn#BR;ELM4eCbr;d)* z@4p+tQ->Z@HY@g1rH!oentijKhKRGIn1KWyjI07FhT>dPz8zV9z0~@5y3iWx#C-1QBJWT+WbUP!9ua1zatU~ zp_j9^F>M90w(+`d9dWm>TX(-Mr2*(_-yelPI~b}=xj%p`uYLcK`~UjA|LcLTFMa|R zuDH7sDN{q=-<@2_cy5B%blyGrAcJl0%rxqNVGvFKYFXps-P&h+LbLse4t_o~b$s!M zY`&QC9A69rIG-U?foB-#%Vjdss2Z@qhDq$K_TemNH`SLQ#?w&4<#smQSq= z{5Tijh83INlb{2Na3n8)&W}O4g)hCRR=e2d6<% zDAAh~RRd-<{;5=CXL6D_h7NFir;5N`X4G2?h5k{hmf`8gIKm`M4^>nB2C07JSHJjetZt0a zLnbSc@#%B@KXDd513ptiW-NAFRZBXMNtHHD0R!QY~M(o9#2qX5#MO( zOM#d-D}3i7p+w?9%q6J!Ts+Vn2Q!)wV zJwf!N)H0SqAPSm@TFK#AeryVLjL9&}qbPsqKl1>+xl*ax>|EBff{vcgw+(P_U{^)V zG*=#SZ5ky$pFpKgf_dwjG!PrenktLV6qu>|;aty$vrW!vK5H&lcSTcLMN0x0ehJzJ zYF005|ArV>(o7SxDNPcOww4&eIMP^3--IUIgt{ALbxus$7*^*dq|VX9lujkgOP^Oe zlAMODG^p3c@?O0bI!w_`&rBOgN~2PbQx8;=cCFsKvsjx@-mG1Ng|8SI9m@=~bzXw1N76w#YNas99ed%f@tLrU+F0)}{L z08DLHr|OHV`ze1ld5h{O(_wg-HeJ(tMFL5J4k2ZNFO?FEn#&hrbL%}seT@pBwo*N8 z&wS*kB@eX}Xj{sR&d8{xec=Lq_<k|o%!6UDa%j!E=G)2I#?@gDze2aGtiMoA%tWn zvLhZWXqmxG?pC!9i&`t25+4)pVMaNNGuOp@;G37_w9%a)6 zO_Ce0SV-I`Ty!UU@xeDj$Vn~R0kH+mNB_$vkGjTw(_b3yZ#12N!iM|0O)L$~NoXkU zY>Cr`@k`p8(JJ>Ni*wV}%DQ#ys_XRn6TCL6YuB!=t*tlKio1-FU<_tG2|3jg4|t;? z#2A7bbFQml$@E1qKwt&tk0pJNSZ3rkQr z^&D%M8mU2y^IXU|F;atxo49>2@a5A_r(-!2R&5{dHjiS-=|7q7L9On8Dn&xkJdgM2 z5iaTSD{In5}shi%$*Qt}ZK)wV^dPAdKiowXY8~nF3 z_U+2?-x_BvH_D7)lfC|T(u2MJJ@K0WId$iaHgutH=sas^^$Tt2uR=pxM;kiFH?;Kx z8d{D%p-=6@M@14SlM^GAX0#OjXZZ#RWut)P(1@wx-&Y zEHxVMJwI~1AuHGJcgOU)-6hCWEFdX>xAwb7EOF?IUhzdyhfDcrQXdT}{y*$()qZz; zP(^Kj(XMJ~sD(i#O#3VVv=Y5|YFa<;W}7SjR!SANsN7$AdwB+hPMT%?1h+!nMwI{q zjEwdT8kjBC1}5Zfnt$BaWhsvjrAQUyO zuNWxJm}m-TwLU}1mW3wj#8M6u!6gj?1xPwFmM8Nx3)@L>7sROPzb^! z_2n#`rrXd~10y6qX8)m4TESzCc4UfRFz>N&eiz1fw+{2BLot|lKiThB_FzJS=m}iV z_4Z3_m(p*uL`tEo*O;{Tn4QUCY?F3Q|7z}ho51xMN{d|0kvYmBNaBr^m<>&l%59ho z%aW}xDJ`mpygKhspQ*d*+rLz2#Z0U&<-{FySxXx0@<2fa%TTEKtvW^<*!JCXU7&Ug;$8rJ_nvMv*D2Np;tnxT zYOKVpOPP!Ukuc_<*TdgF);C7SOz$V|1l3Y<93!aJ$x6nLjV>;e#G#RIPVW0>zH$dt z@&0%1+2-G6cZ=Jf)M7$R#)Mv>UB|Q^JNSNE@{qN~vQRk(_>8InJ>DCh7=?5n6A$(- z#TOtaB}P1P?JGK%&>?~Fh_B37jvjCC9=fktRy5b8I{#cWlWeCqdAC8iJ_vyMN5coq zwmEr9wFp}#?sUwf?r|0F#3@*nZbSG)772+gG3hKF!~70*G(KWE zadPxOVU0x8@bpHLM(`xH03@qFpwy;STpnxx!8RxsHF$WSng{Lehi+qrcFxi1Y z4RL6jK7s{!#AtjAq~MMB_D3|e`W_mBwb(0=4e?$)(lrsY29GhFZ_3*A}?|42USA+vs{T!d{re_){m56itzuxU+#`D z3~40lip8gsC>Bq9zR*l1R`;`{h)#e`#hD{0mmm4^T;V7y!7^~dVp?zVzb0fA$fsG2 zmHb(4hUWsNg@FeKn^@#0eNbhRBXYN>-xA4@+Z2pd#HO5^tR~@(LzdU(@sOa{mYfA& zM5xBwFn{Su`pag1+qvCKL?|qBadS2Jx0zi3RNB8Inb-Wn>ZCQW86C%F6bGncEdOFN z4ZA!3F#tvdvW>;=y&i^5R2f_7S!PdyVFM}mp<=^-w5V{kb4Yo@2e56mVzRJpAN`MF z80qf)CXMm9b*_E4V3b(bK)}}kXDrDw+CnTbM)Ufx|FYZ=mVMiLsDzjMw-y}1IS~B(4H=JR% z)1(2t=&e4i?&=Ug;0=y7hn?(8wA~I0Gu1LgDh~G%+OzM(M5z;#X}J>82@Z6pB-t>lB+?PeW9B8U2=Pn5_nN0E{pwPvk@mAbGpDY~LgdFPSzHWYDSoR`sNiw;;1KKcrF*2Dk{1`k)9t+&r21j`@+j)P z>flY;)S(VBWFFUw9YQ;$s0bh%Z4)S(NYISQ@zPn(%!{3b_eWeA{HfvZ%FO z>>g))D#Vh3AX0*E#rI}&SAo3Z>; z>UrYtDY<`Q-;r&}m02uj$J9gPXufOth zznQ%WNAO9IN(=~1wYaNUOG;~K!ADw>)fHm^^a6(pi4U=#*H5*mv{pbrFfpr;)*vaS z3N4C#j!@-mS#vV>@I28XoB3NK)qQ1q@7JxNwH;s+L;w*k% z&o>Evqnwt~05!PVn(UA0Yq!-sSVq3pT?J+Ra@~j~b1}6otX`o4Qdnw!oa%*jA#n+G zQ8BGGuNFG=7jZ4#rS>O`Zy2@oK~is)z_lXGJtQNm(EhZ%YzcPL6D5@73<_fEcy36_ zv?9e|BjwG9#z&4O`t-UK8b2I7KduB`yCs5qRJebv6jyW%1v1)1Lu$$0_dzhY&D7U> z{iD=ZQs{tuz2@DG+=V5tuj#uR$iCm}yE8oAr0TYzMXJuNUd!dF&C_|7*5YX^DKllw z_gI08xH-d@sN)cq9~Af2O7kB_&6Q-lLUHC80qzhDnlC^_f82wtJ|6NQpJm{kxQhPM z(-no0C;WYhA%!1JDH+{sZHuGhB>T1jododD6Bv9 zmBER4)B2H-Qn>^p6@F<1DO!SwKC{jxik^~I#TY?I`#=oX4W1P&7^MHq<`-pWbMAJF zq&}vO+v+ukR}gQBX3YI)DmZbOlG$;FCu@e>8ZU`kGrq05#I8XLsG8yUjFKdpmy@Ma zX98Vf(xQ@B_jeTDKdDs4WGmh7P}u`kyz|;@mT8U^*5-xHlA2BE3lLR~u8+xCss~kp zA?Xr0xG_pq27U8SOcxmL7`bN0t4Px_+g0MsFeeP<2t-3-w#*Ayi&)-?d!c7JL5S}X z+!@o_iw-+_pr8~55?St$Tf@+KJC87cZvrrKrt;`BraCe7iK#3OU<^}@X%pNMARyP9 zsoWkV5*Dk0(4?J^$aAv@Zei(7Y3mAO8qBtk{dne3iBNJT^xPH-C3WdZDvN5R*gJ&E zR2GnpARrqh0cp5_x9=VBHiF_lCS{WhJbk>;<10@vA!npA@k|@R7N9s z_c08zhi2wg?-EgWv_Ory_2mnR=h3Bz;X+B7WtWW{G;B?~tJh-ph33YKTr!WUJayDh zL|MZ~4nn=IB%=fS(lQid1yZ=gB3GI-d&2jftX0g&l1nueB+0!f{Kd+DNrl%)==i)9J2U#Q@0U&@%}DWS!6R`KuGM* z`w-Pkm?|?5_8&4!Ekvap3zMpzm?DD(x2qr%7oo%AMi}@IP~s>fQZS7|3U$Hi;~(=A z@T&D8AR=-srHX!*2xhaFqK68a0G2}~#Vkxa>8`z0Rhh>>!dtX!&y^$tXLTu@HNhzf z;4RJadgO4XLvT_?aGJ9shjRd;qSIuN&Jxs)04Y$+X&oDIZ1BpX06ao#T(WNia&XRc z48H@Kpq+rUjeF+1^6k(U!mMc4Tf0;5(!|5kz)b)uC8M%fKPZaAsD4;O%R4JP2h+2l z^5*86jMAHv`$as##-q zW-^_ZxI?P~$zRVrJZVcv&N*7I2np0!@XvFm2g}=GdawiSdC~(S?%A)= z8Zq_la^Hun6+b{RW=cbEW0=}j7fV8Fh}_AVNcO7^9yVS)JKte()TyR?nUfhBo9Uk) z(c5;i)YJp$iIVDj6D(W+VbrG_(1FWS_PffSIV7~nj*Dj@aZMb7@rR}|7)^7Ep9e5~ zpoUfPt_cJbA5Fg+9JIkG-^*~92=#A!t{mxA(j4$6qds|q_{l)NWY2&{wbaac)FPP~ zb7D<;rUxVzJgRxh3?e{RVkeIXE%}pOEUYQwxfuDdLYtY)oiA$UEV`6~)wLkpx>SOG zt9N4*VN~EU7%=+Kl}smw3tK9=kNVu>AvYAZRB|8pxle}NP}ow*eZuGVzhx-R z3tK9=Px;)#AvYAZRB{g~d0TQz$PI-pm0WV3^xbwh`+~QHg5f($rC`XtHRRgww&ck0 z>+Rvyezzre48MLdyxQ-!j#nOYR?jy)V4l z@3!Q@;nxSktNlXi@cNK`A@y*srGkxzeePq!g)NobM}6+`kQ)kHD!GsQ+$TeBC~T?Z zKH+owQW*w?EtS`&eD2|p8wy)0xrdYtsY7lkY^mhl>~n7mxuLM7l6$MqJrZ(5VM`_V zcAtAk$PI-pmE2GI+)stvP}ow*z0>F36LLdgOC|SipL;CihQgLg?!7+u{*W6ATPnHt z`P>IXZYXT2ex&E&Eur88z9374x0)zg66S)q$KkS>bpUmVPmSurK z{CHPasvl%FmQ`dJ6B}kgeM@04xFWYgs{nN7cas&Mk+|EXGQN4FVnxTcC|ts)q{Jm@ z+D81VUUJ94Bw3 zh=LIRnj;CGK0&(ph_%RUy=fsGz#(yJ^q1<;SgFcH#E^{95J7{rfv4sQ4KyFR zG9<{>ky_-`3#_O~EYVkDI|fPP%*Gn0=x7C^^Ath~OKJE^sjP*yb(I925)-q%5*?D& z0vavJh!gL6;uQ!fFR24@OcDXNC@ojgn539E9g3a2LvE#eOuxDJvjX0m_Rk&vw!x8T2WtFnEjA8R2JR7Ua*H-G zS@dC|6Z=oZJtmb6thK+cFToL5*__-Wg;mQ!MJ3uxvK^EG#^&U31TU}}Yxqvg3-t3&H|BDKO5_I&o zs3m5^wZEaV-W&>!a7b*S>OQN?QSCbaqZ{XphUafct^ zxBaNvVU)c8KPmM~qNBD`JZP_v=-=F*nID0Z#5{Y^tEG;YbXBf(NF5yplcYeERGSS^sp+DzgTax>hz zwG#54e6BDPPm@*eQbpjewbERmVk2wM@aJPmBA3nC7JUHkvr%+&2GWg>Waw)%>|}U> zv*bvn3n7wkgZd%q0Pux_%#JNX%gTesC2I#k<1sZ`R0KVoVN#CeAVbe#9+ymk({$L1 z9AvT=L~Eh+v^CnB z2~9Y4r`Ua2YuFZL(U*Ucv&~lNL-cHOa!vX6m1zHw zzNR?6-V$vVaD-7~^`fdt5!Yx8uG-!$Ao8;D%iIDsd2go>J{@_>4d{fZlNu2wNVXQ6 zC<7`XfDzM>2_z~jXDkTB%Sz!1MZw^s-lIBY>MWZN=C(v=a$=YhL+%s| zqg^05!yu}rK=DO+yk1UA5O4m}hWj?S9r#ljoa5+FYpu^+s)`HOaLl(%)|Mhy3K_}~l)mTAzp!Y`m(#J1{aLj?~q1+3vClnSFJ ze7IQw+#)}v2tw-LsGgmW_xgpG&6*jExu=`79~0~bq8YSpt_`4Nwtrca5SIb_w#2}O z#S7}d>_#1Nx)_H~2O~hEuHI?2PBk2CD8_*j|Aj`usp5Y4%(i*ilLQ-JxU*i(9({HC zBF;Ck4rpq{scyoYi59Vqqpqbnz|WNOP~_%;ktc*4TTjbGHUsOfI^no?%ile3`W6V`(v7blHqQ&|g^WjS~W}=#&Cg&>%;+ z4S*59`LNq)@EPYJEEJX^NcDuwg#z^;YrWcK0LQtC5)Y{;Wwk2+EVb)70L

>|+8L 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�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 literal 0 HcmV?d00001 From 85d3f329558028ab6d1906188c0cb629dd812854 Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 14 Jun 2022 01:40:50 +0400 Subject: [PATCH 33/63] feat: upgrade near-api-js --- packages/frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index d3cf062a5f..b31f520b02 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", From 58c8be25d5c496b86cac9e1c67fe70c22fb4cd1f Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 14 Jun 2022 02:06:33 +0400 Subject: [PATCH 34/63] feat: move cleanup.wasm to buildOutputPath --- packages/frontend/ci/ParcelBundler.js | 1 + 1 file changed, 1 insertion(+) 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; From 868dcd1799a35bf505e5e9c858a9d83edcf162da Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 24 Jun 2022 03:36:56 +0400 Subject: [PATCH 35/63] chore: update yarn.lock --- yarn.lock | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) 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" From fcc601e29372845189cda10255dba24c44620edb Mon Sep 17 00:00:00 2001 From: Patrick Tajima Date: Fri, 24 Jun 2022 11:12:12 -0400 Subject: [PATCH 36/63] fix: skip await --- packages/frontend/src/services/FungibleTokens.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/services/FungibleTokens.js b/packages/frontend/src/services/FungibleTokens.js index 3f70bc4ff6..e5ea2290ac 100644 --- a/packages/frontend/src/services/FungibleTokens.js +++ b/packages/frontend/src/services/FungibleTokens.js @@ -102,7 +102,7 @@ export default class FungibleTokens { 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 await getTotalGasFee(contractName ? FT_TRANSFER_GAS : SEND_NEAR_GAS); + return getTotalGasFee(contractName ? FT_TRANSFER_GAS : SEND_NEAR_GAS); } } From 740e176149f1f07d2da255174c78d0629b65c01d Mon Sep 17 00:00:00 2001 From: esaminu Date: Fri, 24 Jun 2022 19:13:35 +0400 Subject: [PATCH 37/63] refactor: localKeyIsNullOrNonMultisigLAK condition --- packages/frontend/src/utils/wallet.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index 859f224474..bfe96ee969 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -202,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.access_key.permission !== 'FullAccess') && !this.isMultisigKeyInfoView(accountId, localKey)) { + const localKeyIsNullOrNonMultisigLAK = !localKey || (localKey.access_key.permission !== 'FullAccess' && !this.isMultisigKeyInfoView(accountId, localKey)); + if (ledgerKey && localKeyIsNullOrNonMultisigLAK) { return PublicKey.from(ledgerKey.public_key); } } From 958005697bcb9fb5ecdceeaa002ca519b2aa6e6f Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Fri, 24 Jun 2022 12:21:19 -0700 Subject: [PATCH 38/63] feat(ci): only bundle mainnet when it will be deployed --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 213a2864ba..3a2a65d791 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -194,7 +194,7 @@ pipeline { stage('frontend:bundle:mainnet') { when { expression { env.BUILD_FRONTEND == 'true' }; - anyOf { branch 'master' ; branch 'stable' } + branch 'stable' } environment { NEAR_WALLET_ENV = 'mainnet' From bddfbf25bdcb839d9aaa77007218eb467b52b614 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Fri, 24 Jun 2022 12:22:42 -0700 Subject: [PATCH 39/63] feat(ci): don't test other environments when deploying to mainnet --- Jenkinsfile | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Jenkinsfile b/Jenkinsfile index 3a2a65d791..ff4286b408 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -67,6 +67,9 @@ pipeline { } parallel { stage('frontend:prebuild:testnet-staging') { + when { + not { branch 'stable' } + } environment { NEAR_WALLET_ENV = 'testnet_STAGING' } @@ -78,6 +81,9 @@ pipeline { } stage('frontend:prebuild:testnet') { + when { + not { branch 'stable' } + } environment { NEAR_WALLET_ENV = 'testnet' } @@ -89,6 +95,9 @@ pipeline { } stage('frontend:prebuild:mainnet-staging') { + when { + not { branch 'stable' } + } environment { NEAR_WALLET_ENV = 'mainnet_STAGING' } From 896fcaec9f7a4454ca6977251df942cd7423abd8 Mon Sep 17 00:00:00 2001 From: esaminu Date: Mon, 27 Jun 2022 20:16:42 +0400 Subject: [PATCH 40/63] fix: localKey.permission condition --- packages/frontend/src/utils/wallet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index bfe96ee969..789f7edc60 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -202,7 +202,7 @@ export default class Wallet { if (accessKeys) { const localKey = await this.getLocalAccessKey(accountId, accessKeys); const ledgerKey = accessKeys.find((accessKey) => accessKey.meta.type === 'ledger'); - const localKeyIsNullOrNonMultisigLAK = !localKey || (localKey.access_key.permission !== 'FullAccess' && !this.isMultisigKeyInfoView(accountId, localKey)); + const localKeyIsNullOrNonMultisigLAK = !localKey || (localKey.permission !== 'FullAccess' && !this.isMultisigKeyInfoView(accountId, localKey)); if (ledgerKey && localKeyIsNullOrNonMultisigLAK) { return PublicKey.from(ledgerKey.public_key); } From 3229fead466a8b0851d757b25962630082aea7a0 Mon Sep 17 00:00:00 2001 From: esaminu Date: Mon, 27 Jun 2022 21:01:56 +0400 Subject: [PATCH 41/63] refactor: addLedgerAccessKey, exportToLedgerWallet --- .../accounts/batch_ledger_export/AccountExportModal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js b/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js index 49a2e332a3..493ffe9e76 100644 --- a/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js +++ b/packages/frontend/src/components/accounts/batch_ledger_export/AccountExportModal.js @@ -69,7 +69,7 @@ const AccountExportModal = ({ account, onSuccess, onFail }) => { setAddingKey(false); } dispatch(checkAndHideLedgerModal()); - }, [onSuccess, account.accountId, ledgerConnectionAvailable]); + }, [path, account.accountId, ledgerConnectionAvailable]); return ( Date: Mon, 27 Jun 2022 21:02:48 +0400 Subject: [PATCH 42/63] refactor: addLedgerAccessKey, exportToLedgerWallet --- packages/frontend/src/utils/wallet.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/frontend/src/utils/wallet.js b/packages/frontend/src/utils/wallet.js index 789f7edc60..dffef7ac5c 100644 --- a/packages/frontend/src/utils/wallet.js +++ b/packages/frontend/src/utils/wallet.js @@ -636,11 +636,11 @@ export default class Wallet { const accountId = accountIdOverride || this.accountId; const ledgerPublicKey = await this.getLedgerPublicKey(path); const accessKeys = await this.getAccessKeys(accountId); - const accountHasLedgerKey = accessKeys.map((key) => key.public_key).includes(ledgerPublicKey.toString()); + 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() }); } @@ -649,12 +649,12 @@ export default class Wallet { async exportToLedgerWallet(path, accountId) { const ledgerPublicKey = await this.getLedgerPublicKey(path); const accessKeys = await this.getAccessKeys(accountId); - const accountHasLedgerKey = accessKeys.map((key) => key.public_key).includes(ledgerPublicKey.toString()); - - const account = await this.getAccount(accountId); - const has2fa = await TwoFactor.has2faEnabled(account); + 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})); } From d49ce85955b90c1b84e2f5608f6c2acd5c8bcc16 Mon Sep 17 00:00:00 2001 From: esaminu Date: Mon, 27 Jun 2022 22:02:00 +0400 Subject: [PATCH 43/63] fix: typo --- packages/frontend/src/services/StakingFarmContracts.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/services/StakingFarmContracts.js b/packages/frontend/src/services/StakingFarmContracts.js index 940ee27da8..d8372c0e4b 100644 --- a/packages/frontend/src/services/StakingFarmContracts.js +++ b/packages/frontend/src/services/StakingFarmContracts.js @@ -83,11 +83,11 @@ export default class StakingFarmContracts { account_id, from_index, limit, - }.then( + }).then( (farmListWithBalance) => farmListWithBalance.filter(({ balance }) => +balance > 0) .length > 0 - )) + ) ); }; } From ef9be31500b4b039767fb1f31c1346bc8d4b71ed Mon Sep 17 00:00:00 2001 From: esaminu Date: Sat, 19 Mar 2022 03:59:00 +0400 Subject: [PATCH 44/63] feat: add disable-2fa script package --- .../disable-2fa-with-seedphrase/.gitignore | 1 + .../disable-2fa-with-seedphrase/README.md | 9 ++ packages/disable-2fa-with-seedphrase/index.js | 83 +++++++++++++++++++ .../disable-2fa-with-seedphrase/package.json | 11 +++ 4 files changed, 104 insertions(+) create mode 100644 packages/disable-2fa-with-seedphrase/.gitignore create mode 100644 packages/disable-2fa-with-seedphrase/README.md create mode 100644 packages/disable-2fa-with-seedphrase/index.js create mode 100644 packages/disable-2fa-with-seedphrase/package.json diff --git a/packages/disable-2fa-with-seedphrase/.gitignore b/packages/disable-2fa-with-seedphrase/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/disable-2fa-with-seedphrase/README.md b/packages/disable-2fa-with-seedphrase/README.md new file mode 100644 index 0000000000..3786d11514 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/README.md @@ -0,0 +1,9 @@ +## Disable 2fa with seedphrase + +This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase + +## 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' +``` \ No newline at end of file diff --git a/packages/disable-2fa-with-seedphrase/index.js b/packages/disable-2fa-with-seedphrase/index.js new file mode 100644 index 0000000000..b2c7a73394 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/index.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +const yargs = require("yargs/yargs"); +const { hideBin } = require("yargs/helpers"); +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"); + +yargs(hideBin(process.argv)) + .command({ + 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, + }) + .parse(); + +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/disable-2fa-with-seedphrase/package.json b/packages/disable-2fa-with-seedphrase/package.json new file mode 100644 index 0000000000..ffdd6711c6 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/package.json @@ -0,0 +1,11 @@ +{ + "name": "disable-2fa-with-seedphrase", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "near-api-js": "near/near-api-js#feat-disable-2fa-with-fak", + "near-seed-phrase": "^0.2.0", + "yargs": "^17.3.1" + } +} From a3d1db181341ed05e4ab00dc95ea0887065636e3 Mon Sep 17 00:00:00 2001 From: esaminu Date: Thu, 24 Mar 2022 00:51:30 +0400 Subject: [PATCH 45/63] fix: remove old near cli 2fa disable script --- .../disable-2fa-with-seedphrase/README.md | 13 ---------- .../disable-2fa-with-seedphrase.sh | 24 ------------------- 2 files changed, 37 deletions(-) delete mode 100644 packages/frontend/tools/disable-2fa-with-seedphrase/README.md delete mode 100644 packages/frontend/tools/disable-2fa-with-seedphrase/disable-2fa-with-seedphrase.sh 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 From ba708a943bbdd5cda38ebe05f6323a8c84aed00e Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 29 Mar 2022 01:23:27 +0400 Subject: [PATCH 46/63] refactor: rename module to utilities --- packages/disable-2fa-with-seedphrase/index.js | 83 ------------------- .../.gitignore | 0 .../README.md | 0 packages/utilities/commands/disable2fa.js | 78 +++++++++++++++++ packages/utilities/index.js | 7 ++ .../package.json | 2 +- 6 files changed, 86 insertions(+), 84 deletions(-) delete mode 100644 packages/disable-2fa-with-seedphrase/index.js rename packages/{disable-2fa-with-seedphrase => utilities}/.gitignore (100%) rename packages/{disable-2fa-with-seedphrase => utilities}/README.md (100%) create mode 100644 packages/utilities/commands/disable2fa.js create mode 100644 packages/utilities/index.js rename packages/{disable-2fa-with-seedphrase => utilities}/package.json (83%) diff --git a/packages/disable-2fa-with-seedphrase/index.js b/packages/disable-2fa-with-seedphrase/index.js deleted file mode 100644 index b2c7a73394..0000000000 --- a/packages/disable-2fa-with-seedphrase/index.js +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env node -const yargs = require("yargs/yargs"); -const { hideBin } = require("yargs/helpers"); -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"); - -yargs(hideBin(process.argv)) - .command({ - 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, - }) - .parse(); - -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/disable-2fa-with-seedphrase/.gitignore b/packages/utilities/.gitignore similarity index 100% rename from packages/disable-2fa-with-seedphrase/.gitignore rename to packages/utilities/.gitignore diff --git a/packages/disable-2fa-with-seedphrase/README.md b/packages/utilities/README.md similarity index 100% rename from packages/disable-2fa-with-seedphrase/README.md rename to packages/utilities/README.md diff --git a/packages/utilities/commands/disable2fa.js b/packages/utilities/commands/disable2fa.js new file mode 100644 index 0000000000..261e6154f3 --- /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"); +1 +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 }); +} \ No newline at end of file diff --git a/packages/utilities/index.js b/packages/utilities/index.js new file mode 100644 index 0000000000..23347f5b3d --- /dev/null +++ b/packages/utilities/index.js @@ -0,0 +1,7 @@ +const yargs = require("yargs/yargs"); +const { hideBin } = require("yargs/helpers"); +const disable2fa = require("./commands/disable2fa"); + +yargs(hideBin(process.argv)) + .command(disable2fa) + .parse(); diff --git a/packages/disable-2fa-with-seedphrase/package.json b/packages/utilities/package.json similarity index 83% rename from packages/disable-2fa-with-seedphrase/package.json rename to packages/utilities/package.json index ffdd6711c6..f77a4b1b34 100644 --- a/packages/disable-2fa-with-seedphrase/package.json +++ b/packages/utilities/package.json @@ -1,5 +1,5 @@ { - "name": "disable-2fa-with-seedphrase", + "name": "@near-wallet/utilities", "version": "1.0.0", "main": "index.js", "license": "MIT", From c07dbaeb03fe6e349efdac40f35fc389ea993944 Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 29 Mar 2022 03:05:41 +0400 Subject: [PATCH 47/63] refactor: update utilities README --- packages/utilities/README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/utilities/README.md b/packages/utilities/README.md index 3786d11514..1d0856c966 100644 --- a/packages/utilities/README.md +++ b/packages/utilities/README.md @@ -1,8 +1,21 @@ -## Disable 2fa with seedphrase +# 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 | + + +## Disable 2fa This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase -## Usage +### 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' From 47041f1833fa26ead9a6846431d2a38977a77d05 Mon Sep 17 00:00:00 2001 From: esaminu Date: Wed, 30 Mar 2022 00:35:17 +0400 Subject: [PATCH 48/63] refactor: format file --- packages/utilities/commands/disable2fa.js | 134 +++++++++++----------- 1 file changed, 67 insertions(+), 67 deletions(-) diff --git a/packages/utilities/commands/disable2fa.js b/packages/utilities/commands/disable2fa.js index 261e6154f3..11ccf42a19 100644 --- a/packages/utilities/commands/disable2fa.js +++ b/packages/utilities/commands/disable2fa.js @@ -2,77 +2,77 @@ 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 Environments = require("../../../features/environments.json"); const { Account2FA } = require("near-api-js/lib/account_multisig"); -1 + 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, -} + 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 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 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, - }); + const account = new Account2FA(connection, accountId, { + helperUrl: helperUrl || walletEnvConfig.helperUrl, + }); - await account.disableWithFAK({ contractBytes: emptyContractBytes, cleanupContractBytes }); -} \ No newline at end of file + await account.disableWithFAK({ contractBytes: emptyContractBytes, cleanupContractBytes }); +} From 11d3b4ddc2432a171227387bb6f9292c203c76a8 Mon Sep 17 00:00:00 2001 From: esaminu Date: Thu, 31 Mar 2022 00:56:02 +0400 Subject: [PATCH 49/63] chore: update README --- packages/utilities/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/utilities/README.md b/packages/utilities/README.md index 1d0856c966..e1de824a94 100644 --- a/packages/utilities/README.md +++ b/packages/utilities/README.md @@ -13,7 +13,9 @@ _Click on a command for more information and examples._ ## Disable 2fa -This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase +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 From 711889363d2b98652c3fa09348a15cb815c56eed Mon Sep 17 00:00:00 2001 From: Patrick Tajima Date: Mon, 27 Jun 2022 16:31:44 -0400 Subject: [PATCH 50/63] fix: move token transfer-related gas to config --- .../frontend/src/components/nft/NFTDetail.js | 4 +-- .../src/components/nft/NFTTransferModal.js | 4 +-- .../src/config/configFromEnvironment.js | 7 ++++ .../frontend/src/services/FungibleTokens.js | 35 +++++++------------ .../src/services/NonFungibleTokens.js | 6 ++-- 5 files changed, 25 insertions(+), 31 deletions(-) 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/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/services/FungibleTokens.js b/packages/frontend/src/services/FungibleTokens.js index e5ea2290ac..59a8d879b0 100644 --- a/packages/frontend/src/services/FungibleTokens.js +++ b/packages/frontend/src/services/FungibleTokens.js @@ -1,7 +1,15 @@ 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, @@ -18,26 +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'); - -// TODO: Convert all above constants into yoctoNEAR and add to config - -// Estimated gas required to call ft_transfer function call (remaining gas is refunded) -const FT_TRANSFER_GAS = '15000000000000'; // 15 TGAS - -// https://docs.near.org/docs/concepts/gas#the-cost-of-common-actions -const SEND_NEAR_GAS = '450000000000'; // 0.45 TGAS - -// 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 { @@ -141,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, @@ -164,7 +153,7 @@ export default class FungibleTokens { receiver_id: receiverId, }, FT_TRANSFER_GAS, - FT_TRANSFER_DEPOSIT + TOKEN_TRANSFER_DEPOSIT ), ], }); @@ -202,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 ) ] }); From 316e3b6bfdbdf8f556e09674b544396a2741f794 Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 28 Jun 2022 00:32:44 +0400 Subject: [PATCH 51/63] fix: near-api-js version --- packages/utilities/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/utilities/package.json b/packages/utilities/package.json index f77a4b1b34..883e4c6a40 100644 --- a/packages/utilities/package.json +++ b/packages/utilities/package.json @@ -4,7 +4,7 @@ "main": "index.js", "license": "MIT", "dependencies": { - "near-api-js": "near/near-api-js#feat-disable-2fa-with-fak", + "near-api-js": "^0.45.1", "near-seed-phrase": "^0.2.0", "yargs": "^17.3.1" } From 84c6456802edbc9ada466a7eaac727eed7b85db2 Mon Sep 17 00:00:00 2001 From: esaminu Date: Sat, 19 Mar 2022 03:59:00 +0400 Subject: [PATCH 52/63] feat: add disable-2fa script package --- .../disable-2fa-with-seedphrase/.gitignore | 1 + .../disable-2fa-with-seedphrase/README.md | 9 ++ packages/disable-2fa-with-seedphrase/index.js | 83 +++++++++++++++++++ .../disable-2fa-with-seedphrase/package.json | 11 +++ 4 files changed, 104 insertions(+) create mode 100644 packages/disable-2fa-with-seedphrase/.gitignore create mode 100644 packages/disable-2fa-with-seedphrase/README.md create mode 100644 packages/disable-2fa-with-seedphrase/index.js create mode 100644 packages/disable-2fa-with-seedphrase/package.json diff --git a/packages/disable-2fa-with-seedphrase/.gitignore b/packages/disable-2fa-with-seedphrase/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/disable-2fa-with-seedphrase/README.md b/packages/disable-2fa-with-seedphrase/README.md new file mode 100644 index 0000000000..3786d11514 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/README.md @@ -0,0 +1,9 @@ +## Disable 2fa with seedphrase + +This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase + +## 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' +``` \ No newline at end of file diff --git a/packages/disable-2fa-with-seedphrase/index.js b/packages/disable-2fa-with-seedphrase/index.js new file mode 100644 index 0000000000..b2c7a73394 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/index.js @@ -0,0 +1,83 @@ +#!/usr/bin/env node +const yargs = require("yargs/yargs"); +const { hideBin } = require("yargs/helpers"); +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"); + +yargs(hideBin(process.argv)) + .command({ + 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, + }) + .parse(); + +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/disable-2fa-with-seedphrase/package.json b/packages/disable-2fa-with-seedphrase/package.json new file mode 100644 index 0000000000..ffdd6711c6 --- /dev/null +++ b/packages/disable-2fa-with-seedphrase/package.json @@ -0,0 +1,11 @@ +{ + "name": "disable-2fa-with-seedphrase", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "near-api-js": "near/near-api-js#feat-disable-2fa-with-fak", + "near-seed-phrase": "^0.2.0", + "yargs": "^17.3.1" + } +} From a3d0b52fe0ca3a0b70ed937876ae56cedea76524 Mon Sep 17 00:00:00 2001 From: esaminu Date: Thu, 31 Mar 2022 00:07:23 +0400 Subject: [PATCH 53/63] feat: add restore-account-contract command --- .../commands/restoreAccountContract.js | 57 +++++++++++++++++++ packages/utilities/index.js | 2 + 2 files changed, 59 insertions(+) create mode 100644 packages/utilities/commands/restoreAccountContract.js 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 index 23347f5b3d..52e997ed27 100644 --- a/packages/utilities/index.js +++ b/packages/utilities/index.js @@ -1,7 +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(); From 54ac471a5d08ffaf754a52b2dc025abfd5e0cfce Mon Sep 17 00:00:00 2001 From: esaminu Date: Thu, 31 Mar 2022 01:10:54 +0400 Subject: [PATCH 54/63] chore: update README.md --- packages/utilities/README.md | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/utilities/README.md b/packages/utilities/README.md index e1de824a94..34758d3f2c 100644 --- a/packages/utilities/README.md +++ b/packages/utilities/README.md @@ -6,9 +6,10 @@ The uitilites package provides various wallet related command line utilities. _Click on a command for more information and examples._ -| Command | Description | -| ----------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| [`disable-2fa`](#disable-2fa) | disable 2fa using the provided seedphrase | +| 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 @@ -21,4 +22,15 @@ This script allows the user to disable NEAR Wallet 2fa using the provided seedph ```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 From 4d61b083e1b2c7edcefd3b1b55fb8adc237ec26d Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 28 Jun 2022 00:22:05 +0400 Subject: [PATCH 55/63] chore: remove redundant package --- .../disable-2fa-with-seedphrase/.gitignore | 1 - .../disable-2fa-with-seedphrase/README.md | 9 -- packages/disable-2fa-with-seedphrase/index.js | 83 ------------------- .../disable-2fa-with-seedphrase/package.json | 11 --- 4 files changed, 104 deletions(-) delete mode 100644 packages/disable-2fa-with-seedphrase/.gitignore delete mode 100644 packages/disable-2fa-with-seedphrase/README.md delete mode 100644 packages/disable-2fa-with-seedphrase/index.js delete mode 100644 packages/disable-2fa-with-seedphrase/package.json diff --git a/packages/disable-2fa-with-seedphrase/.gitignore b/packages/disable-2fa-with-seedphrase/.gitignore deleted file mode 100644 index 3c3629e647..0000000000 --- a/packages/disable-2fa-with-seedphrase/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/packages/disable-2fa-with-seedphrase/README.md b/packages/disable-2fa-with-seedphrase/README.md deleted file mode 100644 index 3786d11514..0000000000 --- a/packages/disable-2fa-with-seedphrase/README.md +++ /dev/null @@ -1,9 +0,0 @@ -## Disable 2fa with seedphrase - -This script allows the user to disable NEAR Wallet 2fa using the provided seedphrase - -## 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' -``` \ No newline at end of file diff --git a/packages/disable-2fa-with-seedphrase/index.js b/packages/disable-2fa-with-seedphrase/index.js deleted file mode 100644 index b2c7a73394..0000000000 --- a/packages/disable-2fa-with-seedphrase/index.js +++ /dev/null @@ -1,83 +0,0 @@ -#!/usr/bin/env node -const yargs = require("yargs/yargs"); -const { hideBin } = require("yargs/helpers"); -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"); - -yargs(hideBin(process.argv)) - .command({ - 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, - }) - .parse(); - -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/disable-2fa-with-seedphrase/package.json b/packages/disable-2fa-with-seedphrase/package.json deleted file mode 100644 index ffdd6711c6..0000000000 --- a/packages/disable-2fa-with-seedphrase/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "disable-2fa-with-seedphrase", - "version": "1.0.0", - "main": "index.js", - "license": "MIT", - "dependencies": { - "near-api-js": "near/near-api-js#feat-disable-2fa-with-fak", - "near-seed-phrase": "^0.2.0", - "yargs": "^17.3.1" - } -} From fc8bb9ed6502bb142adca243eafe7d7c80198320 Mon Sep 17 00:00:00 2001 From: esaminu Date: Tue, 28 Jun 2022 01:06:04 +0400 Subject: [PATCH 56/63] fix: add onCancel functionality --- .../src/components/accounts/batch_ledger_export/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/accounts/batch_ledger_export/index.js b/packages/frontend/src/components/accounts/batch_ledger_export/index.js index aca44c3f2a..6de441d265 100644 --- a/packages/frontend/src/components/accounts/batch_ledger_export/index.js +++ b/packages/frontend/src/components/accounts/batch_ledger_export/index.js @@ -2,6 +2,7 @@ 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'; @@ -37,7 +38,7 @@ const CustomContainer = styled.div` } `; -const BatchLedgerExport = ({ onCancel }) => { +const BatchLedgerExport = ({ history }) => { const availableAccountsIsLoading = useSelector(selectAvailableAccountsIsLoading); const [, setLedgerAccounts] = useState([]); @@ -104,7 +105,7 @@ const BatchLedgerExport = ({ onCancel }) => {

history.goBack()} className="gray-blue" disabled={availableAccountsIsLoading} > @@ -136,4 +137,4 @@ const BatchLedgerExport = ({ onCancel }) => { ); }; -export default BatchLedgerExport; +export default withRouter(BatchLedgerExport); From 724a5c913564c14f3e506d2e386f402ffa91d869 Mon Sep 17 00:00:00 2001 From: Andrey K Date: Tue, 28 Jun 2022 13:24:01 +0300 Subject: [PATCH 57/63] Changed the nearpay link --- packages/frontend/src/config/buyNearConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/config/buyNearConfig.js b/packages/frontend/src/config/buyNearConfig.js index f94585d18e..b6a9ec0cee 100644 --- a/packages/frontend/src/config/buyNearConfig.js +++ b/packages/frontend/src/config/buyNearConfig.js @@ -20,7 +20,7 @@ export const getPayMethods = ({ accountId, moonPayAvailable, signedMoonPayUrl, u nearPay: { icon: payNear, name: 'NearPay', - link: 'https://www.nearpay.co/', + link: 'https://widget.nearpay.co/', track: () => Mixpanel.track('Wallet Click Buy with Nearpay') }, utorg: { From 2edbaf16847bd233812d99daa6b1a7ab326df9fa Mon Sep 17 00:00:00 2001 From: Den Ilin Date: Tue, 28 Jun 2022 15:03:34 +0300 Subject: [PATCH 58/63] fix: changed footer community link to discord channel --- packages/frontend/src/components/common/Footer.js | 2 +- packages/frontend/src/redux/actions/staking.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/frontend/src/components/common/Footer.js b/packages/frontend/src/components/common/Footer.js index 9529a79fab..accaf9deea 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/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'; From 1d4681fcb4008a451ce8baa2b101f138f9583907 Mon Sep 17 00:00:00 2001 From: Patrick Tajima Date: Tue, 28 Jun 2022 11:43:39 -0400 Subject: [PATCH 59/63] fix: fix wrong config import --- packages/frontend/src/redux/actions/staking.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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'; From 85178ab67ff103a2fa27c04ec660ddcfd99d8ae5 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Tue, 28 Jun 2022 10:35:42 -0700 Subject: [PATCH 60/63] feat(ci): add lint to prebuild step --- Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Jenkinsfile b/Jenkinsfile index ff4286b408..d8fb65bb14 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -46,6 +46,7 @@ pipeline { } steps { dir("$WORKSPACE/packages/frontend") { + sh 'yarn lint' sh 'yarn install --frozen-lockfile' } } From 9ac28bcf5c706a30c5dbd5c1a79e1bc1961a2589 Mon Sep 17 00:00:00 2001 From: Andy Haynes Date: Tue, 28 Jun 2022 10:37:18 -0700 Subject: [PATCH 61/63] feat: lint on push --- packages/frontend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/package.json b/packages/frontend/package.json index b31f520b02..e1d9b6c3b7 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -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": [ From 9d048f45641674cefdd0cbd40332fd83871d8770 Mon Sep 17 00:00:00 2001 From: andy-haynes <36863574+andy-haynes@users.noreply.github.com> Date: Tue, 28 Jun 2022 12:35:14 -0700 Subject: [PATCH 62/63] fix: lint after install Co-authored-by: Morgan McCauley --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index d8fb65bb14..e57a00041c 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -46,8 +46,8 @@ pipeline { } steps { dir("$WORKSPACE/packages/frontend") { - sh 'yarn lint' sh 'yarn install --frozen-lockfile' + sh 'yarn lint' } } } From ab66f987f78da99b8151c23ca3c56b4ef89a0b8c Mon Sep 17 00:00:00 2001 From: Den Ilin Date: Wed, 29 Jun 2022 14:01:27 +0300 Subject: [PATCH 63/63] fix: fix footer --- packages/frontend/src/components/common/Footer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/frontend/src/components/common/Footer.js b/packages/frontend/src/components/common/Footer.js index accaf9deea..a406209653 100644 --- a/packages/frontend/src/components/common/Footer.js +++ b/packages/frontend/src/components/common/Footer.js @@ -133,7 +133,7 @@ const Footer = () => {