diff --git a/android/app/build.gradle b/android/app/build.gradle index 3bc69bd5..7c3c6734 100755 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -4,8 +4,8 @@ import com.android.build.OutputFile def versionMajor = 1 def versionMinor = 0 -def versionRevision = 0 -def versionBuild = 1 +def versionRevision = 1 +def versionBuild = 0 def keystorePropertiesFile = rootProject.file("keystore.properties"); diff --git a/android/build.gradle b/android/build.gradle index 5cc12fc6..de8df98d 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,8 +4,8 @@ buildscript { ext { buildToolsVersion = "31.0.0" minSdkVersion = 21 - compileSdkVersion = 31 - targetSdkVersion = 31 + compileSdkVersion = 33 + targetSdkVersion = 33 if (System.properties['os.arch'] == "aarch64") { // For M1 Users we need to use the NDK 24 which added support for aarch64 diff --git a/env/main.android.json b/env/main.android.json index 4c919734..996689f4 100644 --- a/env/main.android.json +++ b/env/main.android.json @@ -1,5 +1,5 @@ { - "APP_VERSION": "1.0.0-1", + "APP_VERSION": "1.0.1", "ELECTRUM_PROTOCOL_CHANGE": 1.4, "KEY_DERIVATION_VERSION": 1, @@ -28,6 +28,7 @@ }, "ETH_HOMESTEAD": "homestead", + "ETH_GOERLI": "goerli", "VERUSID_NETWORK_DEFAULT": "VRSC", "BIOMETRIC_SECURITY_THRESHOLD": "SECURE_HARDWARE", @@ -46,6 +47,7 @@ "NOTIFICATIONS_STORAGE_INTERNAL_KEY": "notifications", "WIDGET_STORAGE_INTERNAL_KEY": "widgets", "CURRENCY_DEFINITION_STORAGE_INTERNAL_KEY": "currencyDefinitions", + "CONTRACT_DEFINITION_STORAGE_INTERNAL_KEY": "ethereumContractDefinitions", "WYRE_ACCESSIBLE": true } diff --git a/env/main.ios.json b/env/main.ios.json index b3fe1a75..99b83142 100644 --- a/env/main.ios.json +++ b/env/main.ios.json @@ -1,5 +1,5 @@ { - "APP_VERSION": "1.0.0-1", + "APP_VERSION": "1.0.1", "ELECTRUM_PROTOCOL_CHANGE": 1.4, "KEY_DERIVATION_VERSION": 1, @@ -28,6 +28,7 @@ }, "ETH_HOMESTEAD": "homestead", + "ETH_GOERLI": "goerli", "VERUSID_NETWORK_DEFAULT": "VRSC", "BIOMETRIC_SECURITY_THRESHOLD": "SECURE_HARDWARE", @@ -46,6 +47,7 @@ "NOTIFICATIONS_STORAGE_INTERNAL_KEY": "notifications", "WIDGET_STORAGE_INTERNAL_KEY": "widgets", "CURRENCY_DEFINITION_STORAGE_INTERNAL_KEY": "currencyDefinitions", + "CONTRACT_DEFINITION_STORAGE_INTERNAL_KEY": "ethereumContractDefinitions", "WYRE_ACCESSIBLE": true } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 6a677cf1..5b95dec6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -259,7 +259,7 @@ PODS: - React - react-native-cameraroll (5.0.4): - React-Core - - react-native-image-picker (4.0.6): + - react-native-image-picker (5.7.0): - React-Core - react-native-netinfo (6.0.2): - React-Core @@ -354,7 +354,7 @@ PODS: - React-Core - RNFS (2.18.0): - React - - RNGestureHandler (2.8.0): + - RNGestureHandler (2.9.0): - React-Core - RNKeychain (6.2.0): - React @@ -813,7 +813,7 @@ SPEC CHECKSUMS: React-logger: 98f663b292a60967ebbc6d803ae96c1381183b6d react-native-camera: a49f960eb362b9fbff87667c0ce639eef1161530 react-native-cameraroll: 38b40d9033e4077b6c603f92f95c6d05fa7907df - react-native-image-picker: a6e56460d34905c849ada551db30897dc7f3d535 + react-native-image-picker: 3269f75c251cdcd61ab51b911dd30d6fff8c6169 react-native-netinfo: 92e6e4476eb8bf6fc2d7c0a6ca0a1406f663d73a react-native-randombytes: 421f1c7d48c0af8dbcd471b0324393ebf8fe7846 react-native-safe-area-context: f0906bf8bc9835ac9a9d3f97e8bde2a997d8da79 @@ -837,7 +837,7 @@ SPEC CHECKSUMS: RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f RNDateTimePicker: e073697ac3e8a378968d68ab0581fef542b8af8a RNFS: 3ab21fa6c56d65566d1fb26c2228e2b6132e5e32 - RNGestureHandler: 62232ba8f562f7dea5ba1b3383494eb5bf97a4d3 + RNGestureHandler: 071d7a9ad81e8b83fe7663b303d132406a7d8f39 RNKeychain: b8e0711b959a19c5b057d1e970d3c83d159b6da5 RNOS: 728cdea6d232de48cfede274443dbe3f669b86e9 RNPermissions: 4b54095940aea8c03fa3e6c92d4ac3647b31ed4e @@ -866,6 +866,6 @@ SPEC CHECKSUMS: Yoga: c4d61225a466f250c35c1ee78d2d0b3d41fe661c ZcashLightClientKit: 4e4273f2e841e982fedad461a9008cca19089f5c -PODFILE CHECKSUM: 2ea7addfb99cd8f4e5f9742ed28cac88e62bc28c +PODFILE CHECKSUM: 2dd5cff0057e34322095c85f7a7d5ca86846e0e1 -COCOAPODS: 1.11.3 +COCOAPODS: 1.13.0 diff --git a/ios/assets/env/main.json b/ios/assets/env/main.json index b3fe1a75..99b83142 100644 --- a/ios/assets/env/main.json +++ b/ios/assets/env/main.json @@ -1,5 +1,5 @@ { - "APP_VERSION": "1.0.0-1", + "APP_VERSION": "1.0.1", "ELECTRUM_PROTOCOL_CHANGE": 1.4, "KEY_DERIVATION_VERSION": 1, @@ -28,6 +28,7 @@ }, "ETH_HOMESTEAD": "homestead", + "ETH_GOERLI": "goerli", "VERUSID_NETWORK_DEFAULT": "VRSC", "BIOMETRIC_SECURITY_THRESHOLD": "SECURE_HARDWARE", @@ -46,6 +47,7 @@ "NOTIFICATIONS_STORAGE_INTERNAL_KEY": "notifications", "WIDGET_STORAGE_INTERNAL_KEY": "widgets", "CURRENCY_DEFINITION_STORAGE_INTERNAL_KEY": "currencyDefinitions", + "CONTRACT_DEFINITION_STORAGE_INTERNAL_KEY": "ethereumContractDefinitions", "WYRE_ACCESSIBLE": true } diff --git a/ios/verusMobile.xcodeproj/project.pbxproj b/ios/verusMobile.xcodeproj/project.pbxproj index b64ef6ed..03ff731f 100644 --- a/ios/verusMobile.xcodeproj/project.pbxproj +++ b/ios/verusMobile.xcodeproj/project.pbxproj @@ -669,7 +669,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = J8Y97B5NMQ; ENABLE_BITCODE = NO; @@ -721,7 +721,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-webview\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/rn-fetch-blob\"", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = org.reactjs.native.verusmobile; PRODUCT_NAME = verusmobile; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -740,7 +740,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 11; + CURRENT_PROJECT_VERSION = 3; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = J8Y97B5NMQ; ENABLE_BITCODE = NO; @@ -792,7 +792,7 @@ "\"${PODS_CONFIGURATION_BUILD_DIR}/react-native-webview\"", "\"${PODS_CONFIGURATION_BUILD_DIR}/rn-fetch-blob\"", ); - MARKETING_VERSION = 1.0.0; + MARKETING_VERSION = 1.0.1; PRODUCT_BUNDLE_IDENTIFIER = org.reactjs.native.verusmobile; PRODUCT_NAME = verusmobile; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/package.json b/package.json index a918a9d5..5dabf544 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "verusmobile", - "version": "1.0.0", + "version": "1.0.1", "private": true, "scripts": { "postinstall": "./node_modules/.bin/rn-nodeify --hack --install --yarn && npx jetify", @@ -73,9 +73,9 @@ "react-native-elements": "1.0.0", "react-native-format-currency": "https://github.com/michaeltout/react-native-format-currency.git", "react-native-fs": "2.18.0", - "react-native-gesture-handler": "2.8.0", + "react-native-gesture-handler": "2.9.0", "react-native-haptic-feedback": "1.11.0", - "react-native-image-picker": "4.0.6", + "react-native-image-picker": "5.7.0", "react-native-keyboard-aware-scroll-view": "0.9.5", "react-native-keychain": "6.2.0", "react-native-level-fs": "3.0.0", diff --git a/src/VerusMobile.js b/src/VerusMobile.js index ee22df82..d1c6e059 100644 --- a/src/VerusMobile.js +++ b/src/VerusMobile.js @@ -39,6 +39,7 @@ import { NavigationContainer } from "@react-navigation/native"; import LoadingModal from "./components/LoadingModal/LoadingModal"; import { CoinDirectory } from "./utils/CoinData/CoinDirectory"; import { removeInactiveCurrencyDefinitions } from "./utils/asyncStore/currencyDefinitionStorage"; +import { removeInactiveContractDefinitions } from "./utils/asyncStore/contractDefinitionStorage"; class VerusMobile extends React.Component { constructor(props) { @@ -107,8 +108,13 @@ class VerusMobile extends React.Component { clearCachedVersions() .then(async () => { await clearCachedVrpcResponses() - await removeInactiveCurrencyDefinitions(await purgeUnusedCoins()) - await CoinDirectory.populateCurrencyDefinitionsFromStorage() + + const usedCoins = await purgeUnusedCoins() + await removeInactiveCurrencyDefinitions(usedCoins) + await removeInactiveContractDefinitions(usedCoins) + + await CoinDirectory.populateEthereumContractDefinitionsFromStorage() + await CoinDirectory.populatePbaasCurrencyDefinitionsFromStorage() return initCache() }) diff --git a/src/actions/actions/UserData.js b/src/actions/actions/UserData.js index 81ce2fe8..37ba4816 100644 --- a/src/actions/actions/UserData.js +++ b/src/actions/actions/UserData.js @@ -159,7 +159,7 @@ export const setTestnetOverrides = async (userID, testnetOverrides) => { }) .catch(err => reject(err)); }); -} +}; export const deleteProfile = async (account, dispatch) => { // Clear existing account lifecycles @@ -205,7 +205,37 @@ export const fetchUsers = () => { : user.keyDerivationVersion, disabledServices: user.disabledServices == null ? {} : user.disabledServices, - testnetOverrides: user.testnetOverrides == null ? {} : user.testnetOverrides, + testnetOverrides: + user.testnetOverrides == null ? {} : user.testnetOverrides, + }; + } else { + return user; + } + }); + + await setUsers(users); + } + + // Update testnet overrides to include ETH + if ( + users.some( + value => + value.testnetOverrides != null && + value.testnetOverrides.hasOwnProperty('VRSC') && + !value.testnetOverrides.hasOwnProperty('ETH'), + ) + ) { + console.warn('Updating testnet profile to account for goerli ETH'); + + users = users.map(user => { + if ( + user.testnetOverrides != null && + user.testnetOverrides.hasOwnProperty('VRSC') && + !user.testnetOverrides.hasOwnProperty('ETH') + ) { + return { + ...user, + testnetOverrides: {...user.testnetOverrides, ETH: 'GETH'}, }; } else { return user; @@ -300,7 +330,10 @@ export const authenticateAccount = async (account, password) => { ? {} : {[WYRE_SERVICE_ID]: true} : account.disabledServices, - testnetOverrides: account.testnetOverrides == null ? {} : account.testnetOverrides, + testnetOverrides: + account.testnetOverrides == null + ? {} + : account.testnetOverrides, }, await initSession(password), ), diff --git a/src/actions/actions/account/dispatchers/account.js b/src/actions/actions/account/dispatchers/account.js index 35bc35b8..22e2b6a1 100644 --- a/src/actions/actions/account/dispatchers/account.js +++ b/src/actions/actions/account/dispatchers/account.js @@ -1,4 +1,4 @@ -import { LOADING_ACCOUNT, VALIDATING_ACCOUNT } from "../../../../utils/constants/constants"; +import { ADDRESS_BLOCKLIST_FROM_WEBSERVER, LOADING_ACCOUNT, VALIDATING_ACCOUNT } from "../../../../utils/constants/constants"; import { signIntoAuthenticatedAccount } from "../../../actionCreators"; import { COIN_MANAGER_MAP, fetchActiveCoins, setUserCoins } from "../../coins/Coins"; import { @@ -13,6 +13,7 @@ import { fetchUsers, validateLogin } from "../../UserData"; import { initSettings, saveGeneralSettings } from "../../WalletSettings"; import { DISABLED_CHANNELS } from '../../../../../env/index' import store from "../../../../store"; +import { getAddressBlocklistFromServer } from "../../../../utils/api/channels/general/addressBlocklist/getAddressBlocklist"; export const initializeAccountData = async ( account, @@ -40,7 +41,37 @@ export const initializeAccountData = async ( ); const {activeCoinsForUser} = setUserCoinsAction.payload; - store.dispatch(await initSettings()); + const settingsAction = await initSettings() + store.dispatch(settingsAction); + + try { + const { addressBlocklistDefinition, addressBlocklist } = store.getState().settings.generalWalletSettings; + + if (addressBlocklistDefinition.type === ADDRESS_BLOCKLIST_FROM_WEBSERVER) { + const fetchedBlocklist = await getAddressBlocklistFromServer(); + const currentBlocklist = [...addressBlocklist]; + + for (const address of fetchedBlocklist) { + if (currentBlocklist.find(x => x.address === address) == null) { + currentBlocklist.unshift({ + address, + details: '', + lastModified: Math.floor(Date.now() / 1000) + }); + } + } + + const saveGeneralSettingsAction = await saveGeneralSettings({ + addressBlocklist: currentBlocklist + }); + + store.dispatch(saveGeneralSettingsAction); + } + } catch(e) { + console.warn("Failed to fetch address blocklist"); + console.warn(e); + } + store.dispatch(accountAuthenticator); store.dispatch(coinList); diff --git a/src/actions/actions/channels/erc20/dispatchers/Erc20WalletReduxManager.js b/src/actions/actions/channels/erc20/dispatchers/Erc20WalletReduxManager.js index 47786832..0cab5e67 100644 --- a/src/actions/actions/channels/erc20/dispatchers/Erc20WalletReduxManager.js +++ b/src/actions/actions/channels/erc20/dispatchers/Erc20WalletReduxManager.js @@ -7,7 +7,7 @@ import { export const initErc20Wallet = async (coinObj) => { Store.dispatch({ type: INIT_ERC20_CHANNEL_START, - payload: { chainTicker: coinObj.id, contractAddress: coinObj.currency_id } + payload: { chainTicker: coinObj.id, contractAddress: coinObj.currency_id, network: coinObj.network } }) return @@ -16,7 +16,7 @@ export const initErc20Wallet = async (coinObj) => { export const closeErc20Wallet = async (coinObj) => { Store.dispatch({ type: CLOSE_ERC20_CHANNEL, - payload: { chainTicker: coinObj.id, contractAddress: coinObj.currency_id } + payload: { chainTicker: coinObj.id, contractAddress: coinObj.currency_id, network: coinObj.network } }) return diff --git a/src/actions/actions/sendModal/dispatchers/sendModal.js b/src/actions/actions/sendModal/dispatchers/sendModal.js index 8128dcc2..d4672dc8 100644 --- a/src/actions/actions/sendModal/dispatchers/sendModal.js +++ b/src/actions/actions/sendModal/dispatchers/sendModal.js @@ -30,7 +30,10 @@ import { SEND_MODAL_SHOW_EXPORTTO_FIELD, SEND_MODAL_SHOW_VIA_FIELD, SEND_MODAL_PRICE_ESTIMATE, - SEND_MODAL_ADVANCED_FORM + SEND_MODAL_ADVANCED_FORM, + SEND_MODAL_CONTRACT_ADDRESS_FIELD, + ADD_ERC20_TOKEN_MODAL, + SEND_MODAL_SHOW_MAPPING_FIELD } from '../../../../utils/constants/sendModal'; import { CLOSE_SEND_COIN_MODAL, @@ -97,6 +100,7 @@ export const openConvertOrCrossChainSendModal = (coinObj, subWallet, data) => { [SEND_MODAL_SHOW_CONVERTTO_FIELD]: true, [SEND_MODAL_SHOW_EXPORTTO_FIELD]: true, [SEND_MODAL_SHOW_VIA_FIELD]: true, + [SEND_MODAL_SHOW_MAPPING_FIELD]: true, [SEND_MODAL_ADVANCED_FORM]: false } : data, @@ -135,6 +139,21 @@ export const openAddPbaasCurrencyModal = (coinObj, data) => { ); }; +export const openAddErc20TokenModal = (coinObj, data) => { + openSendModal( + `Add ERC20 Token`, + coinObj, + null, + data == null + ? { + [SEND_MODAL_CONTRACT_ADDRESS_FIELD]: '', + } + : data, + ADD_ERC20_TOKEN_MODAL, + 'To add an ERC20 token to your wallet, enter its contract address here and press continue.', + ); +}; + export const openProvisionIdentityModal = (coinObj, req) => { openSendModal( `Request VerusID`, diff --git a/src/actions/actions/wallet/dispatchers/UpdateLedgerValue.js b/src/actions/actions/wallet/dispatchers/UpdateLedgerValue.js index 13846782..ae6d7146 100644 --- a/src/actions/actions/wallet/dispatchers/UpdateLedgerValue.js +++ b/src/actions/actions/wallet/dispatchers/UpdateLedgerValue.js @@ -1,6 +1,7 @@ import { DLIGHT_PRIVATE } from '../../../../utils/constants/intervalConstants' import { getCoinObj } from '../../../../utils/CoinData/CoinData' import { dlightEnabled } from '../../../../utils/enabledChannels'; +import { CoinDirectory } from '../../../../utils/CoinData/CoinDirectory'; /** * Fetches the appropriate data from the store for the specified channel's type @@ -24,7 +25,7 @@ export const updateLedgerValue = async ( fetchChannels ) => { const activeUser = state.authentication.activeAccount; - const coinObj = getCoinObj(state.coins.activeCoinsForUser, chainTicker); + const coinObj = CoinDirectory.findCoinObj(chainTicker, activeUser.id); let channelsPassed = []; const channelMap = fetchChannels(activeUser) diff --git a/src/actions/actions/wallet/dispatchers/erc20/updates.js b/src/actions/actions/wallet/dispatchers/erc20/updates.js index 7e3e42fd..37233988 100644 --- a/src/actions/actions/wallet/dispatchers/erc20/updates.js +++ b/src/actions/actions/wallet/dispatchers/erc20/updates.js @@ -12,6 +12,7 @@ export const updateErc20Balances = async (activeUser, coinObj) => { activeUser.keys[coinObj.id][ERC20].addresses[0], coinObj.currency_id, coinObj.decimals, + coinObj.network ) ).toString(); @@ -50,6 +51,7 @@ export const updateErc20Transactions = async (activeUser, coinObj) => { activeUser.keys[coinObj.id][ERC20].addresses[0], coinObj.currency_id, coinObj.decimals, + coinObj.network ), }; } else { diff --git a/src/actions/actions/wallet/dispatchers/eth/updates.js b/src/actions/actions/wallet/dispatchers/eth/updates.js index 6e66cfff..6be4ff2b 100644 --- a/src/actions/actions/wallet/dispatchers/eth/updates.js +++ b/src/actions/actions/wallet/dispatchers/eth/updates.js @@ -8,7 +8,7 @@ export const updateEthBalances = async (activeUser, coinObj) => { activeUser.keys[coinObj.id][ETH].addresses.length > 0 ) { const balance = ( - await getStandardEthBalance(activeUser.keys[coinObj.id][ETH].addresses[0]) + await getStandardEthBalance(activeUser.keys[coinObj.id][ETH].addresses[0], coinObj.network) ).toString(); return { @@ -44,6 +44,7 @@ export const updateEthTransactions = async (activeUser, coinObj) => { header: {}, body: await getStandardEthTransactions( activeUser.keys[coinObj.id][ETH].addresses[0], + coinObj.network ), }; } else { diff --git a/src/components/CoinDetailsModal/CoinDetailsModal.js b/src/components/CoinDetailsModal/CoinDetailsModal.js index 0a77d191..78097e15 100644 --- a/src/components/CoinDetailsModal/CoinDetailsModal.js +++ b/src/components/CoinDetailsModal/CoinDetailsModal.js @@ -150,10 +150,11 @@ class CoinDetailsModal extends Component { theme_color, display_name, id, + proto, website } = data const ticker = id == null ? 'VRSC' : id - const Logo = getCoinLogo(ticker) + const Logo = getCoinLogo(ticker, proto) return ( { + return ( + + + + + { + showConversionField ? ( + + { + advancedForm ? ( + onConvertToChange(text)} + autoCapitalize={'none'} + autoCorrect={false} + style={{ flex: 1 }} + /> + ) : ( + handleFieldFocusConvertTo()} + style={{ + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 10, + borderWidth: 1, + borderColor: Colors.verusDarkGray, + borderRadius: 4, + }}> + + {isConversion + ? isPreconvert ? `Preconvert to: ${convertToField}` : `Convert to: ${convertToField}` + : isPreconvert ? 'Select currency to preconvert to' : 'Select currency to convert to'} + + + + ) + } + + ) : null + } + { + showViaField ? ( + + { + advancedForm ? ( + onViaChange(text)} + autoCapitalize={'none'} + autoCorrect={false} + style={{ flex: 1 }} + /> + ) : ( + handleFieldFocusVia()} + style={{ + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 10, + borderWidth: 1, + borderColor: Colors.verusDarkGray, + borderRadius: 4, + }}> + + {isVia ? `Convert via: ${viaField}` : 'Select currency to convert via'} + + + + ) + } + + ) : null + } + + ); +}; + +export default ConvertFormModule; \ No newline at end of file diff --git a/src/components/FormModules/CoreSendFormModule.js b/src/components/FormModules/CoreSendFormModule.js new file mode 100644 index 00000000..f8cbefe1 --- /dev/null +++ b/src/components/FormModules/CoreSendFormModule.js @@ -0,0 +1,95 @@ +import React from 'react'; +import { View } from 'react-native'; +import { Button, TextInput , Text} from 'react-native-paper'; +import Colors from '../../globals/colors'; +import Styles from '../../styles'; + +const CoreSendFormModule = ({ + sendingFromLabel = "Sending from", + sendingFromValue, + recipientAddressLabel = "Recipient address", + recipientAddressValue, + onRecipientAddressChange, + onSelfPress, + amountLabel = "Amount", + amountValue, + onAmountChange, + onMaxPress, + maxButtonDisabled = false, + estimatedResultSubtitle = null, + networkName +}) => { + return ( + + + + { + networkName != null ? ( + + {`on ${networkName} network`} + + ) : null + } + + + + + + + + + + + + + { + estimatedResultSubtitle != null ? ( + + {estimatedResultSubtitle} + + ) : null + } + + + ); +}; + +export default CoreSendFormModule; \ No newline at end of file diff --git a/src/components/FormModules/ExportFormModule.js b/src/components/FormModules/ExportFormModule.js new file mode 100644 index 00000000..67705f42 --- /dev/null +++ b/src/components/FormModules/ExportFormModule.js @@ -0,0 +1,129 @@ +import React from 'react'; +import { View, TouchableOpacity } from 'react-native'; +import { IconButton, Divider, Text, TextInput } from 'react-native-paper'; +import Colors from '../../globals/colors'; +import Styles from '../../styles'; +import { VETH } from '../../utils/constants/web3Constants'; + +const ExportFormModule = ({ + isExport, + isConversion, + exportToField, + handleNetworkFieldFocus, + handleMappingFieldFocus, + onSystemChange, + onMappingChange, + localNetworkDefinition, + advancedForm, + showMappingField, + mappingField +}) => { + return ( + + + + + + { + advancedForm ? ( + onSystemChange(text)} + autoCapitalize={'none'} + autoCorrect={false} + style={{ flex: 1 }} + /> + ) : ( + handleNetworkFieldFocus()} + style={{ + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 10, + borderWidth: 1, + borderColor: Colors.verusDarkGray, + borderRadius: 4, + }}> + + { + isExport + ? exportToField.toLowerCase() === VETH.toLowerCase() + ? 'To Ethereum network' + : `To network: ${exportToField}` + : isConversion + ? `On ${ + localNetworkDefinition + ? localNetworkDefinition.fullyqualifiedname + : 'current' + } network` + : 'Select network to send to' + } + + + + ) + } + + { + showMappingField && !isConversion && ( + + {advancedForm ? ( + 0 ? "Currency to receive as (required)" : "Currency to receive as (optional)"} + value={mappingField} + mode="outlined" + multiline={true} + onChangeText={text => onMappingChange(text)} + autoCapitalize={'none'} + autoCorrect={false} + style={{flex: 1}} + /> + ) : ( + handleMappingFieldFocus()} + style={{ + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: 16, + paddingVertical: 10, + borderWidth: 1, + borderColor: Colors.verusDarkGray, + borderRadius: 4, + }}> + 0 + ? Colors.quaternaryColor + : Colors.verusDarkGray, + }}> + {mappingField != null && mappingField.length > 0 + ? `Receive as: ${mappingField}` + : 'Select currency to receive as'} + + + + )} + + ) + } + + ); +}; + +export default ExportFormModule; \ No newline at end of file diff --git a/src/components/SendModal/AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm.js b/src/components/SendModal/AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm.js new file mode 100644 index 00000000..9b2b4b4b --- /dev/null +++ b/src/components/SendModal/AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm.js @@ -0,0 +1,119 @@ +import React, {useState, useCallback} from 'react'; +import {useDispatch, useSelector} from 'react-redux'; +import {Alert} from 'react-native'; +import {addCoin, addKeypairs, setUserCoins} from '../../../../actions/actionCreators'; +import { + refreshActiveChainLifecycles +} from '../../../../actions/actions/intervals/dispatchers/lifecycleManager'; +import { + SEND_MODAL_FORM_STEP_FORM, + SEND_MODAL_FORM_STEP_RESULT, +} from '../../../../utils/constants/sendModal'; +import {AddErc20TokenConfirmRender} from './AddErc20TokenConfirm.render'; +import { CoinDirectory } from '../../../../utils/CoinData/CoinDirectory'; +import { coinsList } from '../../../../utils/CoinData/CoinsList'; +import { ERC20 } from '../../../../utils/constants/intervalConstants'; + +const AddErc20TokenConfirm = props => { + const [contract, setCurrency] = useState(props.route.params.contract); + + const dispatch = useDispatch(); + const sendModal = useSelector(state => state.sendModal); + const activeAccount = useSelector( + state => state.authentication.activeAccount, + ); + const activeCoinList = useSelector(state => state.coins.activeCoinList); + const activeCoinsForUser = useSelector(state => state.coins.activeCoinsForUser); + const testAccount = useSelector(state => (Object.keys(state.authentication.activeAccount.testnetOverrides).length > 0)) + + const goBack = useCallback(() => { + props.setModalHeight(); + props.navigation.navigate(SEND_MODAL_FORM_STEP_FORM); + }, [props]); + + const submitData = useCallback(async () => { + await props.setLoading(true); + await props.setPreventExit(true); + + try { + const {coinObj} = sendModal; + let fullCoinData; + + for (const key in coinsList) { + if ( + coinsList[key].proto === ERC20 && + ((coinsList[key].testnet && testAccount) || + (!coinsList[key].testnet && !testAccount)) && + coinsList[key].currency_id.toLowerCase() === contract.address.toLowerCase() + ) { + fullCoinData = CoinDirectory.findCoinObj(key); + } + } + + if (fullCoinData == null) { + await CoinDirectory.addErc20Token(contract, coinObj.network); + fullCoinData = CoinDirectory.findCoinObj(contract.address); + } + + const activeCoinIndex = activeCoinsForUser.findIndex(coin => { + return coin.id === fullCoinData.id + }); + if (activeCoinIndex > -1) throw new Error(`${fullCoinData.display_ticker} already added.`) + + dispatch( + await addKeypairs( + fullCoinData, + activeAccount.keys, + activeAccount.keyDerivationVersion == null + ? 0 + : activeAccount.keyDerivationVersion, + ), + ); + + const addCoinAction = await addCoin( + fullCoinData, + activeCoinList, + activeAccount.id, + fullCoinData.compatible_channels, + ); + + if (addCoinAction) { + dispatch(addCoinAction); + + const setUserCoinsAction = setUserCoins( + activeCoinList, + activeAccount.id, + ); + dispatch(setUserCoinsAction); + + refreshActiveChainLifecycles(setUserCoinsAction.payload.activeCoinsForUser); + } else { + throw new Error('Error adding coin'); + } + + props.navigation.navigate(SEND_MODAL_FORM_STEP_RESULT, { + contract + }); + } catch (e) { + Alert.alert('Could not add currency', e.message); + } + + props.setPreventExit(false); + props.setLoading(false); + }, [ + contract, + sendModal, + activeAccount, + activeCoinList, + dispatch, + props, + ]); + + return AddErc20TokenConfirmRender({ + contract, + goBack, + submitData + }); +}; + +export default AddErc20TokenConfirm; diff --git a/src/components/SendModal/AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm.render.js b/src/components/SendModal/AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm.render.js new file mode 100644 index 00000000..28278906 --- /dev/null +++ b/src/components/SendModal/AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm.render.js @@ -0,0 +1,118 @@ +import React from 'react'; +import { View, SafeAreaView, FlatList, TouchableOpacity } from 'react-native'; +import { List, Text, Divider, Button } from 'react-native-paper'; +import Colors from '../../../../globals/colors'; +import Styles from '../../../../styles'; +import { copyToClipboard } from '../../../../utils/clipboard/clipboard'; + +export const AddErc20TokenConfirmRender = ({ + contract, + goBack, + submitData +}) => { + return ( + + + { + if (item.condition == null || item.condition === true) { + return ( + + item.onPress()}> + + item.right ? ( + + {item.right} + + ) : null + } + /> + + + + ); + } else { + return null; + } + }} + data={[ + { + key: 'Contract Address', + data: contract.address, + numLines: 100, + onPress: () => copyToClipboard( + contract.address, {title: 'Address copied', message: `${contract.address} copied to clipboard.`} + ), + }, + { + key: 'Token Symbol', + data: contract.symbol, + numLines: 100, + onPress: () => copyToClipboard( + contract.symbol, {title: 'Symbol copied', message: `${contract.symbol} copied to clipboard.`} + ), + }, + { + key: 'Token Name', + data: contract.name, + numLines: 100, + onPress: () => copyToClipboard( + contract.name, {title: 'Name copied', message: `${contract.name} copied to clipboard.`} + ), + }, + { + key: 'Token Decimals', + data: contract.decimals, + numLines: 100, + onPress: () => copyToClipboard( + contract.decimals, {title: 'Decimals copied', message: `${contract.decimals} copied to clipboard.`} + ), + } + ]} + /> + + + + + + + ); +}; diff --git a/src/components/SendModal/AddErc20Token/AddErc20TokenForm/AddErc20TokenForm.js b/src/components/SendModal/AddErc20Token/AddErc20TokenForm/AddErc20TokenForm.js new file mode 100644 index 00000000..e4fdfd0b --- /dev/null +++ b/src/components/SendModal/AddErc20Token/AddErc20TokenForm/AddErc20TokenForm.js @@ -0,0 +1,117 @@ +import {useCallback, useState} from 'react'; +import {useSelector, useDispatch} from 'react-redux'; +import {Alert} from 'react-native'; +import {createAlert} from '../../../../actions/actions/alert/dispatchers/alert'; +import { + SEND_MODAL_FORM_STEP_CONFIRM, + SEND_MODAL_CONTRACT_ADDRESS_FIELD, +} from '../../../../utils/constants/sendModal'; +import {getWeb3ProviderForNetwork} from '../../../../utils/web3/provider'; +import {AddErc20TokenFormRender} from './AddErc20TokenForm.render'; +import { getCurrency } from '../../../../utils/api/channels/verusid/callCreators'; +import { getCurrenciesMappedToEth } from '../../../../utils/api/channels/vrpc/requests/getCurrenciesMappedToEth'; + +const AddErc20TokenForm = props => { + const dispatch = useDispatch(); + const sendModal = useSelector(state => state.sendModal); + + const [useMappedCurrency, setUseMappedCurrency] = useState(false); + + const formHasError = useCallback(() => { + const {data} = sendModal; + + const tokenAddr = + data[SEND_MODAL_CONTRACT_ADDRESS_FIELD] != null + ? data[SEND_MODAL_CONTRACT_ADDRESS_FIELD].trim() + : ''; + + if (!tokenAddr || tokenAddr.length < 1) { + createAlert('Required Field', 'Address is a required field.'); + return true; + } + + return false; + }, [sendModal, dispatch]); + + const submitData = useCallback(async () => { + if (formHasError()) { + return; + } + + props.setLoading(true); + + const {coinObj, data} = sendModal; + + const contractAddressField = data[SEND_MODAL_CONTRACT_ADDRESS_FIELD]; + + try { + const provider = getWeb3ProviderForNetwork( + coinObj.network, + ) + + let contractAddress; + + if (useMappedCurrency) { + const getCurrencyRes = await getCurrency(provider.getVrscSystem(), contractAddressField); + + if (getCurrencyRes.error) { + throw new Error(getCurrencyRes.error.message); + } + + const currencyDef = getCurrencyRes.result; + + const mappedCurrenciesRes = await getCurrenciesMappedToEth(provider.getVrscSystem(), provider.network); + + if (mappedCurrenciesRes.error) { + throw new Error(mappedCurrenciesRes.error.message); + } + + const mappedCurrenciesResult = mappedCurrenciesRes.result; + + const isMapped = mappedCurrenciesResult.currencyIdToContractAddressMap.has(currencyDef.currencyid); + + if (!isMapped) { + throw new Error("Cannot get ERC20 contract from non-mapped PBaaS currency.") + } + + contractAddress = mappedCurrenciesResult.currencyIdToContractAddressMap + .get(currencyDef.currencyid) + .values() + .next().value; + + } else { + contractAddress = contractAddressField; + } + + const { name, symbol, decimals } = await provider.getContractInfo(contractAddress); + + Alert.alert( + 'Warning', + 'Data about this ERC20 token was retrieved from the token contract itself. Verify that the contract address you entered is correct before trusting the displayed symbol, name, and decimals.', + ); + + props.navigation.navigate(SEND_MODAL_FORM_STEP_CONFIRM, { + contract: { + address: contractAddress, + symbol, + decimals, + name, + }, + }); + } catch (e) { + Alert.alert('Error', e.message); + } + + props.setLoading(false); + }, [formHasError, sendModal, dispatch, props, useMappedCurrency]); + + return AddErc20TokenFormRender({ + submitData, + updateSendFormData: props.updateSendFormData, + formDataValue: sendModal.data[SEND_MODAL_CONTRACT_ADDRESS_FIELD], + useMappedCurrency, + setUseMappedCurrency + }); +}; + +export default AddErc20TokenForm; diff --git a/src/components/SendModal/AddErc20Token/AddErc20TokenForm/AddErc20TokenForm.render.js b/src/components/SendModal/AddErc20Token/AddErc20TokenForm/AddErc20TokenForm.render.js new file mode 100644 index 00000000..78521e25 --- /dev/null +++ b/src/components/SendModal/AddErc20Token/AddErc20TokenForm/AddErc20TokenForm.render.js @@ -0,0 +1,59 @@ +import React from "react"; +import { ScrollView, View, TouchableWithoutFeedback, Keyboard } from "react-native"; +import { TextInput, Button, Checkbox } from "react-native-paper"; +import Styles from "../../../../styles"; +import { SEND_MODAL_CONTRACT_ADDRESS_FIELD } from "../../../../utils/constants/sendModal"; +import Colors from "../../../../globals/colors"; + +export const AddErc20TokenFormRender = ({ + submitData, + updateSendFormData, + formDataValue, + useMappedCurrency, + setUseMappedCurrency +}) => { + return ( + + + + + updateSendFormData(SEND_MODAL_CONTRACT_ADDRESS_FIELD, text) + } + autoCapitalize={"none"} + autoCorrect={false} + /> + + + setUseMappedCurrency(!useMappedCurrency)} + mode="android" + /> + + + + + + + ); +}; \ No newline at end of file diff --git a/src/components/SendModal/AddErc20Token/AddErc20TokenResult/AddErc20TokenResult.js b/src/components/SendModal/AddErc20Token/AddErc20TokenResult/AddErc20TokenResult.js new file mode 100644 index 00000000..c28615a2 --- /dev/null +++ b/src/components/SendModal/AddErc20Token/AddErc20TokenResult/AddErc20TokenResult.js @@ -0,0 +1,18 @@ +import {useState} from 'react'; +import {closeSendModal} from '../../../../actions/actions/sendModal/dispatchers/sendModal'; +import {AddErc20TokenResultRender} from './AddErc20TokenResult.render'; + +const AddErc20TokenResult = (props) => { + const [contract, setContract] = useState(props.route.params == null ? {} : props.route.params.contract); + + const finishSend = () => { + closeSendModal() + }; + + return AddErc20TokenResultRender({ + contract, + finishSend + }); +}; + +export default AddErc20TokenResult; diff --git a/src/components/SendModal/AddErc20Token/AddErc20TokenResult/AddErc20TokenResult.render.js b/src/components/SendModal/AddErc20Token/AddErc20TokenResult/AddErc20TokenResult.render.js new file mode 100644 index 00000000..acc29a46 --- /dev/null +++ b/src/components/SendModal/AddErc20Token/AddErc20TokenResult/AddErc20TokenResult.render.js @@ -0,0 +1,71 @@ +import React from 'react'; +import {ScrollView, View, TouchableOpacity} from 'react-native'; +import {Button, Text} from 'react-native-paper'; +import Colors from '../../../../globals/colors'; +import Styles from '../../../../styles'; +import {copyToClipboard} from '../../../../utils/clipboard/clipboard'; +import AnimatedSuccessCheckmark from '../../../AnimatedSuccessCheckmark'; + +export const AddErc20TokenResultRender = ({contract, finishSend}) => { + return ( + + + copyToClipboard(contract.symbol, { + title: 'Token copied', + message: `${contract.symbol} copied to clipboard.`, + }) + } + style={{ + width: '75%', + marginTop: 16, + }}> + + {`${contract.symbol} added`} + + + + + + + + {`${contract.symbol} will now show on your wallet home page.`} + + + + + + + ); +}; diff --git a/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyConfirm/AddPbaasCurrencyConfirm.js b/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyConfirm/AddPbaasCurrencyConfirm.js index a66ec4c8..60aa6f06 100644 --- a/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyConfirm/AddPbaasCurrencyConfirm.js +++ b/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyConfirm/AddPbaasCurrencyConfirm.js @@ -11,7 +11,6 @@ import { } from '../../../../utils/constants/sendModal'; import {AddPbaasCurrencyConfirmRender} from './AddPbaasCurrencyConfirm.render'; import { CoinDirectory } from '../../../../utils/CoinData/CoinDirectory'; -import { IS_TOKEN_FLAG } from '../../../../utils/constants/currencies'; const AddPbaasCurrencyConfirm = props => { const [currency, setCurrency] = useState(props.route.params.currency); @@ -39,14 +38,6 @@ const AddPbaasCurrencyConfirm = props => { await props.setPreventExit(true); try { - // const isToken = (currency.options & IS_TOKEN_FLAG) == IS_TOKEN_FLAG; - - // if (!isToken) { - // throw new Error(`Currently, only adding on-chain currencies is supported in Verus Mobile. ${ - // currency.fullyqualifiedname - // } is an independant blockchain and can be accessed in native mode through Verus Desktop or the Verus CLI.`); - // } - await CoinDirectory.addPbaasCurrency(currency, Object.keys(activeAccount.testnetOverrides).length > 0, true) const fullCoinData = CoinDirectory.findCoinObj(currency.currencyid) diff --git a/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyForm/AddPbaasCurrencyForm.js b/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyForm/AddPbaasCurrencyForm.js index c2c64937..8006fc9f 100644 --- a/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyForm/AddPbaasCurrencyForm.js +++ b/src/components/SendModal/AddPbaasCurrency/AddPbaasCurrencyForm/AddPbaasCurrencyForm.js @@ -36,17 +36,6 @@ const AddPbaasCurrencyForm = (props) => { return false; }, [sendModal, dispatch]); - const getPotentialPrimaryAddresses = useCallback(async (coinObj, channel) => { - const seeds = await requestSeeds(); - - const seed = seeds[channel]; - - const keyObj = await deriveKeyPair(seed, coinObj, channel); - const {addresses} = keyObj; - - return addresses; - }, []); - const submitData = useCallback(async () => { if (formHasError()) { return; @@ -88,7 +77,7 @@ const AddPbaasCurrencyForm = (props) => { } props.setLoading(false) - }, [formHasError, getPotentialPrimaryAddresses, sendModal, dispatch, props]); + }, [formHasError, sendModal, dispatch, props]); return AddPbaasCurrencyFormRender({ submitData, diff --git a/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendConfirm/ConvertOrCrossChainSendConfirm.js b/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendConfirm/ConvertOrCrossChainSendConfirm.js index 6b604bf5..04315aa0 100644 --- a/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendConfirm/ConvertOrCrossChainSendConfirm.js +++ b/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendConfirm/ConvertOrCrossChainSendConfirm.js @@ -1,29 +1,51 @@ -import React, { useEffect, useRef, useState } from "react"; -import { ScrollView, View, TouchableOpacity, Alert } from "react-native"; -import { Button, List, Divider, Text } from "react-native-paper"; -import { useDispatch, useSelector } from 'react-redux' -import { expireCoinData } from "../../../../actions/actionCreators"; -import { traditionalCryptoSend } from "../../../../actions/actionDispatchers"; -import { copyToClipboard } from "../../../../utils/clipboard/clipboard"; -import { USD } from "../../../../utils/constants/currencies"; -import { API_GET_BALANCES, API_GET_FIATPRICE, API_GET_TRANSACTIONS, API_SEND } from "../../../../utils/constants/intervalConstants"; -import { SEND_MODAL_FORM_STEP_FORM, SEND_MODAL_FORM_STEP_RESULT } from "../../../../utils/constants/sendModal"; -import { coinsToSats, satsToCoins, truncateDecimal } from "../../../../utils/math"; -import Colors from "../../../../globals/colors"; -import Styles from "../../../../styles"; -import BigNumber from "bignumber.js"; -import { TransferDestination } from "verus-typescript-primitives"; -import { sendCurrencyTransfer } from "../../../../utils/api/channels/vrpc/callCreators"; -import { CoinDirectory } from "../../../../utils/CoinData/CoinDirectory"; - -function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModalHeight, setPreventExit }) { +import React, {useEffect, useRef, useState} from 'react'; +import {ScrollView, View, TouchableOpacity, Alert} from 'react-native'; +import {Button, List, Divider, Text} from 'react-native-paper'; +import {useDispatch, useSelector} from 'react-redux'; +import {expireCoinData} from '../../../../actions/actionCreators'; +import {copyToClipboard} from '../../../../utils/clipboard/clipboard'; +import { + API_GET_BALANCES, + API_GET_FIATPRICE, + API_GET_TRANSACTIONS, + API_SEND, + ERC20, + ETH, +} from '../../../../utils/constants/intervalConstants'; +import { + SEND_MODAL_FORM_STEP_FORM, + SEND_MODAL_FORM_STEP_RESULT, + SEND_MODAL_PRICE_ESTIMATE, +} from '../../../../utils/constants/sendModal'; +import { + coinsToSats, + satsToCoins +} from '../../../../utils/math'; +import Colors from '../../../../globals/colors'; +import Styles from '../../../../styles'; +import BigNumber from 'bignumber.js'; +import {TransferDestination} from 'verus-typescript-primitives'; +import {sendCurrencyTransfer} from '../../../../utils/api/channels/vrpc/callCreators'; +import {CoinDirectory} from '../../../../utils/CoinData/CoinDirectory'; +import { sendConvertOrCrossChain } from '../../../../utils/api/routers/sendConvertOrCrossChain'; + +function ConvertOrCrossChainSendConfirm({ + navigation, + route, + setLoading, + setModalHeight, + setPreventExit, +}) { const sendModal = useSelector(state => state.sendModal); + const activeAccount = useSelector(state => state.authentication.activeAccount); const networkName = useSelector(state => { try { const subwallet = state.sendModal.subWallet; - return subwallet.network ? CoinDirectory.getBasicCoinObj(subwallet.network).display_ticker : null; - } catch(e) { + return subwallet.network + ? CoinDirectory.getBasicCoinObj(subwallet.network).display_ticker + : null; + } catch (e) { console.error(e); return null; } @@ -50,7 +72,7 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa inputs, converterdef, submittedsats, - estimate + estimate, } = params; /** @@ -63,16 +85,7 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa */ const nameMap = names; - /** - * @type {string} - */ - const txHex = hex; - - const { - change, - fees, - sent - } = validation; + const {change, fees, sent} = validation; // const validation = { // change: {iJhCezBExJHvtyH3fGhNnt2NhU4Ztkf2yq: '399690000'}, @@ -104,36 +117,49 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa */ const destination = address; + const primaryCurrencyAmountSats = sent[currency] ? sent[currency] : 0; + const primaryCurrencyAmount = satsToCoins(BigNumber(primaryCurrencyAmountSats)).toNumber(); + const remainingBalances = {}; deltaMap.forEach((value, key) => { if (balances.hasOwnProperty(key) && !value.isEqualTo(BigNumber(0))) { const satBalance = coinsToSats(BigNumber(balances[key])); - remainingBalances[key] = (satBalance.plus(value)).toString(); + remainingBalances[key] = satBalance.plus(value).toString(); } }); let conversionFeeMultiplier = BigNumber(0); - + if (convertto != null && via != null) { conversionFeeMultiplier = BigNumber(0.05); } else if (convertto != null) { conversionFeeMultiplier = BigNumber(0.025); } - const conversionFee = satsToCoins(BigNumber(satoshis)).multipliedBy(conversionFeeMultiplier); + const conversionFee = satsToCoins(BigNumber(satoshis)).multipliedBy( + conversionFeeMultiplier, + ); const destAddrString = destination.getAddressString(); - const toAddress = nameMap.has(destAddrString) ? `${nameMap.get(destAddrString)}@ (${destAddrString})` : destAddrString; - - const createAccordion = (label, description, left, currencies, showZeroValues = false) => { - const fields = [] + const toAddress = nameMap.has(destAddrString) + ? `${nameMap.get(destAddrString)}@ (${destAddrString})` + : destAddrString; + + const createAccordion = ( + label, + description, + left, + currencies, + showZeroValues = false, + ) => { + const fields = []; for (const key in currencies) { - const value = currencies[key] + const value = currencies[key]; const currencyName = nameMap.has(key) ? nameMap.get(key) : key; - const valueBn = satsToCoins(BigNumber(value)) + const valueBn = satsToCoins(BigNumber(value)); if (showZeroValues || !valueBn.isEqualTo(BigNumber(0))) { const readableValue = valueBn.toString(); @@ -155,13 +181,13 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa label, description, left, - fields - } - } + fields, + }; + }; - const tryRenderFriendlyName = (address) => { - return nameMap.has(address) ? nameMap.get(address) : address - } + const tryRenderFriendlyName = address => { + return nameMap.has(address) ? nameMap.get(address) : address; + }; setConfirmationFields([ { @@ -172,7 +198,7 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa copyToClipboard(source, { title: 'Address copied', message: `${source} copied to clipboard.`, - }) + }), }, { key: 'Destination', @@ -186,39 +212,61 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa }, { key: 'Converting To', - data: via != null && via.length > 0 ? - `${tryRenderFriendlyName(convertto)} via ${tryRenderFriendlyName(via)}` - : - tryRenderFriendlyName(convertto), + data: + via != null && via.length > 0 + ? `${tryRenderFriendlyName(convertto)} via ${tryRenderFriendlyName( + via, + )}` + : tryRenderFriendlyName(convertto), numLines: 100, onPress: () => copyToClipboard(tryRenderFriendlyName(convertto), { title: 'Currency copied', message: `${tryRenderFriendlyName(convertto)} copied to clipboard.`, }), - condition: convertto != null && convertto.length > 0 + condition: convertto != null && convertto.length > 0, }, { key: 'Estimated To Receive', - data: estimate != null ? `${estimate.estimatedcurrencyout} ${tryRenderFriendlyName(convertto)}` : "", + data: + estimate != null + ? `${estimate.estimatedcurrencyout} ${tryRenderFriendlyName( + convertto, + )}` + : !!(sendModal.data[SEND_MODAL_PRICE_ESTIMATE]) ? `${Number( + ( + primaryCurrencyAmount * sendModal.data[SEND_MODAL_PRICE_ESTIMATE].price + ).toFixed(8), + )} ${tryRenderFriendlyName(convertto)}` : "", numLines: 100, onPress: () => copyToClipboard(tryRenderFriendlyName(convertto), { title: 'Amount copied', message: `${estimate.estimatedcurrencyout} copied to clipboard.`, }), - condition: estimate != null && convertto != null && convertto.length > 0 + condition: + (estimate != null || !!(sendModal.data[SEND_MODAL_PRICE_ESTIMATE])) && convertto != null && convertto.length > 0, }, { key: 'Estimated Time Until Arrival', - data: exportto != null ? "20-30 minutes" : convertto != null ? "2-10 minutes" : "1-5 minutes", + data: + ((sendModal.coinObj.proto === ETH || + sendModal.coinObj.proto === ERC20) && convertto != null && exportto == null) ? + '40-60 minutes' : + exportto != null + ? '20-30 minutes' + : convertto != null + ? '2-10 minutes' + : '1-5 minutes', numLines: 100, }, { key: 'Preconvert', - data: tryRenderFriendlyName(convertto) + " hasn't launched yet. You will receive your converted funds or have your transaction refunded once the currency launches or fails to launch.", + data: + tryRenderFriendlyName(convertto) + + " hasn't launched yet. You will receive your converted funds or have your transaction refunded once the currency launches or fails to launch.", numLines: 100, - condition: preconvert != null && preconvert + condition: preconvert != null && preconvert, }, { key: 'From Network', @@ -229,7 +277,8 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa title: 'Currency copied', message: `${networkName} copied to clipboard.`, }), - condition: networkName != null && exportto != null && exportto.length > 0 + condition: + networkName != null && exportto != null && exportto.length > 0, }, { key: 'To Network', @@ -240,19 +289,19 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa title: 'Currency copied', message: `${tryRenderFriendlyName(exportto)} copied to clipboard.`, }), - condition: exportto != null && exportto.length > 0 + condition: exportto != null && exportto.length > 0, }, createAccordion( 'Currency to send', 'The currencies that are being sent as part of this transaction', props => , - sent + sent, ), createAccordion( - 'Transaction Fees', + (sendModal.coinObj.proto === ETH || sendModal.coinObj.proto === ERC20) ? 'Maximum Transaction Fees' : 'Transaction Fees', 'Fees deducted from your wallet to pay for this transaction', props => , - fees + fees, ), { key: 'Conversion Fee (taken from send amount)', @@ -263,89 +312,94 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa title: 'Fee copied', message: `${conversionFee.toString()} copied to clipboard.`, }), - condition: !conversionFee.isEqualTo(BigNumber(0)) + condition: !conversionFee.isEqualTo(BigNumber(0)), }, createAccordion( 'Remaining Balances', - 'Your currency remaining in the address you\'re sending from after subtracting currency sent and fees (only affected balances shown)', + "Your currency remaining in the address you're sending from after subtracting currency sent and fees (only affected balances shown)", props => , remainingBalances, - true + true, ), ]); setLoading(false); setTimeout(() => { scrollRef.current.flashScrollIndicators(); - }, 500) + }, 500); }, []); - const toggleAccordion = (key) => { + const toggleAccordion = key => { setClosedAccordions({ ...closedAccordions, - [key]: !(closedAccordions[key]) - }) - } + [key]: !closedAccordions[key], + }); + }; const goBack = () => { - setModalHeight() - navigation.navigate(SEND_MODAL_FORM_STEP_FORM) - } + setModalHeight(); + navigation.navigate(SEND_MODAL_FORM_STEP_FORM); + }; const submitData = async () => { - await setLoading(true) - await setPreventExit(true) + await setLoading(true); + await setPreventExit(true); - const { - output, - validation, - hex, - names, - deltas, - source, - inputs - } = params; + const {output, validation, hex, names, deltas, source, inputs} = params; try { const destAddrString = output.address.getAddressString(); - const toAddress = names.hasOwnProperty(destAddrString) ? names[destAddrString] : destAddrString; + const toAddress = names.hasOwnProperty(destAddrString) + ? names[destAddrString] + : destAddrString; - const res = await sendCurrencyTransfer(sendModal.coinObj, sendModal.subWallet.api_channels[API_SEND], hex, inputs); + const res = await sendConvertOrCrossChain( + sendModal.coinObj, + activeAccount, + sendModal.subWallet.api_channels[API_SEND], + params + ); if (res.err) throw new Error(res.result); - else navigation.navigate(SEND_MODAL_FORM_STEP_RESULT, { ...res.result, output, destination: toAddress }); - } catch(e) { - Alert.alert("Error", e.message) + else + navigation.navigate(SEND_MODAL_FORM_STEP_RESULT, { + ...res.result, + output, + destination: toAddress, + }); + } catch (e) { + Alert.alert('Error', e.message); } dispatch(expireCoinData(sendModal.coinObj.id, API_GET_FIATPRICE)); dispatch(expireCoinData(sendModal.coinObj.id, API_GET_TRANSACTIONS)); dispatch(expireCoinData(sendModal.coinObj.id, API_GET_BALANCES)); - setPreventExit(false) - setLoading(false) + setPreventExit(false); + setLoading(false); }; const renderItem = (item, index, divide = true) => { return ( - item.onPress()}> + item.onPress()}> + right={props => item.right ? ( + }}> {item.right} ) : null @@ -355,18 +409,20 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa ); - } + }; return ( - + ref={scrollRef}> {confirmationFields.map((item, index) => { - if ((item.accordion || item.data != null) && (item.condition == null || item.condition === true)) + if ( + (item.accordion || item.data != null) && + (item.condition == null || item.condition === true) + ) if (item.accordion) { const key = index.toString(); @@ -384,9 +440,7 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa ); } else { - return ( - renderItem(item, index) - ); + return renderItem(item, index); } else return null; })} @@ -395,25 +449,22 @@ function ConvertOrCrossChainSendConfirm({ navigation, route, setLoading, setModa style={{ ...Styles.fullWidthBlock, paddingHorizontal: 16, - flexDirection: "row", - justifyContent: "space-between" - }} - > + flexDirection: 'row', + justifyContent: 'space-between', + }}> diff --git a/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js b/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js index 5e085d8e..05071726 100644 --- a/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js +++ b/src/components/SendModal/ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm.js @@ -5,7 +5,7 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view import { TextInput, Button, Divider, Checkbox, List, Text, IconButton } from "react-native-paper"; import { useSelector } from 'react-redux'; import { createAlert } from "../../../../actions/actions/alert/dispatchers/alert"; -import { API_SEND, DLIGHT_PRIVATE } from "../../../../utils/constants/intervalConstants"; +import { API_SEND, DLIGHT_PRIVATE, ERC20, ETH } from "../../../../utils/constants/intervalConstants"; import { SEND_MODAL_ADVANCED_FORM, SEND_MODAL_AMOUNT_FIELD, @@ -13,9 +13,11 @@ import { SEND_MODAL_EXPORTTO_FIELD, SEND_MODAL_FORM_STEP_CONFIRM, SEND_MODAL_IS_PRECONVERT, + SEND_MODAL_MAPPING_FIELD, SEND_MODAL_PRICE_ESTIMATE, SEND_MODAL_SHOW_CONVERTTO_FIELD, SEND_MODAL_SHOW_EXPORTTO_FIELD, + SEND_MODAL_SHOW_MAPPING_FIELD, SEND_MODAL_SHOW_VIA_FIELD, SEND_MODAL_TO_ADDRESS_FIELD, SEND_MODAL_VIA_FIELD, @@ -26,19 +28,33 @@ import Styles from "../../../../styles"; import { useEffect } from "react"; import { getConversionPaths } from "../../../../utils/api/routers/getConversionPaths"; import AnimatedActivityIndicatorBox from "../../../AnimatedActivityIndicatorBox"; -import { getCoinLogo } from "../../../../utils/CoinData/CoinData"; -import { getCurrency, getIdentity } from "../../../../utils/api/channels/verusid/callCreators"; +import { getCoinLogo, getSystemNameFromSystemId } from "../../../../utils/CoinData/CoinData"; +import { getCurrency } from "../../../../utils/api/channels/verusid/callCreators"; import selectAddresses from "../../../../selectors/address"; import MissingInfoRedirect from "../../../MissingInfoRedirect/MissingInfoRedirect"; -import { getAddressBalances, preflightCurrencyTransfer } from "../../../../utils/api/channels/vrpc/callCreators"; -import { DEST_ETH, DEST_ID, DEST_PKH, TransferDestination, fromBase58Check } from "verus-typescript-primitives"; +import { getInfo, preflightCurrencyTransfer } from "../../../../utils/api/channels/vrpc/callCreators"; +import { DEST_ETH, DEST_ID, DEST_PKH, TransferDestination, fromBase58Check, toIAddress } from "verus-typescript-primitives"; import { CoinDirectory } from "../../../../utils/CoinData/CoinDirectory"; import { ethers } from "ethers"; +import CoreSendFormModule from "../../../FormModules/CoreSendFormModule"; +import ConvertFormModule from "../../../FormModules/ConvertFormModule"; +import ExportFormModule from "../../../FormModules/ExportFormModule"; +import { getAddressBalances } from "../../../../utils/api/routers/getAddressBalance"; +import { coinsList } from "../../../../utils/CoinData/CoinsList"; +import { ETH_CONTRACT_ADDRESS, VETH } from "../../../../utils/constants/web3Constants"; +import { preflightConvertOrCrossChain } from "../../../../utils/api/routers/preflightConvertOrCrossChain"; +import { getIdentity } from "../../../../utils/api/routers/getIdentity"; +import { addressIsBlocked } from "../../../../utils/addressBlocklist"; +import selectAddressBlocklist from "../../../../selectors/settings"; +import { I_ADDRESS_VERSION, R_ADDRESS_VERSION } from "../../../../utils/constants/constants"; +import { getWeb3ProviderForNetwork } from "../../../../utils/web3/provider"; const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFormData, navigation }) => { const sendModal = useSelector(state => state.sendModal); + const activeUser = useSelector(state => state.authentication.activeAccount); const addresses = useSelector(state => selectAddresses(state)); const activeAccount = useSelector(state => state.authentication.activeAccount); + const addressBlocklist = useSelector(selectAddressBlocklist); const networkName = useSelector(state => { try { const subwallet = state.sendModal.subWallet; @@ -53,7 +69,8 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor const FIELD_TITLES = { [SEND_MODAL_EXPORTTO_FIELD]: "Destination network", [SEND_MODAL_VIA_FIELD]: "Convert via", - [SEND_MODAL_CONVERTTO_FIELD]: "Convert to" + [SEND_MODAL_CONVERTTO_FIELD]: sendModal.data[SEND_MODAL_IS_PRECONVERT] ? "Preconvert to" : "Convert to", + [SEND_MODAL_MAPPING_FIELD]: "Receive as" } const [searchMode, setSearchMode] = useState(false); @@ -61,7 +78,7 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor const [processedAmount, setProcessedAmount] = useState(null); - const [localNetworkDefinition, setLocalNetworkDefinition] = useState(null); + const [localNetworkName, setLocalNetworkName] = useState(null); const [localBalances, setLocalBalances] = useState(null); const [suggestions, setSuggestions] = useState([]); @@ -71,16 +88,20 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor const [conversionPaths, setConversionPaths] = useState(null); const [isConversion, setIsConversion] = useState( - sendModal.data[SEND_MODAL_CONVERTTO_FIELD] && - sendModal.data[SEND_MODAL_CONVERTTO_FIELD].length, + !!(sendModal.data[SEND_MODAL_CONVERTTO_FIELD] != null && + sendModal.data[SEND_MODAL_CONVERTTO_FIELD].length > 0) ); const [isVia, setIsVia] = useState( - sendModal.data[SEND_MODAL_VIA_FIELD] && - sendModal.data[SEND_MODAL_VIA_FIELD].length, + !!(sendModal.data[SEND_MODAL_VIA_FIELD] != null && + sendModal.data[SEND_MODAL_VIA_FIELD].length > 0) ); const [isExport, setIsExport] = useState( - sendModal.data[SEND_MODAL_EXPORTTO_FIELD] && - sendModal.data[SEND_MODAL_EXPORTTO_FIELD].length, + !!(sendModal.data[SEND_MODAL_EXPORTTO_FIELD] != null && + sendModal.data[SEND_MODAL_EXPORTTO_FIELD].length > 0) + ); + const [isMapping, setIsMapping] = useState( + !!(sendModal.data[SEND_MODAL_MAPPING_FIELD] != null && + sendModal.data[SEND_MODAL_MAPPING_FIELD].length > 0) ); const [showConversionField, setShowConversionField] = useState( @@ -98,6 +119,11 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor isExport ) + const [showMappingField, setShowMappingField] = useState( + sendModal.data[SEND_MODAL_MAPPING_FIELD] || + isMapping + ) + const fadeSearchMode = useRef(new Animated.Value(0)).current; const fadeNormalForm = useRef(new Animated.Value(1)).current; @@ -175,6 +201,356 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor else return "" } + const processConverttoSuggestionPaths = async (flatPaths, coinObj) => { + switch (coinObj.proto) { + case 'vrsc': + return flatPaths.filter(x => { + if (sendModal.data[SEND_MODAL_IS_PRECONVERT]) { + return !x.mapping && x.prelaunch + } else return !x.mapping && !x.prelaunch + }).map((path, index) => { + const priceFixed = Number(path.price.toFixed(2)) + + return { + title: path.destination.fullyqualifiedname, + logoid: path.destination.currencyid, + key: index.toString(), + description: path.via + ? `${ + path.exportto + ? path.gateway + ? `off-system to ${path.exportto.fullyqualifiedname} network ` + : `off-chain to ${path.exportto.fullyqualifiedname} network ` + : '' + }via ${path.via.fullyqualifiedname}` + : path.exportto + ? path.gateway && path.exportto + ? `off-system to ${path.exportto.fullyqualifiedname} network` + : `off-chain to ${path.exportto.fullyqualifiedname} network` + : 'direct', + values: { + [SEND_MODAL_VIA_FIELD]: path.via ? path.via.fullyqualifiedname : '', + [SEND_MODAL_CONVERTTO_FIELD]: path.destination.fullyqualifiedname, + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto + ? path.exportto.fullyqualifiedname + : '', + [SEND_MODAL_PRICE_ESTIMATE]: { price: path.price, name: path.destination.fullyqualifiedname } + }, + right: `${ + priceFixed === 0 + ? '<0.01' + : priceFixed === path.price + ? priceFixed + : `~${priceFixed}` + }`, + keywords: [path.destination.currencyid, path.destination.fullyqualifiedname] + }; + }); + case 'eth': + case 'erc20': + return flatPaths.filter(x => { + return !x.mapping + }).map((path, index) => { + const priceFixed = Number(path.price.toFixed(2)) + const addr = path.ethdest ? path.destination.address : null; + + const name = path.ethdest + ? path.destination.address === ETH_CONTRACT_ADDRESS ? path.destination.name : `${path.destination.name} (${ + addr.substring(0, 8) + '...' + addr.substring(addr.length - 8) + })` + : path.destination.fullyqualifiedname; + + return { + title: name, + logoid: path.ethdest ? path.destination.address : path.destination.currencyid, + logoproto: path.ethdest ? "erc20" : null, + key: index.toString(), + description: path.via + ? `${ + path.exportto + ? path.gateway + ? `off-system to ${path.exportto.fullyqualifiedname} network ` + : `off-chain to ${path.exportto.fullyqualifiedname} network ` + : '' + }via ${path.via.fullyqualifiedname}` + : path.exportto + ? path.gateway && path.exportto + ? `off-system to ${path.exportto.fullyqualifiedname} network` + : `off-chain to ${path.exportto.fullyqualifiedname} network` + : 'direct', + values: { + [SEND_MODAL_VIA_FIELD]: path.via ? path.via.fullyqualifiedname : '', + [SEND_MODAL_CONVERTTO_FIELD]: path.ethdest ? path.destination.mapto.fullyqualifiedname : path.destination.fullyqualifiedname, + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto + ? path.exportto.fullyqualifiedname + : '', + [SEND_MODAL_PRICE_ESTIMATE]: { price: path.price, name: name } + }, + right: `${ + priceFixed === 0 + ? '<0.01' + : priceFixed === path.price + ? priceFixed + : `~${priceFixed}` + }`, + keywords: path.ethdest + ? [path.destination.address, path.destination.symbol, path.destination.name] + : [path.destination.currencyid, path.destination.fullyqualifiedname] + }; + }); + default: + return [] + } + } + + const processViaSuggestionPaths = async (flatPaths, coinObj) => { + switch (coinObj.proto) { + case 'vrsc': + return flatPaths.filter(x => { + if (isConversion) { + const destinationCurrency = sendModal.data[SEND_MODAL_CONVERTTO_FIELD]; + return ( + x.via != null && + (x.destination.currencyid === destinationCurrency || + destinationCurrency.toLowerCase() === + x.destination.fullyqualifiedname.toLowerCase()) + ); + } else return x.via != null; + }).map((path, index) => { + const priceFixed = Number(path.price.toFixed(2)) + + return { + title: path.via.fullyqualifiedname, + logoid: path.via.currencyid, + key: index.toString(), + description: `${ + path.exportto + ? path.gateway + ? `off-system to ${path.exportto.fullyqualifiedname} network ` + : `off-chain to ${path.exportto.fullyqualifiedname} network ` + : '' + }to ${path.destination.fullyqualifiedname}`, + values: { + [SEND_MODAL_VIA_FIELD]: path.via.fullyqualifiedname, + [SEND_MODAL_CONVERTTO_FIELD]: path.destination.fullyqualifiedname, + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto + ? path.exportto.fullyqualifiedname + : '', + [SEND_MODAL_PRICE_ESTIMATE]: { price: path.price, name: path.destination.fullyqualifiedname } + }, + right: `${ + priceFixed === 0 + ? '<0.01' + : priceFixed === path.price + ? priceFixed + : `~${priceFixed}` + }`, + keywords: [path.via.currencyid, path.via.fullyqualifiedname] + }; + }) + case 'eth': + case 'erc20': + return flatPaths.filter(x => { + if (isConversion) { + const destinationCurrency = sendModal.data[SEND_MODAL_CONVERTTO_FIELD]; + const destinationCurrencyMatch = !x.ethdest && (x.destination.currencyid === destinationCurrency || + destinationCurrency.toLowerCase() === + x.destination.fullyqualifiedname.toLowerCase()) + const destinationTokenMatch = x.ethdest && (x.destination.address === destinationCurrency || + destinationCurrency.toLowerCase() === + x.destination.address.toLowerCase()) + + return ( + x.via != null && + (destinationCurrencyMatch || destinationTokenMatch) + ); + } else return x.via != null; + }).map((path, index) => { + const priceFixed = Number(path.price.toFixed(2)) + const destname = path.ethdest + ? path.destination.address === ETH_CONTRACT_ADDRESS ? path.destination.name : `${path.destination.name} (${ + path.destination.address.substring(0, 8) + '...' + path.destination.address.substring(path.destination.address.length - 8) + })` + : path.destination.fullyqualifiedname; + + return { + title: path.via.fullyqualifiedname, + logoid: path.via.currencyid, + key: index.toString(), + description: `${ + path.exportto + ? path.gateway + ? `off-system to ${path.exportto.fullyqualifiedname} network ` + : `off-chain to ${path.exportto.fullyqualifiedname} network ` + : '' + }to ${destname}`, + values: { + [SEND_MODAL_VIA_FIELD]: path.via.fullyqualifiedname, + [SEND_MODAL_CONVERTTO_FIELD]: path.ethdest ? path.destination.mapto.fullyqualifiedname : path.destination.fullyqualifiedname, + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto + ? path.exportto.fullyqualifiedname + : '', + [SEND_MODAL_PRICE_ESTIMATE]: { price: path.price, name: destname } + }, + right: `${ + priceFixed === 0 + ? '<0.01' + : priceFixed === path.price + ? priceFixed + : `~${priceFixed}` + }`, + keywords: [path.via.currencyid, path.via.fullyqualifiedname] + }; + }) + default: + return [] + } + } + + const processExporttoSuggestionPaths = async (flatPaths, coinObj) => { + switch (coinObj.proto) { + case 'vrsc': + const seenSystems = {} + return flatPaths.filter(x => { + if (x.exportto == null) return false; + + if (sendModal.data[SEND_MODAL_IS_PRECONVERT]) { + if ( + !x.prelaunch || (!x.mapping && flatPaths.find( + y => + y.mapping && + y.exportto != null && + y.exportto.currencyid === x.exportto.currencyid, + ) != null) + ) + return false; + } else { + if ( + x.prelaunch || (!x.mapping && flatPaths.find( + y => + y.mapping && + y.exportto != null && + y.exportto.currencyid === x.exportto.currencyid, + ) != null) + ) + return false; + } + + const seen = seenSystems[x.exportto.currencyid] != null; + + if (!seen) { + seenSystems[x.exportto.currencyid] = true; + return true + } else return false; + }).map((path, index) => { + + return { + title: path.exportto.fullyqualifiedname, + logoid: path.exportto.currencyid, + key: index.toString(), + description: path.exportto.currencyid, + values: { + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto.fullyqualifiedname, + [SEND_MODAL_MAPPING_FIELD]: path.mapping ? path.destination.symbol : coinObj.display_ticker + }, + right: "", + keywords: [path.exportto.currencyid, path.exportto.fullyqualifiedname] + }; + }); + case 'eth': + case 'erc20': + return [ + { + title: coinObj.testnet + ? coinsList.VRSCTEST.display_name + : coinsList.VRSC.display_name, + logoid: coinObj.testnet ? coinsList.VRSCTEST.id : coinsList.VRSC.id, + key: 0, + description: coinObj.testnet + ? 'Send to the Verus testnet' + : 'Send to the Verus network', + values: { + [SEND_MODAL_EXPORTTO_FIELD]: coinObj.testnet + ? coinsList.VRSCTEST.display_ticker + : coinsList.VRSC.display_ticker, + }, + right: '', + keywords: coinObj.testnet + ? [coinsList.VRSCTEST.display_name, coinsList.VRSCTEST.id] + : [coinsList.VRSC.display_name, coinsList.VRSCTEST.id], + }, + ]; + default: + return [] + } + } + + const processMappingSuggestionPaths = async (flatPaths, coinObj) => { + switch (coinObj.proto) { + case 'vrsc': + return flatPaths + .filter(x => { + return x.mapping; + }) + .map((path, index) => { + const name = + path.destination.address === ETH_CONTRACT_ADDRESS + ? path.destination.name + : `${path.destination.name} (${ + path.destination.address.substring(0, 8) + + '...' + + path.destination.address.substring(path.destination.address.length - 8) + })`; + + return { + title: name, + logoid: path.destination.address, + logoproto: 'erc20', + key: index.toString(), + description: path.destination.address === ETH_CONTRACT_ADDRESS ? "receive as Ethereum" : `receive as the ${path.destination.symbol} ERC20 token`, + values: { + [SEND_MODAL_VIA_FIELD]: '', + [SEND_MODAL_MAPPING_FIELD]: path.destination.symbol, + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto.fullyqualifiedname, + [SEND_MODAL_PRICE_ESTIMATE]: { price: path.price, name: name }, + }, + right: "", + keywords: [ + path.destination.symbol, + path.destination.name, + ], + }; + }); + case 'eth': + case 'erc20': + return flatPaths + .filter(x => { + return x.mapping; + }) + .map((path, index) => { + return { + title: path.destination.fullyqualifiedname, + logoid: path.destination.currencyid, + key: index.toString(), + description: `receive on ${path.exportto.fullyqualifiedname} network`, + values: { + [SEND_MODAL_VIA_FIELD]: '', + [SEND_MODAL_MAPPING_FIELD]: path.destination.fullyqualifiedname, + [SEND_MODAL_EXPORTTO_FIELD]: path.exportto.fullyqualifiedname, + [SEND_MODAL_PRICE_ESTIMATE]: { price: path.price, name: path.destination.fullyqualifiedname }, + }, + right: "", + keywords: [ + path.destination.currencyid, + path.destination.fullyqualifiedname, + ], + }; + }); + default: + return []; + } + }; + const fetchSuggestionsBase = async (field) => { if (loadingSuggestions) return; let newSuggestionsBase = [] @@ -185,11 +561,13 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor // {[destinationid: string]: Array<{ // via?: CurrencyDefinition; // destination: CurrencyDefinition; - // exportto?: CurrencyDefinition; + // exportto?: string; // price: number; // viapriceinroot?: number; // destpriceinvia?: number; // gateway: boolean; + // mapping: boolean; + // bounceback: boolean; // }>} const paths = conversionPaths @@ -212,120 +590,20 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor switch (field) { case SEND_MODAL_CONVERTTO_FIELD: - newSuggestionsBase = flatPaths.map((path, index) => { - const priceFixed = Number(path.price.toFixed(2)) - - return { - title: path.destination.fullyqualifiedname, - logoid: path.destination.currencyid, - key: index.toString(), - description: path.via - ? `${ - path.exportto - ? path.gateway - ? `off-system to ${path.exportto.fullyqualifiedname} network ` - : `off-chain to ${path.exportto.fullyqualifiedname} network ` - : '' - }via ${path.via.fullyqualifiedname}` - : path.exportto - ? path.gateway && path.exportto - ? `off-system to ${path.exportto.fullyqualifiedname} network` - : `off-chain to ${path.exportto.fullyqualifiedname} network` - : 'direct', - values: { - [SEND_MODAL_VIA_FIELD]: path.via ? path.via.fullyqualifiedname : '', - [SEND_MODAL_CONVERTTO_FIELD]: path.destination.fullyqualifiedname, - [SEND_MODAL_EXPORTTO_FIELD]: path.exportto - ? path.exportto.fullyqualifiedname - : '', - [SEND_MODAL_PRICE_ESTIMATE]: path.price - }, - right: `${ - priceFixed === 0 - ? '<0.01' - : priceFixed === path.price - ? priceFixed - : `~${priceFixed}` - }`, - keywords: [path.destination.currencyid, path.destination.fullyqualifiedname] - }; - }); - + newSuggestionsBase = await processConverttoSuggestionPaths(flatPaths, sendModal.coinObj); setSuggestionBase(newSuggestionsBase); break; case SEND_MODAL_VIA_FIELD: - newSuggestionsBase = flatPaths.filter(x => { - if (isConversion) { - const destinationCurrency = sendModal.data[SEND_MODAL_CONVERTTO_FIELD]; - return ( - x.via != null && - (x.destination.currencyid === destinationCurrency || - destinationCurrency.toLowerCase() === - x.destination.fullyqualifiedname.toLowerCase()) - ); - } else return x.via != null; - }).map((path, index) => { - const priceFixed = Number(path.price.toFixed(2)) - - return { - title: path.via.fullyqualifiedname, - logoid: path.via.currencyid, - key: index.toString(), - description: `${ - path.exportto - ? path.gateway - ? `off-system to ${path.exportto.fullyqualifiedname} network ` - : `off-chain to ${path.exportto.fullyqualifiedname} network ` - : '' - }to ${path.destination.fullyqualifiedname}`, - values: { - [SEND_MODAL_VIA_FIELD]: path.via.fullyqualifiedname, - [SEND_MODAL_CONVERTTO_FIELD]: path.destination.fullyqualifiedname, - [SEND_MODAL_EXPORTTO_FIELD]: path.exportto - ? path.exportto.fullyqualifiedname - : '', - [SEND_MODAL_PRICE_ESTIMATE]: path.price - }, - right: `${ - priceFixed === 0 - ? '<0.01' - : priceFixed === path.price - ? priceFixed - : `~${priceFixed}` - }`, - keywords: [path.via.currencyid, path.via.fullyqualifiedname] - }; - }) - + newSuggestionsBase = await processViaSuggestionPaths(flatPaths, sendModal.coinObj); setSuggestionBase(newSuggestionsBase); break; case SEND_MODAL_EXPORTTO_FIELD: - const seenSystems = {} - newSuggestionsBase = flatPaths.filter(x => { - if (x.exportto == null) return false; - - const seen = seenSystems[x.exportto.currencyid] != null; - - if (!seen) { - seenSystems[x.exportto.currencyid] = true; - return true - } else return false; - }).map((path, index) => { - return { - title: path.exportto.fullyqualifiedname, - logoid: path.exportto.currencyid, - key: index.toString(), - description: path.exportto.currencyid, - values: { - [SEND_MODAL_EXPORTTO_FIELD]: path.exportto.fullyqualifiedname, - }, - right: "", - keywords: [path.exportto.currencyid, path.exportto.fullyqualifiedname] - }; - }); - + newSuggestionsBase = await processExporttoSuggestionPaths(flatPaths, sendModal.coinObj); setSuggestionBase(newSuggestionsBase); break; + case SEND_MODAL_MAPPING_FIELD: + newSuggestionsBase = await processMappingSuggestionPaths(flatPaths, sendModal.coinObj); + setSuggestionBase(newSuggestionsBase); default: setSuggestionBase(newSuggestionsBase); break; @@ -374,22 +652,40 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor leaveSearchMode(); }; - const fetchLocalNetworkDefinition = async () => { - const [channelName, address, systemId] = sendModal.subWallet.api_channels[API_SEND].split('.'); + const fetchLocalNetworkInfo = async () => { + let address; - const response = await getCurrency(sendModal.coinObj.system_id, systemId); + if (sendModal.coinObj.proto === 'erc20' || sendModal.coinObj.proto === 'eth') { + address = + sendModal.coinObj.proto === 'erc20' + ? activeUser.keys[sendModal.coinObj.id][ERC20].addresses[0] + : activeUser.keys[sendModal.coinObj.id][ETH].addresses[0]; - if (response.error) createAlert("Error", "Error fetching current network."); - else { - setLocalNetworkDefinition(response.result); - - const balanceRes = await getAddressBalances(response.result.currencyid, [address]); + setLocalNetworkName(coinsList.ETH.display_ticker); + } else { + const [channelName, channelAddress, systemId] = sendModal.subWallet.api_channels[API_SEND].split('.'); - if (balanceRes.error) createAlert("Error", "Error fetching balances."); + address = channelAddress; - setLocalBalances( - balanceRes.result.currencybalance ? balanceRes.result.currencybalance : {}, + const response = await getCurrency(systemId, sendModal.coinObj.system_id); + + if (response.error) Alert.alert("Error fetching current network", response.error.message); + else { + setLocalNetworkName(response.result.fullyqualifiedname); + } + } + + try { + const balances = await getAddressBalances( + sendModal.coinObj, + sendModal.subWallet.api_channels[API_SEND], + {address}, ); + + setLocalBalances(balances); + } catch(e) { + console.error(e) + Alert.alert("Error fetching balances", e.message); } } @@ -405,29 +701,38 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor setShowExportField(sendModal.data[SEND_MODAL_SHOW_EXPORTTO_FIELD] || isExport) }, [sendModal.data[SEND_MODAL_SHOW_EXPORTTO_FIELD], isExport]) + useEffect(() => { + setShowMappingField(sendModal.data[SEND_MODAL_SHOW_MAPPING_FIELD] || isMapping) + }, [sendModal.data[SEND_MODAL_SHOW_MAPPING_FIELD], isMapping]) + useEffect(() => { setProcessedAmount(getProcessedAmount()) }, [sendModal.data[SEND_MODAL_AMOUNT_FIELD]]) useEffect(() => { - fetchLocalNetworkDefinition() + fetchLocalNetworkInfo() }, [sendModal.subWallet.api_channels[API_SEND]]) useEffect(() => { - setIsConversion(sendModal.data[SEND_MODAL_CONVERTTO_FIELD] && - sendModal.data[SEND_MODAL_CONVERTTO_FIELD].length) + setIsConversion(!!(sendModal.data[SEND_MODAL_CONVERTTO_FIELD] != null && + sendModal.data[SEND_MODAL_CONVERTTO_FIELD].length > 0)) }, [sendModal.data[SEND_MODAL_CONVERTTO_FIELD]]) useEffect(() => { - setIsVia(sendModal.data[SEND_MODAL_VIA_FIELD] && - sendModal.data[SEND_MODAL_VIA_FIELD].length) + setIsVia(!!(sendModal.data[SEND_MODAL_VIA_FIELD] != null && + sendModal.data[SEND_MODAL_VIA_FIELD].length > 0)) }, [sendModal.data[SEND_MODAL_VIA_FIELD]]) useEffect(() => { - setIsExport(sendModal.data[SEND_MODAL_EXPORTTO_FIELD] && - sendModal.data[SEND_MODAL_EXPORTTO_FIELD].length) + setIsExport(!!(sendModal.data[SEND_MODAL_EXPORTTO_FIELD] != null && + sendModal.data[SEND_MODAL_EXPORTTO_FIELD].length > 0)) }, [sendModal.data[SEND_MODAL_EXPORTTO_FIELD]]) + useEffect(() => { + setIsMapping(!!(sendModal.data[SEND_MODAL_MAPPING_FIELD] != null && + sendModal.data[SEND_MODAL_MAPPING_FIELD].length > 0)) + }, [sendModal.data[SEND_MODAL_MAPPING_FIELD]]) + const fillAmount = (amount) => { let displayAmount = BigNumber(amount); if (displayAmount.isLessThan(BigNumber(0))) { @@ -478,6 +783,9 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor if (!toAddress || toAddress.length < 1) { createAlert("Required Field", "Address is a required field."); return true; + } else if (addressIsBlocked(toAddress, addressBlocklist)) { + createAlert("Blocked Address", "The address you are trying to send to is included in your address blocklist."); + return true; } if (amount == null) { @@ -522,7 +830,7 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor let keyhash; if (addr.endsWith("@")) { - const identityRes = await getIdentity(coinObj.system_id, addr); + const identityRes = await getIdentity(coinObj, activeAccount, channel, addr); if (identityRes.error) throw new Error("Failed to fetch " + addr); @@ -544,29 +852,59 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor }) } else { const { hash, version } = fromBase58Check(keyhash); + let type; + + if (version === R_ADDRESS_VERSION) type = DEST_PKH; + else if (version === I_ADDRESS_VERSION) type = DEST_ID; + else throw new Error("Incompatible address type."); return new TransferDestination({ destination_bytes: hash, - type: version === 60 ? DEST_PKH : DEST_ID + type }) } } + const amount = getProcessedAmount(); + let output = { currency: coinObj.currency_id, + mapto: selectData(data[SEND_MODAL_MAPPING_FIELD]), convertto: selectData(data[SEND_MODAL_CONVERTTO_FIELD]), exportto: selectData(data[SEND_MODAL_EXPORTTO_FIELD]), via: selectData(data[SEND_MODAL_VIA_FIELD]), address: await selectAddress(data[SEND_MODAL_TO_ADDRESS_FIELD]), - satoshis: coinsToSats(BigNumber(data[SEND_MODAL_AMOUNT_FIELD])).toString(), - preconvert: selectData(data[SEND_MODAL_IS_PRECONVERT]), + satoshis: coinsToSats(BigNumber(amount)).toString(), + preconvert: selectData(data[SEND_MODAL_IS_PRECONVERT]) + } + + if ( + coinObj.proto === 'vrsc' && + output.exportto != null && + (output.exportto.toLowerCase() === toIAddress(VETH, coinObj.testnet ? 'VRSCTEST' : 'VRSC').toLowerCase() || output.exportto.toLowerCase() === 'veth') + ) { + try { + const provider = getWeb3ProviderForNetwork( + coinObj.testnet ? 'goerli' : 'homestead', + ); + + if (!(await provider.isVerusBridgeDelegatorActive())) { + output.feecurrency = toIAddress( + VETH, + coinObj.testnet ? 'VRSCTEST' : 'VRSC', + ); + output.bridgeprelaunch = true; + } + } catch (e) { + console.warn(e); + } } for (const key in output) { if (output[key] == null) delete output[key] } - const res = await preflightCurrencyTransfer(coinObj, channel, activeAccount, output) + const res = await preflightConvertOrCrossChain(coinObj, activeAccount, channel, output) setModalHeight(696); @@ -577,10 +915,11 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor const { converterdef, submittedsats, - estimate + estimate, + output: resout } = res.result; - if (output.convertto != null && estimate == null) { + if (resout.convertto != null && estimate == null && sendModal.data[SEND_MODAL_PRICE_ESTIMATE] == null) { Alert.alert("Could not estimate conversion result", 'Failed to calculate an estimated result for this conversion.') } @@ -590,13 +929,13 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor }, a centralized currency. The controller, ${converterdef.fullyqualifiedname}@, has the ability to mint new supply.`) } - if (submittedsats !== output.satoshis) { + if (submittedsats !== resout.satoshis) { Alert.alert( 'Amount changed', `You have insufficient funds to send your submitted amount of ${satsToCoins( BigNumber(submittedsats), ).toString()} ${coinObj.display_ticker} with the transaction fee, so the transaction amount has been changed to the maximum sendable value of ${satsToCoins( - BigNumber(output.satoshis), + BigNumber(resout.satoshis), ).toString()} ${coinObj.display_ticker}.`, ); } @@ -685,7 +1024,7 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor } right={() => - selectedField !== SEND_MODAL_EXPORTTO_FIELD ? ( + selectedField !== SEND_MODAL_EXPORTTO_FIELD && selectedField !== SEND_MODAL_MAPPING_FIELD ? ( {sendModal.coinObj.display_ticker + ' price estimate'} @@ -703,7 +1042,7 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor {item.right} )} left={props => { - const Logo = getCoinLogo(item.logoid); + const Logo = getCoinLogo(item.logoid, item.logoproto, 'dark'); return ( @@ -741,258 +1080,64 @@ const ConvertOrCrossChainSendForm = ({ setLoading, setModalHeight, updateSendFor enableOnAndroid={true} extraScrollHeight={150} keyboardShouldPersistTaps="handled"> - - + updateSendFormData(SEND_MODAL_TO_ADDRESS_FIELD, text) + } + onSelfPress={() => setAddressSelf()} + amountValue={sendModal.data[SEND_MODAL_AMOUNT_FIELD]} + onAmountChange={text => updateSendFormData(SEND_MODAL_AMOUNT_FIELD, text)} + onMaxPress={() => maxAmount()} + maxButtonDisabled={localBalances == null} + networkName={networkName} + estimatedResultSubtitle={ + isConversion && + sendModal.data[SEND_MODAL_PRICE_ESTIMATE] != null && + processedAmount != null + ? `≈ ${Number( + (processedAmount * sendModal.data[SEND_MODAL_PRICE_ESTIMATE].price).toFixed( + 8, + ), + )} ${sendModal.data[SEND_MODAL_PRICE_ESTIMATE].name}` + : null + } /> - { - networkName != null ? ( - - {`on ${networkName} network`} - - ) : null - } - - - - - updateSendFormData(SEND_MODAL_TO_ADDRESS_FIELD, text) - } - autoCapitalize={'none'} - autoCorrect={false} - style={{ - flex: 1, - }} - /> - - - - - - - updateSendFormData(SEND_MODAL_AMOUNT_FIELD, text) - } - style={{ - flex: 1, - }} - /> - - - { - isConversion && sendModal.data[SEND_MODAL_PRICE_ESTIMATE] != null && processedAmount != null ? ( - - {`≈ ${Number((processedAmount*sendModal.data[SEND_MODAL_PRICE_ESTIMATE]).toFixed(8))} ${sendModal.data[SEND_MODAL_CONVERTTO_FIELD]}`} - - ) : null - } - { - showConversionField || showViaField ? ( - - - - - { - showConversionField && ( - - { - (sendModal.data[SEND_MODAL_IS_PRECONVERT] || sendModal.data[SEND_MODAL_ADVANCED_FORM]) ? ( - - updateSendFormData(SEND_MODAL_CONVERTTO_FIELD, text) - } - autoCapitalize={'none'} - autoCorrect={false} - style={{ - flex: 1, - }} - /> - ) : ( - handleFieldFocus(SEND_MODAL_CONVERTTO_FIELD)} - style={{ - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 16, - paddingVertical: 10, - borderWidth: 1, - borderColor: Colors.verusDarkGray, - borderRadius: 4, - }}> - - {isConversion - ? `Convert to: ${sendModal.data[SEND_MODAL_CONVERTTO_FIELD]}` - : 'Select currency to convert to'} - - - - ) - } - - ) - } - { - showViaField && ( - - { - (sendModal.data[SEND_MODAL_IS_PRECONVERT] || sendModal.data[SEND_MODAL_ADVANCED_FORM]) ? ( - updateSendFormData(SEND_MODAL_VIA_FIELD, text)} - autoCapitalize={'none'} - autoCorrect={false} - style={{ - flex: 1, - }} - /> - ) : ( - handleFieldFocus(SEND_MODAL_VIA_FIELD)} - style={{ - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 16, - paddingVertical: 10, - borderWidth: 1, - borderColor: Colors.verusDarkGray, - borderRadius: 4, - }}> - - {isVia - ? `Convert via: ${sendModal.data[SEND_MODAL_VIA_FIELD]}` - : isConversion - ? 'Direct conversion' - : 'Select currency to convert via'} - - - - ) - } - - ) - } - + (showConversionField || showViaField) ? ( + handleFieldFocus(SEND_MODAL_VIA_FIELD)} + handleFieldFocusConvertTo={() => handleFieldFocus(SEND_MODAL_CONVERTTO_FIELD)} + onViaChange={(text) => updateSendFormData(SEND_MODAL_VIA_FIELD, text)} + onConvertToChange={(text) => updateSendFormData(SEND_MODAL_CONVERTTO_FIELD, text)} + isVia={isVia} + showConversionField={showConversionField} + showViaField={showViaField} + /> ) : null } { showExportField && ( - - - - - - { - (sendModal.data[SEND_MODAL_IS_PRECONVERT] || sendModal.data[SEND_MODAL_ADVANCED_FORM]) ? ( - updateSendFormData(SEND_MODAL_EXPORTTO_FIELD, text)} - autoCapitalize={'none'} - autoCorrect={false} - style={{ - flex: 1, - }} - /> - ) : ( - handleFieldFocus(SEND_MODAL_EXPORTTO_FIELD)} - style={{ - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: 16, - paddingVertical: 10, - borderWidth: 1, - borderColor: Colors.verusDarkGray, - borderRadius: 4, - }}> - - {isExport - ? `To network: ${sendModal.data[SEND_MODAL_EXPORTTO_FIELD]}` - : isConversion - ? `On ${ - localNetworkDefinition - ? localNetworkDefinition.fullyqualifiedname - : 'current' - } network` - : 'Select network to send to'} - - - - ) - } - - + handleFieldFocus(SEND_MODAL_EXPORTTO_FIELD)} + onSystemChange={(text) => updateSendFormData(SEND_MODAL_EXPORTTO_FIELD, text)} + localNetworkName={localNetworkName} + advancedForm={sendModal.data[SEND_MODAL_ADVANCED_FORM]} + isPreconvert={sendModal.data[SEND_MODAL_IS_PRECONVERT]} + showMappingField={sendModal.data[SEND_MODAL_SHOW_MAPPING_FIELD]} + mappingField={sendModal.data[SEND_MODAL_MAPPING_FIELD]} + handleMappingFieldFocus={() => handleFieldFocus(SEND_MODAL_MAPPING_FIELD)} + onMappingChange={(text) => updateSendFormData(SEND_MODAL_MAPPING_FIELD, text)} + /> ) } { diff --git a/src/components/SendModal/SendModal.js b/src/components/SendModal/SendModal.js index 8f9506f4..543f40ef 100644 --- a/src/components/SendModal/SendModal.js +++ b/src/components/SendModal/SendModal.js @@ -17,6 +17,7 @@ import { PROVISION_IDENTITY_SEND_MODAL, ADD_PBAAS_CURRENCY_MODAL, CONVERT_OR_CROSS_CHAIN_SEND_MODAL, + ADD_ERC20_TOKEN_MODAL, } from "../../utils/constants/sendModal"; import { SendModalRender } from "./SendModal.render" @@ -33,6 +34,7 @@ class SendModal extends Component { [PROVISION_IDENTITY_SEND_MODAL]: 442, [AUTHENTICATE_USER_SEND_MODAL]: 442, [ADD_PBAAS_CURRENCY_MODAL]: 442, + [ADD_ERC20_TOKEN_MODAL]: 442, [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: 696 }; diff --git a/src/components/SendModal/SendModal.render.js b/src/components/SendModal/SendModal.render.js index 4ade29c0..e63bef45 100644 --- a/src/components/SendModal/SendModal.render.js +++ b/src/components/SendModal/SendModal.render.js @@ -3,6 +3,7 @@ import { Platform, SafeAreaView } from "react-native"; import { Text, Portal, Button } from "react-native-paper"; import { Colors } from "react-native/Libraries/NewAppScreen"; import { + ADD_ERC20_TOKEN_MODAL, ADD_PBAAS_CURRENCY_MODAL, AUTHENTICATE_USER_SEND_MODAL, CONVERSION_SEND_MODAL, @@ -48,6 +49,9 @@ import AddPbaasCurrencyResult from "./AddPbaasCurrency/AddPbaasCurrencyResult/Ad import ConvertOrCrossChainSendForm from "./ConvertOrCrossChainSend/ConvertOrCrossChainSendForm/ConvertOrCrossChainSendForm"; import ConvertOrCrossChainSendConfirm from "./ConvertOrCrossChainSend/ConvertOrCrossChainSendConfirm/ConvertOrCrossChainSendConfirm"; import ConvertOrCrossChainSendResult from "./ConvertOrCrossChainSend/ConvertOrCrossChainSendResult/ConvertOrCrossChainSendResult"; +import AddErc20TokenForm from "./AddErc20Token/AddErc20TokenForm/AddErc20TokenForm"; +import AddErc20TokenConfirm from "./AddErc20Token/AddErc20TokenConfirm/AddErc20TokenConfirm"; +import AddErc20TokenResult from "./AddErc20Token/AddErc20TokenResult/AddErc20TokenResult"; const TopTabs = createMaterialTopTabNavigator(); const Root = createStackNavigator(); @@ -61,7 +65,8 @@ const SEND_FORMS = { [PROVISION_IDENTITY_SEND_MODAL]: ProvisionIdentityForm, [AUTHENTICATE_USER_SEND_MODAL]: AuthenticateUserForm, [ADD_PBAAS_CURRENCY_MODAL]: AddPbaasCurrencyForm, - [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: ConvertOrCrossChainSendForm + [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: ConvertOrCrossChainSendForm, + [ADD_ERC20_TOKEN_MODAL]: AddErc20TokenForm }; const SEND_CONFIRMATION = { @@ -73,7 +78,8 @@ const SEND_CONFIRMATION = { [PROVISION_IDENTITY_SEND_MODAL]: ProvisionIdentityConfirm, [AUTHENTICATE_USER_SEND_MODAL]: AuthenticateUserPassword, [ADD_PBAAS_CURRENCY_MODAL]: AddPbaasCurrencyConfirm, - [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: ConvertOrCrossChainSendConfirm + [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: ConvertOrCrossChainSendConfirm, + [ADD_ERC20_TOKEN_MODAL]: AddErc20TokenConfirm }; const SEND_RESULTS = { @@ -85,7 +91,8 @@ const SEND_RESULTS = { [PROVISION_IDENTITY_SEND_MODAL]: ProvisionIdentityResult, [AUTHENTICATE_USER_SEND_MODAL]: AuthenticateUserResult, [ADD_PBAAS_CURRENCY_MODAL]: AddPbaasCurrencyResult, - [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: ConvertOrCrossChainSendResult + [CONVERT_OR_CROSS_CHAIN_SEND_MODAL]: ConvertOrCrossChainSendResult, + [ADD_ERC20_TOKEN_MODAL]: AddErc20TokenResult }; export const SendModalRender = function () { diff --git a/src/components/SendModal/TraditionalCryptoSend/TraditionalCryptoSendForm/TraditionalCryptoSendForm.js b/src/components/SendModal/TraditionalCryptoSend/TraditionalCryptoSendForm/TraditionalCryptoSendForm.js index 5729a0c5..259d11c5 100644 --- a/src/components/SendModal/TraditionalCryptoSend/TraditionalCryptoSendForm/TraditionalCryptoSendForm.js +++ b/src/components/SendModal/TraditionalCryptoSend/TraditionalCryptoSendForm/TraditionalCryptoSendForm.js @@ -14,10 +14,13 @@ import Colors from "../../../../globals/colors"; import Styles from "../../../../styles"; import { useEffect } from "react"; import { CoinDirectory } from "../../../../utils/CoinData/CoinDirectory"; +import selectAddressBlocklist from "../../../../selectors/settings"; +import { addressIsBlocked } from "../../../../utils/addressBlocklist"; const TraditionalCryptoSendForm = ({ setLoading, setModalHeight, updateSendFormData, navigation }) => { const [amountFiat, setAmountFiat] = useState(false); const sendModal = useSelector(state => state.sendModal); + const addressBlocklist = useSelector(selectAddressBlocklist); const balances = useSelector(state => { const chainTicker = state.sendModal.coinObj.id; const balance_channel = state.sendModal.subWallet.api_channels[API_GET_BALANCES]; @@ -136,11 +139,14 @@ const TraditionalCryptoSendForm = ({ setLoading, setModalHeight, updateSendFormD const toAddress = data[SEND_MODAL_TO_ADDRESS_FIELD] != null ? data[SEND_MODAL_TO_ADDRESS_FIELD].trim() : ""; - const amount = getProcessedAmount() + const amount = getProcessedAmount(); if (!toAddress || toAddress.length < 1) { createAlert("Required Field", "Address is a required field."); return true; + } else if (addressIsBlocked(toAddress, addressBlocklist)) { + createAlert("Blocked Address", "The address you are trying to send to is included in your address blocklist."); + return true; } if (amount == null) { diff --git a/src/containers/AddCoin/AddCoin.js b/src/containers/AddCoin/AddCoin.js index 8e34e922..7cbcd343 100644 --- a/src/containers/AddCoin/AddCoin.js +++ b/src/containers/AddCoin/AddCoin.js @@ -5,73 +5,55 @@ qr. */ -import React, {Component} from 'react'; -import {SearchBar} from 'react-native-elements'; +import React, {useState, useEffect} from 'react'; import {FlatList, TouchableOpacity, View} from 'react-native'; -import {List, FAB} from 'react-native-paper'; -import {Searchbar, Portal} from 'react-native-paper'; -import {connect} from 'react-redux'; +import {List, Portal, Searchbar} from 'react-native-paper'; +import {useSelector} from 'react-redux'; import Styles from '../../styles/index'; import {RenderSquareCoinLogo} from '../../utils/CoinData/Graphics'; import CoinDetailsModal from '../../components/CoinDetailsModal/CoinDetailsModal'; import {WYRE_SERVICE} from '../../utils/constants/intervalConstants'; -import { CoinDirectory } from '../../utils/CoinData/CoinDirectory'; - -class AddCoin extends Component { - constructor(props) { - super(props); - this.state = { - loading: true, - error: null, - query: '', - coinList: [], - fullCoinDetails: null, - activeCoinIds: [] +import {CoinDirectory} from '../../utils/CoinData/CoinDirectory'; + +const AddCoin = props => { + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [query, setQuery] = useState(''); + const [coinList, setCoinList] = useState([]); + const [fullCoinDetails, setFullCoinDetails] = useState(null); + + const testAccount = useSelector( + state => + Object.keys(state.authentication.activeAccount.testnetOverrides).length > + 0, + ); + const activeAccount = useSelector( + state => state.authentication.activeAccount, + ); + const activeCoinsForUser = useSelector( + state => state.coins.activeCoinsForUser, + ); + const activeCoinList = useSelector(state => state.coins.activeCoinList); + + useEffect(() => { + setCoinList(getCoinList()); + }, [query, activeCoinsForUser]); + + useEffect(() => { + return () => { + if (props.route.params && props.route.params.refresh) { + props.route.params.refresh(); + } }; - } - - componentDidMount() { - this.setState({coinList: this.getCoinList()}); - } - - componentDidUpdate(lastProps, lastState) { - if (lastState.query !== this.state.query || this.props.activeCoinsForUser !== lastProps.activeCoinsForUser) { - this.setState({coinList: this.getCoinList(), activeCoinIds: this.props.activeCoinsForUser.map( - coinObj => coinObj.id, - )}); - } - } - - componentWillUnmount() { - if (this.props.route.params && this.props.route.params.refresh) { - this.props.route.params.refresh(); - } - } - - renderHeader = () => { - return ( - this.searchFilterFunction(text)} - autoCorrect={false} - /> - ); - }; - - _openDetails = item => { - this.setState({fullCoinDetails: item.coinObj}); - }; + }, []); - getCoinList = () => { - const {query} = this.state; - const displayedCoinList = this.props.testAccount + const getCoinList = () => { + const displayedCoinList = testAccount ? CoinDirectory.testCoinList - : this.props.activeAccount.disabledServices[WYRE_SERVICE] + : activeAccount.disabledServices[WYRE_SERVICE] ? CoinDirectory.enabledNameList : CoinDirectory.supportedCoinList; - const activeCoinIds = this.props.activeCoinsForUser.map( - coinObj => coinObj.id, - ); + const activeCoinIds = activeCoinsForUser.map(coinObj => coinObj.id); return displayedCoinList .map(x => { @@ -81,7 +63,7 @@ class AddCoin extends Component { }; }) .filter(item => { - const { coinObj } = item; + const {coinObj} = item; const queryLc = query.toLowerCase(); const coinIdLc = coinObj.id.toLowerCase(); const coinNameLc = coinObj.display_name.toLowerCase(); @@ -101,13 +83,29 @@ class AddCoin extends Component { if ( currencyB.id === 'VRSC' || currencyB.id === 'BTC' || - currencyB.id === 'VRSCTEST' + currencyB.id === 'VRSCTEST' || + currencyB.id === "iGBs4DWztRNvNEJBt4mqHszLxfKTNHTkhM" || + currencyB.id === "iCkKJuJScy4Z6NSDK7Mt42ZAB2NEnAE1o4" || + currencyB.id === "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X" || + currencyB.id === "i3f7tSctFkiPpiedY8QR5Tep9p4qDVebDx" || + currencyB.id === '0xBc2738BA63882891094C99E59a02141Ca1A1C36a' || + currencyB.id === '0xE6052Dcc60573561ECef2D9A4C0FEA6d3aC5B9A2' || + currencyB.id === 'MKR' || + currencyB.id === 'DAI' ) { return 1; } else if ( currencyA.id === 'VRSC' || currencyA.id === 'BTC' || - currencyA.id === 'VRSCTEST' + currencyA.id === 'VRSCTEST' || + currencyA.id === "iGBs4DWztRNvNEJBt4mqHszLxfKTNHTkhM" || + currencyA.id === "iCkKJuJScy4Z6NSDK7Mt42ZAB2NEnAE1o4" || + currencyA.id === "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X" || + currencyA.id === "i3f7tSctFkiPpiedY8QR5Tep9p4qDVebDx" || + currencyA.id === '0xBc2738BA63882891094C99E59a02141Ca1A1C36a' || + currencyA.id === '0xE6052Dcc60573561ECef2D9A4C0FEA6d3aC5B9A2' || + currencyA.id === 'MKR' || + currencyA.id === 'DAI' ) { return -1; } else { @@ -116,82 +114,67 @@ class AddCoin extends Component { }); }; - onEndReached = () => { - this.setState({loading: false}); + const onEndReached = () => { + setLoading(false); }; - render() { - const activeCoinIds = this.state.activeCoinIds - - return ( - - - - this.setState({ - fullCoinDetails: null, - }) - } - visible={this.state.fullCoinDetails != null} - animationType="slide" - /> - - this.setState({query})} - value={this.state.query} - autoCorrect={false} - /> + const activeCoinIds = activeCoinsForUser.map(coinObj => coinObj.id); + + return ( + + + { - const { added, coinObj } = item - const {display_name, display_ticker} = coinObj; - - return ( - this._openDetails(item)}> - RenderSquareCoinLogo(coinObj.id)} - right={props => { - return added ? ( - - ) : null; - }} - style={{ - backgroundColor: 'white', - }} - /> - - ); - }} - keyExtractor={item => item.coinObj.id} + activeAccount={activeAccount} + activeCoinList={activeCoinList} + cancel={() => setFullCoinDetails(null)} + visible={fullCoinDetails != null} + animationType="slide" /> - - ); - } -} - -const mapStateToProps = state => { - return { - testAccount: Object.keys(state.authentication.activeAccount.testnetOverrides).length > 0, - activeAccount: state.authentication.activeAccount, - activeCoinsForUser: state.coins.activeCoinsForUser, - activeCoinList: state.coins.activeCoinList, - }; + + setQuery(text)} + value={query} + autoCorrect={false} + /> + } + style={{...Styles.fullWidth, ...Styles.backgroundColorWhite}} + data={coinList} + onEndReached={onEndReached} + onEndReachedThreshold={50} + renderItem={({item}) => { + const {added, coinObj} = item; + const {display_name, display_ticker} = coinObj; + + return ( + setFullCoinDetails(item.coinObj)}> + RenderSquareCoinLogo(coinObj.id)} + right={props => { + return added ? ( + + ) : null; + }} + style={{ + backgroundColor: 'white', + }} + /> + + ); + }} + keyExtractor={item => item.coinObj.id} + /> + + ); }; -export default connect(mapStateToProps)(AddCoin); +export default AddCoin; diff --git a/src/containers/Coin/DynamicHeader.js b/src/containers/Coin/DynamicHeader.js index 33326524..d7c60903 100644 --- a/src/containers/Coin/DynamicHeader.js +++ b/src/containers/Coin/DynamicHeader.js @@ -12,7 +12,8 @@ import SnapCarousel from '../../components/SnapCarousel'; import { API_GET_BALANCES, API_GET_FIATPRICE, - API_GET_INFO + API_GET_INFO, + ERC20 } from '../../utils/constants/intervalConstants'; import Colors from '../../globals/colors'; import {Card, Avatar, Paragraph, Text} from 'react-native-paper'; @@ -25,6 +26,8 @@ import {CONNECTION_ERROR} from '../../utils/api/errors/errorMessages'; import {truncateDecimal} from '../../utils/math'; import {USD} from '../../utils/constants/currencies'; import { CoinDirectory } from '../../utils/CoinData/CoinDirectory'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import { VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT, VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT } from '../../utils/constants/web3Constants'; class DynamicHeader extends Component { constructor(props) { @@ -32,7 +35,8 @@ class DynamicHeader extends Component { this.state = { carouselItems: [], currentIndex: 0, - loadingCarouselItems: true + loadingCarouselItems: true, + mappedCoinObj: null }; this.fadeAnimation = new Animated.Value(0); @@ -41,9 +45,17 @@ class DynamicHeader extends Component { componentDidMount() { this.fadeIn(); + let mappedCoinObj; + + if (this.props.activeCoin.mapped_to != null) { + try { + mappedCoinObj = CoinDirectory.getBasicCoinObj(this.props.activeCoin.mapped_to) + } catch(e) {} + } this.setState({ - loadingCarouselItems: true + loadingCarouselItems: true, + mappedCoinObj }, () => { this.setState({ carouselItems: this.prepareCarouselItems( @@ -254,6 +266,14 @@ class DynamicHeader extends Component { } render() { + const mappedToEth = + this.props.activeCoin.mapped_to != null && + this.state.mappedCoinObj != null && + (this.state.mappedCoinObj.currency_id.toLowerCase() === + VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT.toLowerCase() || + this.state.mappedCoinObj.currency_id.toLowerCase() === + VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT.toLowerCase()); + return ( ) } + { + this.props.pendingBalance.isEqualTo(0) && + this.props.activeCoin.mapped_to != null && + this.state.mappedCoinObj != null && ( + + + { + ` mapped to ${ + (mappedToEth) + ? 'Ethereum' + : this.state.mappedCoinObj.display_ticker.length > 15 + ? this.state.mappedCoinObj.display_ticker.substring(0, 15) + '...' + : this.state.mappedCoinObj.display_ticker + }${ + !mappedToEth && this.state.mappedCoinObj.proto === ERC20 + ? ` (${ + this.state.mappedCoinObj.currency_id.substring(0, 5) + + '...' + + this.state.mappedCoinObj.currency_id.substring( + this.state.mappedCoinObj.currency_id.length - 5, + ) + })` + : '' + }` + } + + + ) + } { return { chainTicker, displayTicker: state.coins.activeCoin.display_ticker, + activeCoin: state.coins.activeCoin, selectedSubWallet: state.coinMenus.activeSubWallets[chainTicker], allSubWallets: state.coinMenus.allSubWallets[chainTicker], balances: extractLedgerData( diff --git a/src/containers/Coin/Overview/Overview.js b/src/containers/Coin/Overview/Overview.js index 647914b9..986aac71 100644 --- a/src/containers/Coin/Overview/Overview.js +++ b/src/containers/Coin/Overview/Overview.js @@ -27,10 +27,11 @@ import { API_GET_SERVICE_TRANSFERS, API_GET_SERVICE_RATES, IS_PBAAS_ROOT, - IS_PBAAS + IS_PBAAS, + ERC20 } from "../../../utils/constants/intervalConstants"; import { selectTransactions } from '../../../selectors/transactions'; -import { DEFAULT_DECIMALS, ETHERS } from "../../../utils/constants/web3Constants"; +import { DEFAULT_DECIMALS, ETHERS, VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT, VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT } from "../../../utils/constants/web3Constants"; import { Portal, List, Text, Badge } from "react-native-paper"; import BigNumber from "bignumber.js"; import { TransactionLogos } from '../../../images/customIcons/index' @@ -202,6 +203,22 @@ class Overview extends Component { } else if (item.type === "sent") { AvatarImg = TX_LOGOS.out; subtitle = item.address == null ? "??" : item.address; + + if ( + this.props.activeCoin.proto === ETH || + this.props.activeCoin.proto === ERC20 + ) { + if ( + (this.props.activeCoin.testnet && + VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT != null && + subtitle.toLowerCase() === VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT.toLowerCase()) || + (!this.props.activeCoin.testnet && + VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT != null && + subtitle.toLowerCase() === VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT.toLowerCase()) + ) { + subtitle = 'Verus-Ethereum Bridge Contract'; + } + } } else if (item.type === "self") { if (item.amount !== "??" && amount.isLessThan(0)) { subtitle = "me"; diff --git a/src/containers/Coin/SendCoin/SendCoin.js b/src/containers/Coin/SendCoin/SendCoin.js index 7f3689f5..a07dd098 100644 --- a/src/containers/Coin/SendCoin/SendCoin.js +++ b/src/containers/Coin/SendCoin/SendCoin.js @@ -14,10 +14,12 @@ import { SEND_MODAL_CONVERTTO_FIELD, SEND_MODAL_EXPORTTO_FIELD, SEND_MODAL_IS_PRECONVERT, + SEND_MODAL_MAPPING_FIELD, SEND_MODAL_MEMO_FIELD, SEND_MODAL_PRICE_ESTIMATE, SEND_MODAL_SHOW_CONVERTTO_FIELD, SEND_MODAL_SHOW_EXPORTTO_FIELD, + SEND_MODAL_SHOW_MAPPING_FIELD, SEND_MODAL_SHOW_VIA_FIELD, SEND_MODAL_TO_ADDRESS_FIELD, SEND_MODAL_VIA_FIELD, @@ -49,10 +51,12 @@ const SendCoin = ({ navigation }) => { [SEND_MODAL_CONVERTTO_FIELD]: '', [SEND_MODAL_EXPORTTO_FIELD]: '', [SEND_MODAL_VIA_FIELD]: '', + [SEND_MODAL_MAPPING_FIELD]: '', [SEND_MODAL_PRICE_ESTIMATE]: null, [SEND_MODAL_IS_PRECONVERT]: false, [SEND_MODAL_SHOW_CONVERTTO_FIELD]: true, [SEND_MODAL_SHOW_EXPORTTO_FIELD]: true, + [SEND_MODAL_SHOW_MAPPING_FIELD]: false, [SEND_MODAL_SHOW_VIA_FIELD]: true, [SEND_MODAL_ADVANCED_FORM]: false }, @@ -68,18 +72,20 @@ const SendCoin = ({ navigation }) => { [SEND_MODAL_CONVERTTO_FIELD]: '', [SEND_MODAL_EXPORTTO_FIELD]: '', [SEND_MODAL_VIA_FIELD]: '', + [SEND_MODAL_MAPPING_FIELD]: '', [SEND_MODAL_PRICE_ESTIMATE]: null, [SEND_MODAL_IS_PRECONVERT]: false, [SEND_MODAL_SHOW_CONVERTTO_FIELD]: false, [SEND_MODAL_SHOW_EXPORTTO_FIELD]: true, + [SEND_MODAL_SHOW_MAPPING_FIELD]: true, [SEND_MODAL_SHOW_VIA_FIELD]: false, [SEND_MODAL_ADVANCED_FORM]: false }, }, { key: 'preconvert', - title: 'Pre-convert', - description: `Participate in a currency pre-convert that accepts ${activeCoin.display_ticker}`, + title: 'Preconvert', + description: `Participate in a currency preconvert that accepts ${activeCoin.display_ticker}`, data: { [SEND_MODAL_TO_ADDRESS_FIELD]: '', [SEND_MODAL_AMOUNT_FIELD]: '', @@ -87,18 +93,20 @@ const SendCoin = ({ navigation }) => { [SEND_MODAL_CONVERTTO_FIELD]: '', [SEND_MODAL_EXPORTTO_FIELD]: '', [SEND_MODAL_VIA_FIELD]: '', + [SEND_MODAL_MAPPING_FIELD]: '', [SEND_MODAL_PRICE_ESTIMATE]: null, [SEND_MODAL_IS_PRECONVERT]: true, [SEND_MODAL_SHOW_CONVERTTO_FIELD]: true, [SEND_MODAL_SHOW_EXPORTTO_FIELD]: true, - [SEND_MODAL_SHOW_VIA_FIELD]: true, + [SEND_MODAL_SHOW_MAPPING_FIELD]: false, + [SEND_MODAL_SHOW_VIA_FIELD]: false, [SEND_MODAL_ADVANCED_FORM]: false }, }, { key: 'advanced', title: 'Advanced', - description: 'Send off-chain, convert, or pre-convert using an unguided form', + description: 'Send off-chain, convert, or preconvert using an unguided form', data: { [SEND_MODAL_TO_ADDRESS_FIELD]: '', [SEND_MODAL_AMOUNT_FIELD]: '', @@ -106,23 +114,32 @@ const SendCoin = ({ navigation }) => { [SEND_MODAL_CONVERTTO_FIELD]: '', [SEND_MODAL_EXPORTTO_FIELD]: '', [SEND_MODAL_VIA_FIELD]: '', + [SEND_MODAL_MAPPING_FIELD]: '', [SEND_MODAL_PRICE_ESTIMATE]: null, [SEND_MODAL_IS_PRECONVERT]: false, [SEND_MODAL_SHOW_CONVERTTO_FIELD]: true, [SEND_MODAL_SHOW_EXPORTTO_FIELD]: true, + [SEND_MODAL_SHOW_MAPPING_FIELD]: (activeCoin.proto === 'erc20' || activeCoin.proto === 'eth'), [SEND_MODAL_SHOW_VIA_FIELD]: true, [SEND_MODAL_ADVANCED_FORM]: true }, }, ]; + if (activeCoin.proto === 'eth' || activeCoin.proto === 'erc20') { + CONVERT_OR_CROSS_CHAIN_OPTIONS.splice(2, 1); + } + const [ convertOrCrossChainOptionsModalOpen, setConvertOrCrossChainOptionsModalOpen, ] = useState(false); const channel = subWallet.channel.split(".")[0]; - const allowConvertOrOffchain = activeCoin.tags.includes(IS_PBAAS) && channel === VRPC; + const allowConvertOrOffchain = + (activeCoin.tags.includes(IS_PBAAS) && channel === VRPC) || + activeCoin.proto === 'erc20' || + activeCoin.proto === 'eth'; const selectConvertOrCrossChainOption = (option) => { setConvertOrCrossChainOptionsModalOpen(false) diff --git a/src/containers/CoinDetails/CoinDetails.js b/src/containers/CoinDetails/CoinDetails.js index 9dfa6ba1..616dba41 100644 --- a/src/containers/CoinDetails/CoinDetails.js +++ b/src/containers/CoinDetails/CoinDetails.js @@ -108,7 +108,7 @@ class CoinDetails extends Component { } render() { - const Logo = getCoinLogo(this.state.fullCoinData.id, 'dark') + const Logo = getCoinLogo(this.state.fullCoinData.id, this.state.fullCoinData.proto, 'dark') return ( diff --git a/src/containers/Home/Home.js b/src/containers/Home/Home.js index 8b60d569..ee859df8 100644 --- a/src/containers/Home/Home.js +++ b/src/containers/Home/Home.js @@ -53,7 +53,7 @@ import {createAlert} from '../../actions/actions/alert/dispatchers/alert'; import {VERUSID_SERVICE_ID} from '../../utils/constants/services'; import { dragDetectionEnabled } from '../../utils/dragDetection'; import { CoinDirectory } from '../../utils/CoinData/CoinDirectory'; -import { openAddPbaasCurrencyModal } from '../../actions/actions/sendModal/dispatchers/sendModal'; +import { openAddErc20TokenModal, openAddPbaasCurrencyModal } from '../../actions/actions/sendModal/dispatchers/sendModal'; class Home extends Component { constructor(props) { @@ -510,6 +510,14 @@ class Home extends Component { ); }; + _addErc20Token = () => { + openAddErc20TokenModal( + CoinDirectory.findCoinObj( + this.props.testnetOverrides.ETH ? this.props.testnetOverrides.ETH : 'ETH', + ), + ); + }; + render() { return HomeRender.call(this); } diff --git a/src/containers/Home/Home.render.js b/src/containers/Home/Home.render.js index 5076a4af..4cf45757 100644 --- a/src/containers/Home/Home.render.js +++ b/src/containers/Home/Home.render.js @@ -54,6 +54,7 @@ export const HomeRender = function () { handleVerusPay={() => this._verusPay()} handleEditCards={() => this.setEditingCards(!this.state.editingCards)} handleAddPbaasCurrency={() => this._addPbaasCurrency()} + handleAddErc20Token={() => this._addErc20Token()} showConfigureHomeCards={!dragDetection} /> { - const { handleAddCoin, handleVerusPay, handleEditCards, showConfigureHomeCards, handleAddPbaasCurrency } = props + const { + handleAddCoin, + handleVerusPay, + handleEditCards, + showConfigureHomeCards, + handleAddPbaasCurrency, + handleAddErc20Token, + } = props; + const [state, setState] = React.useState({ open: false }); const onStateChange = ({ open }) => setState({ open }); @@ -23,6 +31,11 @@ const HomeFAB = (props) => { label: 'Manage Coins', onPress: handleAddCoin, }, + { + icon: 'rocket-launch', + label: 'Add ERC20 Token', + onPress: handleAddErc20Token, + }, { icon: 'rocket-launch', label: 'Add PBaaS Currency', @@ -45,6 +58,11 @@ const HomeFAB = (props) => { label: 'Manage Coins', onPress: handleAddCoin, }, + { + icon: 'ethereum', + label: 'Add ERC20 Token', + onPress: handleAddErc20Token, + }, { icon: 'rocket-launch', label: 'Add PBaaS Currency', diff --git a/src/containers/Home/HomeWidgets/CurrencyWidget.js b/src/containers/Home/HomeWidgets/CurrencyWidget.js index 85a98fba..fca9635b 100644 --- a/src/containers/Home/HomeWidgets/CurrencyWidget.js +++ b/src/containers/Home/HomeWidgets/CurrencyWidget.js @@ -1,25 +1,26 @@ import BigNumber from 'bignumber.js'; -import React, { useState, useEffect } from 'react'; -import { View, Dimensions } from 'react-native'; -import { Avatar, Card, Paragraph } from 'react-native-paper'; -import { useSelector } from 'react-redux'; -import { CoinLogos, getCoinLogo } from '../../../utils/CoinData/CoinData'; -import { USD } from '../../../utils/constants/currencies'; -import { GENERAL } from '../../../utils/constants/intervalConstants'; -import { truncateDecimal } from '../../../utils/math'; -import { formatCurrency } from "react-native-format-currency"; -import SubWalletsLogo from '../../../images/customIcons/SubWallets.svg' -import { extractDisplaySubWallets } from '../../../utils/subwallet/extractSubWallets'; -import { normalizeNum } from '../../../utils/normalizeNum'; +import React, {useState, useEffect} from 'react'; +import {View, Dimensions} from 'react-native'; +import {Avatar, Card, Paragraph} from 'react-native-paper'; +import {useSelector} from 'react-redux'; +import {getCoinLogo} from '../../../utils/CoinData/CoinData'; +import {USD} from '../../../utils/constants/currencies'; +import {GENERAL} from '../../../utils/constants/intervalConstants'; +import {formatCurrency} from 'react-native-format-currency'; +import SubWalletsLogo from '../../../images/customIcons/SubWallets.svg'; +import {extractDisplaySubWallets} from '../../../utils/subwallet/extractSubWallets'; +import {normalizeNum} from '../../../utils/normalizeNum'; +import MaterialCommunityIcons from 'react-native-vector-icons/MaterialCommunityIcons'; +import Colors from '../../../globals/colors'; const CurrencyWidget = props => { - const { currencyBalance, coinObj } = props; - const { width } = Dimensions.get('window'); + const {currencyBalance, coinObj} = props; + const {width} = Dimensions.get('window'); - const Logo = getCoinLogo(coinObj.id) - const themeColor = coinObj.theme_color ? coinObj.theme_color : '#1C1C1C' - - const allSubwallets = useSelector(state => extractDisplaySubWallets(state)) + const Logo = getCoinLogo(coinObj.id, coinObj.proto); + const themeColor = coinObj.theme_color ? coinObj.theme_color : '#1C1C1C'; + + const allSubwallets = useSelector(state => extractDisplaySubWallets(state)); const displayCurrency = useSelector(state => state.settings.generalWalletSettings.displayCurrency ? state.settings.generalWalletSettings.displayCurrency @@ -32,23 +33,29 @@ const CurrencyWidget = props => { : null, ); - const [uniValueDisplay, setUniValueDisplay] = useState("-"); - + const [uniValueDisplay, setUniValueDisplay] = useState('-'); + // Recalculate fiat value useEffect(() => { if (uniRate != null && currencyBalance != null && displayCurrency != null) { const price = BigNumber(uniRate); - const displayValueRaw = normalizeNum(Number((BigNumber(currencyBalance).multipliedBy(price))), 2)[3] + const displayValueRaw = normalizeNum( + Number(BigNumber(currencyBalance).multipliedBy(price)), + 2, + )[3]; const [valueFormattedWithSymbol, valueFormattedWithoutSymbol, symbol] = formatCurrency({amount: displayValueRaw, code: displayCurrency}); - setUniValueDisplay(valueFormattedWithSymbol) + setUniValueDisplay(valueFormattedWithSymbol); } }, [currencyBalance, uniRate, displayCurrency]); - const displayedName = coinObj.display_name.length > 8 ? coinObj.display_ticker : coinObj.display_name; + const displayedName = + coinObj.display_name.length > 8 + ? coinObj.display_ticker + : coinObj.display_name; return ( { style={{ display: 'flex', flexDirection: 'row', - alignItems: "flex-start", - justifyContent: "space-between", + alignItems: 'flex-start', + justifyContent: 'space-between', }}> { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', - overflow: "visible" + overflow: 'visible', }}> {Logo == null ? ( { /> )} {displayedName} @@ -104,33 +116,54 @@ const CurrencyWidget = props => { alignItems: 'center', justifyContent: 'center', marginRight: -10, - marginTop: -12 + marginTop: -12, }}> - {allSubwallets[coinObj.id] - ? allSubwallets[coinObj.id].length - : 1} + {allSubwallets[coinObj.id] ? allSubwallets[coinObj.id].length : 1} + { + coinObj.mapped_to != null && ( + + ) + } - - {coinObj.testnet || uniValueDisplay === '-' ? `${currencyBalance == null ? '-' : normalizeNum(Number(currencyBalance), 8)[3]} ${ - coinObj.display_ticker - }` : uniValueDisplay} + + {coinObj.testnet || uniValueDisplay === '-' + ? `${ + currencyBalance == null + ? '-' + : normalizeNum(Number(currencyBalance), 8)[3] + } ${coinObj.display_ticker}` + : uniValueDisplay} - - {coinObj.testnet ? "Testnet Currency" : coinObj.pbaas_options ? "PBaaS Currency" : - uniValueDisplay === '-' ? uniValueDisplay : `${currencyBalance == null ? '-' : normalizeNum(Number(currencyBalance), 8)[3]} ${ - coinObj.display_ticker - }` - } + + {coinObj.testnet && coinObj.proto === 'erc20' + ? 'Testnet ERC20 Token' + : coinObj.testnet + ? 'Testnet Currency' + : (coinObj.pbaas_options && !coinObj.compatible_channels.includes(GENERAL)) + ? 'PBaaS Currency' + : uniValueDisplay === '-' + ? coinObj.proto === 'erc20' + ? 'ERC20 Token' + : uniValueDisplay + : `${ + currencyBalance == null + ? '-' + : normalizeNum(Number(currencyBalance), 8)[3] + } ${coinObj.display_ticker}`} ); }; - - -export default CurrencyWidget; \ No newline at end of file +export default CurrencyWidget; diff --git a/src/containers/Personal/PersonalImages/PersonalImages.render.js b/src/containers/Personal/PersonalImages/PersonalImages.render.js index 78bee16b..386b3d3e 100644 --- a/src/containers/Personal/PersonalImages/PersonalImages.render.js +++ b/src/containers/Personal/PersonalImages/PersonalImages.render.js @@ -24,17 +24,6 @@ export const PersonalImagesRender = function () { { - return image.uris.length == 0 ? null : ( - - ); - }} title={documentRender.title} description={documentRender.description} right={(props) => ( diff --git a/src/containers/RootStack/MainStackScreens/MainStackScreens.js b/src/containers/RootStack/MainStackScreens/MainStackScreens.js index 585454c9..21e0bfbd 100644 --- a/src/containers/RootStack/MainStackScreens/MainStackScreens.js +++ b/src/containers/RootStack/MainStackScreens/MainStackScreens.js @@ -15,6 +15,7 @@ import CoinSettings from '../../Settings/WalletSettings/CoinSettings/CoinSetting import DeleteProfile from '../../Settings/ProfileSettings/DeleteProfile/DeleteProfile' import SecureLoading from '../../SecureLoading/SecureLoading' import HomeTabScreens from '../HomeTabScreens/HomeTabScreens'; +import AddressBlocklist from '../../Settings/WalletSettings/AddressBlocklist/AddressBlocklist'; const MainStack = createStackNavigator(); @@ -90,6 +91,14 @@ const MainStackScreens = props => { }} /> + + , [VERUSID_SERVICE_ID]: , + [PBAAS_PRECONVERT_SERVICE_ID]: } } diff --git a/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertService.js b/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertService.js new file mode 100644 index 00000000..5278ee42 --- /dev/null +++ b/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertService.js @@ -0,0 +1,73 @@ +import React, { useState, useEffect } from "react"; +import { useDispatch, useSelector } from 'react-redux'; +import { setServiceLoading } from "../../../../actions/actionCreators"; +import { createAlert } from "../../../../actions/actions/alert/dispatchers/alert"; +import { PBAAS_PRECONVERT_SERVICE_ID } from "../../../../utils/constants/services"; +import { PbaasPreconvertServiceRender } from "./PbaasPreconvertService.render"; +import { listCurrencies } from "../../../../utils/api/channels/vrpc/requests/listCurrencies"; +import { getInfo } from "../../../../utils/api/channels/vrpc/callCreators"; +import BigNumber from "bignumber.js"; +import { coinsList } from "../../../../utils/CoinData/CoinsList"; + +const PbaasPreconvertService = (props) => { + const [currenciesInPreconvert, setCurrenciesInPreconvert] = useState(null); + const loading = useSelector(state => state.services.loading[PBAAS_PRECONVERT_SERVICE_ID]); + const activeAccount = useSelector(state => state.authentication.activeAccount); + const dispatch = useDispatch(); + + props.navigation.setOptions({ title: 'Preconvert' }); + + const getCurrenciesInPreconvert = async () => { + dispatch(setServiceLoading(true, PBAAS_PRECONVERT_SERVICE_ID)); + + try { + const isTestnet = Object.keys(activeAccount.testnetOverrides).length > 0; + const systemId = isTestnet ? coinsList.VRSCTEST.system_id : coinsList.VRSC.system_id; + const allImportedCurrencies = await listCurrencies(systemId, "imported"); + const allLocalCurrencies = await listCurrencies(systemId, "local"); + const allPbaasCurrencies = await listCurrencies(systemId, "pbaas"); + + const chainInfo = await getInfo(systemId); + + if ( + allImportedCurrencies.error || + allLocalCurrencies.error || + allPbaasCurrencies.error || + chainInfo.error + ) + throw new Error('Error fetching currencies'); + + const longestChain = BigNumber(chainInfo.result.longestchain); + const preconvertCurrenciesMap = new Map(); + const allCurrencies = [...allImportedCurrencies.result, ...allLocalCurrencies.result, ...allPbaasCurrencies.result]; + + for (const currency of allCurrencies) { + const blocksLeft = BigNumber(currency.currencydefinition.startblock).minus(longestChain); + const inPreconvert = blocksLeft.isGreaterThan(BigNumber(0)); + + if (inPreconvert && !preconvertCurrenciesMap.has(currency.currencydefinition.currencyid)) { + preconvertCurrenciesMap.set(currency.currencydefinition.currencyid, {...currency, blocks_left: blocksLeft.toNumber()}) + } + } + + setCurrenciesInPreconvert(Array.from(preconvertCurrenciesMap.values())) + } catch (e) { + createAlert('Error Loading Linked VerusIDs', e.message); + } + + dispatch(setServiceLoading(false, PBAAS_PRECONVERT_SERVICE_ID)); + }; + + useEffect(() => { + getCurrenciesInPreconvert() + }, []) + + return PbaasPreconvertServiceRender({ + navigation: props.navigation, + loading, + currenciesInPreconvert, + refreshCurrenciesInPreconvert: getCurrenciesInPreconvert, + }); +} + +export default PbaasPreconvertService; \ No newline at end of file diff --git a/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertService.render.js b/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertService.render.js new file mode 100644 index 00000000..6fb12c55 --- /dev/null +++ b/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertService.render.js @@ -0,0 +1,38 @@ +import React from "react"; +import { View } from 'react-native' +import styles from "../../../../styles"; +import AnimatedActivityIndicator from "../../../../components/AnimatedActivityIndicator"; +import PbaasPreconvertServiceOverview from "./PbaasPreconvertServiceOverview/PbaasPreconvertServiceOverview"; + +export const PbaasPreconvertServiceRender = ({ navigation, loading, currenciesInPreconvert, refreshCurrenciesInPreconvert }) => { + return ( + + {(loading || currenciesInPreconvert == null) && ( + + + + )} + {!loading && currenciesInPreconvert != null && ( + + + + )} + + ); +}; \ No newline at end of file diff --git a/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertServiceOverview/PbaasPreconvertServiceOverview.js b/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertServiceOverview/PbaasPreconvertServiceOverview.js new file mode 100644 index 00000000..0bd98e8d --- /dev/null +++ b/src/containers/Services/ServiceComponents/PbaasPreconvertService/PbaasPreconvertServiceOverview/PbaasPreconvertServiceOverview.js @@ -0,0 +1,113 @@ +/* + This component is for displaying a list of all the default coins + that the user can add to their wallet. These coins should come + available with the wallet download and do not need to be added by + qr. +*/ + +import React, {useState, useEffect} from 'react'; +import {FlatList, TouchableOpacity, View} from 'react-native'; +import {List, Searchbar} from 'react-native-paper'; +import Styles from '../../../../../styles/index'; +import { blocksToTime } from '../../../../../utils/math'; +import { getCoinLogo } from '../../../../../utils/CoinData/CoinData'; + +const PbaasPreconvertServiceOverview = props => { + const { navigation, currenciesInPreconvert, refreshCurrenciesInPreconvert } = props; + + const [query, setQuery] = useState(''); + const [currencyList, setCurrencyList] = useState([]); + + useEffect(() => { + setCurrencyList(getCurrencyList()); + }, [query, currenciesInPreconvert]); + + const getCurrencyList = () => { + return currenciesInPreconvert + .map(x => { + return { + ...x, + time_left_string: blocksToTime(x.blocks_left) + } + }) + .filter(item => { + const {currencydefinition} = item; + const {fullyqualifiedname, currencyid} = currencydefinition; + + const queryLc = query.toLowerCase(); + const fqnLc = fullyqualifiedname.toLowerCase(); + const currencyIdLc = currencyid.toLowerCase(); + + return ( + query.length == 0 || + fqnLc.includes(queryLc) || + currencyIdLc.includes(queryLc) + ); + }) + .sort((a, b) => { + if (a.blocks_left <= b.blocks_left) return 1; + else return -1; + }); + }; + + return ( + + setQuery(text)} + value={query} + autoCorrect={false} + /> + } + style={{...Styles.fullWidth, ...Styles.backgroundColorWhite}} + data={currencyList} + onEndReachedThreshold={50} + refreshing={false} + onRefresh={refreshCurrenciesInPreconvert} + renderItem={({item}) => { + const { currencydefinition, time_left_string } = item; + const { fullyqualifiedname, currencyid } = currencydefinition; + const Logo = getCoinLogo(currencyid, 'vrsc'); + + return ( + setFullCoinDetails(item.coinObj)}> + ( + + + + )} + description={`${time_left_string} left`} + descriptionNumberOfLines={1} + titleNumberOfLines={1} + right={props => { + return ; + }} + style={{ + backgroundColor: 'white', + display: 'flex', + }} + /> + + ); + }} + keyExtractor={item => item.currencydefinition.currencyid} + /> + + ); +}; + +export default PbaasPreconvertServiceOverview; diff --git a/src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js b/src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js index b1d14376..41844e9e 100644 --- a/src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js +++ b/src/containers/Settings/ProfileSettings/DisplaySeed/DisplaySeed.js @@ -15,8 +15,11 @@ import QRCode from 'react-native-qrcode-svg'; import Styles from '../../../../styles/index' import Colors from "../../../../globals/colors"; import { CommonActions } from '@react-navigation/native'; -import { DLIGHT_PRIVATE, ELECTRUM, WYRE_SERVICE } from "../../../../utils/constants/intervalConstants"; +import { DLIGHT_PRIVATE, ELECTRUM, ETH, WYRE_SERVICE } from "../../../../utils/constants/intervalConstants"; import { Card, Paragraph, Title, Button } from 'react-native-paper' +import { deriveKeyPair, dlightSeedToBytes, isSeedPhrase } from "../../../../utils/keys"; +import { createAlert } from "../../../../actions/actions/alert/dispatchers/alert"; +import { coinsList } from "../../../../utils/CoinData/CoinsList"; class DisplaySeed extends Component { constructor() { @@ -26,12 +29,16 @@ class DisplaySeed extends Component { fromDeleteAccount: false, selectedSeedType: ELECTRUM, dualSameSeed: false, + derivedKeys: {}, + toggleDerivedKey: {}, + fetchingDerivedKey: {} }; this.SEED_NAMES = { - [DLIGHT_PRIVATE]: "Secondary (Z-Address) Seed", - [ELECTRUM]: "Primary Seed", - [WYRE_SERVICE]: "Wyre Account Seed" + [DLIGHT_PRIVATE]: "Secondary (Z-Address)", + [ELECTRUM]: "Primary", + [ETH]: "Ethereum/ERC20", + [WYRE_SERVICE]: "Wyre Account" } } @@ -44,7 +51,7 @@ class DisplaySeed extends Component { const seedsArray = Object.values(data.seeds) this.setState({ - seeds: data.seeds, + seeds: {...data.seeds, [ETH]: data.seeds[ELECTRUM]}, dualSameSeed: seedsArray.every(s => seedsArray.length > 1 && s === seedsArray[0]) }); } @@ -83,8 +90,49 @@ class DisplaySeed extends Component { this.props.navigation.dispatch(NavigationActions.back()); }; + toggleDerived = async (key) => { + if (!this.state.derivedKeys[key]) { + try { + this.setState({ fetchingDerivedKey: { ...this.state.fetchingDerivedKey, [key]: true } }); + const derivedKey = await this.deriveKeyFromSeed(this.state.seeds[key], key); + this.setState({ + derivedKeys: { ...this.state.derivedKeys, [key]: derivedKey }, + fetchingDerivedKey: { ...this.state.fetchingDerivedKey, [key]: false }, + toggleDerivedKey: { ...this.state.toggleDerivedKey, [key]: true } + }); + } catch(e) { + createAlert("Failed to fetch derived key", e.message); + this.setState({ fetchingDerivedKey: { ...this.state.fetchingDerivedKey, [key]: false } }); + } + } else { + this.setState(prevState => ({ + toggleDerivedKey: { + ...prevState.toggleDerivedKey, + [key]: !prevState.toggleDerivedKey[key] + } + })); + } + } + + // Method to derive the key from seed. Replace this with your actual implementation + deriveKeyFromSeed = async (seed, key) => { + switch (key) { + case DLIGHT_PRIVATE: + return Buffer.from(await dlightSeedToBytes(seed)).toString('hex'); + case ETH: + return (await deriveKeyPair( + seed, + coinsList.ETH, + key, + this.props.activeAccount.keyDerivationVersion, + )).privKey; + default: + return seed + } + } + render() { - const { seeds } = this.state; + const { seeds, toggleDerivedKey, fetchingDerivedKey, derivedKeys } = this.state; return ( @@ -95,21 +143,30 @@ class DisplaySeed extends Component { }} > - {Object.keys(seeds).map((key, index) => { - return seeds[key] == null ? null : ( - - - - {this.SEED_NAMES[key]} - {seeds[key]} - - - - - - - ); - })} + {Object.keys(seeds).map((key, index) => { + const isToggleOn = toggleDerivedKey[key]; + const displayedValue = isToggleOn ? derivedKeys[key] : seeds[key]; + + return seeds[key] == null ? null : ( + + + + {this.SEED_NAMES[key]} + {displayedValue} + + + + + {((key === DLIGHT_PRIVATE && isSeedPhrase(seeds[key])) || key === ETH) && ( + + )} + + + + ); + })} diff --git a/src/containers/Settings/WalletSettings/AddressBlocklist/AddressBlocklist.js b/src/containers/Settings/WalletSettings/AddressBlocklist/AddressBlocklist.js new file mode 100644 index 00000000..541c520c --- /dev/null +++ b/src/containers/Settings/WalletSettings/AddressBlocklist/AddressBlocklist.js @@ -0,0 +1,296 @@ +import { Component } from "react" +import { connect } from 'react-redux' +import { AddressBlocklistRender } from "./AddressBlocklist.render" +import { ADDRESS_BLOCKLIST_FROM_VERUSID, ADDRESS_BLOCKLIST_FROM_WEBSERVER, ADDRESS_BLOCKLIST_MANUAL } from "../../../../utils/constants/constants"; +import { Alert } from "react-native"; +import { saveGeneralSettings } from "../../../../actions/actionCreators"; + +const EDIT = 'edit' +const REMOVE = 'remove' + +class AddressBlocklist extends Component { + constructor() { + super(); + this.state = { + addressBlocklistSettings: { + addressBlocklist: [], + addressBlocklistDefinition: { + type: ADDRESS_BLOCKLIST_FROM_WEBSERVER, + data: null + } + }, + addBlockedAddressModal: { + open: false, + label: "", + index: null + }, + editPropertyModal: { + open: false, + label: "", + index: null + }, + selectBlockTypeModal: { + open: false, + label: "", + index: null + }, + editBlockDefinitionDataModal: { + open: false, + label: "", + index: null + }, + loading: false + }; + + this.EDIT_PROPERTY_BUTTONS = [{ + key: EDIT, + title: "Edit" + }, { + key: REMOVE, + title: "Remove" + }] + + this.BLOCKLIST_TYPE_BUTTONS = [{ + key: ADDRESS_BLOCKLIST_FROM_WEBSERVER, + title: "Fetch blocklist from web server" + }, { + key: ADDRESS_BLOCKLIST_MANUAL, + title: "Manage blocklist manually" + }] + + this.ADDRESS_BLOCKLIST_TYPE_DESCRIPTORS = { + [ADDRESS_BLOCKLIST_FROM_WEBSERVER]: { + title: 'Server', + description: 'On login, your blocklist will be fetched from the internet' + }, + [ADDRESS_BLOCKLIST_MANUAL]: { + title: 'Manual', + description: 'You set your address blocklist manually' + }, + [ADDRESS_BLOCKLIST_FROM_VERUSID]: { + title: 'VerusID', + description: '' + } + } + } + + componentDidMount() { + this.loadSettings() + } + + closeAddBlockedAddressModal() { + this.setState({ + addBlockedAddressModal: { + open: false, + label: "", + index: null + } + }) + } + + closeEditBlockDefinitionDataModal() { + this.setState({ + editBlockDefinitionDataModal: { + open: false, + label: "", + index: null + } + }) + } + + closeEditPropertyModal() { + this.setState({ + editPropertyModal: { + open: false, + label: "", + index: null + } + }) + } + + openEditPropertyModal(label, index = null) { + this.setState({ + editPropertyModal: { + open: true, + label, + index + } + }) + } + + closeSelectBlockTypeModal() { + this.setState({ + selectBlockTypeModal: { + open: false, + label: "", + index: null + } + }) + } + + openSelectBlockTypeModal(label) { + this.setState({ + selectBlockTypeModal: { + open: true, + label + } + }) + } + + selectEditPropertyButton(button) { + switch (button) { + case EDIT: + this.setState({ + addBlockedAddressModal: { + open: true, + label: "Edit Blocked Address", + index: this.state.editPropertyModal.index + } + }) + break; + case REMOVE: + this.removeBlockedAddress(this.state.editPropertyModal.index) + break; + default: + break; + } + } + + selectBlockTypeButton(button) { + this.updateBlockedAddressListDefinition({ + type: button, + data: null + }) + } + + componentDidUpdate(lastProps) { + if (lastProps.settings !== this.props.settings) { + this.loadSettings() + } + } + + finishEditBlockedAddress(blockedAddress, index) { + this.setState({ + addBlockedAddressModal: { + open: false, + label: "", + index: null + } + }, () => this.editBlockedAddress({ address: blockedAddress, details: '', lastModified: Math.floor(Date.now() / 1000) }, index)) + } + + editBlockedAddress(address, index) { + let addressBlocklist = this.state.addressBlocklistSettings.addressBlocklist ? this.state.addressBlocklistSettings.addressBlocklist : [] + + if (index == null) { + this.updateBlockedAddressList([...addressBlocklist, address]) + } else { + addressBlocklist[index] = address + this.updateBlockedAddressList(addressBlocklist) + } + } + + removeBlockedAddress(index) { + let addressBlocklist = this.state.addressBlocklistSettings.addressBlocklist ? this.state.addressBlocklistSettings.addressBlocklist : [] + addressBlocklist.splice(index, 1); + this.updateBlockedAddressList(addressBlocklist) + } + + openAddBlockedAddressModal() { + this.setState({ + addBlockedAddressModal: { + open: true, + label: "Block Address", + index: null + } + }) + } + + openEditBlockDefinitionDataModal() { + this.setState({ + editBlockDefinitionDataModal: { + open: true, + label: "Edit Blocklist Details", + index: null + } + }) + } + + updateBlockedAddressList(list) { + this.setState( + { addressBlocklistSettings: { ...this.state.addressBlocklistSettings, addressBlocklist: list }, loading: true }, + async () => { + try { + await this.saveSettings() + } catch(e) { + Alert.alert('Error', e.message) + } + + this.setState({ loading: false }); + } + ); + } + + finishEditBlockDefinitionData(data) { + this.setState({ + editBlockDefinitionDataModal: { + open: false, + label: "", + index: null + } + }, () => this.updateBlockedAddressListDefinition({ + type: this.state.addressBlocklistSettings.addressBlocklistDefinition.type, + data: data.length === 0 ? null : data + }, this.state.addressBlocklistSettings.addressBlocklist)) + } + + updateBlockedAddressListDefinition(definition, blocklist = []) { + this.setState( + { addressBlocklistSettings: { addressBlocklist: blocklist, addressBlocklistDefinition: definition }, loading: true }, + async () => { + try { + await this.saveSettings() + } catch(e) { + Alert.alert('Error', e.message) + } + + this.setState({ loading: false }); + } + ); + } + + async saveSettings() { + const stateChanges = { + addressBlocklistDefinition: this.state.addressBlocklistSettings.addressBlocklistDefinition, + addressBlocklist: this.state.addressBlocklistSettings.addressBlocklist, + }; + + const res = await saveGeneralSettings(stateChanges); + this.props.dispatch(res); + }; + + loadSettings() { + this.setState({ + addressBlocklistSettings: { + addressBlocklist: this.props.settings.addressBlocklist == null ? [] : this.props.settings.addressBlocklist, + addressBlocklistDefinition: this.props.settings.addressBlocklistDefinition == null ? { + type: ADDRESS_BLOCKLIST_FROM_WEBSERVER, + data: null + } : this.props.settings.addressBlocklistDefinition + } + }) + } + + render() { + return AddressBlocklistRender.call(this); + } +} + +const mapStateToProps = (state) => { + return { + activeAccount: state.authentication.activeAccount, + settings: state.settings.generalWalletSettings + } +}; + +export default connect(mapStateToProps)(AddressBlocklist); \ No newline at end of file diff --git a/src/containers/Settings/WalletSettings/AddressBlocklist/AddressBlocklist.render.js b/src/containers/Settings/WalletSettings/AddressBlocklist/AddressBlocklist.render.js new file mode 100644 index 00000000..c33d4191 --- /dev/null +++ b/src/containers/Settings/WalletSettings/AddressBlocklist/AddressBlocklist.render.js @@ -0,0 +1,129 @@ +import React from "react"; +import { SafeAreaView, ScrollView } from "react-native"; +import { Divider, List, Portal } from "react-native-paper"; +import ListSelectionModal from "../../../../components/ListSelectionModal/ListSelectionModal"; +import TextInputModal from "../../../../components/TextInputModal/TextInputModal"; +import Styles from "../../../../styles"; +import { unixToDate } from "../../../../utils/math"; +import { ADDRESS_BLOCKLIST_MANUAL, DEFAULT_ADDRESS_BLOCKLIST_WEBSERVER } from "../../../../utils/constants/constants"; + +export const AddressBlocklistRender = function () { + const blocklistType = this.state.addressBlocklistSettings.addressBlocklistDefinition.type; + const { title: blocklistTypeTitle, description: blocklistTypeDescription } = this.ADDRESS_BLOCKLIST_TYPE_DESCRIPTORS[blocklistType]; + + return ( + + + + {this.state.addBlockedAddressModal.open && ( + {}} + cancel={(text) => + this.finishEditBlockedAddress( + text, + this.state.addBlockedAddressModal.index + ) + } + /> + )} + {this.state.editBlockDefinitionDataModal.open && ( + {}} + cancel={(text) => + this.finishEditBlockDefinitionData( + text + ) + } + /> + )} + {this.state.editPropertyModal.open && ( + this.selectEditPropertyButton(item.key)} + data={this.EDIT_PROPERTY_BUTTONS} + cancel={() => this.closeEditPropertyModal()} + /> + )} + {this.state.selectBlockTypeModal.open && ( + this.selectBlockTypeButton(item.key)} + data={this.BLOCKLIST_TYPE_BUTTONS} + cancel={() => this.closeSelectBlockTypeModal()} + /> + )} + + + {"Address Blocklist Details"} + + + this.openSelectBlockTypeModal( + `Blocklist Type` + ) + } + /> + + { + blocklistType !== ADDRESS_BLOCKLIST_MANUAL && ( + + this.openEditBlockDefinitionDataModal()} + /> + + + ) + } + {"Blocked Addresses"} + + } + onPress={() => this.openAddBlockedAddressModal()} + /> + + {this.state.addressBlocklistSettings.addressBlocklist.map((blockedAddress, index) => { + return ( + + ( + + )} + onPress={() => + this.openEditPropertyModal( + `Address ${index + 1}`, + index + ) + } + /> + + + ); + }) + } + + + ); +}; diff --git a/src/containers/Settings/WalletSettings/GeneralWalletSettings/GeneralWalletSettings.js b/src/containers/Settings/WalletSettings/GeneralWalletSettings/GeneralWalletSettings.js index 08164c41..4a8cfdfe 100644 --- a/src/containers/Settings/WalletSettings/GeneralWalletSettings/GeneralWalletSettings.js +++ b/src/containers/Settings/WalletSettings/GeneralWalletSettings/GeneralWalletSettings.js @@ -26,6 +26,7 @@ import ListSelectionModal from '../../../../components/ListSelectionModal/ListSe import {saveGeneralSettings} from '../../../../actions/actionCreators'; import {createAlert} from '../../../../actions/actions/alert/dispatchers/alert'; import {NavigationActions} from '@react-navigation/compat'; +import { ADDRESS_BLOCKLIST_FROM_WEBSERVER } from '../../../../utils/constants/constants'; const NO_DEFAULT = 'None'; @@ -89,11 +90,18 @@ const WalletSettings = props => { maxTxCount: Number(settings.maxTxCount), displayCurrency: settings.displayCurrency, defaultAccount: - settings.defaultAccount === NO_DEFAULT - ? null - : settings.defaultAccount, + settings.defaultAccount === NO_DEFAULT ? null : settings.defaultAccount, homeCardDragDetection, - ackedCurrencyDisclaimer: settings.ackedCurrencyDisclaimer + ackedCurrencyDisclaimer: settings.ackedCurrencyDisclaimer, + addressBlocklistDefinition: + settings.addressBlocklistDefinition == null + ? { + type: ADDRESS_BLOCKLIST_FROM_WEBSERVER, + data: null, + } + : settings.addressBlocklistDefinition, + addressBlocklist: + settings.addressBlocklist == null ? [] : settings.addressBlocklist, }; const res = await saveGeneralSettings(stateToSave); dispatch(res); diff --git a/src/containers/Settings/WalletSettings/WalletSettings.js b/src/containers/Settings/WalletSettings/WalletSettings.js index 43bb0c74..d33d9e47 100644 --- a/src/containers/Settings/WalletSettings/WalletSettings.js +++ b/src/containers/Settings/WalletSettings/WalletSettings.js @@ -24,6 +24,7 @@ import { RenderSquareCoinLogo } from "../../../utils/CoinData/Graphics"; const GENERAL_WALLET_SETTINGS = "GeneralWalletSettings" const COIN_SETTINGS = "CoinSettings" +const ADDRESS_BLOCKLIST = "AddressBlocklist" class WalletSettings extends Component { constructor(props) { @@ -111,6 +112,20 @@ class WalletSettings extends Component { /> + this._openSettings(ADDRESS_BLOCKLIST)} + > + ( + + )} + right={(props) => ( + + )} + /> + + {"Wallet Actions"} this.clearCache()}> diff --git a/src/containers/SideMenu/SideMenu.render.js b/src/containers/SideMenu/SideMenu.render.js index 4063c2ed..d89ff71d 100644 --- a/src/containers/SideMenu/SideMenu.render.js +++ b/src/containers/SideMenu/SideMenu.render.js @@ -136,7 +136,7 @@ export const renderMainDrawerComponents = function() { data={this.props.activeCoinsForUser} style={Styles.underflow} renderItem={({ item, index }) => { - const Logo = getCoinLogo(item.id, 'dark'); + const Logo = getCoinLogo(item.id, item.proto, 'dark'); return ( { + VrpcProvider.addDefaultEndpoints(); + }) } function* handleFinishVerusidInit(action) { diff --git a/src/sagas/channels/vrpc.js b/src/sagas/channels/vrpc.js index 4f0e94bb..07b74a67 100644 --- a/src/sagas/channels/vrpc.js +++ b/src/sagas/channels/vrpc.js @@ -29,7 +29,11 @@ function* handleVrpcChannelClose(action) { } function * handleSignOut() { - VrpcProvider.deleteAllEndpoints() + VrpcProvider.deleteAllEndpoints(); + + setImmediate(() => { + VrpcProvider.addDefaultEndpoints(); + }) } function * handleFinishVrpcInit(action) { diff --git a/src/sagas/coins.js b/src/sagas/coins.js index 7ed4916d..c2efab48 100644 --- a/src/sagas/coins.js +++ b/src/sagas/coins.js @@ -10,6 +10,7 @@ import { } from '../utils/constants/storeType'; import { getDefaultSubWallets } from '../utils/defaultSubWallets'; import { getVerusIdCurrency } from '../utils/CoinData/CoinData'; +import { IS_PBAAS_CHAIN } from '../utils/constants/currencies'; export default function* setUserCoinsSaga() { yield all([takeEvery(SET_USER_COINS, handleFinishSetUserCoins)]); @@ -42,6 +43,8 @@ function* handleFinishSetUserCoins(action) { if (coin.tags.includes(IS_PBAAS) && !nonNativeSystems.includes(coin.system_id) && coin.system_id !== coinObj.system_id && + coin.system_options != null && + (coin.system_options & IS_PBAAS_CHAIN) === IS_PBAAS_CHAIN && coin.testnet === coinObj.testnet) { nonNativeSystems.push(coin.system_id); } @@ -49,7 +52,8 @@ function* handleFinishSetUserCoins(action) { } return addresses.map((addr, index) => { - const systems = [coinObj.system_id, ...nonNativeSystems] + const nonChainSystem = coinObj.system_options != null && (coinObj.system_options & IS_PBAAS_CHAIN) !== IS_PBAAS_CHAIN + const systems = nonChainSystem ? nonNativeSystems : [coinObj.system_id, ...nonNativeSystems] return systems.map(system => { const channelId = `${VRPC}.${addr}.${system}`; diff --git a/src/selectors/settings.js b/src/selectors/settings.js new file mode 100644 index 00000000..341a20a8 --- /dev/null +++ b/src/selectors/settings.js @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; + +const getAddressBlocklistFromServer = state => + state.settings.generalWalletSettings.addressBlocklist + ? state.settings.generalWalletSettings.addressBlocklist + : []; + +export const selectAddressBlocklist = createSelector( + [getAddressBlocklistFromServer], + (addressBlocklist) => { + return addressBlocklist + } +); + +export default selectAddressBlocklist; diff --git a/src/utils/CoinData/CoinData.js b/src/utils/CoinData/CoinData.js index aa0f125b..816b4aae 100644 --- a/src/utils/CoinData/CoinData.js +++ b/src/utils/CoinData/CoinData.js @@ -107,6 +107,7 @@ export const explorers = { VRSC: 'https://insight.verus.io/', VRSCTEST: 'https://testex.verus.io', ETH: 'https://etherscan.io', + GETH: 'https://goerli.etherscan.io/', RFOX: 'https://etherscan.io', BAT: 'https://etherscan.io', DAI: 'https://etherscan.io', @@ -149,6 +150,7 @@ export const CoinLogos = { DAI: CoinLogoIcons.web3.DAI, DAIWYRE: CoinLogoIcons.web3.DAI, ETH: CoinLogoIcons.web3.ETH, + GETH: CoinLogoIcons.web3.ETH, BAL: CoinLogoIcons.web3.BAL, BNT: CoinLogoIcons.web3.BNT, HOT: CoinLogoIcons.web3.HOT, @@ -283,8 +285,9 @@ export const getCoinObj = (coinList, coinId) => { }) } -export const getCoinLogo = (id, theme = 'light') => { +export const getCoinLogo = (id, proto, theme = 'light') => { if (CoinLogos[id]) return CoinLogos[id][theme] + else if (proto === 'erc20') return CoinLogos.ETH[theme] else return CoinLogoIcons.pbaas.RenderPbaasCurrencyLogo(id)[theme] } diff --git a/src/utils/CoinData/CoinDirectory.js b/src/utils/CoinData/CoinDirectory.js index 6986e3eb..4e641521 100644 --- a/src/utils/CoinData/CoinDirectory.js +++ b/src/utils/CoinData/CoinDirectory.js @@ -1,12 +1,16 @@ -import { DLIGHT_PRIVATE, ELECTRUM, IS_PBAAS, IS_VERUS, IS_ZCASH, VERUSID, VRPC, WYRE_SERVICE } from "../constants/intervalConstants"; +import { DLIGHT_PRIVATE, ELECTRUM, ERC20, GENERAL, IS_PBAAS, IS_VERUS, IS_ZCASH, VERUSID, VRPC, WYRE_SERVICE } from "../constants/intervalConstants"; import { getDefaultApps, getSystemNameFromSystemId } from "./CoinData"; import { electrumServers } from './electrum/servers'; import { ENABLE_VERUS_IDENTITIES } from '../../../env/index' import { VerusdRpcInterface } from "verusd-rpc-ts-client"; import { VERUS_APPS, coinsList } from "./CoinsList"; -import { DEFAULT_DECIMALS } from "../constants/web3Constants"; +import { DEFAULT_DECIMALS, VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT, VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT } from "../constants/web3Constants"; import { getStoredCurrencyDefinitions, storeCurrencyDefinitionForCurrencyId } from "../asyncStore/currencyDefinitionStorage"; import { timeout } from "../promises"; +import { getStoredContractDefinitions, storeContractDefinitionForNetwork } from "../asyncStore/contractDefinitionStorage"; +import { DEST_ETH, FLAG_MASK } from "verus-typescript-primitives"; +import { BN } from "bn.js"; +import { getWeb3ProviderForNetwork } from "../web3/provider"; class _CoinDirectory { fullCoinList = []; @@ -116,7 +120,7 @@ class _CoinDirectory { } } - // Function to add PBaaS currency. This function is not implemented. + // Function to add PBaaS currency async addPbaasCurrency(currencyDefinition, isTestnet = false, checkEndpoint = true, trySystemFallback = true, storeResults = true) { const id = currencyDefinition.currencyid const system = currencyDefinition.systemid @@ -135,18 +139,59 @@ class _CoinDirectory { let endpoints = []; let secondsPerBlock = 60; + let systemOptions; + + const isMapped = + currencyDefinition.proofprotocol === 3 && + currencyDefinition.nativecurrencyid != null; + let mappedCurrencyId = null; + const delegatorContractAddr = isTestnet + ? VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT + : VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT; + + try { + if (isMapped) { + const nativeCurrencyIdTypeNoFlags = new BN( + currencyDefinition.nativecurrencyid.type, + ).and(FLAG_MASK.notn(FLAG_MASK.bitLength())); + + if (nativeCurrencyIdTypeNoFlags.eq(DEST_ETH) && delegatorContractAddr != null) { + const contractAddress = currencyDefinition.nativecurrencyid.address; + + if (contractAddress === delegatorContractAddr) { + mappedCurrencyId = isTestnet ? coinsList.GETH.id : coinsList.ETH.id; + } else { + const mappedCoin = Object.values(this.coins).find(x => x.currency_id === contractAddress); + if (mappedCoin != null) { + mappedCurrencyId = mappedCoin.id; + } else { + const network = isTestnet ? 'goerli' : 'homestead' + const { name, symbol, decimals } = await getWeb3ProviderForNetwork(network).getContractInfo(contractAddress); + + this.addErc20Token({ address: contractAddress, symbol, name, decimals }, network, true); + mappedCurrencyId = contractAddress; + } + } + } + } + } catch(e) { + console.warn(e) + } try { const systemObj = this.findCoinObj(system, null, true); endpoints = systemObj.vrpc_endpoints; secondsPerBlock = systemObj.seconds_per_block; + systemOptions = systemObj.pbaas_options; } catch(e) { if (this.coinExistsInDirectory(system)) { endpoints = this.coins[system].vrpc_endpoints; secondsPerBlock = this.coins[system].seconds_per_block; + systemOptions = this.coins[system].pbaas_options; } else if (currencyDefinition.nodes) { - const firstNode = currencyDefinition.nodes[0].networkaddress - secondsPerBlock = currencyDefinition.blocktime + const firstNode = currencyDefinition.nodes[0].networkaddress; + secondsPerBlock = currencyDefinition.blocktime; + systemOptions = currencyDefinition.options; const firstNodeSplit = firstNode.split(':') const firstNodePort = firstNodeSplit[firstNodeSplit.length - 1] @@ -188,6 +233,7 @@ class _CoinDirectory { await this.addPbaasCurrency(systemDefinition.result, isTestnet, checkEndpoint); endpoints = this.coins[systemDefinition.result.currencyid].vrpc_endpoints; secondsPerBlock = this.coins[systemDefinition.result.currencyid].seconds_per_block; + systemOptions = this.coins[systemDefinition.result.currencyid].pbaas_options; } else { throw new Error('Failed to connect to find ' + system); } @@ -197,9 +243,11 @@ class _CoinDirectory { const currencyCoinObj = { testnet: isTestnet, pbaas_options: currencyDefinition.options, + system_options: systemOptions, id: currencyDefinition.currencyid, currency_id: currencyDefinition.currencyid, system_id: currencyDefinition.systemid, + launch_system_id: currencyDefinition.launchsystemid, bitgojs_network_key: isTestnet ? 'verustest' : 'verus', display_ticker: currencyDefinition.fullyqualifiedname, display_name: currencyDefinition.fullyqualifiedname, @@ -212,7 +260,8 @@ class _CoinDirectory { decimals: DEFAULT_DECIMALS, seconds_per_block: secondsPerBlock, default_app: 'wallet', - apps: VERUS_APPS + apps: VERUS_APPS, + mapped_to: mappedCurrencyId } if (storeResults) { @@ -227,7 +276,58 @@ class _CoinDirectory { this.updateCoinLists(); } - async populateCurrencyDefinitionsFromStorage() { + /** + * Adds a custom ERC20 token + * @param {{ address: string, symbol: string, name: string, decimals: number }} contractDefinition + * @param {string} network + * @param {boolean} storeResults + * @returns + */ + async addErc20Token(contractDefinition, network = 'homestead', storeResults = true) { + const id = contractDefinition.address; + + if (this.coinExistsInDirectory(id)) { + if (this.coins[id].network === network) { + return; + } else { + throw new Error( + 'Currency already exists in directory on ' + + + this.coins[id].network + + ' network', + ); + } + } + + const tokenCoinObj = { + id: contractDefinition.address, + currency_id: contractDefinition.address, + system_id: '.eth', + display_ticker: contractDefinition.symbol, + display_name: contractDefinition.name, + alt_names: [], + theme_color: '#141C30', + compatible_channels: [ERC20], + dominant_channel: ERC20, + decimals: contractDefinition.decimals, + tags: [], + proto: 'erc20', + network, + testnet: network !== 'homestead' + } + + if (storeResults) { + await storeContractDefinitionForNetwork( + network, + contractDefinition.address, + contractDefinition, + ); + } + + Object.assign(this.coins, { [tokenCoinObj.id]: tokenCoinObj }); + this.updateCoinLists(); + } + + async populatePbaasCurrencyDefinitionsFromStorage() { const storedDefinitions = await getStoredCurrencyDefinitions() const mainnetCurrencies = storedDefinitions[coinsList.VRSC.currency_id] ? storedDefinitions[coinsList.VRSC.currency_id] @@ -260,6 +360,36 @@ class _CoinDirectory { ) } } + + async populateEthereumContractDefinitionsFromStorage() { + const storedDefinitions = await getStoredContractDefinitions() + const mainnetTokens = storedDefinitions[coinsList.ETH.network] + ? storedDefinitions[coinsList.ETH.network] + : {}; + const testnetTokens = storedDefinitions[coinsList.GETH.network] + ? storedDefinitions[coinsList.GETH.network] + : {}; + + for (const key in mainnetTokens) { + if (this.coinExistsInDirectory(key)) continue; + + await this.addErc20Token( + mainnetTokens[key], + coinsList.ETH.network, + false + ) + } + + for (const key in testnetTokens) { + if (this.coinExistsInDirectory(key)) continue; + + await this.addErc20Token( + testnetTokens[key], + coinsList.GETH.network, + false + ) + } + } } export const CoinDirectory = new _CoinDirectory(JSON.parse(JSON.stringify(coinsList))); \ No newline at end of file diff --git a/src/utils/CoinData/CoinsList.js b/src/utils/CoinData/CoinsList.js index 17b9faac..b6d22f14 100644 --- a/src/utils/CoinData/CoinsList.js +++ b/src/utils/CoinData/CoinsList.js @@ -59,11 +59,13 @@ export const VERUS_APPS = { export const coinsList = { VRSC: { id: 'VRSC', + system_options: 264, currency_id: 'i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV', system_id: 'i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV', bitgojs_network_key: 'verus', display_ticker: 'VRSC', display_name: 'Verus', + pbaas_options: 264, rate_url_params: {coin_paprika: 'vrsc-verus-coin'}, alt_names: ['verus'], theme_color: '#3165D4', @@ -81,8 +83,10 @@ export const coinsList = { }, VRSCTEST: { testnet: true, + system_options: 264, mainnet_id: 'VRSC', id: 'VRSCTEST', + pbaas_options: 264, currency_id: 'iJhCezBExJHvtyH3fGhNnt2NhU4Ztkf2yq', system_id: 'iJhCezBExJHvtyH3fGhNnt2NhU4Ztkf2yq', bitgojs_network_key: 'verustest', @@ -101,6 +105,97 @@ export const coinsList = { default_app: 'wallet', apps: VERUS_APPS }, + ["iGBs4DWztRNvNEJBt4mqHszLxfKTNHTkhM"]: { + pbaas_options: 32, + system_options: 128, + id: "iGBs4DWztRNvNEJBt4mqHszLxfKTNHTkhM", + currency_id: "iGBs4DWztRNvNEJBt4mqHszLxfKTNHTkhM", + system_id: "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X", + launch_system_id: "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X", + bitgojs_network_key: "verus", + display_ticker: "DAI.vETH", + display_name: "DAI on Verus", + alt_names: [], + theme_color: "#232323", + compatible_channels: [VERUSID, VRPC], + tags: [IS_VERUS, IS_ZCASH, IS_PBAAS], + proto: "vrsc", + vrpc_endpoints: ["https://api.verus.services"], + decimals: 8, + seconds_per_block: 60, + default_app: "wallet", + apps: VERUS_APPS, + rate_url_params: {coin_paprika: 'dai-dai-coin'}, + website: 'https://verus.io' + }, + ["iCkKJuJScy4Z6NSDK7Mt42ZAB2NEnAE1o4"]: { + pbaas_options: 32, + system_options: 128, + id: "iCkKJuJScy4Z6NSDK7Mt42ZAB2NEnAE1o4", + currency_id: "iCkKJuJScy4Z6NSDK7Mt42ZAB2NEnAE1o4", + system_id: "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X", + launch_system_id: "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X", + bitgojs_network_key: "verus", + display_ticker: "MKR.vETH", + display_name: "Maker on Verus", + alt_names: [], + theme_color: "#232323", + compatible_channels: [VERUSID, VRPC], + tags: [IS_VERUS, IS_ZCASH, IS_PBAAS], + proto: "vrsc", + vrpc_endpoints: ["https://api.verus.services"], + decimals: 8, + seconds_per_block: 60, + default_app: "wallet", + apps: VERUS_APPS, + rate_url_params: {coin_paprika: 'mkr-maker'}, + website: 'https://verus.io' + }, + ["i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X"]: { + pbaas_options: 128, + system_options: 264, + id: "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X", + currency_id: "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X", + system_id: "i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV", + launch_system_id: "i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV", + bitgojs_network_key: "verus", + display_ticker: "vETH", + display_name: "Ethereum on Verus", + alt_names: [], + theme_color: "#232323", + compatible_channels: [VERUSID, VRPC], + tags: [IS_VERUS, IS_ZCASH, IS_PBAAS], + proto: "vrsc", + vrpc_endpoints: ["https://api.verus.services"], + decimals: 8, + seconds_per_block: 60, + default_app: "wallet", + apps: VERUS_APPS, + rate_url_params: {coin_paprika: 'eth-ethereum'}, + website: 'https://verus.io' + }, + ["i3f7tSctFkiPpiedY8QR5Tep9p4qDVebDx"]: { + pbaas_options: 545, + system_options: 264, + id: "i3f7tSctFkiPpiedY8QR5Tep9p4qDVebDx", + currency_id: "i3f7tSctFkiPpiedY8QR5Tep9p4qDVebDx", + system_id: "i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV", + launch_system_id: "i5w5MuNik5NtLcYmNzcvaoixooEebB6MGV", + bitgojs_network_key: "verus", + display_ticker: "Bridge.vETH", + display_name: "Bridge.vETH", + alt_names: [], + theme_color: "#232323", + compatible_channels: [VERUSID, VRPC], + tags: [IS_VERUS, IS_ZCASH, IS_PBAAS], + proto: "vrsc", + vrpc_endpoints: ["https://api.verus.services"], + decimals: 8, + seconds_per_block: 60, + default_app: "wallet", + apps: VERUS_APPS, + website: 'https://verus.io' + }, KMD: { id: 'KMD', display_name: 'Komodo', @@ -134,7 +229,7 @@ export const coinsList = { }, ETH: { id: 'ETH', - currency_id: '', + currency_id: '0x0000000000000000000000000000000000000000', system_id: '.eth', display_ticker: 'ETH', display_name: 'Ethereum', @@ -148,6 +243,23 @@ export const coinsList = { decimals: ETHERS, network: "homestead" }, + GETH: { + id: 'GETH', + currency_id: '0x0000000000000000000000000000000000000000', + system_id: '.eth', + display_ticker: 'gETH', + display_name: 'Testnet Ethereum', + alt_names: [], + theme_color: '#141C30', + website: 'https://ethereum.org/en/', + compatible_channels: [ETH], + dominant_channel: ETH, + tags: [], + proto: 'eth', + decimals: ETHERS, + network: "goerli", + testnet: true + }, BAT: { id: 'BAT', currency_id: '0x0d8775f648430679a709e98d2b0cb6250d2887ef', @@ -603,6 +715,37 @@ export const coinsList = { proto: 'erc20', rate_url_params: {coin_paprika: 'sushi-sushi'}, }, + ["0xBc2738BA63882891094C99E59a02141Ca1A1C36a"]: { + id: '0xBc2738BA63882891094C99E59a02141Ca1A1C36a', + currency_id: '0xBc2738BA63882891094C99E59a02141Ca1A1C36a', + system_id: '.eth', + display_ticker: 'VRSC', + display_name: 'Verus on Ethereum', + alt_names: [], + website: 'https://verus.io/', + compatible_channels: [ERC20, GENERAL], + rate_url_params: {coin_paprika: 'vrsc-verus-coin'}, + theme_color: '#141C30', + dominant_channel: ERC20, + decimals: ETHERS, + tags: [], + proto: 'erc20', + }, + ["0xE6052Dcc60573561ECef2D9A4C0FEA6d3aC5B9A2"]: { + id: '0xE6052Dcc60573561ECef2D9A4C0FEA6d3aC5B9A2', + currency_id: '0xE6052Dcc60573561ECef2D9A4C0FEA6d3aC5B9A2', + system_id: '.eth', + display_ticker: 'VBRID', + display_name: 'Bridge.vETH on Ethereum', + alt_names: [], + website: 'https://verus.io/', + compatible_channels: [ERC20, GENERAL], + theme_color: '#141C30', + dominant_channel: ERC20, + decimals: ETHERS, + tags: [], + proto: 'erc20', + }, MKR: { id: 'MKR', currency_id: '0x9f8f72aa9304c8b593d555f12ef6589cc3a579a2', diff --git a/src/utils/CoinData/Graphics.js b/src/utils/CoinData/Graphics.js index 103f3de6..85da4ee3 100644 --- a/src/utils/CoinData/Graphics.js +++ b/src/utils/CoinData/Graphics.js @@ -26,9 +26,10 @@ export const RenderSquareLogo = (LogoComponent, color, width = 40, height = 40) }; export const RenderSquareCoinLogo = (chainTicker, style = {}, width = 40, height = 40) => { - const Logo = getCoinLogo(chainTicker); const coinObj = CoinDirectory.findCoinObj(chainTicker) + const Logo = getCoinLogo(chainTicker, coinObj.proto); + return RenderSquareLogo( { + for (const blockedAddress of blockList) { + if (blockedAddress.address.toLowerCase() === address.toLowerCase()) { + return true; + } + } + + return false; +} \ No newline at end of file diff --git a/src/utils/api/channels/erc20/requests/getErc20Balance.js b/src/utils/api/channels/erc20/requests/getErc20Balance.js index f0dd8439..95ee23b7 100644 --- a/src/utils/api/channels/erc20/requests/getErc20Balance.js +++ b/src/utils/api/channels/erc20/requests/getErc20Balance.js @@ -1,11 +1,11 @@ import { ethers } from "ethers" import { ETHERS } from "../../../../constants/web3Constants" -import Web3Provider from "../../../../web3/provider" +import { getWeb3ProviderForNetwork } from "../../../../web3/provider" import BigNumber from "bignumber.js"; /** * Gets the balance of an address in an ERC20 token as a big number - * @param {String} address The ethereum address to get the balance of + * @param {string} address The ethereum address to get the balance of * @param {Object} contract The contract object of the ERC20 token created in the Web3Interface */ export const getErc20Balance = async (address, contract) => { @@ -14,12 +14,12 @@ export const getErc20Balance = async (address, contract) => { } else throw new Error(`ERC20 contract ${contract.address} does not support a known balance function.`) } -export const getStandardErc20Balance = async (address, contractAddress, decimals = ETHERS) => { +export const getStandardErc20Balance = async (address, contractAddress, decimals = ETHERS, network = 'homestead') => { return BigNumber( ethers.utils.formatUnits( await getErc20Balance( address, - Web3Provider.getContract(contractAddress, [ + getWeb3ProviderForNetwork(network).getContract(contractAddress, [ { "constant":true, "inputs":[{"name":"_owner","type":"address"}], diff --git a/src/utils/api/channels/erc20/requests/getErc20Transactions.js b/src/utils/api/channels/erc20/requests/getErc20Transactions.js index 9db2ca18..e41082be 100644 --- a/src/utils/api/channels/erc20/requests/getErc20Transactions.js +++ b/src/utils/api/channels/erc20/requests/getErc20Transactions.js @@ -1,4 +1,4 @@ -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' import { ETHERS } from '../../../../constants/web3Constants' import { ethers } from 'ethers' import { ETH } from '../../../../constants/intervalConstants' @@ -7,13 +7,13 @@ import BigNumber from 'bignumber.js' import { standardizeEthTxObj } from '../../../../standardization/standardizeTxObj' // Gets an ERC20 token transaction list for an address -export const getErc20Transactions = async (address, contractAddress) => { - return await Web3Provider.EtherscanProvider.getHistory(address, null, null, contractAddress) +export const getErc20Transactions = async (address, contractAddress, network = 'homestead') => { + return await getWeb3ProviderForNetwork(network).EtherscanProvider.getHistory(address, null, null, contractAddress) } -export const getStandardErc20Transactions = async(address, contractAddress, decimals = ETHERS) => { +export const getStandardErc20Transactions = async(address, contractAddress, decimals = ETHERS, network) => { let processedTxs = standardizeEthTxObj( - await getErc20Transactions(address, contractAddress), + await getErc20Transactions(address, contractAddress, network), address, decimals ); @@ -22,7 +22,7 @@ export const getStandardErc20Transactions = async(address, contractAddress, deci let tx = processedTxs[i] if (tx.type === 'self') { - const txReceipt = await getTxReceipt(tx.txid) + const txReceipt = await getTxReceipt(tx.txid, network) const fee = ethers.utils.formatEther(txReceipt.gasUsed.mul(ethers.utils.parseEther(tx.gasPrice))).toString(); processedTxs[i] = { ...tx, ...txReceipt, amount: "0", fee, feeCurr: ETH.toUpperCase() } diff --git a/src/utils/api/channels/erc20/requests/preflight.js b/src/utils/api/channels/erc20/requests/preflight.js index f57f5862..0401d952 100644 --- a/src/utils/api/channels/erc20/requests/preflight.js +++ b/src/utils/api/channels/erc20/requests/preflight.js @@ -1,12 +1,54 @@ import { ethers } from "ethers" -import Web3Provider from '../../../../web3/provider' -import { ERC20, ETH } from "../../../../constants/intervalConstants" -import { scientificToDecimal } from "../../../../math" -import { ETHERS } from "../../../../constants/web3Constants" +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' +import { ERC20, ETH, VRPC } from "../../../../constants/intervalConstants" +import { coinsToSats, coinsToUnits, satsToCoins, scientificToDecimal } from "../../../../math" +import { + DAI_VETH, + ETHERS, + ETH_BRIDGE_NAME, + ETH_VERUS_BRIDGE_CONTRACT_RESERVE_TRANSFER_FEE, + VETH, + ETH_VERUS_BRIDGE_DEST_SYSTEM_ID, + MINIMUM_GAS_PRICE_WEI_DELEGATOR_CONTRACT, + GAS_TRANSACTION_IMPORT_FEE, + ETH_CONTRACT_ADDRESS, + INITIAL_GAS_LIMIT, + ERC20_BRIDGE_TRANSFER_GAS_LIMIT, + FALLBACK_GAS_BRIDGE_TRANSFER, + NULL_ETH_ADDRESS, + ETH_VERUS_BRIDGE_CONTRACT_PRELAUNCH_RESERVE_TRANSFER_FEE, + MKR_VETH +} from '../../../../constants/web3Constants'; +import { getCurrency, getIdentity } from "../../verusid/callCreators" +import { getSystemNameFromSystemId } from "../../../../CoinData/CoinData" +import { + DEST_ETH, + DEST_ID, + DEST_PKH, + FLAG_DEST_AUX, + FLAG_DEST_GATEWAY, + RESERVE_TRANSFER_CONVERT, + RESERVE_TRANSFER_IMPORT_TO_SOURCE, + RESERVE_TRANSFER_RESERVE_TO_RESERVE, + RESERVE_TRANSFER_VALID, + TransferDestination, + fromBase58Check, + toBase58Check, + toIAddress, +} from 'verus-typescript-primitives'; + +import BigNumber from "bignumber.js" +import { requestPrivKey } from "../../../../auth/authBox"; +import { BufferWriter } from "@bitgo/utxo-lib/dist/src/bufferutils"; +import { BN } from "bn.js"; +import { getStandardEthBalance } from "../../eth/callCreator"; +import { ECPair, networks } from "@bitgo/utxo-lib"; // TODO: Add balance recalculation with eth gas export const txPreflight = async (coinObj, activeUser, address, amount, params) => { try { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network); + const fromAddress = activeUser.keys[coinObj.id][ERC20].addresses[0] const signer = new ethers.VoidSigner(fromAddress, Web3Provider.DefaultProvider) const contract = Web3Provider.getContract(coinObj.currency_id).connect(signer) @@ -45,6 +87,382 @@ export const txPreflight = async (coinObj, activeUser, address, amount, params) } catch(e) { console.error(e) + return { + err: true, + result: e.message + } + } +} + +export const preflightBridgeTransfer = async (coinObj, channelId, activeUser, output, fallbackSubtractBalance = true, amountSubmitted = null) => { + try { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network); + const activeUserKeys = activeUser.keys[coinObj.id][coinObj.proto === 'erc20' ? ERC20 : ETH] + const fromAddress = activeUserKeys.addresses[0]; + const submittedAmount = amountSubmitted == null ? output.satoshis : amountSubmitted; + + const signer = new ethers.VoidSigner(fromAddress, Web3Provider.DefaultProvider); + + const delegatorContract = Web3Provider.getVerusBridgeDelegatorContract().connect(signer); + const systemId = Web3Provider.getVrscSystem(); + const systemName = getSystemNameFromSystemId(systemId); + const tokenContract = coinObj.currency_id; + + const pastPrelaunch = await delegatorContract.callStatic.bridgeConverterActive(); + + const { + currency, + convertto, + exportto, + feecurrency, + via, + feesatoshis, + preconvert, + address, + mapto, + satoshis + } = output; + + const isConversion = convertto != null; + + if (address.isETHAccount() && !isConversion) { + throw new Error("Cannot send to ETH address across bridge without converting. Maybe you meant to use a Verus address?") + } else if (address.isETHAccount() && exportto != null) { + throw new Error("Cannot send to ETH address across bridge to non-eth network. Maybe you meant to use a Verus address?") + } + + let mappedCurrencyIAddress; + let isBridge = false; + const vEthIAddress = toIAddress(VETH, systemName); + const bridgeIAddress = toIAddress(ETH_BRIDGE_NAME, systemName); + + // Find the i address of the currency mapped to the erc20 you're sending as + if (mapto != null) { + // If mapto is defined, search for the currency trying to map to and get + // i-address + const mappedCurrencyRes = await getCurrency(systemId, mapto); + + if (mappedCurrencyRes.error) throw new Error(mappedCurrencyRes.error.message); + else { + if (mappedCurrencyRes.result.fullyqualifiedname === ETH_BRIDGE_NAME) { + isBridge = true; + } + + mappedCurrencyIAddress = mappedCurrencyRes.result.currencyid; + } + } else if (isConversion) { + if (!pastPrelaunch) { + throw new Error("Cannot make conversions while bridge is in pre-launch phase.") + } + + // If mapto is undefined, assume conversion and look for which convertable + // currency is mapped to the current erc20 address + const convertableCurrencies = coinObj.testnet ? [ + systemId, + bridgeIAddress, + vEthIAddress, + toIAddress(DAI_VETH, systemName) + ] : [ + systemId, + bridgeIAddress, + vEthIAddress, + toIAddress(DAI_VETH, systemName), + toIAddress(MKR_VETH, systemName) + ]; + const tokenList = await delegatorContract.callStatic.getTokenList(0, 0); + + for (const tokenInfo of tokenList) { + const [iAddrBytesHex, contractAddr, nftTokenId, flags] = tokenInfo; + + const iAddrBytes = Buffer.from(iAddrBytesHex.substring(2), 'hex'); + const iAddr = toBase58Check(iAddrBytes, 102); + + if (contractAddr.toLowerCase() === tokenContract.toLowerCase() && convertableCurrencies.includes(iAddr)) { + if (bridgeIAddress === iAddr) { + isBridge = true; + } + + mappedCurrencyIAddress = iAddr; + break; + } + } + + if (mappedCurrencyIAddress == null) { + throw new Error("Failed to find a currency mapped to sending erc20 token that can be converted"); + } + } else { + throw new Error("Cannot send to Verus network without a mapped currency, please select a currency to receive as"); + } + + const toEthAddress = (iAddr) => { + return "0x" + fromBase58Check(iAddr).hash.toString('hex'); + } + + // Turn the i-address you found into a hex address for bridge contract, this may not need to be done + // once bridge contract accepts serialization instead of JSON + const mappedCurrencyHexIAddress = toEthAddress(mappedCurrencyIAddress); + + // Calculate the flags for the reserve transfer + let flagsBN = RESERVE_TRANSFER_VALID; + let importToSource = false; + let reserveToReserve = false; + let isGateway = false; + let secondreserveid; + let destinationcurrency; + let converterDefinition; + + // Get the vETH hex address for the fee currency + const vEthHexAddress = toEthAddress(vEthIAddress); + const vrscHexAddress = toEthAddress(systemId); + const bridgeHexAddress = toEthAddress(bridgeIAddress); + + if (isConversion) { + flagsBN = flagsBN.xor(RESERVE_TRANSFER_CONVERT); + + if (isBridge) { + flagsBN = flagsBN.xor(RESERVE_TRANSFER_IMPORT_TO_SOURCE); + importToSource = true; + } + + if (via != null) { + flagsBN = flagsBN.xor(RESERVE_TRANSFER_RESERVE_TO_RESERVE); + reserveToReserve = true; + } + + // Get information about conversion currency to set destinationcurrency and potentially + // secondreserveid + const convertToRes = await getCurrency(systemId, convertto); + + if (convertToRes.error) throw new Error(convertToRes.error.message); + else { + const converterDefinition = convertToRes.result; + const finalDestinationCurrencyAddress = toEthAddress(converterDefinition.currencyid); + + if (via != null) { + secondreserveid = finalDestinationCurrencyAddress; + } + + if (importToSource) { + destinationcurrency = finalDestinationCurrencyAddress; + } else { + destinationcurrency = bridgeHexAddress; + } + } + } else if (!pastPrelaunch) { + destinationcurrency = vrscHexAddress; + secondreserveid = NULL_ETH_ADDRESS; + } else { + destinationcurrency = bridgeHexAddress; + secondreserveid = NULL_ETH_ADDRESS; + } + + let destinationtype, destinationaddress; + + const baseGasPrice = await Web3Provider.DefaultProvider.getGasPrice(); + const minGasPrice = ethers.BigNumber.from(MINIMUM_GAS_PRICE_WEI_DELEGATOR_CONTRACT); + const gasPriceModifier = ethers.BigNumber.from("2"); + const modifiedGasPrice = baseGasPrice.add(baseGasPrice.div(gasPriceModifier)); + let gasPrice; + + if (modifiedGasPrice.gte(minGasPrice)) { + gasPrice = modifiedGasPrice; + } else gasPrice = minGasPrice; + + const gasFeeWei = gasPrice.mul(ethers.BigNumber.from(GAS_TRANSACTION_IMPORT_FEE)); + const gasFeeString = ethers.utils.formatUnits( + gasFeeWei, + ETHERS + ); + const gasFeeSatsString = coinsToSats(BigNumber(gasFeeString)).toString(); + let approvalGasFee = ethers.BigNumber.from("0"); + + if (address.isETHAccount()) { + const refundAddress = ECPair.fromPublicKeyBuffer( + Buffer.from(activeUserKeys.pubKey, 'hex'), + networks.verus, + ).getAddress(); + + // Manually construct a CTransferDestination (remove when switching to serialized method from JSON) + const destAddrBytes = address.destination_bytes; + + const destination = new TransferDestination({ + type: DEST_ETH.xor(FLAG_DEST_GATEWAY).xor(FLAG_DEST_AUX), + destination_bytes: destAddrBytes, + gateway_id: vEthIAddress, + gateway_code: toBase58Check(Buffer.from(NULL_ETH_ADDRESS, 'hex'), 102), + fees: new BN(gasFeeSatsString), + aux_dests: [ + new TransferDestination({ + type: DEST_PKH, + destination_bytes: fromBase58Check(refundAddress).hash + }) + ] + }) + + isGateway = true; + destinationtype = destination.type.toNumber(); + destinationaddress = '0x' + destination.toBuffer().toString('hex').slice(4); + } else { + destinationtype = address.type.toNumber(); + destinationaddress = '0x' + address.destination_bytes.toString('hex'); + } + + const reserveTransfer = { + version: 1, + currencyvalue: { currency: mappedCurrencyHexIAddress, amount: satoshis }, + flags: flagsBN.toNumber(), + feecurrencyid: pastPrelaunch ? vEthHexAddress : vrscHexAddress, + fees: pastPrelaunch ? ETH_VERUS_BRIDGE_CONTRACT_RESERVE_TRANSFER_FEE : ETH_VERUS_BRIDGE_CONTRACT_PRELAUNCH_RESERVE_TRANSFER_FEE, + destcurrencyid: destinationcurrency, + secondreserveid, + destsystemid: ETH_VERUS_BRIDGE_DEST_SYSTEM_ID, + destination: { destinationtype, destinationaddress } + } + + const baseFee = ethers.BigNumber.from("3000000000000000"); + + let ethValueForContract = baseFee; + + if (isGateway) { + ethValueForContract = ethValueForContract.add(gasFeeWei); + } + + if (coinObj.currency_id === ETH_CONTRACT_ADDRESS) { + ethValueForContract = ethValueForContract.add( + ethers.BigNumber.from(satoshis).mul(ethers.BigNumber.from('10000000000')), + ); + } + + let transferGas; + + try { + transferGas = coinObj.currency_id !== ETH_CONTRACT_ADDRESS + ? ethers.BigNumber.from('0') + : await delegatorContract.estimateGas.sendTransfer(reserveTransfer, { + from: fromAddress, + gasLimit: INITIAL_GAS_LIMIT, + value: ethValueForContract.toString(), + }); + } catch(e) { + transferGas = ethers.BigNumber.from(FALLBACK_GAS_BRIDGE_TRANSFER); + } + + let gasEst = + coinObj.currency_id !== ETH_CONTRACT_ADDRESS + ? ethers.BigNumber.from(ERC20_BRIDGE_TRANSFER_GAS_LIMIT) + : transferGas.add(transferGas.div(gasPriceModifier)); + + if (coinObj.currency_id !== ETH_CONTRACT_ADDRESS) { + const contract = Web3Provider.getContract(coinObj.currency_id).connect(signer); + + approvalGasFee = (await contract.estimateGas.approve( + delegatorContract.address, + coinsToUnits(satsToCoins(BigNumber(satoshis)), coinObj.decimals).toString(), + { from: fromAddress, gasLimit: INITIAL_GAS_LIMIT }, + )) + approvalGasFee = approvalGasFee.add(approvalGasFee.div(gasPriceModifier)); + gasEst = gasEst.add(approvalGasFee) + } + + const gasLimit = gasEst; + + const maxTotalFee = (gasLimit.mul(gasPrice)).add(gasFeeWei).add(baseFee); + + const ethBalance = coinsToSats(await getStandardEthBalance(fromAddress, Web3Provider.network)); + + const maxTotalFeeSatoshis = coinsToSats(BigNumber(ethers.utils.formatEther(maxTotalFee))); + const satoshisBn = coinObj.currency_id === ETH_CONTRACT_ADDRESS ? BigNumber(satoshis) : BigNumber(0); + + if (maxTotalFeeSatoshis.plus(satoshisBn).isGreaterThan(ethBalance)) { + if (coinObj.currency_id === ETH_CONTRACT_ADDRESS && satoshisBn.isGreaterThan(0) && fallbackSubtractBalance) { + const newSatoshis = satoshisBn.minus(maxTotalFeeSatoshis); + + if (newSatoshis.isGreaterThan(0)) { + const modifiedOutput = { + ...output, + satoshis: newSatoshis.toString() + } + return preflightBridgeTransfer(coinObj, channelId, activeUser, modifiedOutput, false, satoshisBn.toString()); + } + } + + throw new Error("Wallet does not contain enough ETH to pay maximum fee of " + (ethers.utils.formatEther(maxTotalFee))); + } + + if (coinObj.currency_id === ETH_CONTRACT_ADDRESS) { + // Try making sure the tx can go through + await delegatorContract.callStatic.sendTransfer( + reserveTransfer, + {from: fromAddress, gasLimit: gasLimit.toNumber(), value: ethValueForContract.toString() }, + ); + } + + // result: { + // output, + // validation, + // hex: fundRes.result.hex, + // names: friendlyNames, + // deltas, + // source, + // inputs, + // converterdef: output.convertto ? currencyDefs.get(output.convertto) : output.convertto, + // submittedsats: submittedAmount, + // estimate: conversionEstimate + // }, + + const names = new Map( + coinObj.currency_id === ETH_CONTRACT_ADDRESS + ? [[ETH_CONTRACT_ADDRESS, 'ETH']] + : [ + [ETH_CONTRACT_ADDRESS, 'ETH'], + [coinObj.currency_id, coinObj.display_ticker], + ], + ); + + const validation = { + valid: true, + fees: { [ETH_CONTRACT_ADDRESS]: maxTotalFeeSatoshis.toString() }, + sent: { [coinObj.currency_id]: satoshis } + } + + const deltas = new Map(); + + for (const key in validation.sent) { + if (deltas.has(key)) deltas.set(key, deltas.get(key).minus(BigNumber(validation.sent[key]))) + else deltas.set(key, BigNumber(validation.sent[key]).multipliedBy(BigNumber(-1))) + } + + for (const key in validation.fees) { + if (deltas.has(key)) deltas.set(key, deltas.get(key).minus(BigNumber(validation.fees[key]))) + else deltas.set(key, BigNumber(validation.fees[key]).multipliedBy(BigNumber(-1))) + } + + return { + err: false, + result: { + output, + validation, + names, + deltas, + source: fromAddress, + submittedsats: submittedAmount, + converterdef: converterDefinition, + estimate: null, + approvalparams: coinObj.currency_id === ETH_CONTRACT_ADDRESS ? null : [ + delegatorContract.address, + coinsToUnits(satsToCoins(BigNumber(satoshis)), coinObj.decimals).toString(), + { from: fromAddress, gasLimit: approvalGasFee.toNumber() } + ], + transferparams: [ + reserveTransfer, + {from: fromAddress, gasLimit: gasLimit.sub(approvalGasFee).toNumber(), value: ethValueForContract.toString() } + ], + gasprice: gasPrice.toString() + }, + }; + } catch(e) { + console.error(e) + return { err: true, result: e.message diff --git a/src/utils/api/channels/erc20/requests/send.js b/src/utils/api/channels/erc20/requests/send.js index a6e9a8e7..4070b7e5 100644 --- a/src/utils/api/channels/erc20/requests/send.js +++ b/src/utils/api/channels/erc20/requests/send.js @@ -1,13 +1,15 @@ import { ethers } from "ethers" -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' import { ERC20, ETH } from "../../../../constants/intervalConstants" import { scientificToDecimal } from "../../../../math" import { requestPrivKey } from "../../../../auth/authBox" -import { ETHERS } from "../../../../constants/web3Constants" +import { ETHERS, ETH_CONTRACT_ADDRESS } from "../../../../constants/web3Constants" export const send = async (coinObj, activeUser, address, amount, params) => { try { - const privKey = await requestPrivKey(coinObj.id, ERC20) + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network) + + const privKey = await requestPrivKey(coinObj.id, ERC20); const contract = Web3Provider.getContract(coinObj.currency_id) const gasPrice = await Web3Provider.DefaultProvider.getGasPrice() const amountBn = ethers.utils.parseUnits(scientificToDecimal(amount.toString()), coinObj.decimals) @@ -51,4 +53,52 @@ export const send = async (coinObj, activeUser, address, amount, params) => { result: e.message.includes('processing response error') ? "Error creating transaction" : e.message } } +} + +export const sendBridgeTransfer = async (coinObj, [reserveTransfer, transferOptions], approvalParams, maxGasPrice) => { + try { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network); + + const privKey = await requestPrivKey(coinObj.id, coinObj.proto); + const signer = new ethers.Wallet(ethers.utils.hexlify(privKey), Web3Provider.DefaultProvider); + const gasPrice = await Web3Provider.DefaultProvider.getGasPrice(); + const maxGasPriceBn = ethers.BigNumber.from(maxGasPrice); + + const delegatorContract = Web3Provider.getVerusBridgeDelegatorContract().connect(signer); + + if (gasPrice.gt(maxGasPriceBn)) { + throw new Error("Current gas price exceeds maximum confirmed value, try re-entering form data and sending again.") + } + + if (coinObj.currency_id !== ETH_CONTRACT_ADDRESS) { + const [delegatorAddress, approvalAmount, approvalOptions] = approvalParams + const contract = Web3Provider.getContract(coinObj.currency_id).connect(signer); + + const approval = await contract.approve(delegatorContract.address, approvalAmount, approvalOptions); + const reply = await approval.wait(); + + if (reply.status === 0) { + throw new Error("Authorising ERC20 token spend failed, please check your balance."); + } + } + + const response = await delegatorContract.sendTransfer( + reserveTransfer, + transferOptions + ); + + return { + err: false, + result: { + txid: response.hash + }, + }; + } catch(e) { + console.error(e) + + return { + err: true, + result: e.message + } + } } \ No newline at end of file diff --git a/src/utils/api/channels/erc20/requests/specific/rfox/claimAccountBalance.js b/src/utils/api/channels/erc20/requests/specific/rfox/claimAccountBalance.js index ed91916f..59fc49de 100644 --- a/src/utils/api/channels/erc20/requests/specific/rfox/claimAccountBalance.js +++ b/src/utils/api/channels/erc20/requests/specific/rfox/claimAccountBalance.js @@ -1,5 +1,5 @@ import { RFOX_UTILITY_CONTRACT } from "../../../../../../constants/web3Constants" -import Web3Provider from "../../../../../../web3/provider" +import { getWeb3ProviderForNetwork } from "../../../../../../web3/provider" import { computePublicKey } from '@ethersproject/signing-key' import { ethers } from "ethers" @@ -8,6 +8,8 @@ import { ethers } from "ethers" * @param {String} pubKey The public key that is stored in the accountObj of the user */ export const estimateGasClaimRFoxAccountBalances = async (pubKey, fromAddress) => { + const Web3Provider = getWeb3ProviderForNetwork('homestead') + let contract = null const signer = new ethers.VoidSigner(fromAddress, Web3Provider.DefaultProvider) @@ -32,6 +34,8 @@ export const estimateGasClaimRFoxAccountBalances = async (pubKey, fromAddress) = * @param {String} pubKey The public key that is stored in the accountObj of the user */ export const claimRFoxAccountBalances = async (privKey, pubKey) => { + const Web3Provider = getWeb3ProviderForNetwork('homestead') + let contract = null try { diff --git a/src/utils/api/channels/erc20/requests/specific/rfox/getTotalAccountBalance.js b/src/utils/api/channels/erc20/requests/specific/rfox/getTotalAccountBalance.js index 439f74d6..e2798a99 100644 --- a/src/utils/api/channels/erc20/requests/specific/rfox/getTotalAccountBalance.js +++ b/src/utils/api/channels/erc20/requests/specific/rfox/getTotalAccountBalance.js @@ -1,5 +1,5 @@ import { RFOX_UTILITY_CONTRACT } from "../../../../../../constants/web3Constants" -import Web3Provider from "../../../../../../web3/provider" +import { getWeb3ProviderForNetwork } from "../../../../../../web3/provider" import { computePublicKey } from '@ethersproject/signing-key' /** @@ -7,6 +7,8 @@ import { computePublicKey } from '@ethersproject/signing-key' * @param {String} pubKey The public key that is stored in the accountObj of the user */ export const getRfoxAccountBalances = async (pubKey) => { + const Web3Provider = getWeb3ProviderForNetwork('homestead') + let contract = null try { diff --git a/src/utils/api/channels/eth/requests/getEthBalance.js b/src/utils/api/channels/eth/requests/getEthBalance.js index c883dea8..799f7de0 100644 --- a/src/utils/api/channels/eth/requests/getEthBalance.js +++ b/src/utils/api/channels/eth/requests/getEthBalance.js @@ -1,13 +1,13 @@ -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' import ethers from 'ethers'; import BigNumber from "bignumber.js"; // Gets the Ethereum balance of an address or name as a big number -export const getEthBalance = async (address) => { - return await Web3Provider.DefaultProvider.getBalance(address) +export const getEthBalance = async (address, network = 'homestead') => { + return await getWeb3ProviderForNetwork(network).DefaultProvider.getBalance(address) } //TODO: Handle BigNumbers -export const getStandardEthBalance = async (address) => { - return BigNumber(ethers.utils.formatUnits(await getEthBalance(address))) +export const getStandardEthBalance = async (address, network) => { + return BigNumber(ethers.utils.formatUnits(await getEthBalance(address, network))) } \ No newline at end of file diff --git a/src/utils/api/channels/eth/requests/getEthTransactions.js b/src/utils/api/channels/eth/requests/getEthTransactions.js index 029b8cd2..0c870be8 100644 --- a/src/utils/api/channels/eth/requests/getEthTransactions.js +++ b/src/utils/api/channels/eth/requests/getEthTransactions.js @@ -1,17 +1,17 @@ -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' import { ethers } from 'ethers' import { getTxReceipt } from './getTxReceipt' import BigNumber from 'bignumber.js' import { standardizeEthTxObj } from '../../../../standardization/standardizeTxObj' // Gets the Ethereum transaction history of an address or name -export const getEthTransactions = async (address) => { - return await Web3Provider.EtherscanProvider.getHistory(address) +export const getEthTransactions = async (address, network = 'homestead') => { + return await getWeb3ProviderForNetwork(network).EtherscanProvider.getHistory(address) } -export const getStandardEthTransactions = async (address) => { +export const getStandardEthTransactions = async (address, network) => { let processedTxs = standardizeEthTxObj( - await getEthTransactions(address), + await getEthTransactions(address, network), address ); @@ -19,7 +19,7 @@ export const getStandardEthTransactions = async (address) => { let tx = processedTxs[i] if (tx.type === 'self') { - const txReceipt = await getTxReceipt(tx.txid) + const txReceipt = await getTxReceipt(tx.txid, network) const fee = ethers.utils.formatEther(txReceipt.gasUsed.mul(ethers.utils.parseEther(tx.gasPrice))).toString() processedTxs[i] = { ...tx, ...txReceipt, amount: fee, fee } diff --git a/src/utils/api/channels/eth/requests/getTxReceipt.js b/src/utils/api/channels/eth/requests/getTxReceipt.js index 7a435411..9ba0dd0b 100644 --- a/src/utils/api/channels/eth/requests/getTxReceipt.js +++ b/src/utils/api/channels/eth/requests/getTxReceipt.js @@ -1,9 +1,9 @@ import store from '../../../../../store/index'; import { saveEthTxReceipt } from '../../../../../actions/actionCreators'; import { MIN_ETH_TX_CONFS } from '../../../../../../env/index' -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' -export const getTxReceipt = async (txid) => { +export const getTxReceipt = async (txid, network) => { let cachedReceipts = store.getState().ethtxreceipts.txReceipts; //If already loaded into redux store (from cache), get data from there and avoid call @@ -11,7 +11,7 @@ export const getTxReceipt = async (txid) => { return cachedReceipts[txid] } - const txReceipt = await Web3Provider.DefaultProvider.getTransactionReceipt(txid) + const txReceipt = await getWeb3ProviderForNetwork(network).DefaultProvider.getTransactionReceipt(txid) if (txReceipt.confirmations >= MIN_ETH_TX_CONFS) await saveEthTxReceipt(txReceipt, txid, store) diff --git a/src/utils/api/channels/eth/requests/preflight.js b/src/utils/api/channels/eth/requests/preflight.js index acfad04a..dcc09b4e 100644 --- a/src/utils/api/channels/eth/requests/preflight.js +++ b/src/utils/api/channels/eth/requests/preflight.js @@ -1,5 +1,5 @@ import { ethers } from "ethers" -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' import { ETH } from "../../../../constants/intervalConstants" import { ETH_NETWORK_IDS } from "../../../../constants/constants" import { ETH_HOMESTEAD } from '../../../../../../env/index' @@ -8,6 +8,8 @@ import { scientificToDecimal } from "../../../../math" export const txPreflight = async (coinObj, activeUser, address, amount, params) => { try { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network) + const fromAddress = activeUser.keys[coinObj.id][ETH].addresses[0] const signer = new ethers.VoidSigner(fromAddress, Web3Provider.DefaultProvider) const balance = await signer.getBalance() diff --git a/src/utils/api/channels/eth/requests/send.js b/src/utils/api/channels/eth/requests/send.js index 69980790..6ac1d49c 100644 --- a/src/utils/api/channels/eth/requests/send.js +++ b/src/utils/api/channels/eth/requests/send.js @@ -1,5 +1,5 @@ import { ethers } from "ethers" -import Web3Provider from '../../../../web3/provider' +import { getWeb3ProviderForNetwork } from '../../../../web3/provider' import { ETH } from "../../../../constants/intervalConstants" import { ETH_NETWORK_IDS } from "../../../../constants/constants" import { ETH_HOMESTEAD } from '../../../../../../env/index' @@ -8,6 +8,8 @@ import { requestPrivKey } from "../../../../auth/authBox" export const send = async (coinObj, activeUser, address, amount, params) => { try { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network) + const fromAddress = activeUser.keys[coinObj.id][ETH].addresses[0] const privKey = await requestPrivKey(coinObj.id, ETH) const voidSigner = new ethers.VoidSigner(fromAddress, Web3Provider.DefaultProvider) diff --git a/src/utils/api/channels/general/addressBlocklist/getAddressBlocklist.js b/src/utils/api/channels/general/addressBlocklist/getAddressBlocklist.js new file mode 100644 index 00000000..d65e9a4d --- /dev/null +++ b/src/utils/api/channels/general/addressBlocklist/getAddressBlocklist.js @@ -0,0 +1,22 @@ +import { DEFAULT_ADDRESS_BLOCKLIST_WEBSERVER } from '../../../../constants/constants'; +import { isJson } from '../../../../objectManip' +import axios from 'axios' + +export const getAddressBlocklistFromServer = async (source) => { + const server = source == null || source.length === 0 ? DEFAULT_ADDRESS_BLOCKLIST_WEBSERVER : source; + + const res = await axios.get(server); + + if (!isJson(res.data)) { + throw new Error( + "Invalid JSON for address blocklist" + ); + } + + const lists = Object.values(res.data); + + return lists.flat().map(x => { + if (typeof x !== 'string') throw new Error("Invalid format in blocklist response"); + else return x + }); +} \ No newline at end of file diff --git a/src/utils/api/channels/vrpc/requests/calculateCurrencyTransferFee.js b/src/utils/api/channels/vrpc/requests/calculateCurrencyTransferFee.js index e05c5643..c92f0511 100644 --- a/src/utils/api/channels/vrpc/requests/calculateCurrencyTransferFee.js +++ b/src/utils/api/channels/vrpc/requests/calculateCurrencyTransferFee.js @@ -4,23 +4,25 @@ import { getCurrency } from "../../verusid/callCreators"; import { coinsToSats } from "../../../../math"; export const calculateCurrencyTransferFee = async (systemId, currency, exportto, convertto, feecurrency, via, source, address) => { - if (feecurrency !== systemId) throw new Error("Fee currencies different from system not yet supported."); - - if (exportto == null) { + if (feecurrency === systemId && exportto == null) { return "25000"; } else { const { error, result: destinationSystem } = await getCurrency(systemId, exportto); if (error) throw new Error(error.message); - const sendCurrencyResponse = await VrpcProvider.getEndpoint(systemId).sendCurrency(source, [{ + const params = { currency, amount: 0, exportto, convertto, via, address - }], 1, 0.0001, true); + } + + if (feecurrency != null) params.feecurrency = feecurrency; + + const sendCurrencyResponse = await VrpcProvider.getEndpoint(systemId).sendCurrency(source, [params], 1, 0.0001, true); if (sendCurrencyResponse.error) throw new Error(sendCurrencyResponse.error.message); diff --git a/src/utils/api/channels/vrpc/requests/getCurrenciesMappedToEth.js b/src/utils/api/channels/vrpc/requests/getCurrenciesMappedToEth.js new file mode 100644 index 00000000..b2cb061b --- /dev/null +++ b/src/utils/api/channels/vrpc/requests/getCurrenciesMappedToEth.js @@ -0,0 +1,93 @@ +import { DEST_ETH, toBase58Check } from "verus-typescript-primitives"; +import { listCurrencies } from "./listCurrencies"; +import { getWeb3ProviderForNetwork } from "../../../../web3/provider"; + +/** + * Returns a map of key: contract address and values: array of currencydefinitions + * @param {string} systemId + * @param {string} ethNetwork + * @returns {Promise<{result?: {contractAddressToCurrencyDefinitionMap: Map, currencyIdToContractAddressMap: Map}, error?: string}>} + */ +export const getCurrenciesMappedToEth = async (systemId, ethNetwork) => { + // TODO: Switch to using subset getter when support is added + const allImportedCurrencies = await listCurrencies(systemId, "imported"); + const allLocalCurrencies = await listCurrencies(systemId, "local"); + const allPbaasCurrencies = await listCurrencies(systemId, "pbaas"); + + if (allImportedCurrencies.error) return allImportedCurrencies; + if (allLocalCurrencies.error) return allImportedCurrencies; + if (allPbaasCurrencies.error) return allImportedCurrencies; + + const allCurrencyMap = new Map(); + const allCurrencies = [...allImportedCurrencies.result, ...allLocalCurrencies.result, ...allPbaasCurrencies.result]; + + for (const currency of allCurrencies) { + if (!allCurrencyMap.has(currency.currencydefinition.currencyid)) { + allCurrencyMap.set(currency.currencydefinition.currencyid, currency) + } + } + + /** + * @type {Map>} + */ + const mapped = new Map(); + + /** + * @type {Map>} + */ + const inversemap = new Map(); + + try { + for (const currency of allImportedCurrencies.result) { + if ( + currency.currencydefinition != null && + currency.currencydefinition.nativecurrencyid != null && + currency.currencydefinition.nativecurrencyid.type === DEST_ETH.toNumber() + ) { + + const contractAddr = currency.currencydefinition.nativecurrencyid.address + + if (mapped.has(contractAddr)) { + mapped.get(contractAddr).set(currency.currencydefinition.currencyid, currency) + } else { + mapped.set(contractAddr, new Map([[currency.currencydefinition.currencyid, currency]])) + } + } + } + + try { + const tokenList = await getWeb3ProviderForNetwork(ethNetwork) + .getVerusBridgeDelegatorContract() + .callStatic.getTokenList(0, 0); + + tokenList.forEach(tokenInfo => { + const [iAddrBytesHex, contractAddr, nftTokenId, flags] = tokenInfo; + + const iAddrBytes = Buffer.from(iAddrBytesHex.substring(2), 'hex'); + const iAddr = toBase58Check(iAddrBytes, 102); + + if (allCurrencyMap.has(iAddr)) { + const currency = allCurrencyMap.get(iAddr); + + if (mapped.has(contractAddr)) { + mapped.get(contractAddr).set(currency.currencydefinition.currencyid, currency); + } else { + mapped.set(contractAddr, new Map([[currency.currencydefinition.currencyid, currency]])); + } + + if (inversemap.has(iAddr)) { + inversemap.get(iAddr).add(contractAddr); + } else { + inversemap.set(iAddr, new Set([contractAddr])) + } + } + }) + } catch(e) { + console.error(e) + } + + return { result: {contractAddressToCurrencyDefinitionMap: mapped, currencyIdToContractAddressMap: inversemap} } + } catch(e) { + return { error: { code: -1, message: e.message } } + } +} \ No newline at end of file diff --git a/src/utils/api/channels/vrpc/requests/getCurrencyConversionPaths.js b/src/utils/api/channels/vrpc/requests/getCurrencyConversionPaths.js index 10291c3b..8b4b4d3a 100644 --- a/src/utils/api/channels/vrpc/requests/getCurrencyConversionPaths.js +++ b/src/utils/api/channels/vrpc/requests/getCurrencyConversionPaths.js @@ -1,32 +1,299 @@ +import { ethers } from "ethers"; import VrpcProvider from "../../../../vrpc/vrpcInterface" +import { getCurrenciesMappedToEth } from "./getCurrenciesMappedToEth"; +import { ETH_BRIDGE_NAME, ETH_CONTRACT_ADDRESS, VETH } from "../../../../constants/web3Constants"; +import { getWeb3ProviderForNetwork } from "../../../../web3/provider"; +import { toIAddress } from "verus-typescript-primitives"; +import { getSystemNameFromSystemId } from "../../../../CoinData/CoinData"; -export const getCurrencyConversionPaths = async (systemId, src, dest) => { - let sourceDefinition, destDefinition; +export const getCurrencyConversionPaths = async (systemId, src, ethNetwork) => { + const ethSrc = src === ETH_CONTRACT_ADDRESS || ethers.utils.isAddress(src); const endpoint = VrpcProvider.getEndpoint(systemId); - const sourceResponse = await endpoint.getCurrency(src); + const systemCurrencyResponse = await endpoint.getCurrency(systemId); + if (systemCurrencyResponse.error) throw new Error(systemCurrencyResponse.error.message); + const systemDefinition = systemCurrencyResponse.result; - if (sourceResponse.error) throw new Error(sourceResponse.error.message) + if (ethSrc) { + const isEth = src === ETH_CONTRACT_ADDRESS; + const paths = {}; + const mappedCurrenciesResponse = await getCurrenciesMappedToEth(systemId, ethNetwork); - sourceDefinition = sourceResponse.result + if (mappedCurrenciesResponse.error) throw new Error(sourceResponse.error.message); - if (dest != null) { - const destResponse = await endpoint.getCurrency(dest); + // Determine which currencies can be converted + const bridgeCurrencyResponse = await endpoint.getCurrency(ETH_BRIDGE_NAME); + if (bridgeCurrencyResponse.error) throw new Error(bridgeCurrencyResponse.error.message); + const bridgeCurrencyDefinition = bridgeCurrencyResponse.result; + const { currencies: convertableCurrencies } = bridgeCurrencyDefinition; - if (destResponse.error) throw new Error(destResponse.error.message) + const {contractAddressToCurrencyDefinitionMap, currencyIdToContractAddressMap} = mappedCurrenciesResponse.result; - destDefinition = destResponse.result - } + if (contractAddressToCurrencyDefinitionMap.has(src)) { + const mappedToSource = contractAddressToCurrencyDefinitionMap.get(src); - const paths = await endpoint.getCurrencyConversionPaths(sourceDefinition, destDefinition); + // Establish which currencies the ETH currency can be mapped to + for (const [key, currency] of mappedToSource) { + const { currencydefinition } = currency; + const { nativecurrencyid, currencyid } = currencydefinition; + const { bestcurrencystate } = bridgeCurrencyDefinition; + const { currencies: convertableCurrencyStates } = bestcurrencystate; + const isBridge = currencyid === bridgeCurrencyDefinition.currencyid; - for (const destinationid in paths) { - paths[destinationid] = paths[destinationid].filter(x => { - const offSystem = (x.destination.systemid != systemId) || (x.via != null && x.via.systemid != systemId) - - return !(offSystem && x.exportto == null) - }); - } + // If mapped to a currency that can be converted through the bridge or is the bridge + if ( + isEth || + (convertableCurrencies.includes(currencyid) || isBridge) + ) { + // You can always convert to the bridge if you're convertable, unless you're the bridge + if (!isBridge) { + const priceData = convertableCurrencyStates[currencyid]; + + paths[bridgeCurrencyDefinition.currencyid] = [{ + destination: bridgeCurrencyDefinition, + exportto: systemDefinition, + price: 1 / priceData.lastconversionprice, + gateway: true + }]; + } + + for (const convertableCurrencyId of convertableCurrencies) { + paths[convertableCurrencyId] = []; + + const convertableCurrencyResponse = await endpoint.getCurrency(convertableCurrencyId); + if (convertableCurrencyResponse.error) throw new Error(convertableCurrencyResponse.error.message); + const convertableCurrencyDefinition = convertableCurrencyResponse.result; + + const erc20sMappedToCurrency = currencyIdToContractAddressMap.get(convertableCurrencyDefinition.currencyid); + + if (isBridge) { + // If you are the bridge, you can convert to all of the bridge reserve currencies + const priceData = convertableCurrencyStates[convertableCurrencyId]; + + // You can convert to VRSC network + paths[convertableCurrencyId].push({ + destination: convertableCurrencyDefinition, + exportto: systemDefinition, + price: priceData.lastconversionprice, + gateway: true + }); + + // Or bounce back to ETH through any currency mapped to your destination + for (const contractAddress of erc20sMappedToCurrency) { + paths[contractAddress] = [] + + try { + if (contractAddress !== ETH_CONTRACT_ADDRESS) { + const { name, symbol, decimals } = await getWeb3ProviderForNetwork(ethNetwork).getContractInfo(contractAddress); + + paths[contractAddress].push({ + via: convertableCurrencyDefinition, + destination: { + address: contractAddress, + symbol, + decimals, + name, + mapto: convertableCurrencyDefinition + }, + price: priceData.lastconversionprice, + gateway: true, + bounceback: true, + ethdest: true + }); + } else { + paths[ETH_CONTRACT_ADDRESS].push({ + via: convertableCurrencyDefinition, + destination: { + address: ETH_CONTRACT_ADDRESS, + symbol: "ETH", + decimals: 18, + name: "Ethereum", + mapto: convertableCurrencyDefinition + }, + price: priceData.lastconversionprice, + gateway: true, + bounceback: true, + ethdest: true + }); + } + } catch(e) { + console.log("Error fetching information for contract " + contractAddress) + console.warn(e) + } + } + } else if (convertableCurrencyId !== currencyid) { + // If you're convertable and not the bridge, you can convert to either the bridge (above) + // or any of the bridge reserve currencies except for yourself via the bridge + const destPriceData = convertableCurrencyStates[convertableCurrencyId]; + const rootPriceData = convertableCurrencyStates[currencyid]; + + const viapriceinroot = 1 / rootPriceData.lastconversionprice; + const destpriceinvia = destPriceData.lastconversionprice; + const price = viapriceinroot*destpriceinvia; + + paths[convertableCurrencyId].push({ + destination: convertableCurrencyDefinition, + exportto: systemDefinition, + via: bridgeCurrencyDefinition, + price, + viapriceinroot, + destpriceinvia, + gateway: true + }); - return paths; + // Or bounce back to ETH through any currency mapped to your destination + for (const contractAddress of erc20sMappedToCurrency) { + paths[contractAddress] = [] + + try { + if (contractAddress !== ETH_CONTRACT_ADDRESS) { + const { name, symbol, decimals } = await getWeb3ProviderForNetwork(ethNetwork).getContractInfo(contractAddress); + + paths[contractAddress].push({ + via: bridgeCurrencyDefinition, + destination: { + address: contractAddress, + symbol, + decimals, + name, + mapto: convertableCurrencyDefinition + }, + price, + gateway: true, + bounceback: true, + ethdest: true + }); + } else { + paths[ETH_CONTRACT_ADDRESS].push({ + via: bridgeCurrencyDefinition, + destination: { + address: ETH_CONTRACT_ADDRESS, + symbol: "ETH", + decimals: 18, + name: "Ethereum", + mapto: convertableCurrencyDefinition + }, + price, + gateway: true, + bounceback: true, + ethdest: true + }); + } + } catch(e) { + console.log("Error fetching information for contract " + contractAddress) + console.warn(e) + } + } + } + } + } + + paths[currencyid] = [{ + destination: currencydefinition, + exportto: systemDefinition, + price: 1, + gateway: true, + mapping: true + }]; + } + } + + return paths; + } else { + let sourceDefinition, destDefinition; + + const sourceResponse = await endpoint.getCurrency(src); + + if (sourceResponse.error) throw new Error(sourceResponse.error.message); + + sourceDefinition = sourceResponse.result; + + // if (dest != null) { + // const destResponse = await endpoint.getCurrency(dest); + + // if (destResponse.error) throw new Error(destResponse.error.message) + + // destDefinition = destResponse.result + // } + + const paths = await endpoint.getCurrencyConversionPaths(sourceDefinition, destDefinition); + + const prelaunchCurrenciesRes = await endpoint.listCurrencies({ launchstate: "prelaunch" }); + const prelaunchCurrencyIds = prelaunchCurrenciesRes.result + ? prelaunchCurrenciesRes.result.map(x => x.currencydefinition.currencyid) + : null; + + for (const destinationid in paths) { + paths[destinationid] = paths[destinationid].filter(x => { + const offSystem = (x.destination.systemid != systemId) || (x.via != null && x.via.systemid != systemId); + + return !(offSystem && x.exportto == null); + }).map(x => { + if (prelaunchCurrencyIds != null && prelaunchCurrencyIds.includes(x.destination.currencyid)) { + return { + ...x, + prelaunch: true + } + } else return x; + }); + } + + const mappedCurrenciesResponse = await getCurrenciesMappedToEth(systemId, ethNetwork); + if (mappedCurrenciesResponse.error) throw new Error(sourceResponse.error.message); + const {currencyIdToContractAddressMap} = mappedCurrenciesResponse.result; + + if (currencyIdToContractAddressMap.has(sourceDefinition.currencyid)) { + const mappedEthCurrencies = currencyIdToContractAddressMap.get(sourceDefinition.currencyid); + + for (const contractAddress of mappedEthCurrencies) { + paths[contractAddress] = [] + + // Find vETH on VRSC/VRSCTEST + const vEthCurrencyResponse = await endpoint.getCurrency( + toIAddress( + VETH, + systemDefinition.parent + ? getSystemNameFromSystemId(systemDefinition.parent) + : getSystemNameFromSystemId(systemDefinition.currencyid) + ), + ); + + if (vEthCurrencyResponse.error == null) { + const vEthCurrencyDefinition = vEthCurrencyResponse.result; + + if (contractAddress === ETH_CONTRACT_ADDRESS) { + paths[contractAddress].push({ + destination: { + address: ETH_CONTRACT_ADDRESS, + symbol: "ETH", + decimals: 18, + name: "Ethereum" + }, + exportto: vEthCurrencyDefinition, + price: 1, + gateway: true, + mapping: true + }) + } else { + const { name, symbol, decimals } = await getWeb3ProviderForNetwork(ethNetwork).getContractInfo(contractAddress); + + paths[contractAddress].push({ + destination: { + address: contractAddress, + symbol, + decimals, + name + }, + exportto: vEthCurrencyDefinition, + price: 1, + gateway: true, + mapping: true + }) + } + } + } + } + + return paths; + } } \ No newline at end of file diff --git a/src/utils/api/channels/vrpc/requests/getSendCurrencyTransaction.js b/src/utils/api/channels/vrpc/requests/getSendCurrencyTransaction.js index a4e9c2f1..3b7da58b 100644 --- a/src/utils/api/channels/vrpc/requests/getSendCurrencyTransaction.js +++ b/src/utils/api/channels/vrpc/requests/getSendCurrencyTransaction.js @@ -11,20 +11,21 @@ export const getSendCurrencyTransaction = async ( via, source = '*', ) => { - if (feecurrency !== systemId) - throw new Error('Fee currencies different from system not yet supported.'); + const params = { + currency, + amount, + exportto, + convertto, + via, + address, + }; + + if (feecurrency != null) params.feecurrency = feecurrency; return await VrpcProvider.getEndpoint(systemId).sendCurrency( source, [ - { - currency, - amount, - exportto, - convertto, - via, - address, - }, + params ], 1, 0.0001, diff --git a/src/utils/api/channels/vrpc/requests/listCurrencies.js b/src/utils/api/channels/vrpc/requests/listCurrencies.js new file mode 100644 index 00000000..8b2fe538 --- /dev/null +++ b/src/utils/api/channels/vrpc/requests/listCurrencies.js @@ -0,0 +1,5 @@ +import VrpcProvider from "../../../../vrpc/vrpcInterface" + +export const listCurrencies = (systemId, systemType = "local") => { + return VrpcProvider.getEndpoint(systemId).listCurrencies({ systemtype: systemType }); +} \ No newline at end of file diff --git a/src/utils/api/channels/vrpc/requests/preflight.js b/src/utils/api/channels/vrpc/requests/preflight.js index eebac2a2..f3fcb129 100644 --- a/src/utils/api/channels/vrpc/requests/preflight.js +++ b/src/utils/api/channels/vrpc/requests/preflight.js @@ -1,7 +1,7 @@ import BigNumber from "bignumber.js"; import { getAddressBalances } from "./getAddressBalances"; import { coinsToSats, satsToCoins } from "../../../../math"; -import { DEST_ID, DEST_PKH, ReserveTransfer, TransferDestination, fromBase58Check } from "verus-typescript-primitives"; +import { DEST_ID, DEST_PKH, ReserveTransfer, TransferDestination, fromBase58Check, toIAddress } from "verus-typescript-primitives"; import { Transaction, networks, smarttxs } from "@bitgo/utxo-lib"; import { calculateCurrencyTransferFee } from "./calculateCurrencyTransferFee"; import { getInfo } from "./getInfo"; @@ -14,6 +14,8 @@ import { IS_FRACTIONAL_FLAG } from "../../../../constants/currencies"; import { unpackOutput } from "@bitgo/utxo-lib/dist/src/smart_transactions"; import { coinsList } from "../../../../CoinData/CoinsList"; import { getSendCurrencyTransaction } from "./getSendCurrencyTransaction"; +import { I_ADDRESS_VERSION, R_ADDRESS_VERSION } from "../../../../constants/constants"; +import { VETH } from "../../../../constants/web3Constants"; const { createUnfundedCurrencyTransfer, validateFundedCurrencyTransfer } = smarttxs //TODO: Calculate fee for each coin seperately @@ -33,10 +35,15 @@ export const preflight = async (coinObj, activeUser, address, amount, params, ch } else keyhash = addr; const { hash, version } = fromBase58Check(keyhash); + let type; + + if (version === R_ADDRESS_VERSION) type = DEST_PKH; + else if (version === I_ADDRESS_VERSION) type = DEST_ID; + else throw new Error("Incompatible address type."); return new TransferDestination({ destination_bytes: hash, - type: version === 60 ? DEST_PKH : DEST_ID + type }) } @@ -124,6 +131,7 @@ export const validateCurrencyTransferOutputParams = obj => { 'burn', 'burnweight', 'mintnew', + 'mapto' ]; const allKeys = [...requiredKeys, ...optionalKeys]; @@ -205,6 +213,9 @@ export const preflightCurrencyTransfer = async (coinObj, channelId, activeUser, const friendlyNames = new Map(); const currencyDefs = new Map(); let nativeFeesPaid = BigNumber(0); + let _feeamount; + const ethBridgeDelegatorActive = !(!!(output.bridgeprelaunch)); + delete output.bridgeprelaunch try { validateCurrencyTransferOutputParams(output) @@ -281,7 +292,7 @@ export const preflightCurrencyTransfer = async (coinObj, channelId, activeUser, exportto === "i9nwxtKuVYX4MSbeULLiK2ttVi6rUEhh4X" || // vETH i-addr on VRSC exportto === "iCtawpxUiCc2sEupt7Z4u8SDAncGZpgSKm"); // vETH i-addr on VRSCTEST - let _feeamount = feesatoshis; + _feeamount = feesatoshis; nativeFeesPaid = coinsToSats(BigNumber(parentTransactionFee)); let importToSource = false; @@ -291,11 +302,6 @@ export const preflightCurrencyTransfer = async (coinObj, channelId, activeUser, importToSource = convertto != null && via == null && sourceDefinition.currencies.includes(convertto); } - if (feecurrency != null && _feeamount == null) - throw new Error( - 'Providing a non-default fee currency requires also providing a fee amount.', - ); - if (_feeamount == null && isConversionOrExport) { _feeamount = await calculateCurrencyTransferFee( systemId, @@ -325,8 +331,11 @@ export const preflightCurrencyTransfer = async (coinObj, channelId, activeUser, output.satoshis = txSatsBn.minus(nativeFeesPaid).toString(); const newTxSatsBn = BigNumber(output.satoshis); - if ((newTxSatsBn.plus(nativeFeesPaid)).isGreaterThan(balanceBn)) { - throw new Error("Insufficient funds.") + if ( + newTxSatsBn.plus(nativeFeesPaid).isGreaterThan(balanceBn) || + newTxSatsBn.isLessThanOrEqualTo(BigNumber(0)) + ) { + throw new Error('Insufficient funds.'); } } @@ -375,22 +384,24 @@ export const preflightCurrencyTransfer = async (coinObj, channelId, activeUser, */ const transDest = outputInfo.params[0].data; - if (!transDest.transfer_destination.isGateway()) throw new Error("Expected gateway output"); - if (!transDest.transfer_destination.hasAuxDests()) throw new Error("Expected output with aux dests"); - if (transDest.transfer_destination.getAddressString() !== addrDest) throw new Error("Expected output to match destination address"); - if (transDest.transfer_destination.gateway_id !== exportto) throw new Error("Expected gateway_id to match exportto"); - if (transDest.transfer_destination.gateway_code !== "i3UXS5QPRQGNRDDqVnyWTnmFCTHDbzmsYk") throw new Error("Expected null gateway_code"); - - for (const aux_dest of transDest.transfer_destination.aux_dests) { - if (aux_dest.hasAuxDests()) { - throw new Error("Nested aux destinations not supported"); + if (ethBridgeDelegatorActive) { + if (!transDest.transfer_destination.isGateway()) throw new Error("Expected gateway output"); + if (transDest.transfer_destination.gateway_id !== exportto) throw new Error("Expected gateway_id to match exportto"); + if (transDest.transfer_destination.gateway_code !== "i3UXS5QPRQGNRDDqVnyWTnmFCTHDbzmsYk") throw new Error("Expected null gateway_code"); + if (!transDest.transfer_destination.hasAuxDests()) throw new Error("Expected output with aux dests"); + for (const aux_dest of transDest.transfer_destination.aux_dests) { + if (aux_dest.hasAuxDests()) { + throw new Error("Nested aux destinations not supported"); + } + + if (aux_dest.isGateway()) throw new Error("Expected non gateway output in aux dest"); + + const addrString =aux_dest.getAddressString(); + if (addrString !== addrDest && addrString !== source) throw new Error(`Aux dest ${addrString} does not match source or destination`); } - - if (aux_dest.isGateway()) throw new Error("Expected non gateway output in aux dest"); - - const addrString =aux_dest.getAddressString(); - if (addrString !== addrDest && addrString !== source) throw new Error(`Aux dest ${addrString} does not match source or destination`); } + + if (transDest.transfer_destination.getAddressString() !== addrDest) throw new Error("Expected output to match destination address"); unfundedTxHex = sendCurrencyRes.result.hextx; } else { @@ -548,7 +559,11 @@ export const preflightCurrencyTransfer = async (coinObj, channelId, activeUser, : systemName; const nativeFeeAmount = satsToCoins(nativeFeesPaid).toString(); - message = `Insufficient funds. Ensure you have at least ${nativeFeeAmount} ${feeCurrencyName} on the ${systemName} network to fund the fee for transaction.`; + message = `Insufficient funds. Ensure you have at least ${ + output.feecurrency === systemId + ? nativeFeeAmount + : satsToCoins(BigNumber(_feeamount)).toString() + } ${feeCurrencyName} on the ${systemName} network to fund the fee for transaction.`; } return { diff --git a/src/utils/api/routers/getAddressBalance.js b/src/utils/api/routers/getAddressBalance.js new file mode 100644 index 00000000..7d843f50 --- /dev/null +++ b/src/utils/api/routers/getAddressBalance.js @@ -0,0 +1,54 @@ +import { + ERC20, + ETH, + VRPC +} from "../../constants/intervalConstants"; +import * as vrpc from "../channels/vrpc/callCreators"; +import * as eth from "../channels/eth/callCreator"; +import * as erc20 from "../channels/erc20/callCreator"; + +const ADDRESS_BALANCE_FUNCTION_MAP = { + [VRPC]: async (coinObj, channel, params) => { + const [channelName, iAddress, systemId] = channel.split('.'); + + const balanceRes = await vrpc.getAddressBalances(systemId, [params.address]); + + if (balanceRes.error) throw new Error(balanceRes.error.message); + else { + return balanceRes.result.currencybalance ? balanceRes.result.currencybalance : {}; + } + }, + [ETH]: async (coinObj, channel, params) => { + const balanceRes = await eth.getStandardEthBalance(params.address, coinObj.network); + + return { + [coinObj.currency_id]: balanceRes + } + }, + [ERC20]: async (coinObj, channel, params) => { + const balanceRes = await erc20.getStandardErc20Balance( + params.address, + coinObj.currency_id, + coinObj.decimals, + coinObj.network, + ); + + return { + [coinObj.currency_id]: balanceRes + } + } +}; + +/** + * Returns the address balance + * @param {Object} coinObj The coin object + * @param {String} channel The channel to search on + * @param {Object} params Any other parameters specific to the send channel's search function + */ +export const getAddressBalances = async (coinObj, channel, params) => { + const channelId = channel.split('.')[0] + + if (ADDRESS_BALANCE_FUNCTION_MAP[channelId] == null) + throw new Error(`No address balance function available for channel ${channelId}`); + else return await ADDRESS_BALANCE_FUNCTION_MAP[channelId](coinObj, channel, params); +}; \ No newline at end of file diff --git a/src/utils/api/routers/getConversionPaths.js b/src/utils/api/routers/getConversionPaths.js index eeb988b1..2c700c91 100644 --- a/src/utils/api/routers/getConversionPaths.js +++ b/src/utils/api/routers/getConversionPaths.js @@ -1,16 +1,38 @@ import { + ERC20, + ETH, VRPC, WYRE_SERVICE, } from "../../constants/intervalConstants"; import * as wyre from "../channels/wyre/callCreators"; import * as vrpc from "../channels/vrpc/callCreators"; +import { getWeb3ProviderForNetwork } from "../../web3/provider"; +import { ETH_CONTRACT_ADDRESS } from "../../constants/web3Constants"; +import { + ETH_HOMESTEAD, + ETH_GOERLI +} from "../../../../env/index"; const CONVERSION_PATH_FUNCTION_MAP = { [WYRE_SERVICE]: wyre.getCurrencyConversionPaths, [VRPC]: (coinObj, channel, params) => { const [channelName, iAddress, systemId] = channel.split('.') - return vrpc.getCurrencyConversionPaths(systemId, params.src, params.dest) + return vrpc.getCurrencyConversionPaths(systemId, params.src, coinObj.testnet ? ETH_GOERLI : ETH_HOMESTEAD); + }, + [ERC20]: (coinObj, channel, params) => { + return vrpc.getCurrencyConversionPaths( + getWeb3ProviderForNetwork(coinObj.network).getVrscSystem(), + params.src, + coinObj.network, + ); + }, + [ETH]: (coinObj, channel, params) => { + return vrpc.getCurrencyConversionPaths( + getWeb3ProviderForNetwork(coinObj.network).getVrscSystem(), + ETH_CONTRACT_ADDRESS, + coinObj.network, + ); } }; diff --git a/src/utils/api/routers/getIdentity.js b/src/utils/api/routers/getIdentity.js new file mode 100644 index 00000000..ff91ed67 --- /dev/null +++ b/src/utils/api/routers/getIdentity.js @@ -0,0 +1,32 @@ +// systemId, iAddressOrName, height, txproof, txproofheight +import { + ETH, + ERC20, + VRPC, +} from "../../constants/intervalConstants"; +import { getWeb3ProviderForNetwork } from "../../web3/provider"; +import { getIdentity as vrpcGetIdentity } from "../channels/verusid/callCreators"; + +const IDENTITY_FUNCTION_MAP = { + [ETH]: (coinObj, activeUser, channelId, iAddressOrName, height, txproof, txproofheight) => { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network); + return vrpcGetIdentity(Web3Provider.getVrscSystem(), iAddressOrName, height, txproof, txproofheight); + }, + [ERC20]: (coinObj, activeUser, channelId, iAddressOrName, height, txproof, txproofheight) => { + const Web3Provider = getWeb3ProviderForNetwork(coinObj.network); + return vrpcGetIdentity(Web3Provider.getVrscSystem(), iAddressOrName, height, txproof, txproofheight); + }, + [VRPC]: (coinObj, activeUser, channelId, iAddressOrName, height, txproof, txproofheight) => { + const [channelName, iAddress, systemId] = channelId.split('.'); + + return vrpcGetIdentity(systemId, iAddressOrName, height, txproof, txproofheight); + } +}; + +export const getIdentity = async (coinObj, activeUser, channelId, iAddressOrName, height, txproof, txproofheight) => { + const parentChannel = channelId.split('.')[0] + + if (IDENTITY_FUNCTION_MAP[parentChannel] == null) + throw new Error(`No get identity function available for channel ${channelId}`); + else return await IDENTITY_FUNCTION_MAP[parentChannel](coinObj, activeUser, channelId, iAddressOrName, height, txproof, txproofheight); +}; diff --git a/src/utils/api/routers/preflightConvertOrCrossChain.js b/src/utils/api/routers/preflightConvertOrCrossChain.js new file mode 100644 index 00000000..d2912e0d --- /dev/null +++ b/src/utils/api/routers/preflightConvertOrCrossChain.js @@ -0,0 +1,21 @@ +import { + ETH, + ERC20, + VRPC, +} from "../../constants/intervalConstants"; +import * as erc20 from "../channels/erc20/callCreator"; +import * as vrpc from "../channels/vrpc/callCreators"; + +const PREFLIGHT_FUNCTION_MAP = { + [ETH]: erc20.preflightBridgeTransfer, + [ERC20]: erc20.preflightBridgeTransfer, + [VRPC]: vrpc.preflightCurrencyTransfer +}; + +export const preflightConvertOrCrossChain = async (coinObj, activeUser, channelId, output) => { + const parentChannel = channelId.split('.')[0] + + if (PREFLIGHT_FUNCTION_MAP[parentChannel] == null) + throw new Error(`No preflight function available for channel ${channelId}`); + else return await PREFLIGHT_FUNCTION_MAP[parentChannel](coinObj, channelId, activeUser, output); +}; diff --git a/src/utils/api/routers/sendConvertOrCrossChain.js b/src/utils/api/routers/sendConvertOrCrossChain.js new file mode 100644 index 00000000..b23f6adc --- /dev/null +++ b/src/utils/api/routers/sendConvertOrCrossChain.js @@ -0,0 +1,27 @@ +import { + ETH, + ERC20, + VRPC, +} from "../../constants/intervalConstants"; +import * as erc20 from "../channels/erc20/callCreator"; +import * as vrpc from "../channels/vrpc/callCreators"; + +const SEND_FUNCTION_MAP = { + [ETH]: (coinObj, channelId, activeUser, { approvalparams, transferparams, gasprice }) => { + return erc20.sendBridgeTransfer(coinObj, transferparams, approvalparams, gasprice); + }, + [ERC20]: (coinObj, channelId, activeUser, { approvalparams, transferparams, gasprice }) => { + return erc20.sendBridgeTransfer(coinObj, transferparams, approvalparams, gasprice); + }, + [VRPC]: (coinObj, channelId, activeUser, { hex, inputs }) => { + return vrpc.sendCurrencyTransfer(coinObj, channelId, hex, inputs); + } +}; + +export const sendConvertOrCrossChain = async (coinObj, activeUser, channelId, params) => { + const parentChannel = channelId.split('.')[0] + + if (SEND_FUNCTION_MAP[parentChannel] == null) + throw new Error(`No send function available for channel ${channelId}`); + else return await SEND_FUNCTION_MAP[parentChannel](coinObj, channelId, activeUser, params); +}; diff --git a/src/utils/asyncStore/contractDefinitionStorage.js b/src/utils/asyncStore/contractDefinitionStorage.js new file mode 100644 index 00000000..d3d8b497 --- /dev/null +++ b/src/utils/asyncStore/contractDefinitionStorage.js @@ -0,0 +1,74 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { CONTRACT_DEFINITION_STORAGE_INTERNAL_KEY } from '../../../env/index' + +export const storeContractDefinitions = async (contractDefinitions) => { + return await AsyncStorage.setItem(CONTRACT_DEFINITION_STORAGE_INTERNAL_KEY, JSON.stringify(contractDefinitions)) +}; + +export const getStoredContractDefinitions = async () => { + const res = await AsyncStorage.getItem(CONTRACT_DEFINITION_STORAGE_INTERNAL_KEY); + + if (!res) return {}; + else return JSON.parse(res); +}; + +// Gets contract definitions, sets contractDefinitions[ethNetwork] to data, and stores the new definitions +export const storeContractDefinitionsForNetwork = async (ethNetwork, definitions) => { + const contractDefinitions = await getStoredContractDefinitions(); + contractDefinitions[ethNetwork] = definitions; + await storeContractDefinitions(contractDefinitions); +} + +// Gets contract definitions, sets contractDefinitions[ethNetwork][contractAddress] to definition, and stores the new definitions +export const storeContractDefinitionForNetwork = async (ethNetwork, contractAddress, definition) => { + const contractDefinitions = await getStoredContractDefinitions(); + + if (!contractDefinitions[ethNetwork]) contractDefinitions[ethNetwork] = {} + + contractDefinitions[ethNetwork][contractAddress] = definition; + await storeContractDefinitions(contractDefinitions); +} + +// Gets contract definitions for ethNetwork +export const getStoredContractDefinitionsForNetwork = async (ethNetwork) => { + const contractDefinitions = await getStoredContractDefinitions(); + return contractDefinitions[ethNetwork] ? contractDefinitions[ethNetwork] : {}; +} + +export const removeInactiveContractDefinitions = async (activeCoinList) => { + let storedDefinitions = await getStoredContractDefinitions(); + + const activeCoinIds = {} + activeCoinList.map(coin => { + activeCoinIds[coin.id] = true + + if (coin.mapped_to != null) { + activeCoinIds[coin.mapped_to] = true + } + }) + + for (const network in storedDefinitions) { + const tokens = Object.keys(storedDefinitions[network]) + + for (const contractAddress of tokens) { + if (!activeCoinIds[contractAddress]) { + delete storedDefinitions[network][contractAddress] + } + } + } + + await storeContractDefinitions(storedDefinitions) +} + +// Gets contract definition given ethNetwork and contractAddress +export const getStoredContractDefinitionForNetwork = async ( + ethNetwork, + contractAddress, +) => { + const contractDefinitions = await getStoredContractDefinitions(); + return contractDefinitions[ethNetwork] + ? contractDefinitions[ethNetwork][contractAddress] + ? contractDefinitions[ethNetwork][contractAddress] + : null + : {}; +}; diff --git a/src/utils/constants/abis/verusBridgeDelegatorAbi.js b/src/utils/constants/abis/verusBridgeDelegatorAbi.js new file mode 100644 index 00000000..1d736ad5 --- /dev/null +++ b/src/utils/constants/abis/verusBridgeDelegatorAbi.js @@ -0,0 +1,1235 @@ +export const VERUS_BRIDGE_DELEGATOR_ABI = [ + { + "inputs": [ + { + "internalType": "address[]", + "name": "_notaries", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_notariesEthAddress", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_notariesColdStoreEthAddress", + "type": "address[]" + }, + { + "internalType": "address[]", + "name": "_newContractAddress", + "type": "address[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "_readyExports", + "outputs": [ + { + "internalType": "bytes32", + "name": "exportHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "prevExportHash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "endHeight", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "bestForks", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "bridgeConverterActive", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cceLastEndHeight", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cceLastStartHeight", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "claimableFees", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "contracts", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "exportHeights", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "lastImportInfo", + "outputs": [ + { + "internalType": "bytes32", + "name": "hashOfTransfers", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "exporttxid", + "type": "bytes32" + }, + { + "internalType": "uint32", + "name": "exporttxoutnum", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "height", + "type": "uint32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "lastTxIdImport", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "notaries", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "notaryAddressMapping", + "outputs": [ + { + "internalType": "address", + "name": "main", + "type": "address" + }, + { + "internalType": "address", + "name": "recovery", + "type": "address" + }, + { + "internalType": "uint8", + "name": "state", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "pendingVoteState", + "outputs": [ + { + "internalType": "address", + "name": "txid", + "type": "address" + }, + { + "internalType": "uint32", + "name": "agree", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "count", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "startHeight", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "nullified", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "processedTxids", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "refunds", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "rollingUpgradeVotes", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "rollingVoteIndex", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "saltsUsed", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "storageGlobal", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "tokenList", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "verusToERC20mapping", + "outputs": [ + { + "internalType": "address", + "name": "erc20ContractAddress", + "type": "address" + }, + { + "internalType": "uint8", + "name": "flags", + "type": "uint8" + }, + { + "internalType": "uint256", + "name": "tokenIndex", + "type": "uint256" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint32", + "name": "version", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "address", + "name": "currency", + "type": "address" + }, + { + "internalType": "uint64", + "name": "amount", + "type": "uint64" + } + ], + "internalType": "struct VerusObjects.CCurrencyValueMap", + "name": "currencyvalue", + "type": "tuple" + }, + { + "internalType": "uint32", + "name": "flags", + "type": "uint32" + }, + { + "internalType": "address", + "name": "feecurrencyid", + "type": "address" + }, + { + "internalType": "uint64", + "name": "fees", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "destinationtype", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "destinationaddress", + "type": "bytes" + } + ], + "internalType": "struct VerusObjectsCommon.CTransferDestination", + "name": "destination", + "type": "tuple" + }, + { + "internalType": "address", + "name": "destcurrencyid", + "type": "address" + }, + { + "internalType": "address", + "name": "destsystemid", + "type": "address" + }, + { + "internalType": "address", + "name": "secondreserveid", + "type": "address" + } + ], + "internalType": "struct VerusObjects.CReserveTransfer", + "name": "_transfer", + "type": "tuple" + } + ], + "name": "sendTransfer", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint8", + "name": "version", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "typeC", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "branchType", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "CMerkleBranchBase", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "nIndex", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "nSize", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "extraHashes", + "type": "uint8" + }, + { + "internalType": "bytes32[]", + "name": "branch", + "type": "bytes32[]" + } + ], + "internalType": "struct VerusObjects.CMerkleBranch", + "name": "proofSequence", + "type": "tuple" + } + ], + "internalType": "struct VerusObjects.CTXProof[]", + "name": "txproof", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "elType", + "type": "uint8" + }, + { + "internalType": "uint8", + "name": "elIdx", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "elVchObj", + "type": "bytes" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "branchType", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "CMerkleBranchBase", + "type": "uint8" + }, + { + "internalType": "uint32", + "name": "nIndex", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "nSize", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "extraHashes", + "type": "uint8" + }, + { + "internalType": "bytes32[]", + "name": "branch", + "type": "bytes32[]" + } + ], + "internalType": "struct VerusObjects.CMerkleBranch", + "name": "proofSequence", + "type": "tuple" + } + ], + "internalType": "struct VerusObjects.CTXProof[]", + "name": "elProof", + "type": "tuple[]" + } + ], + "internalType": "struct VerusObjects.CComponents[]", + "name": "components", + "type": "tuple[]" + } + ], + "internalType": "struct VerusObjects.CPtransactionproof", + "name": "partialtransactionproof", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "serializedTransfers", + "type": "bytes" + } + ], + "internalType": "struct VerusObjects.CReserveTransferImport", + "name": "data", + "type": "tuple" + } + ], + "name": "submitImports", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "_startBlock", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "_endBlock", + "type": "uint256" + } + ], + "name": "getReadyExportsByRange", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "exportHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "prevExportHash", + "type": "bytes32" + }, + { + "internalType": "uint64", + "name": "startHeight", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "endHeight", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint32", + "name": "version", + "type": "uint32" + }, + { + "components": [ + { + "internalType": "address", + "name": "currency", + "type": "address" + }, + { + "internalType": "uint64", + "name": "amount", + "type": "uint64" + } + ], + "internalType": "struct VerusObjects.CCurrencyValueMap", + "name": "currencyvalue", + "type": "tuple" + }, + { + "internalType": "uint32", + "name": "flags", + "type": "uint32" + }, + { + "internalType": "address", + "name": "feecurrencyid", + "type": "address" + }, + { + "internalType": "uint64", + "name": "fees", + "type": "uint64" + }, + { + "components": [ + { + "internalType": "uint8", + "name": "destinationtype", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "destinationaddress", + "type": "bytes" + } + ], + "internalType": "struct VerusObjectsCommon.CTransferDestination", + "name": "destination", + "type": "tuple" + }, + { + "internalType": "address", + "name": "destcurrencyid", + "type": "address" + }, + { + "internalType": "address", + "name": "destsystemid", + "type": "address" + }, + { + "internalType": "address", + "name": "secondreserveid", + "type": "address" + } + ], + "internalType": "struct VerusObjects.CReserveTransfer[]", + "name": "transfers", + "type": "tuple[]" + } + ], + "internalType": "struct VerusObjects.CReserveTransferSetCalled[]", + "name": "returnedExports", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "serializedNotarization", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "txid", + "type": "bytes32" + }, + { + "internalType": "uint32", + "name": "n", + "type": "uint32" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "setLatestData", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "launchContractTokens", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "start", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "end", + "type": "uint256" + } + ], + "name": "getTokenList", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "iaddress", + "type": "address" + }, + { + "internalType": "address", + "name": "erc20ContractAddress", + "type": "address" + }, + { + "internalType": "address", + "name": "launchSystemID", + "type": "address" + }, + { + "internalType": "uint8", + "name": "flags", + "type": "uint8" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "ticker", + "type": "string" + }, + { + "internalType": "uint256", + "name": "tokenID", + "type": "uint256" + } + ], + "internalType": "struct VerusObjects.setupToken[]", + "name": "", + "type": "tuple[]" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "_imports", + "type": "bytes32" + } + ], + "name": "checkImport", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "claimfees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint176", + "name": "verusAddress", + "type": "uint176" + } + ], + "name": "claimRefund", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "publicKeyX", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "publicKeyY", + "type": "bytes32" + } + ], + "name": "sendfees", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "latest", + "type": "bool" + } + ], + "name": "getNewProof", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "height", + "type": "uint256" + } + ], + "name": "getProofByHeight", + "outputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "latest", + "type": "bool" + } + ], + "name": "getProofCost", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "upgradeContracts", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newcontract", + "type": "address" + }, + { + "internalType": "uint256", + "name": "contractNo", + "type": "uint256" + } + ], + "name": "replacecontract", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "runContractsUpgrade", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "revokeWithMainAddress", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "revokeWithMultiSig", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "recoverWithRecoveryAddress", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "recoverWithMultiSig", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "item", + "type": "uint256" + } + ], + "name": "getVoteState", + "outputs": [ + { + "components": [ + { + "internalType": "address", + "name": "txid", + "type": "address" + }, + { + "internalType": "address[]", + "name": "pendingContracts", + "type": "address[]" + }, + { + "internalType": "uint32", + "name": "agree", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "count", + "type": "uint32" + }, + { + "internalType": "uint32", + "name": "startHeight", + "type": "uint32" + }, + { + "internalType": "uint8", + "name": "nullified", + "type": "uint8" + } + ], + "internalType": "struct VerusObjects.voteState", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + } +] \ No newline at end of file diff --git a/src/utils/constants/constants.js b/src/utils/constants/constants.js index 921de800..59bdc235 100644 --- a/src/utils/constants/constants.js +++ b/src/utils/constants/constants.js @@ -163,7 +163,8 @@ export const DEVICE_WINDOW_HEIGHT = Dimensions.get('window').height; // Init export const START_COINS = ["VRSC", "BTC", "ETH"] export const TEST_PROFILE_OVERRIDES = { - ['VRSC']: 'VRSCTEST' + ['VRSC']: 'VRSCTEST', + ['ETH']: 'GETH' }; // Account data initialization steps @@ -186,4 +187,16 @@ export const ONE_HOUR = 60; export const ONE_MONTH = 31 * 24 * 60; export const ONE_YEAR = 365 * 24 * 60; export const KOMODO_DIVISOR = 10512000; -export const KOMODO_N_S7_HARDFORK_HEIGHT = 3484958; \ No newline at end of file +export const KOMODO_N_S7_HARDFORK_HEIGHT = 3484958; + +// Address block list +export const ADDRESS_BLOCKLIST_FROM_WEBSERVER = 'ADDRESS_BLOCKLIST_FROM_WEBSERVER'; +export const ADDRESS_BLOCKLIST_FROM_VERUSID = 'ADDRESS_BLOCKLIST_FROM_VERUSID'; +export const ADDRESS_BLOCKLIST_MANUAL = 'ADDRESS_BLOCKLIST_MANUAL'; + +// Address byte versions +export const R_ADDRESS_VERSION = 60; +export const I_ADDRESS_VERSION = 102; + +// Address blocklist +export const DEFAULT_ADDRESS_BLOCKLIST_WEBSERVER = 'https://eth.verusbridge.io/exclude.json' \ No newline at end of file diff --git a/src/utils/constants/currencies.js b/src/utils/constants/currencies.js index cab12a50..28b9c1b5 100644 --- a/src/utils/constants/currencies.js +++ b/src/utils/constants/currencies.js @@ -90,4 +90,5 @@ export const SUPPORTED_BANK_CURRENCIES = Object.keys(CURRENCY_NAMES) export const IS_GATEWAY_FLAG = 0x80 export const IS_TOKEN_FLAG = 0x20 -export const IS_FRACTIONAL_FLAG = 0x01 \ No newline at end of file +export const IS_FRACTIONAL_FLAG = 0x01 +export const IS_PBAAS_CHAIN = 0x100 \ No newline at end of file diff --git a/src/utils/constants/sendModal.js b/src/utils/constants/sendModal.js index 8da45a70..0f2663cf 100644 --- a/src/utils/constants/sendModal.js +++ b/src/utils/constants/sendModal.js @@ -3,6 +3,8 @@ export const SEND_MODAL = 'send_modal' // Send field IDs export const SEND_MODAL_TO_ADDRESS_FIELD = 'SEND_MODAL_TO_ADDRESS_FIELD' export const SEND_MODAL_DESTINATION_FIELD = 'SEND_MODAL_DESTINATION_FIELD' +export const SEND_MODAL_CONTRACT_ADDRESS_FIELD = 'SEND_MODAL_CONTRACT_ADDRESS_FIELD' + export const SEND_MODAL_SOURCE_FIELD = 'SEND_MODAL_SOURCE_FIELD' export const SEND_MODAL_MEMO_FIELD = 'SEND_MODAL_MEMO_FIELD' export const SEND_MODAL_AMOUNT_FIELD = 'SEND_MODAL_AMOUNT_FIELD' @@ -16,11 +18,13 @@ export const SEND_MODAL_PBAAS_CURRENCY_TO_ADD_FIELD = 'SEND_MODAL_PBAAS_CURRENCY export const SEND_MODAL_CONVERTTO_FIELD = 'SEND_MODAL_CONVERTTO_FIELD' export const SEND_MODAL_VIA_FIELD = 'SEND_MODAL_VIA_FIELD' export const SEND_MODAL_EXPORTTO_FIELD = 'SEND_MODAL_EXPORTTO_FIELD' +export const SEND_MODAL_MAPPING_FIELD = 'SEND_MODAL_MAPPING_FIELD' export const SEND_MODAL_IS_PRECONVERT = 'SEND_MODAL_IS_PRECONVERT' export const SEND_MODAL_PRICE_ESTIMATE = 'SEND_MODAL_PRICE_ESTIMATE' export const SEND_MODAL_SHOW_CONVERTTO_FIELD = 'SEND_MODAL_SHOW_CONVERTTO_FIELD' export const SEND_MODAL_SHOW_VIA_FIELD = 'SEND_MODAL_SHOW_VIA_FIELD' export const SEND_MODAL_SHOW_EXPORTTO_FIELD = 'SEND_MODAL_SHOW_EXPORTTO_FIELD' +export const SEND_MODAL_SHOW_MAPPING_FIELD = 'SEND_MODAL_SHOW_MAPPING_FIELD' export const SEND_MODAL_ADVANCED_FORM = 'SEND_MODAL_ADVANCED_FORM' // Send modal types @@ -33,6 +37,7 @@ export const PROVISION_IDENTITY_SEND_MODAL = 'PROVISION_IDENTITY_SEND_MODAL' export const AUTHENTICATE_USER_SEND_MODAL = 'AUTHENTICATE_USER_SEND_MODAL' export const ADD_PBAAS_CURRENCY_MODAL = 'ADD_PBAAS_CURRENCY_MODAL' export const CONVERT_OR_CROSS_CHAIN_SEND_MODAL = 'CONVERT_OR_CROSS_CHAIN_SEND_MODAL' +export const ADD_ERC20_TOKEN_MODAL = 'ADD_ERC20_TOKEN_MODAL' // Form steps export const SEND_MODAL_FORM_STEP_FORM = "SEND_MODAL_FORM_STEP_FORM" diff --git a/src/utils/constants/services.js b/src/utils/constants/services.js index c20ff697..974ed627 100644 --- a/src/utils/constants/services.js +++ b/src/utils/constants/services.js @@ -2,16 +2,22 @@ import { WYRE_SERVICE } from "./intervalConstants" export const WYRE_SERVICE_ID = 'wyre_service' export const VERUSID_SERVICE_ID = 'verusid_service' +export const PBAAS_PRECONVERT_SERVICE_ID = 'pbaas_preconvert' export const CONNECTED_SERVICE_DISPLAY_INFO = { + [WYRE_SERVICE_ID]: { + title: "Wyre", + description: "Connecting your wallet with Wyre allows you to convert between cryptocurrency and fiat" + }, [VERUSID_SERVICE_ID]: { title: "VerusID", description: "By connecting your VerusIDs to your wallet, you can use them to hold funds and/or sign into services", decentralized: true }, - [WYRE_SERVICE_ID]: { - title: "Wyre", - description: "Connecting your wallet with Wyre allows you to convert between cryptocurrency and fiat" + [PBAAS_PRECONVERT_SERVICE_ID]: { + title: "Preconvert Currency", + description: "Participate in a PBaaS currency launch by sending your funds to the currency before it starts", + decentralized: true } } @@ -19,7 +25,7 @@ export const CONNECTED_SERVICE_CHANNELS = { [WYRE_SERVICE_ID]: WYRE_SERVICE } -export const CONNECTED_SERVICES = [VERUSID_SERVICE_ID, WYRE_SERVICE_ID] +export const CONNECTED_SERVICES = [VERUSID_SERVICE_ID, /*PBAAS_PRECONVERT_SERVICE_ID ,*/ WYRE_SERVICE_ID] // Wyre specific constants export const WYRE_INDIVIDUAL_NAME = 'individualLegalName' diff --git a/src/utils/constants/storeType.js b/src/utils/constants/storeType.js index 9fb3a942..987f52b6 100644 --- a/src/utils/constants/storeType.js +++ b/src/utils/constants/storeType.js @@ -211,9 +211,6 @@ export const CLOSE_ETH_CHANNEL = 'CLOSE_ETH_CHANNEL'; export const INIT_ERC20_CHANNEL_START = 'INIT_ERC20_CHANNEL_START'; export const INIT_ERC20_CHANNEL_FINISH = 'INIT_ERC20_CHANNEL_FINISH' export const CLOSE_ERC20_CHANNEL = 'CLOSE_ERC20_CHANNEL'; -export const ADD_WEB3_CONTRACT = 'ADD_WEB3_CONTRACT' -export const REMOVE_WEB3_CONTRACT = 'REMOVE_WEB3_CONTRACT' -export const CLEAR_WEB3_CONTRACTS = 'CLEAR_WEB3_CONTRACTS' // Verusd Rpc export const INIT_VRPC_CHANNEL_START = 'INIT_VRPC_CHANNEL_START'; diff --git a/src/utils/constants/web3Constants.js b/src/utils/constants/web3Constants.js index 0ad0772a..f271ea52 100644 --- a/src/utils/constants/web3Constants.js +++ b/src/utils/constants/web3Constants.js @@ -5,4 +5,26 @@ export const FIAT_DECIMALS = 2 export const STABLECOIN_DECIMALS = 6 // Non-ERC20 utility contracts -export const RFOX_UTILITY_CONTRACT = "0xD82F7e3956d3FF391C927Cd7d0A7A57C360DF5b9" \ No newline at end of file +export const RFOX_UTILITY_CONTRACT = "0xD82F7e3956d3FF391C927Cd7d0A7A57C360DF5b9" + +export const VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT = "0x85a7de2278e52327471e174aeeb280cdfdc6a68a" +export const VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT = "0x71518580f36FeCEFfE0721F06bA4703218cD7F63" + +export const ETH_BRIDGE_NAME = "Bridge.vETH" +export const VETH = 'vETH' +export const DAI_VETH = 'DAI.VETH' +export const MKR_VETH = 'MKR.VETH' +export const NULL_ETH_ADDRESS = '0x0000000000000000000000000000000000000000' +export const ETH_CONTRACT_ADDRESS = "0x0000000000000000000000000000000000000000" + +export const ETH_VERUS_BRIDGE_CONTRACT_RESERVE_TRANSFER_FEE = 300000 +export const ETH_VERUS_BRIDGE_CONTRACT_PRELAUNCH_RESERVE_TRANSFER_FEE = 2000000 +export const ETH_VERUS_BRIDGE_DEST_SYSTEM_ID = "0x0000000000000000000000000000000000000000" + +export const MINIMUM_GAS_PRICE_WEI_DELEGATOR_CONTRACT = "3000000000" + +export const GAS_TRANSACTION_IMPORT_FEE = "1400000" + +export const INITIAL_GAS_LIMIT = 6000000 +export const FALLBACK_GAS_BRIDGE_TRANSFER = 800000 +export const ERC20_BRIDGE_TRANSFER_GAS_LIMIT = 800000 \ No newline at end of file diff --git a/src/utils/keys.js b/src/utils/keys.js index fb5bdebc..3319ad77 100644 --- a/src/utils/keys.js +++ b/src/utils/keys.js @@ -166,6 +166,10 @@ export const parseDlightSeed = async (seed) => { } catch(e) { throw e } } +export const dlightSeedToBytes = (seed) => { + return VerusLightClient.deterministicSeedBytes(seed) +} + export const isSeedPhrase = (seed, minWordLength = 12) => { return ( seed.split(/\s+/g).length >= minWordLength && validateMnemonic(seed) diff --git a/src/utils/math.js b/src/utils/math.js index 414ece1c..134521c8 100644 --- a/src/utils/math.js +++ b/src/utils/math.js @@ -14,7 +14,7 @@ export const unitsToCoins = (units, decimals) => { } export const coinsToSats = (coins) => { - return coinsToUnits(coins, 8) + return BigNumber(coinsToUnits(coins, 8).toFixed(0)) } export const satsToCoins = (satoshis) => { diff --git a/src/utils/personal/displayUtils.js b/src/utils/personal/displayUtils.js index 4de30cda..610d406f 100644 --- a/src/utils/personal/displayUtils.js +++ b/src/utils/personal/displayUtils.js @@ -6,6 +6,7 @@ import { PERSONAL_IMAGE_TYPE_SCHEMA, PERSONAL_IMAGE_SUBTYPE_SCHEMA, } from "../constants/personal"; +import { Platform } from 'react-native'; var RNFS = require('react-native-fs'); export const renderPersonalFullName = (name) => { @@ -41,11 +42,17 @@ export const renderPersonalBirthday = (birthday) => { }; export const getPersonalImageDisplayUri = uri => { - return uri && uri.includes('file://') - ? uri - : uri != null - ? RNFS.DocumentDirectoryPath + `/${uri}` - : ''; + if (uri && uri.startsWith('file://')) { + return uri; + } else if (uri != null) { + const reconstructedUri = RNFS.DocumentDirectoryPath + `/${uri}`; + + if (reconstructedUri.startsWith('file://')) return reconstructedUri; + else if (Platform.OS === 'android') return `file://${reconstructedUri}`; + else return reconstructedUri; + } else { + return ''; + } }; export const renderPersonalDocument = (document) => { @@ -58,7 +65,7 @@ export const renderPersonalDocument = (document) => { {...props} size={96} source={{ - uri: path.includes("file://"), + uri: path, }} /> ); diff --git a/src/utils/verusPay/versions/v0.js b/src/utils/verusPay/versions/v0.js index 45765105..f53d2911 100644 --- a/src/utils/verusPay/versions/v0.js +++ b/src/utils/verusPay/versions/v0.js @@ -5,10 +5,10 @@ const readVerusPay = (data) => { } const writeVerusPayQR = (coinObj, amount, address, note) => { - const { currency_id, system_id, display_name, display_ticker } = coinObj; + const { id, system_id, display_name, display_ticker } = coinObj; return writeRequest( - currency_id, + id, system_id, display_name, display_ticker, diff --git a/src/utils/vrpc/vrpcInterface.js b/src/utils/vrpc/vrpcInterface.js index 509c732b..f279651e 100644 --- a/src/utils/vrpc/vrpcInterface.js +++ b/src/utils/vrpc/vrpcInterface.js @@ -299,6 +299,11 @@ class VrpcInterface { ); return new VerusIdInterface(systemId, endpoint) } + + addDefaultEndpoints = () => { + this.initEndpoint(coinsList.VRSC.system_id, coinsList.VRSC.vrpc_endpoints[0]); + this.initEndpoint(coinsList.VRSCTEST.system_id, coinsList.VRSCTEST.vrpc_endpoints[0]); + } } const VerusMobileVrpcInterface = new VrpcInterface(); @@ -307,8 +312,7 @@ setImmediate(() => { try { // Initialize VRSC and VRSCTEST endpoints to support App functions // that make calls to vrpc even if VRSC/VRSCTEST isn't added as a coin - VerusMobileVrpcInterface.initEndpoint(coinsList.VRSC.system_id, coinsList.VRSC.vrpc_endpoints[0]); - VerusMobileVrpcInterface.initEndpoint(coinsList.VRSCTEST.system_id, coinsList.VRSCTEST.vrpc_endpoints[0]); + VerusMobileVrpcInterface.addDefaultEndpoints() } catch(e) { Alert.alert("Error initializing Verus lite mode", e.message) } diff --git a/src/utils/web3/provider.js b/src/utils/web3/provider.js index 37f22157..4212c3d6 100644 --- a/src/utils/web3/provider.js +++ b/src/utils/web3/provider.js @@ -1,12 +1,34 @@ import Web3Interface from './web3Interface' import { ETH_HOMESTEAD, + ETH_GOERLI, ETHERSCAN_API_KEY, INFURA_PROJECT_ID, } from "../../../env/index"; -// Change the provider here to change ETH provider -export default new Web3Interface(ETH_HOMESTEAD, { - etherscan: ETHERSCAN_API_KEY, - infura: INFURA_PROJECT_ID -}) \ No newline at end of file +const Web3Providers = { + [ETH_HOMESTEAD]: new Web3Interface(ETH_HOMESTEAD, { + etherscan: ETHERSCAN_API_KEY, + infura: INFURA_PROJECT_ID + }), + [ETH_GOERLI]: new Web3Interface(ETH_GOERLI, { + etherscan: ETHERSCAN_API_KEY, + infura: INFURA_PROJECT_ID + }), +} + +Object.freeze(Web3Providers); + +export const getWeb3ProviderForNetwork = (network = ETH_HOMESTEAD) => { + if (Web3Providers.hasOwnProperty(network)) { + return Web3Providers[network]; + } else { + throw new Error(`No web3 provider for network ${network}`); + } +}; + +export const deleteAllWeb3Contracts = () => { + for (const network in Web3Providers) { + Web3Providers[network].deleteAllContracts() + } +} \ No newline at end of file diff --git a/src/utils/web3/web3Interface.js b/src/utils/web3/web3Interface.js index 3bdf8ee7..7f564bb6 100644 --- a/src/utils/web3/web3Interface.js +++ b/src/utils/web3/web3Interface.js @@ -1,16 +1,16 @@ import ethers from 'ethers'; -import Store from '../../store'; import { DEFAULT_ERC20_ABI } from '../constants/abi'; -import { - ADD_WEB3_CONTRACT, - CLEAR_WEB3_CONTRACTS, - REMOVE_WEB3_CONTRACT, -} from "../constants/storeType"; +import { ETHERS, VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT, VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT } from '../constants/web3Constants'; +import { VERUS_BRIDGE_DELEGATOR_ABI } from '../constants/abis/verusBridgeDelegatorAbi'; +import { coinsList } from '../CoinData/CoinsList'; +import { ERC20 } from '../constants/intervalConstants'; class Web3Interface { constructor(network, apiKeys) { this.network = network; - this.keys = apiKeys + this.keys = apiKeys; + + this.web3Contracts = {}; // { [key: contractAddress]: Array[contractAddress, contractAbi]} this.DefaultProvider = new ethers.getDefaultProvider(this.network, apiKeys); @@ -25,23 +25,16 @@ class Web3Interface { ); } - initContract = async (contractAddress) => { - const web3Contracts = Store.getState().channelStore_erc20.web3Contracts - + initContract = async (contractAddress) => { try { - if (web3Contracts[contractAddress]) + if (this.web3Contracts[contractAddress]) throw new Error( "Cannot initialize existing contract " + contractAddress ); const abi = DEFAULT_ERC20_ABI - Store.dispatch({ - type: ADD_WEB3_CONTRACT, - payload: { - contract: [contractAddress, abi], - } - }) + this.web3Contracts[contractAddress] = [contractAddress, abi]; } catch (e) { console.error(e); throw e; @@ -49,31 +42,25 @@ class Web3Interface { }; deleteContract = (contractAddress) => { - const web3Contracts = Store.getState().channelStore_erc20.web3Contracts - try { - if (!web3Contracts[contractAddress]) + if (!this.web3Contracts[contractAddress]) throw new Error( "Cannot delete uninitialized contract " + contractAddress ); - Store.dispatch({ - type: REMOVE_WEB3_CONTRACT, - payload: { - contractAddress, - } - }) + delete this.web3Contracts[contractAddress]; } catch (e) { console.error(e); throw e; } }; - deleteAllContracts = () => Store.dispatch({ type: CLEAR_WEB3_CONTRACTS }) + deleteAllContracts = () => { + this.web3Contracts = {}; + } getContract = (contractAddress, customAbiFragment) => { - const web3Contracts = Store.getState().channelStore_erc20.web3Contracts - const params = web3Contracts[contractAddress] + const params = this.web3Contracts[contractAddress] if (!params) throw new Error(`ERC20 contract ${contractAddress} not initialized`); @@ -83,6 +70,103 @@ class Web3Interface { this.DefaultProvider ); }; + + getUnitializedContractInstance = (contractAddress) => { + const abi = DEFAULT_ERC20_ABI + + return new ethers.Contract( + contractAddress, + abi, + this.DefaultProvider + ); + }; + + getContractInfo = async (contractAddress) => { + if (!ethers.utils.isAddress(contractAddress)) throw new Error("Invalid contract address '" + contractAddress + "'") + + const contract = this.getUnitializedContractInstance(contractAddress); + let name, symbol, decimals; + + for (const key in coinsList) { + if ( + coinsList[key].proto === ERC20 && + ((coinsList[key].testnet && this.network === 'goerli') || + (!coinsList[key].testnet && this.network === 'homestead')) && + coinsList[key].currency_id.toLowerCase() === contractAddress.toLowerCase() + ) { + return { + name: coinsList[key].display_name, + symbol: coinsList[key].display_ticker, + decimals: coinsList[key].decimals, + }; + } + } + + try { + name = await contract.name(); + + if (typeof name !== 'string') throw new Error("Name is not string"); + } catch(e) { + name = contractAddress; + } + + try { + symbol = await contract.symbol(); + + if (typeof name !== 'string') throw new Error("Symbol is not string"); + } catch(e) { + symbol = contractAddress.substring(0, 6); + } + + try { + decimals = await contract.decimals(); + + if (typeof decimals !== 'number') throw new Error("Decimals is not number"); + } catch(e) { + decimals = ETHERS; + } + + return { + name, + symbol, + decimals + } + } + + getVerusBridgeDelegatorContract = () => { + switch (this.network) { + case 'goerli': + return new ethers.Contract( + VERUS_BRIDGE_DELEGATOR_GOERLI_CONTRACT, + VERUS_BRIDGE_DELEGATOR_ABI, + this.DefaultProvider + ); + case 'homestead': + return new ethers.Contract( + VERUS_BRIDGE_DELEGATOR_MAINNET_CONTRACT, + VERUS_BRIDGE_DELEGATOR_ABI, + this.DefaultProvider + ); + default: + throw new Error("No Verus bridge delegator for network " + this.network) + } + } + + isVerusBridgeDelegatorActive = () => { + const delegatorContract = this.getVerusBridgeDelegatorContract(); + const signer = new ethers.VoidSigner(delegatorContract.address, this.DefaultProvider); + + return delegatorContract.connect(signer).callStatic.bridgeConverterActive(); + } + + getVrscSystem = () => { + switch (this.network) { + case 'homestead': + return coinsList.VRSC.currency_id; + default: + return coinsList.VRSCTEST.currency_id; + } + } } export default Web3Interface \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index bfb4f87a..eb168bd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9380,10 +9380,10 @@ react-native-fs@2.18.0: base-64 "^0.1.0" utf8 "^3.0.0" -react-native-gesture-handler@2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.8.0.tgz#ef9857871c10663c95a51546225b6e00cd4740cf" - integrity sha512-poOSfz/w0IyD6Qwq7aaIRRfEaVTl1ecQFoyiIbpOpfNTjm2B1niY2FLrdVQIOtIOe+K9nH55Qal04nr4jGkHdQ== +react-native-gesture-handler@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-2.9.0.tgz#2f63812e523c646f25b9ad660fc6f75948e51241" + integrity sha512-a0BcH3Qb1tgVqUutc6d3VuWQkI1AM3+fJx8dkxzZs9t06qA27QgURYFoklpabuWpsUTzuKRpxleykp25E8m7tg== dependencies: "@egjs/hammerjs" "^2.0.17" hoist-non-react-statics "^3.3.0" @@ -9401,10 +9401,10 @@ react-native-haptic-feedback@1.11.0: resolved "https://registry.yarnpkg.com/react-native-haptic-feedback/-/react-native-haptic-feedback-1.11.0.tgz#adfd841f3b67046532f912c6ec827aea0037d8ad" integrity sha512-KTIy7lExwMtB6pOpCARyUzFj5EzYTh+A1GN/FB5Eb0LrW5C6hbb1kdj9K2/RHyZC+wyAJD1M823ZaDCU6n6cLA== -react-native-image-picker@4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-4.0.6.tgz#0dfa5bb83bcf6ff58635ebb9201a185c2271e8cc" - integrity sha512-Pp3UWKUADuMG1mz12m6dSO/R2KnvXVEd77bldrfTMFpz4PFc4iVKo+bHeS79It0mUBezfzDMgfesg/OPLSugvQ== +react-native-image-picker@5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/react-native-image-picker/-/react-native-image-picker-5.7.0.tgz#3c146550bc89daddfca2fe123aaa0c753b58a5e2" + integrity sha512-LGBrFQzKEAt/u4z+IcY7COZA7N6lCeLQ2dMu1PubK10+d9YBRS8n/ABNj48CJhEpUhcRPt7H3iWbwIuOlcZrDA== react-native-iphone-x-helper@^1.0.3, react-native-iphone-x-helper@^1.3.1: version "1.3.1" @@ -9622,7 +9622,7 @@ react-native-url-polyfill@1.3.0: "react-native-verus-light-client@git+https://github.com/VerusCoin/react-native-verus-light-client.git": version "1.0.0" - resolved "git+https://github.com/VerusCoin/react-native-verus-light-client.git#9ee102f3cf00a8ca08911d52289cb9a3612d06a0" + resolved "git+https://github.com/VerusCoin/react-native-verus-light-client.git#e73aeb774b4a7da5f08eb85e15c0c92af24c5955" dependencies: react "17.0.1" react-native "0.64.0" @@ -11543,7 +11543,7 @@ version_compare@0.0.3: "verusd-rpc-ts-client@git+https://github.com/VerusCoin/verusd-rpc-ts-client.git": version "0.1.0" - resolved "git+https://github.com/VerusCoin/verusd-rpc-ts-client.git#6886b2b9135fac65afa4bcb80099f4c72d5bc784" + resolved "git+https://github.com/VerusCoin/verusd-rpc-ts-client.git#615002f1b39e7c989e40188d4483ef0cce4e7bfe" dependencies: axios "0.27.2" verus-typescript-primitives "https://github.com/VerusCoin/verus-typescript-primitives.git"