diff --git a/build.sbt b/build.sbt index 526db6be7b..43026541c1 100644 --- a/build.sbt +++ b/build.sbt @@ -24,7 +24,7 @@ lazy val commonSettings = Seq( publishTo := sonatypePublishToBundle.value, ) -val scorexVersion = "master-07d30caa-SNAPSHOT" +val scorexVersion = "master-bb48da3a-SNAPSHOT" val sigmaStateVersion = "3.2.1" // for testing current sigmastate build (see sigmastate-ergo-it jenkins job) @@ -42,8 +42,7 @@ libraryDependencies ++= Seq( "org.iq80.leveldb" % "leveldb" % "0.12", ("org.scorexfoundation" %% "scorex-core" % scorexVersion).exclude("ch.qos.logback", "logback-classic"), - - "org.typelevel" %% "cats-free" % "1.6.0", + "javax.xml.bind" % "jaxb-api" % "2.4.0-b180830.0359", "com.iheart" %% "ficus" % "1.4.7", "ch.qos.logback" % "logback-classic" % "1.2.3", diff --git a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/boxes/TrackedBox.scala b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/boxes/TrackedBox.scala index b0e74a0b17..e6b3f91096 100644 --- a/ergo-wallet/src/main/scala/org/ergoplatform/wallet/boxes/TrackedBox.scala +++ b/ergo-wallet/src/main/scala/org/ergoplatform/wallet/boxes/TrackedBox.scala @@ -20,14 +20,13 @@ import org.ergoplatform.ErgoBoxAssets * @param box - Underlying Ergo box * @param scans - Identifiers of scans the box refers to */ -final case class TrackedBox(creationTxId: ModifierId, - creationOutIndex: Short, - inclusionHeightOpt: Option[Int], - spendingTxIdOpt: Option[ModifierId], - spendingHeightOpt: Option[Int], - box: ErgoBox, - scans: Set[ScanId]) extends ErgoBoxAssets { - +case class TrackedBox(creationTxId: ModifierId, + creationOutIndex: Short, + inclusionHeightOpt: Option[Int], + spendingTxIdOpt: Option[ModifierId], + spendingHeightOpt: Option[Int], + box: ErgoBox, + scans: Set[ScanId]) extends ErgoBoxAssets { /** * Whether the box is spent or not @@ -86,6 +85,17 @@ object TrackedBox { box: ErgoBox, appStatuses: Set[ScanId]): TrackedBox = apply(creationTx.id, creationOutIndex, creationHeight, None, None, box, appStatuses) + /** + * Creates unspent box with given inclusion height and scans the box is associated with + * @param box + * @param inclusionHeight + * @param scans + * @return + */ + def apply(box: ErgoBox, inclusionHeight: Int, scans: Set[ScanId]): TrackedBox = { + new TrackedBox(box.transactionId, box.index, Some(inclusionHeight), None, None, box, scans) + } + } object TrackedBoxSerializer extends ErgoWalletSerializer[TrackedBox] { diff --git a/src/main/resources/api/openapi.yaml b/src/main/resources/api/openapi.yaml index 8039955fc6..084dacd7d1 100644 --- a/src/main/resources/api/openapi.yaml +++ b/src/main/resources/api/openapi.yaml @@ -850,6 +850,20 @@ components: boxId: $ref: '#/components/schemas/TransactionBoxId' + ScanIdsBox: + description: Ergo box with associated scans (their respective identifiers) + type: object + required: + - scanIds + - box + properties: + scanIds: + type: array + items: + type: integer + box: + $ref: '#/components/schemas/ErgoTransactionOutput' + PaymentRequest: description: Request for generation of payment transaction to a given address type: object @@ -902,11 +916,6 @@ components: type: integer format: int32 example: 8 - fee: - description: Optional, default transaction fee from settings will be used if not defined - type: integer - format: int64 - example: 1000000 registers: description: Optional, possible values for registers R7...R9 $ref: '#/components/schemas/Registers' @@ -2516,6 +2525,24 @@ paths: schema: $ref: '#/components/schemas/ApiError' + /wallet/rescan: + get: + security: + - ApiKeyAuth: [api_key] + summary: Rescan wallet (all the available full blocks) + operationId: walletRescan + tags: + - wallet + responses: + '200': + description: Wallet storage recreated + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + /wallet/status: get: security: @@ -3579,6 +3606,35 @@ paths: application/json: schema: $ref: '#/components/schemas/ScanIdBoxId' + default: + description: Error + content: + application/json: + schema: + $ref: '#/components/schemas/ApiError' + + /scan/addBox: + post: + security: + - ApiKeyAuth: [api_key] + summary: Adds a box to scans, writes box to database if it is not there. You can use scan number 10 to add a box to the wallet. + operationId: addBox + tags: + - scan + - wallet + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ScanIdsBox' + responses: + '200': + description: It the box is added successfully, then its id is returned + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionId' default: description: Error content: diff --git a/src/main/resources/devnet.conf b/src/main/resources/devnet.conf index 3d05d7cb4b..32f863b1a4 100644 --- a/src/main/resources/devnet.conf +++ b/src/main/resources/devnet.conf @@ -10,10 +10,6 @@ ergo { initialDifficultyHex = "0001d4c0" } wallet.secretStorage.secretDir = ${ergo.directory}"/wallet/keystore" - bootstrap { - resourceUri = "http://188.166.109.25:5000/bootSettings" - pollDelay = 10s - } } scorex { network { diff --git a/src/main/resources/panel/asset-manifest.json b/src/main/resources/panel/asset-manifest.json index ea2531a76a..f44856b100 100644 --- a/src/main/resources/panel/asset-manifest.json +++ b/src/main/resources/panel/asset-manifest.json @@ -1,15 +1,15 @@ { "files": { "main.css": "/static/css/main.f6c8bfb4.chunk.css", - "main.js": "/static/js/main.5a370586.chunk.js", - "main.js.map": "/static/js/main.5a370586.chunk.js.map", + "main.js": "/static/js/main.d2ee9699.chunk.js", + "main.js.map": "/static/js/main.d2ee9699.chunk.js.map", "runtime-main.js": "/static/js/runtime-main.8fa4c17a.js", "runtime-main.js.map": "/static/js/runtime-main.8fa4c17a.js.map", "static/css/2.a94a8b68.chunk.css": "/static/css/2.a94a8b68.chunk.css", "static/js/2.637d7a90.chunk.js": "/static/js/2.637d7a90.chunk.js", "static/js/2.637d7a90.chunk.js.map": "/static/js/2.637d7a90.chunk.js.map", "index.html": "/index.html", - "precache-manifest.d1d7c8d8559118114220728682d484c9.js": "/precache-manifest.d1d7c8d8559118114220728682d484c9.js", + "precache-manifest.c85cf283695a25241e587d74bf1b1e14.js": "/precache-manifest.c85cf283695a25241e587d74bf1b1e14.js", "service-worker.js": "/service-worker.js", "static/css/2.a94a8b68.chunk.css.map": "/static/css/2.a94a8b68.chunk.css.map", "static/css/main.f6c8bfb4.chunk.css.map": "/static/css/main.f6c8bfb4.chunk.css.map", @@ -21,6 +21,6 @@ "static/css/2.a94a8b68.chunk.css", "static/js/2.637d7a90.chunk.js", "static/css/main.f6c8bfb4.chunk.css", - "static/js/main.5a370586.chunk.js" + "static/js/main.d2ee9699.chunk.js" ] } \ No newline at end of file diff --git a/src/main/resources/panel/index.html b/src/main/resources/panel/index.html index 9375a5aff0..519878f5ca 100644 --- a/src/main/resources/panel/index.html +++ b/src/main/resources/panel/index.html @@ -1 +1 @@ -Ergo node interface
\ No newline at end of file +Ergo node interface
\ No newline at end of file diff --git a/src/main/resources/panel/precache-manifest.d1d7c8d8559118114220728682d484c9.js b/src/main/resources/panel/precache-manifest.c85cf283695a25241e587d74bf1b1e14.js similarity index 90% rename from src/main/resources/panel/precache-manifest.d1d7c8d8559118114220728682d484c9.js rename to src/main/resources/panel/precache-manifest.c85cf283695a25241e587d74bf1b1e14.js index 88bb5b675a..a2fcea6549 100644 --- a/src/main/resources/panel/precache-manifest.d1d7c8d8559118114220728682d484c9.js +++ b/src/main/resources/panel/precache-manifest.c85cf283695a25241e587d74bf1b1e14.js @@ -1,6 +1,6 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([ { - "revision": "fa20f17e2c568ac2ce5ac36c4e17de30", + "revision": "886d9d7a31acaa442ac06d534acf79c5", "url": "/index.html" }, { @@ -8,7 +8,7 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([ "url": "/static/css/2.a94a8b68.chunk.css" }, { - "revision": "cd542706258c59b4adcb", + "revision": "dddfb9512708699add4a", "url": "/static/css/main.f6c8bfb4.chunk.css" }, { @@ -16,8 +16,8 @@ self.__precacheManifest = (self.__precacheManifest || []).concat([ "url": "/static/js/2.637d7a90.chunk.js" }, { - "revision": "cd542706258c59b4adcb", - "url": "/static/js/main.5a370586.chunk.js" + "revision": "dddfb9512708699add4a", + "url": "/static/js/main.d2ee9699.chunk.js" }, { "revision": "c51552afbc3cf987b481", diff --git a/src/main/resources/panel/service-worker.js b/src/main/resources/panel/service-worker.js index da2dce3410..7c73e66e64 100644 --- a/src/main/resources/panel/service-worker.js +++ b/src/main/resources/panel/service-worker.js @@ -14,7 +14,7 @@ importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js"); importScripts( - "/precache-manifest.d1d7c8d8559118114220728682d484c9.js" + "/precache-manifest.c85cf283695a25241e587d74bf1b1e14.js" ); self.addEventListener('message', (event) => { diff --git a/src/main/resources/panel/static/js/main.5a370586.chunk.js b/src/main/resources/panel/static/js/main.d2ee9699.chunk.js similarity index 76% rename from src/main/resources/panel/static/js/main.5a370586.chunk.js rename to src/main/resources/panel/static/js/main.d2ee9699.chunk.js index ee78ef38e4..f5cb119960 100644 --- a/src/main/resources/panel/static/js/main.5a370586.chunk.js +++ b/src/main/resources/panel/static/js/main.d2ee9699.chunk.js @@ -1,2 +1,2 @@ -(this["webpackJsonpergo-node-interface"]=this["webpackJsonpergo-node-interface"]||[]).push([[0],{108:function(e,t,a){e.exports=a.p+"static/media/logotype_white.4dcfd639.svg"},111:function(e,t,a){e.exports=a(164)},122:function(e,t,a){},139:function(e,t,a){},159:function(e,t,a){},160:function(e,t,a){},162:function(e,t,a){},164:function(e,t,a){"use strict";a.r(t);var n=a(0),r=a.n(n),l=a(15),c=a.n(l),s=a(49),o=a(18),i=a(42),u=a(39),m=a(7),d=a(8),p=a(10),b=a(9),f=a(11),h=a(19),E=a(22),y=a(52),w=a(97),v={swaggerInterface:"/swagger",website:"https://ergoplatform.org",explorer:"https://explorer.ergoplatform.com",nanoErgInErg:1e9},g={dashboard:{href:"/",icon:r.a.createElement(h.a,{icon:E.b}),title:"Dashboard"},wallet:{href:"/wallet",icon:r.a.createElement(h.a,{icon:E.e}),title:"Wallet"}},O={swaggerInterface:{href:v.swaggerInterface,icon:r.a.createElement(h.a,{icon:E.a}),title:"Swagger"},explorer:{href:v.explorer,icon:r.a.createElement(h.a,{icon:w.a}),title:"Explorer"},website:{href:v.website,icon:r.a.createElement(h.a,{icon:E.g}),title:"Website"}},j=Object(u.f)((function(e){var t=e.location.pathname;return r.a.createElement("div",null,r.a.createElement("p",{className:"h5 pl-3 pt-4"},"Menu"),r.a.createElement("hr",{className:"mb-0"}),r.a.createElement("div",{className:"list-group list-group-flush"},Object.values(g).map((function(e,a){var n=e.href,l=e.icon,c=e.title;return r.a.createElement(i.b,{key:c,className:Object(y.a)("list-group-item list-group-item-action",{"list-group-item-dark":n===t,active:n===t,"border-top-0":0===a}),to:n},l," ",c)}))),r.a.createElement("p",{className:"h5 pl-3 pt-4"},"External links"),r.a.createElement("hr",{className:"mb-0"}),r.a.createElement("div",{className:"list-group list-group-flush"},Object.values(O).map((function(e,t){var a=e.href,n=e.icon,l=e.title;return r.a.createElement("a",{key:l,className:Object(y.a)("list-group-item list-group-item-action",{"border-top-0":0===t}),href:a,rel:"noopener noreferrer",target:"_blank"},n," ",l)}))))})),N=(a(122),a(76)),S=Object(N.a)((function(e){return e.app}),(function(e){return e.apiKey})),k=function(e){return e.wallet},W=Object(N.a)(k,(function(e){return e.isWalletUnlocked})),P=Object(N.a)(k,(function(e){return e.isWalletInitialized})),I=a(36),C=a(28),_=Object(C.c)({name:"walletSlice",initialState:{isWalletUnlocked:null,isWalletInitialized:null},reducers:{setIsWalletUnlocked:function(e,t){var a=t.payload;e.isWalletUnlocked=a},setIsWalletInitialized:function(e,t){var a=t.payload;e.isWalletInitialized=a}}});function A(e,t){var a=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),a.push.apply(a,n)}return a}var K=Object(C.b)("checkWalletStatus"),x=function(e){for(var t=1;t,\n title: 'Dashboard',\n },\n wallet: {\n href: '/wallet',\n icon: ,\n title: 'Wallet',\n },\n}\n\nconst externalRouteList = {\n swaggerInterface: {\n href: constants.swaggerInterface,\n icon: ,\n title: 'Swagger',\n },\n explorer: {\n href: constants.explorer,\n icon: ,\n title: 'Explorer',\n },\n website: {\n href: constants.website,\n icon: ,\n title: 'Website',\n },\n}\n\nconst MenuList = ({ location: { pathname } }) => {\n return (\n
\n

Menu

\n
\n
\n {Object.values(localRouteList).map(({ href, icon, title }, index) => (\n \n {icon} {title}\n \n ))}\n
\n

External links

\n
\n
\n {Object.values(externalRouteList).map(\n ({ href, icon, title }, index) => (\n \n {icon} {title}\n \n ),\n )}\n
\n
\n )\n}\n\nexport default withRouter(MenuList)\n","import { createSelector } from 'redux-starter-kit'\n\nexport const appSelector = state => state.app\n\nexport const apiKeySelector = createSelector(appSelector, app => app.apiKey)\n","import { createSelector } from 'redux-starter-kit'\n\nexport const walletSelector = state => state.wallet\n\nexport const isWalletUnlockedSelector = createSelector(\n walletSelector,\n wallet => wallet.isWalletUnlocked,\n)\n\nexport const isWalletInitializedSelector = createSelector(\n walletSelector,\n wallet => wallet.isWalletInitialized,\n)\n","import { createSlice } from 'redux-starter-kit'\n\nconst initialState = {\n isWalletUnlocked: null,\n isWalletInitialized: null,\n}\n\nexport default createSlice({\n name: 'walletSlice',\n initialState,\n reducers: {\n setIsWalletUnlocked: (state, { payload }) => {\n state.isWalletUnlocked = payload\n },\n setIsWalletInitialized: (state, { payload }) => {\n state.isWalletInitialized = payload\n },\n },\n})\n","import { createAction } from 'redux-starter-kit'\nimport walletSlice from '../slices/walletSlice'\n\nconst checkWalletStatus = createAction('checkWalletStatus')\n\nexport default {\n ...walletSlice.actions,\n checkWalletStatus,\n}\n","import { createSlice } from 'redux-starter-kit'\n\nconst initialState = {\n apiKey: '',\n}\n\nexport default createSlice({\n name: 'appSlice',\n initialState,\n reducers: {\n setApiKey: (state, action) => {\n state.apiKey = action.payload\n },\n },\n})\n","import appSlice from '../slices/appSlice'\n\nexport default {\n ...appSlice.actions,\n}\n","const appConfig = () => {\n return {\n nodeApiLink: '/',\n }\n}\n\nexport default {\n ...appConfig(),\n}\n","import axios from 'axios'\nimport environment from '../utils/environment'\n\nfunction NetworkError({ status, message, data, statusText }) {\n this.name = 'NetworkError'\n this.message = message || statusText\n this.status = status\n this.data = data\n}\n\nNetworkError.prototype = Object.create(Error.prototype)\n\nconst nodeApi = axios.create({\n baseURL: environment.nodeApiLink,\n timeout: 1000 * 10,\n crossDomain: true,\n headers: {\n 'Content-Type': 'application/json',\n },\n})\n\nnodeApi.interceptors.response.use(\n response => Promise.resolve(response),\n error => Promise.reject(new NetworkError(error.response || error)),\n)\n\nexport default nodeApi\n","import { toast } from 'react-toastify'\nimport './index.scss'\n\nconst toastStates = {\n success: (text, options) =>\n toast.success(text, {\n position: 'top-right',\n autoClose: 5000,\n hideProgressBar: false,\n closeOnClick: true,\n pauseOnHover: true,\n draggable: true,\n className: 'n-toast n-toast--success',\n bodyClassName: 'n-toast__body',\n progressClassName: 'n-toast__progress--success',\n ...options,\n }),\n error: (text, options) =>\n toast.error(text, {\n position: 'top-right',\n autoClose: 5000,\n hideProgressBar: false,\n closeOnClick: true,\n pauseOnHover: true,\n draggable: true,\n className: 'n-toast n-toast--error',\n bodyClassName: 'n-toast__body',\n progressClassName: 'n-toast__progress--error',\n ...options,\n }),\n info: toast.info,\n}\n\nexport default (state, text, options) =>\n toastStates[state]\n ? toastStates[state](text, options)\n : new Error(`Bad toast state`)\n","import React from 'react'\nimport { Modal } from 'react-bootstrap'\nimport { Formik, Form, Field } from 'formik'\n\nconst renderButton = (apiKey, handleShow) => {\n if (apiKey === '') {\n return (\n \n )\n }\n\n return (\n \n )\n}\n\nconst ApiKeyModalView = ({\n showModal,\n handleHide,\n submitForm,\n apiKey,\n handleShow,\n}) => {\n return (\n
\n {renderButton(apiKey, handleShow)}\n handleHide()} centered>\n \n {() => (\n
\n \n Authorization\n \n \n

Set API key to access Node requests

\n
\n \n
\n
\n\n \n \n Close\n \n \n \n
\n )}\n
\n
\n
\n )\n}\n\nexport default ApiKeyModalView\n","import ApiKeyModalContainer from './ApiKeyModalContainer'\n\nexport default ApiKeyModalContainer\n","import React, { memo, useState } from 'react'\nimport { connect } from 'react-redux'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport appActions from '../../../store/actions/appActions'\nimport nodeApi from '../../../api/api'\nimport customToast from '../../../utils/toast'\nimport ApiKeyModalView from './ApiKeyModalView'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchSetApiKey: apiKey => dispatch(appActions.setApiKey(apiKey)),\n})\n\nconst ApiKeyModalContainer = props => {\n const { dispatchSetApiKey, apiKey } = props\n\n const [showModal, setShowModal] = useState(false)\n\n const handleShow = () => {\n setShowModal(true)\n }\n\n const handleHide = () => {\n setShowModal(false)\n }\n\n const submitForm = values => {\n // Check API key for random get method\n nodeApi\n .get('/wallet/status', {\n headers: {\n api_key: values.apiKey,\n },\n })\n .then(() => {\n dispatchSetApiKey(values.apiKey.trim())\n customToast('success', 'API key is set successfully')\n handleHide()\n })\n .catch(() => {\n customToast('error', 'Bad API key')\n })\n }\n\n return (\n \n )\n}\n\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(ApiKeyModalContainer))\n","import React, { Component, memo } from 'react'\nimport Modal from 'react-bootstrap/Modal'\nimport { Formik, Field, Form } from 'formik'\nimport { connect } from 'react-redux'\nimport { isWalletUnlockedSelector } from '../../../store/selectors/wallet'\nimport walletActions from '../../../store/actions/walletActions'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport customToast from '../../../utils/toast'\nimport nodeApi from '../../../api/api'\n\nconst mapStateToProps = state => ({\n isWalletUnlocked: isWalletUnlockedSelector(state),\n apiKey: apiKeySelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchSetIsWalletUnlocked: isWalletUnlock =>\n dispatch(walletActions.setIsWalletUnlocked(isWalletUnlock)),\n})\n\nclass WalletStatusForm extends Component {\n state = {\n showModal: false,\n }\n\n handleShow = () => {\n this.setState({ showModal: true })\n }\n\n handleHide = () => {\n this.setState({ showModal: false })\n }\n\n walletUnlock = pass =>\n nodeApi.post(\n '/wallet/unlock',\n { pass },\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n\n walletLock = () =>\n nodeApi.get('/wallet/lock', {\n headers: {\n api_key: this.props.apiKey,\n },\n })\n\n submitWalletUnlockForm = (\n { pass },\n { setSubmitting, resetForm, setStatus },\n ) => {\n setStatus({ status: 'submitting' })\n this.walletUnlock(pass)\n .then(() => {\n resetForm({ pass: '' })\n customToast('success', 'Your wallet is unlocked successfully')\n this.props.dispatchSetIsWalletUnlocked(true)\n this.handleHide()\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n submitWalletLockForm = () => {\n // eslint-disable-next-line\n if (confirm('Are you sure want to lock wallet?')) {\n this.walletLock()\n .then(() => {\n customToast('success', 'Your wallet is locked successfully')\n this.props.dispatchSetIsWalletUnlocked(false)\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n })\n }\n }\n\n renderButton = () => {\n if (!this.props.isWalletUnlocked) {\n return (\n \n )\n }\n\n return (\n \n Lock wallet\n \n )\n }\n\n render() {\n return (\n
\n {this.renderButton()}\n this.handleHide()}\n centered\n aria-labelledby=\"example-custom-modal-styling-title\"\n >\n \n {({ isSubmitting }) => (\n
\n \n \n Unlock wallet form\n \n \n \n
\n \n \n \n * If you have it or leave field empty\n \n
\n
\n\n \n \n Close\n \n \n Save changes\n \n \n
\n )}\n \n \n
\n )\n }\n}\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(WalletStatusForm))\n","import React from 'react'\nimport copy from 'clipboard-copy'\nimport { Overlay, Tooltip } from 'react-bootstrap'\nimport { faCopy } from '@fortawesome/free-solid-svg-icons'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\n\nclass CopyToClipboard extends React.PureComponent {\n constructor(props) {\n super(props)\n\n this.myRef = React.createRef()\n this.state = { showTooltip: false }\n }\n\n render() {\n return (\n <>\n \n {this.props.children}\n  \n \n \n \n Copied!\n \n \n )\n }\n\n startTimer = () => {\n const timerId = setTimeout(\n () => this.setState({ showTooltip: false }),\n 1500,\n )\n this.setState({ timerId })\n }\n\n onCopy = e => {\n e.preventDefault()\n copy(this.props.children)\n this.setState({ showTooltip: true })\n this.startTimer()\n }\n\n handleOnTooltipClose = () => {\n this.setState({ showTooltip: false })\n }\n\n componentWillUnmount() {\n clearTimeout(this.state.timerId)\n }\n}\n\nexport default CopyToClipboard\n","import React, { Component, memo } from 'react'\nimport { Formik, Field, Form } from 'formik'\nimport nodeApi from '../../../api/api'\nimport CopyToClipboard from '../../common/CopyToClipboard'\nimport customToast from '../../../utils/toast'\n\nconst initialFormValues = {\n walletPassword: '',\n mnemonicPass: '',\n}\n\nclass WalletInitializeForm extends Component {\n state = { isShowMnemonic: false }\n\n walletInit = async ({ walletPassword, mnemonicPass }) => {\n const { data } = await nodeApi.post(\n '/wallet/init',\n { pass: walletPassword, mnemonicPass },\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n\n return data\n }\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.walletInit(values)\n .then(result => {\n resetForm(initialFormValues)\n setStatus({\n state: 'success',\n msg: (\n <>\n Your wallet successfully initialized. Please, save your mnemonic -{' '}\n {result.mnemonic}\n \n ),\n })\n this.setState({ isShowMnemonic: true })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n

Initialize wallet

\n \n {({ status, isSubmitting }) => (\n
\n {status && status.state === 'error' && (\n
\n {status.msg}\n
\n )}\n {status &&\n status.state === 'success' &&\n this.state.isShowMnemonic && (\n
\n this.setState({ isShowMnemonic: false })}\n >\n ×\n \n {status.msg}\n
\n )}\n
\n \n \n
\n
\n \n \n
\n \n Send\n \n \n )}\n
\n
\n )\n }\n}\n\nexport default memo(WalletInitializeForm)\n","import React, { Component, memo } from 'react'\nimport { Formik, Field, Form } from 'formik'\nimport nodeApi from '../../../api/api'\nimport customToast from '../../../utils/toast'\n\nconst initialFormValues = {\n walletPassword: '',\n mnemonicPass: '',\n mnemonic: '',\n}\n\nclass WalletInitializeForm extends Component {\n walletRestore = async ({\n walletPassword,\n mnemonicPass = '',\n mnemonic = '',\n }) => {\n if (!mnemonic || !String(mnemonic).trim()) {\n throw Error('Need to set mnemonic')\n }\n\n return nodeApi.post(\n '/wallet/restore',\n { pass: walletPassword, mnemonicPass, mnemonic },\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n }\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.walletRestore(values)\n .then(() => {\n resetForm(initialFormValues)\n customToast('success', 'Your wallet successfully re-stored')\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n

Re-store wallet

\n \n {({ status, isSubmitting }) => (\n
\n {status && status.state === 'error' && (\n
\n {status.msg}\n
\n )}\n {status && status.state === 'success' && (\n
{status.msg}
\n )}\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n \n Send\n \n \n )}\n
\n
\n )\n }\n}\n\nexport default memo(WalletInitializeForm)\n","import React, { Component, memo } from 'react'\nimport Modal from 'react-bootstrap/Modal'\nimport { connect } from 'react-redux'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport appActions from '../../../store/actions/appActions'\nimport WalletInitializeForm from '../../elements/WalletInitializeForm'\nimport RestoreWalletForm from '../../elements/RestoreWalletForm'\nimport walletActions from '../../../store/actions/walletActions'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchCheckWalletStatus: () => dispatch(walletActions.checkWalletStatus()),\n dispatchSetApiKey: apiKey => dispatch(appActions.setApiKey(apiKey)),\n})\n\nclass WalletInitModal extends Component {\n state = {\n showModal: false,\n }\n\n handleShow = () => {\n this.setState({ showModal: true })\n }\n\n handleHide = () => {\n this.props.dispatchCheckWalletStatus()\n this.setState({ showModal: false })\n }\n\n renderButton = () => {\n return (\n \n )\n }\n\n render() {\n const { apiKey } = this.props\n\n return (\n
\n {this.renderButton()}\n this.handleHide()}\n centered\n size=\"lg\"\n >\n \n \n Wallet initialization\n \n \n \n
\n \n
\n
\n \n
\n
\n \n \n Close\n \n \n \n
\n )\n }\n}\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(WalletInitModal))\n","import React, { memo } from 'react'\nimport { Navbar } from 'react-bootstrap'\nimport { Link } from 'react-router-dom'\nimport ApiKeyModal from './ApiKeyModal'\nimport WalletStatusModal from './WalletStatusModal'\nimport WalletInitModal from './WalletInitModal'\nimport logo from '../../assets/images/logotype_white.svg'\n\nconst renderWalletForms = isWalletInitialized => {\n if (isWalletInitialized === null) {\n return <>\n }\n\n if (isWalletInitialized) {\n return (\n
\n \n
\n )\n }\n\n return (\n
\n \n
\n )\n}\n\nconst HeaderView = ({ isApiKeySetted, isWalletInitialized }) => {\n return (\n \n \n \n \"logotype\"\n \n \n
\n \n
\n {isApiKeySetted && renderWalletForms(isWalletInitialized)}\n
\n )\n}\n\nexport default memo(HeaderView)\n","import HeaderContainer from './HeaderContainer'\n\nexport default HeaderContainer\n","import React, { memo, useEffect } from 'react'\nimport { connect } from 'react-redux'\nimport { apiKeySelector } from '../../store/selectors/app'\nimport { isWalletInitializedSelector } from '../../store/selectors/wallet'\nimport walletActions from '../../store/actions/walletActions'\nimport HeaderView from './HeaderView'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n isWalletInitialized: isWalletInitializedSelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchCheckWalletStatus: () => dispatch(walletActions.checkWalletStatus()),\n})\n\nconst HeaderContainer = props => {\n const { apiKey, dispatchCheckWalletStatus, isWalletInitialized } = props\n\n useEffect(() => {\n if (apiKey !== '') {\n dispatchCheckWalletStatus()\n }\n }, [apiKey, dispatchCheckWalletStatus])\n\n const isApiKeySetted = apiKey !== ''\n\n return (\n \n )\n}\n\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(HeaderContainer))\n","import React, { Component } from 'react'\nimport { withRouter } from 'react-router-dom'\nimport MenuList from '../common/MenuList'\nimport './index.scss'\nimport Header from '../Header'\n\nclass Layout extends Component {\n render() {\n return (\n
\n
\n
\n \n
\n
\n
{this.props.children}
\n
\n
\n )\n }\n}\n\nexport default withRouter(Layout)\n","import React from 'react'\nimport clsx from 'clsx'\nimport './index.scss'\n\nconst InfoCard = ({ color, children, className }) => {\n return (\n \n {children}\n \n )\n}\n\nexport default InfoCard\n","import React, { Component } from 'react'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport { faSync, faCheck } from '@fortawesome/free-solid-svg-icons'\nimport InfoCard from '../InfoCard'\nimport './index.scss'\n\nexport default class SynchCard extends Component {\n renderActiveSynchronization = () => (\n <>\n

Current node state

\n

\n Active\n synchronization\n

\n \n )\n\n renderCompleteSynchronization = () => (\n <>\n

Current node state

\n

\n Node is synced\n

\n \n )\n\n renderSynchronizationState = state =>\n ({\n active: this.renderActiveSynchronization,\n complete: this.renderCompleteSynchronization,\n }[state])\n\n getSynchronizationState = ({ fullHeight, headersHeight }) => {\n if (\n fullHeight !== null &&\n headersHeight !== null &&\n fullHeight === headersHeight\n ) {\n return 'complete'\n }\n\n return 'active'\n }\n\n shouldComponentUpdate(nextProps) {\n if (\n this.getSynchronizationState(nextProps) !==\n this.getSynchronizationState(this.props.nodeInfo)\n ) {\n return true\n }\n\n return false\n }\n\n render() {\n const currentSynchState = this.getSynchronizationState(this.props.nodeInfo)\n return (\n \n {this.renderSynchronizationState(currentSynchState)()}\n \n )\n }\n}\n","import React, { Fragment } from 'react'\nimport {\n faExclamationTriangle,\n faSync,\n} from '@fortawesome/free-solid-svg-icons'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport { format } from 'date-fns'\nimport InfoCard from './InfoCard'\nimport SynchCard from './SynchCard'\n\nconst getWalletStatus = (isWalletInitialized, isWalletUnlocked) => {\n if (!isWalletInitialized) {\n return 'Not initialized'\n }\n\n if (!isWalletUnlocked) {\n return 'Initialized'\n }\n\n return 'Unlocked'\n}\n\nconst DashboardView = ({\n error,\n nodeInfo,\n isWalletInitialized,\n isWalletUnlocked,\n apiKey,\n}) => {\n if (error !== null) {\n return (\n \n
\n

\n \n  \n {error}\n

\n
\n
\n )\n }\n\n if (nodeInfo === null) {\n return (\n \n
\n \n
\n
\n )\n }\n\n const {\n peersCount,\n bestHeaderId,\n launchTime,\n fullHeight,\n appVersion,\n isMining,\n } = nodeInfo\n\n return (\n \n
\n
\n
\n \n

Node version

\n

{appVersion}

\n
\n
\n
\n \n
\n
\n \n

Node started at

\n

\n {format(new Date(launchTime), 'MM-dd-yyyy HH:mm:ss')}\n

\n
\n
\n {fullHeight === null ? null : (\n
\n \n

Current height

\n

{fullHeight}

\n
\n
\n )}\n {bestHeaderId === null ? null : (\n
\n \n

Best block id

\n

{bestHeaderId}

\n
\n
\n )}\n
\n \n

Mining enabled

\n

{isMining ? 'true' : 'false'}

\n
\n
\n
\n \n

Peers connected

\n

{peersCount}

\n
\n
\n {apiKey !== '' && (\n
\n \n

Wallet status

\n

\n {getWalletStatus(isWalletInitialized, isWalletUnlocked)}\n

\n
\n
\n )}\n
\n
\n
\n )\n}\n\nexport default DashboardView\n","import { useEffect, useRef } from 'react'\n\nfunction usePrevious(value) {\n // The ref object is a generic container whose current property is mutable ...\n // ... and can hold any value, similar to an instance property on a class\n const ref = useRef()\n\n // Store current value in ref\n useEffect(() => {\n ref.current = value\n }, [value]) // Only re-run if value changes\n\n // Return previous value (happens before update in useEffect above)\n return ref.current\n}\n\nexport default usePrevious\n","import DashboardContainer from './DashboardContainer'\n\nexport default DashboardContainer\n","import React, { useState, useEffect, useCallback, memo } from 'react'\nimport { connect } from 'react-redux'\nimport nodeApi from '../../../api/api'\nimport DashboardView from './DashboardView'\nimport {\n isWalletInitializedSelector,\n isWalletUnlockedSelector,\n} from '../../../store/selectors/wallet'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport usePrevious from '../../../hooks/usePrevious'\nimport walletActions from '../../../store/actions/walletActions'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n isWalletInitialized: isWalletInitializedSelector(state),\n isWalletUnlocked: isWalletUnlockedSelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchCheckWalletStatus: () => dispatch(walletActions.checkWalletStatus()),\n})\n\nconst DashboardContainer = props => {\n const {\n isWalletInitialized,\n isWalletUnlocked,\n apiKey,\n dispatchCheckWalletStatus,\n } = props\n\n const [nodeInfo, setNodeInfo] = useState(null)\n const [error, setError] = useState(null)\n const [timerId, setTimerId] = useState(null)\n\n const getNodeCurrentState = () => nodeApi.get('/info')\n\n const setNodeCurrentState = useCallback(async () => {\n try {\n const { data } = await getNodeCurrentState()\n\n setNodeInfo(data)\n setError(null)\n } catch {\n setError('Node connection is lost.')\n }\n }, [])\n\n const setTimer = useCallback(() => {\n const newTimerId = setInterval(setNodeCurrentState, 2000)\n\n setTimerId(newTimerId)\n }, [setNodeCurrentState])\n\n const prevError = usePrevious(error)\n useEffect(() => {\n if (prevError && prevError !== error) {\n dispatchCheckWalletStatus()\n }\n }, [dispatchCheckWalletStatus, error, prevError])\n\n useEffect(() => {\n setNodeCurrentState()\n setTimer()\n // eslint-disable-next-line\n }, [])\n\n useEffect(\n () => () => {\n clearInterval(timerId)\n },\n [timerId],\n )\n\n return (\n \n )\n}\n\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(DashboardContainer))\n","import React, { PureComponent } from 'react'\nimport { Formik, Field, Form } from 'formik'\nimport nodeApi from '../../../../../api/api'\nimport customToast from '../../../../../utils/toast'\nimport CopyToClipboard from '../../../../common/CopyToClipboard'\nimport constants from '../../../../../utils/constants'\n\nconst initialFormValues = {\n recipientAddress: '',\n amount: '',\n}\n\nclass PaymentSendForm extends PureComponent {\n state = {\n isShowTransactionId: false,\n }\n\n paymentSend = ({ recipientAddress, amount }) =>\n nodeApi.post(\n '/wallet/payment/send',\n [\n {\n address: recipientAddress,\n value: Number(\n (parseFloat(amount) * constants.nanoErgInErg).toFixed(1),\n ),\n },\n ],\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.paymentSend(values)\n .then(({ data }) => {\n resetForm(initialFormValues)\n setStatus({\n state: 'success',\n msg: (\n <>\n

\n Your payment successfully sent. Your transaction ID -{' '}\n {data}\n

\n

\n \n Click Here To Go To The Explorer\n \n

\n \n ),\n })\n this.setState({ isShowTransactionId: true })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n
\n

Payment send

\n \n {({ status, isSubmitting }) => (\n
\n {status &&\n status.state === 'success' &&\n this.state.isShowTransactionId && (\n
\n \n this.setState({ isShowTransactionId: false })\n }\n >\n ×\n \n {status.msg}\n
\n )}\n
\n \n \n
\n
\n \n \n
\n \n Send\n \n \n )}\n \n
\n
\n )\n }\n}\n\nexport default PaymentSendForm\n","import React, { PureComponent } from 'react'\nimport { Formik, Form } from 'formik'\nimport NumberFormat from 'react-number-format'\nimport nodeApi from '../../../../../api/api'\nimport customToast from '../../../../../utils/toast'\n\nconst initialFormValues = {\n walletPassword: '',\n}\n\nclass GetBalanceForm extends PureComponent {\n state = {\n isShowBalance: false,\n }\n\n getBalance = () =>\n nodeApi.get('/wallet/balances', {\n headers: {\n api_key: this.props.apiKey,\n },\n })\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.getBalance(values)\n .then(({ data: { balance } }) => {\n resetForm(initialFormValues)\n setStatus({\n state: 'success',\n msg: (\n <>\n Your wallet balance -{' '}\n \n \n ),\n })\n this.setState({ isShowBalance: true })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n
\n

Get confirmed wallet balance

\n \n {({ status, isSubmitting }) => (\n
\n {status &&\n status.state === 'success' &&\n this.state.isShowBalance && (\n
\n this.setState({ isShowBalance: false })}\n >\n ×\n \n {status.msg}\n
\n )}\n \n Get\n \n \n )}\n \n
\n
\n )\n }\n}\n\nexport default GetBalanceForm\n","import React, { PureComponent } from 'react'\nimport nodeApi from '../../../../../api/api'\nimport customToast from '../../../../../utils/toast'\nimport CopyToClipboard from '../../../../common/CopyToClipboard'\n\nclass GetWalletAddressesForm extends PureComponent {\n state = {\n isShowWalletAddresses: false,\n walletAddresses: [],\n }\n\n getWalletAddresses = () =>\n nodeApi.get('/wallet/addresses', {\n headers: {\n api_key: this.props.apiKey,\n },\n })\n\n handleSubmit = event => {\n event.preventDefault()\n\n this.getWalletAddresses()\n .then(({ data: walletAddresses }) => {\n this.setState({ isShowWalletAddresses: true, walletAddresses })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n })\n }\n\n render() {\n return (\n
\n
\n

Get all wallet addresses

\n
\n {this.state.isShowWalletAddresses && (\n
\n \n this.setState({ isShowWalletAddresses: false })\n }\n >\n ×\n \n

Wallet Addresses:

\n
    \n {this.state.walletAddresses.map(addr => (\n
  • \n {addr}\n
  • \n ))}\n
\n
\n )}\n \n
\n
\n
\n )\n }\n}\n\nexport default GetWalletAddressesForm\n","import React, { Component, memo } from 'react'\nimport { connect } from 'react-redux'\nimport PaymentSendForm from './components/PaymentSendForm'\nimport GetBalanceForm from './components/GetBalanceForm'\nimport GetWalletAddressesForm from './components/GetWalletAddressesForm'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport {\n isWalletInitializedSelector,\n isWalletUnlockedSelector,\n} from '../../../store/selectors/wallet'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n isWalletInitialized: isWalletInitializedSelector(state),\n isWalletUnlocked: isWalletUnlockedSelector(state),\n})\n\nclass Wallet extends Component {\n renderState = state =>\n ({\n unlocked: apiKey => this.renderWalletUnlockedState(apiKey),\n locked: () => this.renderWalletLockedState(),\n initialized: apiKey => this.renderInitializedState(apiKey),\n }[state])\n\n renderWalletUnlockedState = apiKey => (\n
\n
\n \n \n \n
\n
\n )\n\n renderWalletLockedState = () => (\n
\n

\n The wallet UI is locked. You need to unlock the wallet to access its UI.\n

\n
\n )\n\n renderInitializedState = () => (\n
\n

You need to initialize your wallet to access wallet UI.

\n
\n )\n\n render() {\n const { apiKey, isWalletUnlocked, isWalletInitialized } = this.props\n\n if (apiKey === '') {\n return (\n
\n

For continue need to set API key.

\n
\n )\n }\n\n if (!isWalletInitialized) {\n return this.renderState('initialized')(apiKey)\n }\n\n if (isWalletUnlocked) {\n return this.renderState('unlocked')(apiKey)\n }\n\n return this.renderState('locked')()\n }\n}\n\nexport default connect(mapStateToProps)(memo(Wallet))\n","import React from 'react'\nimport { BrowserRouter, Switch, Route } from 'react-router-dom'\nimport Layout from '../components/layout'\nimport Dashboard from '../components/pages/Dashboard'\nimport Wallet from '../components/pages/Wallet'\n\nconst Router = () => (\n \n \n \n \n \n \n \n \n)\n\nexport default Router\n","import { combineReducers } from 'redux'\nimport appSlice from '../slices/appSlice'\nimport walletSlice from '../slices/walletSlice'\n\nexport default combineReducers({\n app: appSlice.reducer,\n wallet: walletSlice.reducer,\n})\n","import walletActions from '../actions/walletActions'\nimport nodeApi from '../../api/api'\nimport { apiKeySelector } from '../selectors/app'\n\nexport default store => next => action => {\n const { dispatch, getState } = store\n const apiKey = apiKeySelector(getState())\n\n switch (action.type) {\n case walletActions.checkWalletStatus.type:\n nodeApi\n .get('/wallet/status', {\n headers: {\n api_key: apiKey,\n },\n })\n .then(({ data: { isUnlocked, isInitialized } }) => {\n dispatch(walletActions.setIsWalletUnlocked(isUnlocked))\n dispatch(walletActions.setIsWalletInitialized(isInitialized))\n })\n .catch(() => {})\n\n break\n\n default:\n break\n }\n next(action)\n}\n","import React from 'react'\nimport { toast } from 'react-toastify'\nimport { Provider } from 'react-redux'\nimport Router from './router/router'\nimport createStore from './store'\n\nimport 'bootstrap/dist/css/bootstrap.min.css'\nimport './assets/styles/index.scss'\nimport 'react-toastify/dist/ReactToastify.min.css'\n\ntoast.configure()\nconst store = createStore()\n\nconst App = () => {\n return (\n \n \n \n )\n}\n\nexport default App\n","import { configureStore, getDefaultMiddleware } from 'redux-starter-kit'\nimport rootReducer from './reducers/rootReducer'\nimport walletMiddleware from './middlewares/walletMiddleware'\n\nexport default () => {\n const store = configureStore({\n reducer: rootReducer,\n middleware: [...getDefaultMiddleware(), walletMiddleware],\n })\n\n return store\n}\n","import React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from './App'\n\nReactDOM.render(, document.getElementById('root'))\n"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["assets/images/logotype_white.svg","utils/constants.js","components/common/MenuList/index.js","store/selectors/app.js","store/selectors/wallet.js","store/slices/walletSlice.js","store/actions/walletActions.js","store/slices/appSlice.js","store/actions/appActions.js","utils/environment.js","api/api.js","utils/toast/index.js","components/Header/ApiKeyModal/ApiKeyModalView.js","components/Header/ApiKeyModal/index.js","components/Header/ApiKeyModal/ApiKeyModalContainer.js","components/Header/WalletStatusModal/index.js","components/common/CopyToClipboard/index.js","components/elements/WalletInitializeForm/index.js","components/elements/RestoreWalletForm/index.js","components/Header/WalletInitModal/index.js","components/Header/HeaderView.js","components/Header/index.js","components/Header/HeaderContainer.js","components/layout/index.js","components/pages/Dashboard/InfoCard/index.js","components/pages/Dashboard/SynchCard/index.js","components/pages/Dashboard/DashboardView.js","hooks/usePrevious.js","components/pages/Dashboard/index.js","components/pages/Dashboard/DashboardContainer.js","components/pages/Wallet/components/PaymentSendForm/index.js","components/pages/Wallet/components/GetBalanceForm/index.js","components/pages/Wallet/components/GetWalletAddressesForm/index.js","components/pages/Wallet/index.js","router/router.js","store/reducers/rootReducer.js","store/middlewares/walletMiddleware.js","App.js","store/index.js","index.js"],"names":["module","exports","swaggerInterface","website","explorer","nanoErgInErg","localRouteList","dashboard","href","icon","faChartLine","title","wallet","faExchangeAlt","externalRouteList","constants","faBook","faWpexplorer","faGlobe","withRouter","pathname","location","className","Object","values","map","index","key","clsx","active","to","rel","target","apiKeySelector","createSelector","state","app","apiKey","walletSelector","isWalletUnlockedSelector","isWalletUnlocked","isWalletInitializedSelector","isWalletInitialized","createSlice","name","initialState","reducers","setIsWalletUnlocked","payload","setIsWalletInitialized","checkWalletStatus","createAction","walletSlice","actions","setApiKey","action","appSlice","nodeApiLink","NetworkError","status","message","data","statusText","this","prototype","create","Error","nodeApi","axios","baseURL","environment","timeout","crossDomain","headers","interceptors","response","use","Promise","resolve","error","reject","toastStates","success","text","options","toast","position","autoClose","hideProgressBar","closeOnClick","pauseOnHover","draggable","bodyClassName","progressClassName","info","ApiKeyModalView","showModal","handleHide","submitForm","handleShow","onClick","renderButton","Modal","show","onHide","centered","initialValues","onSubmit","Header","closeButton","Title","Body","type","placeholder","Footer","ApiKeyModalContainer","connect","dispatch","dispatchSetApiKey","appActions","memo","props","useState","setShowModal","get","api_key","then","trim","customToast","catch","WalletStatusForm","setState","walletUnlock","pass","post","walletLock","submitWalletUnlockForm","setSubmitting","resetForm","setStatus","dispatchSetIsWalletUnlocked","err","errMessage","detail","submitWalletLockForm","confirm","aria-labelledby","isSubmitting","id","htmlFor","disabled","Component","isWalletUnlock","walletActions","CopyToClipboard","startTimer","timerId","setTimeout","showTooltip","onCopy","e","preventDefault","copy","children","handleOnTooltipClose","myRef","React","createRef","ref","faCopy","Overlay","current","placement","Tooltip","clearTimeout","PureComponent","initialFormValues","walletPassword","mnemonicPass","WalletInitializeForm","isShowMnemonic","walletInit","a","handleSubmit","result","msg","mnemonic","role","aria-hidden","walletRestore","String","required","WalletInitModal","dispatchCheckWalletStatus","size","RestoreWalletForm","isApiKeySetted","Navbar","expand","Brand","src","logo","alt","ApiKeyModal","WalletStatusModal","renderWalletForms","HeaderContainer","useEffect","Layout","InfoCard","color","SynchCard","renderActiveSynchronization","faSync","spin","renderCompleteSynchronization","faCheck","renderSynchronizationState","complete","getSynchronizationState","fullHeight","headersHeight","nextProps","nodeInfo","currentSynchState","DashboardView","faExclamationTriangle","peersCount","bestHeaderId","launchTime","appVersion","isMining","format","Date","getWalletStatus","usePrevious","value","useRef","DashboardContainer","setNodeInfo","setError","setTimerId","setNodeCurrentState","useCallback","setTimer","newTimerId","setInterval","prevError","clearInterval","recipientAddress","amount","PaymentSendForm","isShowTransactionId","paymentSend","address","Number","parseFloat","toFixed","GetBalanceForm","isShowBalance","getBalance","balance","displayType","thousandSeparator","suffix","GetWalletAddressesForm","isShowWalletAddresses","walletAddresses","getWalletAddresses","event","addr","Wallet","renderState","unlocked","renderWalletUnlockedState","locked","renderWalletLockedState","initialized","renderInitializedState","Router","basename","exact","path","component","Dashboard","combineReducers","reducer","store","next","getState","isUnlocked","isInitialized","configure","configureStore","rootReducer","middleware","getDefaultMiddleware","walletMiddleware","App","ReactDOM","render","document","getElementById"],"mappings":"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","file":"static/js/main.d2ee9699.chunk.js","sourcesContent":["module.exports = __webpack_public_path__ + \"static/media/logotype_white.4dcfd639.svg\";","export default {\n swaggerInterface: '/swagger',\n website: 'https://ergoplatform.org',\n explorer: 'https://explorer.ergoplatform.com',\n nanoErgInErg: 1000000000,\n}\n","import React from 'react'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport {\n faChartLine,\n faExchangeAlt,\n faGlobe,\n faBook,\n} from '@fortawesome/free-solid-svg-icons'\nimport clsx from 'clsx'\nimport { faWpexplorer } from '@fortawesome/free-brands-svg-icons'\nimport { withRouter, Link } from 'react-router-dom'\nimport constants from '../../../utils/constants'\n\nconst localRouteList = {\n dashboard: {\n href: '/',\n icon: ,\n title: 'Dashboard',\n },\n wallet: {\n href: '/wallet',\n icon: ,\n title: 'Wallet',\n },\n}\n\nconst externalRouteList = {\n swaggerInterface: {\n href: constants.swaggerInterface,\n icon: ,\n title: 'Swagger',\n },\n explorer: {\n href: constants.explorer,\n icon: ,\n title: 'Explorer',\n },\n website: {\n href: constants.website,\n icon: ,\n title: 'Website',\n },\n}\n\nconst MenuList = ({ location: { pathname } }) => {\n return (\n
\n

Menu

\n
\n
\n {Object.values(localRouteList).map(({ href, icon, title }, index) => (\n \n {icon} {title}\n \n ))}\n
\n

External links

\n
\n
\n {Object.values(externalRouteList).map(\n ({ href, icon, title }, index) => (\n \n {icon} {title}\n \n ),\n )}\n
\n
\n )\n}\n\nexport default withRouter(MenuList)\n","import { createSelector } from 'redux-starter-kit'\n\nexport const appSelector = state => state.app\n\nexport const apiKeySelector = createSelector(appSelector, app => app.apiKey)\n","import { createSelector } from 'redux-starter-kit'\n\nexport const walletSelector = state => state.wallet\n\nexport const isWalletUnlockedSelector = createSelector(\n walletSelector,\n wallet => wallet.isWalletUnlocked,\n)\n\nexport const isWalletInitializedSelector = createSelector(\n walletSelector,\n wallet => wallet.isWalletInitialized,\n)\n","import { createSlice } from 'redux-starter-kit'\n\nconst initialState = {\n isWalletUnlocked: null,\n isWalletInitialized: null,\n}\n\nexport default createSlice({\n name: 'walletSlice',\n initialState,\n reducers: {\n setIsWalletUnlocked: (state, { payload }) => {\n state.isWalletUnlocked = payload\n },\n setIsWalletInitialized: (state, { payload }) => {\n state.isWalletInitialized = payload\n },\n },\n})\n","import { createAction } from 'redux-starter-kit'\nimport walletSlice from '../slices/walletSlice'\n\nconst checkWalletStatus = createAction('checkWalletStatus')\n\nexport default {\n ...walletSlice.actions,\n checkWalletStatus,\n}\n","import { createSlice } from 'redux-starter-kit'\n\nconst initialState = {\n apiKey: '',\n}\n\nexport default createSlice({\n name: 'appSlice',\n initialState,\n reducers: {\n setApiKey: (state, action) => {\n state.apiKey = action.payload\n },\n },\n})\n","import appSlice from '../slices/appSlice'\n\nexport default {\n ...appSlice.actions,\n}\n","const appConfig = () => {\n return {\n nodeApiLink: '/',\n }\n}\n\nexport default {\n ...appConfig(),\n}\n","import axios from 'axios'\nimport environment from '../utils/environment'\n\nfunction NetworkError({ status, message, data, statusText }) {\n this.name = 'NetworkError'\n this.message = message || statusText\n this.status = status\n this.data = data\n}\n\nNetworkError.prototype = Object.create(Error.prototype)\n\nconst nodeApi = axios.create({\n baseURL: environment.nodeApiLink,\n timeout: 1000 * 10,\n crossDomain: true,\n headers: {\n 'Content-Type': 'application/json',\n },\n})\n\nnodeApi.interceptors.response.use(\n response => Promise.resolve(response),\n error => Promise.reject(new NetworkError(error.response || error)),\n)\n\nexport default nodeApi\n","import { toast } from 'react-toastify'\nimport './index.scss'\n\nconst toastStates = {\n success: (text, options) =>\n toast.success(text, {\n position: 'top-right',\n autoClose: 5000,\n hideProgressBar: false,\n closeOnClick: true,\n pauseOnHover: true,\n draggable: true,\n className: 'n-toast n-toast--success',\n bodyClassName: 'n-toast__body',\n progressClassName: 'n-toast__progress--success',\n ...options,\n }),\n error: (text, options) =>\n toast.error(text, {\n position: 'top-right',\n autoClose: 5000,\n hideProgressBar: false,\n closeOnClick: true,\n pauseOnHover: true,\n draggable: true,\n className: 'n-toast n-toast--error',\n bodyClassName: 'n-toast__body',\n progressClassName: 'n-toast__progress--error',\n ...options,\n }),\n info: toast.info,\n}\n\nexport default (state, text, options) =>\n toastStates[state]\n ? toastStates[state](text, options)\n : new Error(`Bad toast state`)\n","import React from 'react'\nimport { Modal } from 'react-bootstrap'\nimport { Formik, Form, Field } from 'formik'\n\nconst renderButton = (apiKey, handleShow) => {\n if (apiKey === '') {\n return (\n \n )\n }\n\n return (\n \n )\n}\n\nconst ApiKeyModalView = ({\n showModal,\n handleHide,\n submitForm,\n apiKey,\n handleShow,\n}) => {\n return (\n
\n {renderButton(apiKey, handleShow)}\n handleHide()} centered>\n \n {() => (\n
\n \n Authorization\n \n \n

Set API key to access Node requests

\n
\n \n
\n
\n\n \n \n Close\n \n \n \n
\n )}\n
\n
\n
\n )\n}\n\nexport default ApiKeyModalView\n","import ApiKeyModalContainer from './ApiKeyModalContainer'\n\nexport default ApiKeyModalContainer\n","import React, { memo, useState } from 'react'\nimport { connect } from 'react-redux'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport appActions from '../../../store/actions/appActions'\nimport nodeApi from '../../../api/api'\nimport customToast from '../../../utils/toast'\nimport ApiKeyModalView from './ApiKeyModalView'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchSetApiKey: apiKey => dispatch(appActions.setApiKey(apiKey)),\n})\n\nconst ApiKeyModalContainer = props => {\n const { dispatchSetApiKey, apiKey } = props\n\n const [showModal, setShowModal] = useState(false)\n\n const handleShow = () => {\n setShowModal(true)\n }\n\n const handleHide = () => {\n setShowModal(false)\n }\n\n const submitForm = values => {\n // Check API key for random get method\n nodeApi\n .get('/wallet/status', {\n headers: {\n api_key: values.apiKey,\n },\n })\n .then(() => {\n dispatchSetApiKey(values.apiKey.trim())\n customToast('success', 'API key is set successfully')\n handleHide()\n })\n .catch(() => {\n customToast('error', 'Bad API key')\n })\n }\n\n return (\n \n )\n}\n\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(ApiKeyModalContainer))\n","import React, { Component, memo } from 'react'\nimport Modal from 'react-bootstrap/Modal'\nimport { Formik, Field, Form } from 'formik'\nimport { connect } from 'react-redux'\nimport { isWalletUnlockedSelector } from '../../../store/selectors/wallet'\nimport walletActions from '../../../store/actions/walletActions'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport customToast from '../../../utils/toast'\nimport nodeApi from '../../../api/api'\n\nconst mapStateToProps = state => ({\n isWalletUnlocked: isWalletUnlockedSelector(state),\n apiKey: apiKeySelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchSetIsWalletUnlocked: isWalletUnlock =>\n dispatch(walletActions.setIsWalletUnlocked(isWalletUnlock)),\n})\n\nclass WalletStatusForm extends Component {\n state = {\n showModal: false,\n }\n\n handleShow = () => {\n this.setState({ showModal: true })\n }\n\n handleHide = () => {\n this.setState({ showModal: false })\n }\n\n walletUnlock = pass =>\n nodeApi.post(\n '/wallet/unlock',\n { pass },\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n\n walletLock = () =>\n nodeApi.get('/wallet/lock', {\n headers: {\n api_key: this.props.apiKey,\n },\n })\n\n submitWalletUnlockForm = (\n { pass },\n { setSubmitting, resetForm, setStatus },\n ) => {\n setStatus({ status: 'submitting' })\n this.walletUnlock(pass)\n .then(() => {\n resetForm({ pass: '' })\n customToast('success', 'Your wallet is unlocked successfully')\n this.props.dispatchSetIsWalletUnlocked(true)\n this.handleHide()\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n submitWalletLockForm = () => {\n // eslint-disable-next-line\n if (confirm('Are you sure want to lock wallet?')) {\n this.walletLock()\n .then(() => {\n customToast('success', 'Your wallet is locked successfully')\n this.props.dispatchSetIsWalletUnlocked(false)\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n })\n }\n }\n\n renderButton = () => {\n if (!this.props.isWalletUnlocked) {\n return (\n \n )\n }\n\n return (\n \n Lock wallet\n \n )\n }\n\n render() {\n return (\n
\n {this.renderButton()}\n this.handleHide()}\n centered\n aria-labelledby=\"example-custom-modal-styling-title\"\n >\n \n {({ isSubmitting }) => (\n
\n \n \n Unlock wallet form\n \n \n \n
\n \n \n \n * If you have it or leave field empty\n \n
\n
\n\n \n \n Close\n \n \n Save changes\n \n \n
\n )}\n \n \n
\n )\n }\n}\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(WalletStatusForm))\n","import React from 'react'\nimport copy from 'clipboard-copy'\nimport { Overlay, Tooltip } from 'react-bootstrap'\nimport { faCopy } from '@fortawesome/free-solid-svg-icons'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\n\nclass CopyToClipboard extends React.PureComponent {\n constructor(props) {\n super(props)\n\n this.myRef = React.createRef()\n this.state = { showTooltip: false }\n }\n\n render() {\n return (\n <>\n \n {this.props.children}\n  \n \n \n \n Copied!\n \n \n )\n }\n\n startTimer = () => {\n const timerId = setTimeout(\n () => this.setState({ showTooltip: false }),\n 1500,\n )\n this.setState({ timerId })\n }\n\n onCopy = e => {\n e.preventDefault()\n copy(this.props.children)\n this.setState({ showTooltip: true })\n this.startTimer()\n }\n\n handleOnTooltipClose = () => {\n this.setState({ showTooltip: false })\n }\n\n componentWillUnmount() {\n clearTimeout(this.state.timerId)\n }\n}\n\nexport default CopyToClipboard\n","import React, { Component, memo } from 'react'\nimport { Formik, Field, Form } from 'formik'\nimport nodeApi from '../../../api/api'\nimport CopyToClipboard from '../../common/CopyToClipboard'\nimport customToast from '../../../utils/toast'\n\nconst initialFormValues = {\n walletPassword: '',\n mnemonicPass: '',\n}\n\nclass WalletInitializeForm extends Component {\n state = { isShowMnemonic: false }\n\n walletInit = async ({ walletPassword, mnemonicPass }) => {\n const { data } = await nodeApi.post(\n '/wallet/init',\n { pass: walletPassword, mnemonicPass },\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n\n return data\n }\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.walletInit(values)\n .then(result => {\n resetForm(initialFormValues)\n setStatus({\n state: 'success',\n msg: (\n <>\n Your wallet successfully initialized. Please, save your mnemonic -{' '}\n {result.mnemonic}\n \n ),\n })\n this.setState({ isShowMnemonic: true })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n

Initialize wallet

\n \n {({ status, isSubmitting }) => (\n
\n {status && status.state === 'error' && (\n
\n {status.msg}\n
\n )}\n {status &&\n status.state === 'success' &&\n this.state.isShowMnemonic && (\n
\n this.setState({ isShowMnemonic: false })}\n >\n ×\n \n {status.msg}\n
\n )}\n
\n \n \n
\n
\n \n \n
\n \n Send\n \n \n )}\n
\n
\n )\n }\n}\n\nexport default memo(WalletInitializeForm)\n","import React, { Component, memo } from 'react'\nimport { Formik, Field, Form } from 'formik'\nimport nodeApi from '../../../api/api'\nimport customToast from '../../../utils/toast'\n\nconst initialFormValues = {\n walletPassword: '',\n mnemonicPass: '',\n mnemonic: '',\n}\n\nclass WalletInitializeForm extends Component {\n walletRestore = async ({\n walletPassword,\n mnemonicPass = '',\n mnemonic = '',\n }) => {\n if (!mnemonic || !String(mnemonic).trim()) {\n throw Error('Need to set mnemonic')\n }\n\n return nodeApi.post(\n '/wallet/restore',\n { pass: walletPassword, mnemonicPass, mnemonic },\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n }\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.walletRestore(values)\n .then(() => {\n resetForm(initialFormValues)\n customToast('success', 'Your wallet successfully re-stored')\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n

Re-store wallet

\n \n {({ status, isSubmitting }) => (\n
\n {status && status.state === 'error' && (\n
\n {status.msg}\n
\n )}\n {status && status.state === 'success' && (\n
{status.msg}
\n )}\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n \n Send\n \n \n )}\n
\n
\n )\n }\n}\n\nexport default memo(WalletInitializeForm)\n","import React, { Component, memo } from 'react'\nimport Modal from 'react-bootstrap/Modal'\nimport { connect } from 'react-redux'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport appActions from '../../../store/actions/appActions'\nimport WalletInitializeForm from '../../elements/WalletInitializeForm'\nimport RestoreWalletForm from '../../elements/RestoreWalletForm'\nimport walletActions from '../../../store/actions/walletActions'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchCheckWalletStatus: () => dispatch(walletActions.checkWalletStatus()),\n dispatchSetApiKey: apiKey => dispatch(appActions.setApiKey(apiKey)),\n})\n\nclass WalletInitModal extends Component {\n state = {\n showModal: false,\n }\n\n handleShow = () => {\n this.setState({ showModal: true })\n }\n\n handleHide = () => {\n this.props.dispatchCheckWalletStatus()\n this.setState({ showModal: false })\n }\n\n renderButton = () => {\n return (\n \n )\n }\n\n render() {\n const { apiKey } = this.props\n\n return (\n
\n {this.renderButton()}\n this.handleHide()}\n centered\n size=\"lg\"\n >\n \n \n Wallet initialization\n \n \n \n
\n \n
\n
\n \n
\n
\n \n \n Close\n \n \n \n
\n )\n }\n}\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(WalletInitModal))\n","import React, { memo } from 'react'\nimport { Navbar } from 'react-bootstrap'\nimport { Link } from 'react-router-dom'\nimport ApiKeyModal from './ApiKeyModal'\nimport WalletStatusModal from './WalletStatusModal'\nimport WalletInitModal from './WalletInitModal'\nimport logo from '../../assets/images/logotype_white.svg'\n\nconst renderWalletForms = isWalletInitialized => {\n if (isWalletInitialized === null) {\n return <>\n }\n\n if (isWalletInitialized) {\n return (\n
\n \n
\n )\n }\n\n return (\n
\n \n
\n )\n}\n\nconst HeaderView = ({ isApiKeySetted, isWalletInitialized }) => {\n return (\n \n \n \n \"logotype\"\n \n \n
\n \n
\n {isApiKeySetted && renderWalletForms(isWalletInitialized)}\n
\n )\n}\n\nexport default memo(HeaderView)\n","import HeaderContainer from './HeaderContainer'\n\nexport default HeaderContainer\n","import React, { memo, useEffect } from 'react'\nimport { connect } from 'react-redux'\nimport { apiKeySelector } from '../../store/selectors/app'\nimport { isWalletInitializedSelector } from '../../store/selectors/wallet'\nimport walletActions from '../../store/actions/walletActions'\nimport HeaderView from './HeaderView'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n isWalletInitialized: isWalletInitializedSelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchCheckWalletStatus: () => dispatch(walletActions.checkWalletStatus()),\n})\n\nconst HeaderContainer = props => {\n const { apiKey, dispatchCheckWalletStatus, isWalletInitialized } = props\n\n useEffect(() => {\n if (apiKey !== '') {\n dispatchCheckWalletStatus()\n }\n }, [apiKey, dispatchCheckWalletStatus])\n\n const isApiKeySetted = apiKey !== ''\n\n return (\n \n )\n}\n\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(HeaderContainer))\n","import React, { Component } from 'react'\nimport { withRouter } from 'react-router-dom'\nimport MenuList from '../common/MenuList'\nimport './index.scss'\nimport Header from '../Header'\n\nclass Layout extends Component {\n render() {\n return (\n
\n
\n
\n \n
\n
\n
{this.props.children}
\n
\n
\n )\n }\n}\n\nexport default withRouter(Layout)\n","import React from 'react'\nimport clsx from 'clsx'\nimport './index.scss'\n\nconst InfoCard = ({ color, children, className }) => {\n return (\n \n {children}\n \n )\n}\n\nexport default InfoCard\n","import React, { Component } from 'react'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport { faSync, faCheck } from '@fortawesome/free-solid-svg-icons'\nimport InfoCard from '../InfoCard'\nimport './index.scss'\n\nexport default class SynchCard extends Component {\n renderActiveSynchronization = () => (\n <>\n

Current node state

\n

\n Active\n synchronization\n

\n \n )\n\n renderCompleteSynchronization = () => (\n <>\n

Current node state

\n

\n Node is synced\n

\n \n )\n\n renderSynchronizationState = state =>\n ({\n active: this.renderActiveSynchronization,\n complete: this.renderCompleteSynchronization,\n }[state])\n\n getSynchronizationState = ({ fullHeight, headersHeight }) => {\n if (\n fullHeight !== null &&\n headersHeight !== null &&\n fullHeight === headersHeight\n ) {\n return 'complete'\n }\n\n return 'active'\n }\n\n shouldComponentUpdate(nextProps) {\n if (\n this.getSynchronizationState(nextProps) !==\n this.getSynchronizationState(this.props.nodeInfo)\n ) {\n return true\n }\n\n return false\n }\n\n render() {\n const currentSynchState = this.getSynchronizationState(this.props.nodeInfo)\n return (\n \n {this.renderSynchronizationState(currentSynchState)()}\n \n )\n }\n}\n","import React, { Fragment } from 'react'\nimport {\n faExclamationTriangle,\n faSync,\n} from '@fortawesome/free-solid-svg-icons'\nimport { FontAwesomeIcon } from '@fortawesome/react-fontawesome'\nimport { format } from 'date-fns'\nimport InfoCard from './InfoCard'\nimport SynchCard from './SynchCard'\n\nconst getWalletStatus = (isWalletInitialized, isWalletUnlocked) => {\n if (!isWalletInitialized) {\n return 'Not initialized'\n }\n\n if (!isWalletUnlocked) {\n return 'Initialized'\n }\n\n return 'Unlocked'\n}\n\nconst DashboardView = ({\n error,\n nodeInfo,\n isWalletInitialized,\n isWalletUnlocked,\n apiKey,\n}) => {\n if (error !== null) {\n return (\n \n
\n

\n \n  \n {error}\n

\n
\n
\n )\n }\n\n if (nodeInfo === null) {\n return (\n \n
\n \n
\n
\n )\n }\n\n const {\n peersCount,\n bestHeaderId,\n launchTime,\n fullHeight,\n appVersion,\n isMining,\n } = nodeInfo\n\n return (\n \n
\n
\n
\n \n

Node version

\n

{appVersion}

\n
\n
\n
\n \n
\n
\n \n

Node started at

\n

\n {format(new Date(launchTime), 'MM-dd-yyyy HH:mm:ss')}\n

\n
\n
\n {fullHeight === null ? null : (\n
\n \n

Current height

\n

{fullHeight}

\n
\n
\n )}\n {bestHeaderId === null ? null : (\n
\n \n

Best block id

\n

{bestHeaderId}

\n
\n
\n )}\n
\n \n

Mining enabled

\n

{isMining ? 'true' : 'false'}

\n
\n
\n
\n \n

Peers connected

\n

{peersCount}

\n
\n
\n {apiKey !== '' && (\n
\n \n

Wallet status

\n

\n {getWalletStatus(isWalletInitialized, isWalletUnlocked)}\n

\n
\n
\n )}\n
\n
\n
\n )\n}\n\nexport default DashboardView\n","import { useEffect, useRef } from 'react'\n\nfunction usePrevious(value) {\n // The ref object is a generic container whose current property is mutable ...\n // ... and can hold any value, similar to an instance property on a class\n const ref = useRef()\n\n // Store current value in ref\n useEffect(() => {\n ref.current = value\n }, [value]) // Only re-run if value changes\n\n // Return previous value (happens before update in useEffect above)\n return ref.current\n}\n\nexport default usePrevious\n","import DashboardContainer from './DashboardContainer'\n\nexport default DashboardContainer\n","import React, { useState, useEffect, useCallback, memo } from 'react'\nimport { connect } from 'react-redux'\nimport nodeApi from '../../../api/api'\nimport DashboardView from './DashboardView'\nimport {\n isWalletInitializedSelector,\n isWalletUnlockedSelector,\n} from '../../../store/selectors/wallet'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport usePrevious from '../../../hooks/usePrevious'\nimport walletActions from '../../../store/actions/walletActions'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n isWalletInitialized: isWalletInitializedSelector(state),\n isWalletUnlocked: isWalletUnlockedSelector(state),\n})\n\nconst mapDispatchToProps = dispatch => ({\n dispatchCheckWalletStatus: () => dispatch(walletActions.checkWalletStatus()),\n})\n\nconst DashboardContainer = props => {\n const {\n isWalletInitialized,\n isWalletUnlocked,\n apiKey,\n dispatchCheckWalletStatus,\n } = props\n\n const [nodeInfo, setNodeInfo] = useState(null)\n const [error, setError] = useState(null)\n const [timerId, setTimerId] = useState(null)\n\n const getNodeCurrentState = () => nodeApi.get('/info')\n\n const setNodeCurrentState = useCallback(async () => {\n try {\n const { data } = await getNodeCurrentState()\n\n setNodeInfo(data)\n setError(null)\n } catch {\n setError('Node connection is lost.')\n }\n }, [])\n\n const setTimer = useCallback(() => {\n const newTimerId = setInterval(setNodeCurrentState, 2000)\n\n setTimerId(newTimerId)\n }, [setNodeCurrentState])\n\n const prevError = usePrevious(error)\n useEffect(() => {\n if (prevError && prevError !== error) {\n dispatchCheckWalletStatus()\n }\n }, [dispatchCheckWalletStatus, error, prevError])\n\n useEffect(() => {\n setNodeCurrentState()\n setTimer()\n // eslint-disable-next-line\n }, [])\n\n useEffect(\n () => () => {\n clearInterval(timerId)\n },\n [timerId],\n )\n\n return (\n \n )\n}\n\nexport default connect(\n mapStateToProps,\n mapDispatchToProps,\n)(memo(DashboardContainer))\n","import React, { PureComponent } from 'react'\nimport { Formik, Field, Form } from 'formik'\nimport nodeApi from '../../../../../api/api'\nimport customToast from '../../../../../utils/toast'\nimport CopyToClipboard from '../../../../common/CopyToClipboard'\nimport constants from '../../../../../utils/constants'\n\nconst initialFormValues = {\n recipientAddress: '',\n amount: '',\n}\n\nclass PaymentSendForm extends PureComponent {\n state = {\n isShowTransactionId: false,\n }\n\n paymentSend = ({ recipientAddress, amount }) =>\n nodeApi.post(\n '/wallet/payment/send',\n [\n {\n address: recipientAddress,\n value: Number(\n (parseFloat(amount) * constants.nanoErgInErg).toFixed(1),\n ),\n },\n ],\n {\n headers: {\n api_key: this.props.apiKey,\n },\n },\n )\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.paymentSend(values)\n .then(({ data }) => {\n resetForm(initialFormValues)\n setStatus({\n state: 'success',\n msg: (\n <>\n

\n Your payment has been sent successfully. The transaction ID is -{' '}\n {data}\n

\n

\n \n Click Here To Go To The Explorer\n \n

\n \n ),\n })\n this.setState({ isShowTransactionId: true })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n
\n

Payment send

\n \n {({ status, isSubmitting }) => (\n
\n {status &&\n status.state === 'success' &&\n this.state.isShowTransactionId && (\n
\n \n this.setState({ isShowTransactionId: false })\n }\n >\n ×\n \n {status.msg}\n
\n )}\n
\n \n \n
\n
\n \n \n
\n \n Send\n \n \n )}\n \n
\n
\n )\n }\n}\n\nexport default PaymentSendForm\n","import React, { PureComponent } from 'react'\nimport { Formik, Form } from 'formik'\nimport NumberFormat from 'react-number-format'\nimport nodeApi from '../../../../../api/api'\nimport customToast from '../../../../../utils/toast'\n\nconst initialFormValues = {\n walletPassword: '',\n}\n\nclass GetBalanceForm extends PureComponent {\n state = {\n isShowBalance: false,\n }\n\n getBalance = () =>\n nodeApi.get('/wallet/balances', {\n headers: {\n api_key: this.props.apiKey,\n },\n })\n\n handleSubmit = (values, { setSubmitting, resetForm, setStatus }) => {\n setStatus({ status: 'submitting' })\n this.getBalance(values)\n .then(({ data: { balance } }) => {\n resetForm(initialFormValues)\n setStatus({\n state: 'success',\n msg: (\n <>\n Your wallet balance -{' '}\n \n \n ),\n })\n this.setState({ isShowBalance: true })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n setSubmitting(false)\n })\n }\n\n render() {\n return (\n
\n
\n

Get confirmed wallet balance

\n \n {({ status, isSubmitting }) => (\n
\n {status &&\n status.state === 'success' &&\n this.state.isShowBalance && (\n
\n this.setState({ isShowBalance: false })}\n >\n ×\n \n {status.msg}\n
\n )}\n \n Get\n \n \n )}\n \n
\n
\n )\n }\n}\n\nexport default GetBalanceForm\n","import React, { PureComponent } from 'react'\nimport nodeApi from '../../../../../api/api'\nimport customToast from '../../../../../utils/toast'\nimport CopyToClipboard from '../../../../common/CopyToClipboard'\n\nclass GetWalletAddressesForm extends PureComponent {\n state = {\n isShowWalletAddresses: false,\n walletAddresses: [],\n }\n\n getWalletAddresses = () =>\n nodeApi.get('/wallet/addresses', {\n headers: {\n api_key: this.props.apiKey,\n },\n })\n\n handleSubmit = event => {\n event.preventDefault()\n\n this.getWalletAddresses()\n .then(({ data: walletAddresses }) => {\n this.setState({ isShowWalletAddresses: true, walletAddresses })\n })\n .catch(err => {\n const errMessage = err.data ? err.data.detail : err.message\n customToast('error', errMessage)\n })\n }\n\n render() {\n return (\n
\n
\n

Get all wallet addresses

\n
\n {this.state.isShowWalletAddresses && (\n
\n \n this.setState({ isShowWalletAddresses: false })\n }\n >\n ×\n \n

Wallet Addresses:

\n
    \n {this.state.walletAddresses.map(addr => (\n
  • \n {addr}\n
  • \n ))}\n
\n
\n )}\n \n
\n
\n
\n )\n }\n}\n\nexport default GetWalletAddressesForm\n","import React, { Component, memo } from 'react'\nimport { connect } from 'react-redux'\nimport PaymentSendForm from './components/PaymentSendForm'\nimport GetBalanceForm from './components/GetBalanceForm'\nimport GetWalletAddressesForm from './components/GetWalletAddressesForm'\nimport { apiKeySelector } from '../../../store/selectors/app'\nimport {\n isWalletInitializedSelector,\n isWalletUnlockedSelector,\n} from '../../../store/selectors/wallet'\n\nconst mapStateToProps = state => ({\n apiKey: apiKeySelector(state),\n isWalletInitialized: isWalletInitializedSelector(state),\n isWalletUnlocked: isWalletUnlockedSelector(state),\n})\n\nclass Wallet extends Component {\n renderState = state =>\n ({\n unlocked: apiKey => this.renderWalletUnlockedState(apiKey),\n locked: () => this.renderWalletLockedState(),\n initialized: apiKey => this.renderInitializedState(apiKey),\n }[state])\n\n renderWalletUnlockedState = apiKey => (\n
\n
\n \n \n \n
\n
\n )\n\n renderWalletLockedState = () => (\n
\n

\n The wallet UI is locked. You need to unlock the wallet to access its UI.\n

\n
\n )\n\n renderInitializedState = () => (\n
\n

You need to initialize your wallet to access wallet UI.

\n
\n )\n\n render() {\n const { apiKey, isWalletUnlocked, isWalletInitialized } = this.props\n\n if (apiKey === '') {\n return (\n
\n

To continue, please set your API key.

\n
\n )\n }\n\n if (!isWalletInitialized) {\n return this.renderState('initialized')(apiKey)\n }\n\n if (isWalletUnlocked) {\n return this.renderState('unlocked')(apiKey)\n }\n\n return this.renderState('locked')()\n }\n}\n\nexport default connect(mapStateToProps)(memo(Wallet))\n","import React from 'react'\nimport { BrowserRouter, Switch, Route } from 'react-router-dom'\nimport Layout from '../components/layout'\nimport Dashboard from '../components/pages/Dashboard'\nimport Wallet from '../components/pages/Wallet'\n\nconst Router = () => (\n \n \n \n \n \n \n \n \n)\n\nexport default Router\n","import { combineReducers } from 'redux'\nimport appSlice from '../slices/appSlice'\nimport walletSlice from '../slices/walletSlice'\n\nexport default combineReducers({\n app: appSlice.reducer,\n wallet: walletSlice.reducer,\n})\n","import walletActions from '../actions/walletActions'\nimport nodeApi from '../../api/api'\nimport { apiKeySelector } from '../selectors/app'\n\nexport default store => next => action => {\n const { dispatch, getState } = store\n const apiKey = apiKeySelector(getState())\n\n switch (action.type) {\n case walletActions.checkWalletStatus.type:\n nodeApi\n .get('/wallet/status', {\n headers: {\n api_key: apiKey,\n },\n })\n .then(({ data: { isUnlocked, isInitialized } }) => {\n dispatch(walletActions.setIsWalletUnlocked(isUnlocked))\n dispatch(walletActions.setIsWalletInitialized(isInitialized))\n })\n .catch(() => {})\n\n break\n\n default:\n break\n }\n next(action)\n}\n","import React from 'react'\nimport { toast } from 'react-toastify'\nimport { Provider } from 'react-redux'\nimport Router from './router/router'\nimport createStore from './store'\n\nimport 'bootstrap/dist/css/bootstrap.min.css'\nimport './assets/styles/index.scss'\nimport 'react-toastify/dist/ReactToastify.min.css'\n\ntoast.configure()\nconst store = createStore()\n\nconst App = () => {\n return (\n \n \n \n )\n}\n\nexport default App\n","import { configureStore, getDefaultMiddleware } from 'redux-starter-kit'\nimport rootReducer from './reducers/rootReducer'\nimport walletMiddleware from './middlewares/walletMiddleware'\n\nexport default () => {\n const store = configureStore({\n reducer: rootReducer,\n middleware: [...getDefaultMiddleware(), walletMiddleware],\n })\n\n return store\n}\n","import React from 'react'\nimport ReactDOM from 'react-dom'\nimport App from './App'\n\nReactDOM.render(, document.getElementById('root'))\n"],"sourceRoot":""} \ No newline at end of file diff --git a/src/main/resources/reference.conf b/src/main/resources/reference.conf index 58fdf6953a..11cd130282 100644 --- a/src/main/resources/reference.conf +++ b/src/main/resources/reference.conf @@ -343,7 +343,7 @@ scorex { deliveryTimeout = 10s # Max number of delivery checks. Stop expecting modifier (and penalize peer) if it was not delivered on time - maxDeliveryChecks = 40 + maxDeliveryChecks = 2 ############ # Timeouts # diff --git a/src/main/scala/org/ergoplatform/BootstrapController.scala b/src/main/scala/org/ergoplatform/BootstrapController.scala deleted file mode 100644 index 2031f47f92..0000000000 --- a/src/main/scala/org/ergoplatform/BootstrapController.scala +++ /dev/null @@ -1,59 +0,0 @@ -package org.ergoplatform - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.{HttpMethods, HttpRequest} -import akka.http.scaladsl.unmarshalling.Unmarshal -import akka.stream.ActorMaterializer -import akka.util.ByteString -import io.circe.generic.auto._ -import io.circe.parser._ -import org.ergoplatform.BootstrapController.GenesisSettings -import org.ergoplatform.settings.BootstrapSettings -import scorex.util.ScorexLogging - -import scala.concurrent.duration._ -import scala.concurrent.{Await, ExecutionContext, Future} -import scala.util.{Failure, Success, Try} - -final class BootstrapController(settings: BootstrapSettings)(implicit val as: ActorSystem) - extends ScorexLogging { - - implicit val ec: ExecutionContext = as.getDispatcher - implicit val mat: ActorMaterializer = ActorMaterializer() - - def waitForBootSettings(): (Seq[String], String) = { - def tryFetchSettings: GenesisSettings = { - Try(Await.result(getSettings(settings.resourceUri), atMost = 5.second)) match { - case Success(Some(genesisSettings)) => genesisSettings - case Success(_) => - log.info(s"Wrong response format, retrying in ${settings.pollDelay.toSeconds}s") - Thread.sleep(settings.pollDelay.toMillis) - tryFetchSettings - case Failure(e) => - log.info(s"Failed to fetch genesis settings: ${e.getMessage}. Retrying in ${settings.pollDelay.toSeconds}s") - Thread.sleep(settings.pollDelay.toMillis) - tryFetchSettings - } - } - tryFetchSettings.unapply - } - - private def getSettings(uri: String): Future[Option[GenesisSettings]] = - Http().singleRequest(HttpRequest(method = HttpMethods.GET, uri = uri)) - .flatMap { resp => - Unmarshal(resp.entity) - .to[ByteString] - .map(bs => decode[GenesisSettings](bs.utf8String).toOption) - } - -} - -object BootstrapController { - - final case class GenesisSettings(noPremineProof: Seq[String], - genesisStateDigestHex: String) { - def unapply: (Seq[String], String) = noPremineProof -> genesisStateDigestHex - } - -} diff --git a/src/main/scala/org/ergoplatform/ErgoApp.scala b/src/main/scala/org/ergoplatform/ErgoApp.scala index 159f1185db..a6a9b4378e 100644 --- a/src/main/scala/org/ergoplatform/ErgoApp.scala +++ b/src/main/scala/org/ergoplatform/ErgoApp.scala @@ -44,22 +44,6 @@ class ErgoApp(args: Args) extends ScorexLogging { implicit private val actorSystem: ActorSystem = ActorSystem(settings.network.agentName) implicit private val executionContext: ExecutionContext = actorSystem.dispatcher - ergoSettings = ergoSettings.bootstrapSettingsOpt match { - case Some(bs) if isEmptyState => - log.info("Entering coordinated network bootstrap procedure ..") - val (npmProof, genesisDigest) = - new BootstrapController(bs).waitForBootSettings() - log.info("Boot settings received. Starting the node ..") - ergoSettings.copy( - chainSettings = ergoSettings.chainSettings.copy( - noPremineProof = npmProof, - genesisStateDigestHex = genesisDigest - ) - ) - case _ => - ergoSettings - } - private val features: Seq[PeerFeature] = Seq(ModeFeature(ergoSettings.nodeSettings)) private val timeProvider = new NetworkTimeProvider(settings.ntp) diff --git a/src/main/scala/org/ergoplatform/http/api/EmissionApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/EmissionApiRoute.scala index 7e2a849054..afece4c4d0 100644 --- a/src/main/scala/org/ergoplatform/http/api/EmissionApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/EmissionApiRoute.scala @@ -8,8 +8,8 @@ import org.ergoplatform.settings.ErgoSettings import scorex.core.api.http.ApiResponse import scorex.core.settings.RESTApiSettings -final case class EmissionApiRoute(ergoSettings: ErgoSettings) - (implicit val context: ActorRefFactory) extends ErgoBaseApiRoute { +case class EmissionApiRoute(ergoSettings: ErgoSettings) + (implicit val context: ActorRefFactory) extends ErgoBaseApiRoute { import EmissionApiRoute._ diff --git a/src/main/scala/org/ergoplatform/http/api/ScanApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/ScanApiRoute.scala index 734116b394..ed84177cc5 100644 --- a/src/main/scala/org/ergoplatform/http/api/ScanApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/ScanApiRoute.scala @@ -38,7 +38,8 @@ case class ScanApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSettings) deregisterR ~ listScansR ~ unspentR ~ - stopTrackingR + stopTrackingR ~ + addBoxR } def registerR: Route = (path("register") & post & entity(as[ScanRequest])) { request => @@ -55,7 +56,6 @@ case class ScanApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSettings) } } - //todo: paging? def listScansR: Route = (path("listAll") & get) { withWallet(_.readScans().map(_.apps)) } @@ -74,4 +74,11 @@ case class ScanApiRoute(readersHolder: ActorRef, ergoSettings: ErgoSettings) } } + def addBoxR: Route = (path("addBox") & post & entity(as[BoxWithScanIds])) { scanIdsBox => + withWalletOp(_.addBox(scanIdsBox.box, scanIdsBox.scanIds).map(_.status)) { + case Failure(e) => BadRequest(s"Bad request ($scanIdsBox): ${Option(e.getMessage).getOrElse(e.toString)}") + case Success(_) => ApiResponse(scanIdsBox.box.id) + } + } + } diff --git a/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala b/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala index 382556b57d..312fcbd5fb 100644 --- a/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala +++ b/src/main/scala/org/ergoplatform/http/api/ScanEntities.scala @@ -2,7 +2,7 @@ package org.ergoplatform.http.api import io.circe.{Decoder, HCursor} import org.ergoplatform.ErgoBox.BoxId -import org.ergoplatform.JsonCodecs +import org.ergoplatform.{ErgoBox, JsonCodecs} import org.ergoplatform.wallet.Constants.ScanId /** @@ -32,17 +32,16 @@ object ScanEntities { } - case class ScanIdBox(scanId: ScanId, boxBytes: Array[Byte]) - object ScanIdBox extends JsonCodecs { + case class BoxWithScanIds(box: ErgoBox, scanIds: Set[ScanId]) - implicit val scanIdBoxDecoder: Decoder[ScanIdBox] = { c: HCursor => + object BoxWithScanIds extends JsonCodecs { + implicit val scanIdsBoxDecoder: Decoder[BoxWithScanIds] = { c: HCursor => for { - scanId <- ScanId @@ c.downField("scanId").as[Short] - boxBytes <- c.downField("boxBytes").as[Array[Byte]] - } yield ScanIdBox(scanId, boxBytes) + box <- c.downField("box").as[ErgoBox] + scanIds <- ScanId @@ c.downField("scanIds").as[Set[Short]] + } yield BoxWithScanIds(box, scanIds) } - } } diff --git a/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala b/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala index 648392c1ca..766c860eff 100644 --- a/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala +++ b/src/main/scala/org/ergoplatform/http/api/WalletApiRoute.scala @@ -12,6 +12,7 @@ import org.ergoplatform.nodeView.state.UtxoStateReader import org.ergoplatform.nodeView.wallet._ import org.ergoplatform.nodeView.wallet.requests._ import org.ergoplatform.settings.ErgoSettings +import org.ergoplatform.wallet.Constants import scorex.core.NodeViewHolder.ReceivableMessages.LocallyGeneratedTransaction import scorex.core.api.http.ApiError.{BadRequest, NotExists} import scorex.core.api.http.ApiResponse @@ -55,7 +56,8 @@ case class WalletApiRoute(readersHolder: ActorRef, nodeViewActorRef: ActorRef, e deriveNextKeyR ~ updateChangeAddressR ~ signTransactionR ~ - checkSeedR + checkSeedR ~ + rescanWalletR } } @@ -82,7 +84,7 @@ case class WalletApiRoute(readersHolder: ActorRef, nodeViewActorRef: ActorRef, e private val checkRequest: Directive1[(String, Option[String])] = entity(as[Json]).flatMap { p => p.hcursor.downField("mnemonic").as[String] .flatMap(mnemo => p.hcursor.downField("mnemonicPass").as[Option[String]] - .map(mnemoPassOpt => (mnemo, mnemoPassOpt))) + .map(mnemoPassOpt => (mnemo, mnemoPassOpt))) .fold(_ => reject, s => provide(s)) } @@ -136,19 +138,19 @@ case class WalletApiRoute(readersHolder: ActorRef, nodeViewActorRef: ActorRef, e private def sendTransaction(requests: Seq[TransactionGenerationRequest], inputsRaw: Seq[String], dataInputsRaw: Seq[String]): Route = { - generateTransactionAndProcess(requests, inputsRaw, dataInputsRaw, {tx => + generateTransactionAndProcess(requests, inputsRaw, dataInputsRaw, { tx => nodeViewActorRef ! LocallyGeneratedTransaction[ErgoTransaction](tx) ApiResponse(tx.id) }) } def sendTransactionR: Route = - (path("transaction" / "send") & post & entity(as[RequestsHolder])){ holder => + (path("transaction" / "send") & post & entity(as[RequestsHolder])) { holder => sendTransaction(holder.withFee, holder.inputsRaw, holder.dataInputsRaw) } def generateTransactionR: Route = - (path("transaction" / "generate") & post & entity(as[RequestsHolder])){ holder => + (path("transaction" / "generate") & post & entity(as[RequestsHolder])) { holder => generateTransaction(holder.withFee, holder.inputsRaw, holder.dataInputsRaw) } @@ -249,6 +251,7 @@ case class WalletApiRoute(readersHolder: ActorRef, nodeViewActorRef: ActorRef, e _.transactions .map { _.filter(tx => + tx.wtx.scanIds.exists(scanId => scanId <= Constants.PaymentsScanId) && tx.wtx.inclusionHeight >= minHeight && tx.wtx.inclusionHeight <= maxHeight && tx.numConfirmations >= minConfNum && tx.numConfirmations <= maxConfNum ) @@ -293,8 +296,7 @@ case class WalletApiRoute(readersHolder: ActorRef, nodeViewActorRef: ActorRef, e def checkSeedR: Route = (path("check") & post & checkRequest) { case (mnemo, mnemoPassOpt) => - withWalletOp(_.checkSeed(mnemo, mnemoPassOpt)) - { case matched => + withWalletOp(_.checkSeed(mnemo, mnemoPassOpt)) { matched => ApiResponse( Json.obj( "matched" -> matched.asJson @@ -340,4 +342,13 @@ case class WalletApiRoute(readersHolder: ActorRef, nodeViewActorRef: ActorRef, e } } + def rescanWalletR: Route = (path("rescan") & get) { + withWalletOp(_.rescanWallet()) { + _.fold( + e => BadRequest(e.getMessage), + _ => ApiResponse.toRoute(ApiResponse.OK) + ) + } + } + } diff --git a/src/main/scala/org/ergoplatform/local/CleanupWorker.scala b/src/main/scala/org/ergoplatform/local/CleanupWorker.scala index 912631bbb3..2fc02886e5 100644 --- a/src/main/scala/org/ergoplatform/local/CleanupWorker.scala +++ b/src/main/scala/org/ergoplatform/local/CleanupWorker.scala @@ -102,7 +102,7 @@ class CleanupWorker(nodeViewHolderRef: ActorRef, val (validatedIds, invalidatedIds) = validationLoop(txsToValidate, Seq.empty, Seq.empty, 0L) epochNr += 1 - if (epochNr % CleanupWorker.IndexRevisionInterval == 0) { + if (epochNr % CleanupWorker.RevisionInterval == 0) { // drop old index in order to check potentially outdated transactions again. validatedIndex = TreeSet(validatedIds: _*) } else { @@ -116,9 +116,22 @@ class CleanupWorker(nodeViewHolderRef: ActorRef, object CleanupWorker { - case class RunCleanup(validator: TransactionValidation[ErgoTransaction], - mempool: ErgoMemPoolReader) + /** + * Constant which shows on how many cleanup operations (called when a new block arrives) a transaction + * re-check happens. + * + * If transactions set is large and stable, then about (1/RevisionInterval)-th of the pool is checked + * + */ + val RevisionInterval: Int = 4 - val IndexRevisionInterval: Int = 16 + /** + * + * A command to run (partial) memory pool cleanup + * + * @param validator - a state implementation which provides transaction validation + * @param mempool - mempool reader instance + */ + case class RunCleanup(validator: TransactionValidation[ErgoTransaction], mempool: ErgoMemPoolReader) } diff --git a/src/main/scala/org/ergoplatform/local/MempoolAuditor.scala b/src/main/scala/org/ergoplatform/local/MempoolAuditor.scala index 1f77aa7fd6..ba1ea29f4b 100644 --- a/src/main/scala/org/ergoplatform/local/MempoolAuditor.scala +++ b/src/main/scala/org/ergoplatform/local/MempoolAuditor.scala @@ -8,7 +8,6 @@ import org.ergoplatform.modifiers.ErgoFullBlock import org.ergoplatform.modifiers.history.Header import org.ergoplatform.modifiers.mempool.ErgoTransaction import org.ergoplatform.nodeView.mempool.ErgoMemPoolReader -import org.ergoplatform.nodeView.state.UtxoState import org.ergoplatform.settings.ErgoSettings import scorex.core.NodeViewHolder.ReceivableMessages.GetNodeViewChanges import scorex.core.network.Broadcast @@ -103,31 +102,20 @@ class MempoolAuditor(nodeViewHolderRef: ActorRef, private def rebroadcastTransactions(): Unit = { log.debug("Rebroadcasting transactions") - stateReaderOpt.foreach { st => - poolReaderOpt.foreach { pr => - pr.take(settings.nodeSettings.rebroadcastCount).foreach { tx => - st match { - case utxo: UtxoState => - //todo: currently we're rebroadcasting transactions which are spending on-chain outputs only - //todo: this is to be changed when most of the nodes on the network will support transactions spending - //todo: offchain outputs in the mempool (versions 3.2.1 and further) - if (tx.inputs.forall(i => utxo.boxById(i.boxId).isDefined)) { - log.info(s"Rebroadcasting $tx") - val msg = Message( - new InvSpec(settings.scorexSettings.network.maxInvObjects), - Right(InvData(Transaction.ModifierTypeId, Seq(tx.id))), - None - ) - networkControllerRef ! SendToNetwork(msg, Broadcast) - } else { - log.info(s"Not all the inputs of $tx is in UTXO set") - } - } - } + poolReaderOpt.foreach { pr => + pr.take(settings.nodeSettings.rebroadcastCount).foreach { tx => + log.info(s"Rebroadcasting $tx") + val msg = Message( + new InvSpec(settings.scorexSettings.network.maxInvObjects), + Right(InvData(Transaction.ModifierTypeId, Seq(tx.id))), + None + ) + networkControllerRef ! SendToNetwork(msg, Broadcast) } + } - } + } } object MempoolAuditor { diff --git a/src/main/scala/org/ergoplatform/modifiers/ErgoPersistentModifier.scala b/src/main/scala/org/ergoplatform/modifiers/ErgoPersistentModifier.scala index 2933232af0..62d16d5440 100644 --- a/src/main/scala/org/ergoplatform/modifiers/ErgoPersistentModifier.scala +++ b/src/main/scala/org/ergoplatform/modifiers/ErgoPersistentModifier.scala @@ -7,6 +7,7 @@ import scorex.core.PersistentNodeViewModifier trait ErgoPersistentModifier extends PersistentNodeViewModifier with ErgoNodeViewModifier object ErgoPersistentModifier { + implicit val jsonEncoder: Encoder[ErgoPersistentModifier] = { case h: Header => Header.jsonEncoder(h) case bt: BlockTransactions => BlockTransactions.jsonEncoder(bt) @@ -14,4 +15,5 @@ object ErgoPersistentModifier { case ext: Extension => Extension.jsonEncoder(ext) case other => throw new Exception(s"Unknown persistent modifier type: $other") } + } diff --git a/src/main/scala/org/ergoplatform/modifiers/history/HeaderChain.scala b/src/main/scala/org/ergoplatform/modifiers/history/HeaderChain.scala index af8488673f..365863e11c 100644 --- a/src/main/scala/org/ergoplatform/modifiers/history/HeaderChain.scala +++ b/src/main/scala/org/ergoplatform/modifiers/history/HeaderChain.scala @@ -1,6 +1,7 @@ package org.ergoplatform.modifiers.history case class HeaderChain(headers: IndexedSeq[Header]) { + headers.indices.foreach { i => if (i > 0) require(headers(i).parentId == headers(i - 1).id, s"Incorrect chain: ${headers(i - 1)},${headers(i)}") @@ -38,6 +39,7 @@ case class HeaderChain(headers: IndexedSeq[Header]) { } object HeaderChain { + lazy val empty = HeaderChain(IndexedSeq.empty[Header]) def apply(seq: Seq[Header]): HeaderChain = HeaderChain(seq.toIndexedSeq) diff --git a/src/main/scala/org/ergoplatform/modifiers/history/PoPoWProof.scala b/src/main/scala/org/ergoplatform/modifiers/history/PoPoWProof.scala index eb7e7106fe..2f719031b1 100644 --- a/src/main/scala/org/ergoplatform/modifiers/history/PoPoWProof.scala +++ b/src/main/scala/org/ergoplatform/modifiers/history/PoPoWProof.scala @@ -5,9 +5,6 @@ import org.ergoplatform.modifiers.ErgoPersistentModifier import org.ergoplatform.settings.Algos import scorex.core.ModifierTypeId import scorex.core.serialization.ScorexSerializer -import scorex.core.validation.ModifierValidator -import scorex.core.utils.ScorexEncoding -import scorex.util.serialization.{Reader, Writer} import scorex.util.{ModifierId, bytesToId} case class PoPoWProof(m: Byte, @@ -37,4 +34,4 @@ case class PoPoWProof(m: Byte, object PoPoWProof { val modifierTypeId: ModifierTypeId = ModifierTypeId @@ (105: Byte) -} \ No newline at end of file +} diff --git a/src/main/scala/org/ergoplatform/modifiers/mempool/ErgoTransaction.scala b/src/main/scala/org/ergoplatform/modifiers/mempool/ErgoTransaction.scala index e98bb6b3d8..8b488bc7dc 100644 --- a/src/main/scala/org/ergoplatform/modifiers/mempool/ErgoTransaction.scala +++ b/src/main/scala/org/ergoplatform/modifiers/mempool/ErgoTransaction.scala @@ -13,6 +13,7 @@ import org.ergoplatform.settings.{Algos, ErgoValidationSettings} import org.ergoplatform.utils.BoxUtils import org.ergoplatform.wallet.interpreter.ErgoInterpreter import org.ergoplatform.wallet.protocol.context.TransactionContext +import scorex.core.EphemerealNodeViewModifier import scorex.core.serialization.ScorexSerializer import scorex.core.transaction.Transaction import scorex.core.utils.ScorexEncoding @@ -53,7 +54,7 @@ case class ErgoTransaction(override val inputs: IndexedSeq[Input], override val sizeOpt: Option[Int] = None) extends ErgoLikeTransaction(inputs, dataInputs, outputCandidates) with Transaction - with MempoolModifier + with EphemerealNodeViewModifier with ErgoNodeViewModifier with ScorexLogging { diff --git a/src/main/scala/org/ergoplatform/modifiers/mempool/MempoolModifier.scala b/src/main/scala/org/ergoplatform/modifiers/mempool/MempoolModifier.scala deleted file mode 100644 index 406bf26c9e..0000000000 --- a/src/main/scala/org/ergoplatform/modifiers/mempool/MempoolModifier.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.ergoplatform.modifiers.mempool - -import scorex.core.EphemerealNodeViewModifier - -trait MempoolModifier extends EphemerealNodeViewModifier diff --git a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala index 67a9c3cb18..5e93413098 100644 --- a/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala +++ b/src/main/scala/org/ergoplatform/network/ErgoNodeViewSynchronizer.scala @@ -10,10 +10,12 @@ import org.ergoplatform.settings.Constants import scorex.core.NodeViewHolder._ import scorex.core.{ModifierTypeId, PersistentNodeViewModifier} import scorex.core.network.NetworkController.ReceivableMessages.SendToNetwork -import scorex.core.network.NodeViewSynchronizer.ReceivableMessages.{CheckDelivery, SemanticallySuccessfulModifier} -import scorex.core.network.message.{InvData, Message} +import scorex.core.network.NetworkControllerSharedMessages.ReceivableMessages.DataFromPeer +import scorex.core.network.NodeViewSynchronizer.ReceivableMessages.SemanticallySuccessfulModifier +import scorex.core.network.message.{InvData, InvSpec, Message} import scorex.core.network.{ModifiersStatus, NodeViewSynchronizer, SendToRandom} import scorex.core.settings.NetworkSettings +import scorex.core.transaction.Transaction import scorex.core.utils.NetworkTimeProvider import scorex.util.ModifierId @@ -30,8 +32,8 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, ErgoHistory, ErgoMemPool](networkControllerRef, viewHolderRef, syncInfoSpec, networkSettings, timeProvider, Constants.modifierSerializers) { - override protected val deliveryTracker = new ErgoDeliveryTracker(context.system, deliveryTimeout, maxDeliveryChecks, - self, timeProvider) + override protected val deliveryTracker = + new ErgoDeliveryTracker(context.system, deliveryTimeout, maxDeliveryChecks, self, timeProvider) /** * Approximate number of modifiers to be downloaded simultaneously @@ -45,26 +47,6 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, context.system.scheduler.schedule(toDownloadCheckInterval, toDownloadCheckInterval)(self ! CheckModifiersToDownload) } - //todo: pull back to Scorex with fixing this method - override protected def checkDelivery: Receive = { - case CheckDelivery(peerOpt, modifierTypeId, modifierId) => - if (deliveryTracker.status(modifierId) == ModifiersStatus.Requested) { - peerOpt match { - case Some(peer) => - log.info(s"Peer ${peer.toString} has not delivered asked modifier ${encoder.encodeId(modifierId)} on time") - penalizeNonDeliveringPeer(peer) - deliveryTracker.setUnknown(modifierId) - requestDownload(modifierTypeId, Seq(modifierId)) - case None => - // Random peer did not delivered modifier we need, ask another peer - // We need this modifier - no limit for number of attempts - log.info(s"Modifier ${encoder.encodeId(modifierId)} (type $modifierTypeId) was not delivered on time") - deliveryTracker.setUnknown(modifierId) - requestDownload(modifierTypeId, Seq(modifierId)) - } - } - } - /** * Requests BlockSections with `Unknown` status that are defined by block headers but not downloaded yet. * Trying to keep size of requested queue equals to `desiredSizeOfExpectingQueue`. @@ -77,18 +59,57 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, val toDownload = h.nextModifiersToDownload(desiredSizeOfExpectingQueue - deliveryTracker.requestedSize, downloadRequired) - log.info(s"${toDownload.length} modifiers to be downloaded") + log.info(s"${toDownload.length} persistent modifiers to be downloaded") toDownload.groupBy(_._1).foreach(ids => requestDownload(ids._1, ids._2.map(_._2))) } } + // todo: this method is just a copy of the ancestor from Scorex, however, smarter logic is needed, not just + // asking from a random peer override protected def requestDownload(modifierTypeId: ModifierTypeId, modifierIds: Seq[ModifierId]): Unit = { deliveryTracker.setRequested(modifierIds, modifierTypeId, None) val msg = Message(requestModifierSpec, Right(InvData(modifierTypeId, modifierIds)), None) networkControllerRef ! SendToNetwork(msg, SendToRandom) } + /** + * Object ids coming from other node. + * Filter out modifier ids that are already in process (requested, received or applied), + * request unknown ids from peer and set this ids to requested state. + */ + override protected def processInv: Receive = { + case DataFromPeer(spec, invData: InvData@unchecked, peer) + if spec.messageCode == InvSpec.MessageCode => + + (mempoolReaderOpt, historyReaderOpt) match { + case (Some(mempool), Some(history)) => + + val modifierTypeId = invData.typeId + + val newModifierIds = modifierTypeId match { + case Transaction.ModifierTypeId => + // We download transactions only if the chain is synced + if (history.isHeadersChainSynced && history.fullBlockHeight == history.headersHeight) { + invData.ids.filter(mid => deliveryTracker.status(mid, mempool) == ModifiersStatus.Unknown) + } else { + Seq.empty + } + case _ => + invData.ids.filter(mid => deliveryTracker.status(mid, history) == ModifiersStatus.Unknown) + } + + if (newModifierIds.nonEmpty) { + val msg = Message(requestModifierSpec, Right(InvData(modifierTypeId, newModifierIds)), None) + peer.handlerRef ! msg + deliveryTracker.setRequested(newModifierIds, modifierTypeId, Some(peer)) + } + + case _ => + log.warn(s"Got data from peer while readers are not ready ${(mempoolReaderOpt, historyReaderOpt)}") + } + } + /** * If our requested list is more than half empty, enforce to request more: * - headers, if our headers chain is not synced yet (by sending sync message) @@ -131,6 +152,7 @@ class ErgoNodeViewSynchronizer(networkControllerRef: ActorRef, } object ErgoNodeViewSynchronizer { + def props(networkControllerRef: ActorRef, viewHolderRef: ActorRef, syncInfoSpec: ErgoSyncInfoMessageSpec.type, @@ -148,16 +170,6 @@ object ErgoNodeViewSynchronizer { (implicit context: ActorRefFactory, ex: ExecutionContext): ActorRef = context.actorOf(props(networkControllerRef, viewHolderRef, syncInfoSpec, networkSettings, timeProvider)) - def apply(networkControllerRef: ActorRef, - viewHolderRef: ActorRef, - syncInfoSpec: ErgoSyncInfoMessageSpec.type, - networkSettings: NetworkSettings, - timeProvider: NetworkTimeProvider, - name: String) - (implicit context: ActorRefFactory, ex: ExecutionContext): ActorRef = - context.actorOf(props(networkControllerRef, viewHolderRef, syncInfoSpec, networkSettings, timeProvider), name) - - case object CheckModifiersToDownload } diff --git a/src/main/scala/org/ergoplatform/network/ToDownloadStatus.scala b/src/main/scala/org/ergoplatform/network/ToDownloadStatus.scala deleted file mode 100644 index a81168f26c..0000000000 --- a/src/main/scala/org/ergoplatform/network/ToDownloadStatus.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.ergoplatform.network - -import scorex.core.ModifierTypeId - -case class ToDownloadStatus(tp: ModifierTypeId, firstViewed: Long, lastTry: Long) diff --git a/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ExtensionValidator.scala b/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ExtensionValidator.scala index 759ec67f55..d47781a1ab 100644 --- a/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ExtensionValidator.scala +++ b/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ExtensionValidator.scala @@ -2,10 +2,9 @@ package org.ergoplatform.nodeView.history.storage.modifierprocessors import org.ergoplatform.modifiers.history.PoPowAlgos._ import org.ergoplatform.modifiers.history.{Extension, ExtensionCandidate, Header} -import org.ergoplatform.settings.ErgoValidationSettings import org.ergoplatform.settings.ValidationRules._ import scorex.core.utils.ScorexEncoding -import scorex.core.validation.{ModifierValidator, ValidationState} +import scorex.core.validation.ValidationState import scorex.util.bytesToId /** diff --git a/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ModifierProcessorEnvironment.scala b/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ModifierProcessorEnvironment.scala deleted file mode 100644 index 3fe6591e42..0000000000 --- a/src/main/scala/org/ergoplatform/nodeView/history/storage/modifierprocessors/ModifierProcessorEnvironment.scala +++ /dev/null @@ -1,3 +0,0 @@ -package org.ergoplatform.nodeView.history.storage.modifierprocessors - -case class ModifierProcessorEnvironment(requiredDifficulty: BigInt) diff --git a/src/main/scala/org/ergoplatform/nodeView/mempool/ErgoMemPool.scala b/src/main/scala/org/ergoplatform/nodeView/mempool/ErgoMemPool.scala index b39b3246ee..d2337557be 100644 --- a/src/main/scala/org/ergoplatform/nodeView/mempool/ErgoMemPool.scala +++ b/src/main/scala/org/ergoplatform/nodeView/mempool/ErgoMemPool.scala @@ -63,25 +63,31 @@ class ErgoMemPool private[mempool](pool: OrderedTxPool)(implicit settings: ErgoS def process(tx: ErgoTransaction, state: ErgoState[_]): (ErgoMemPool, ProcessingOutcome) = { val fee = extractFee(tx) val minFee = settings.nodeSettings.minimalFeeAmount + val canAccept = pool.canAccept(tx) + if (fee >= minFee) { - state match { - case utxo: UtxoState if pool.canAccept(tx) => - // Allow proceeded transaction to spend outputs of pooled transactions. - utxo.withTransactions(getAll).validate(tx).fold( - new ErgoMemPool(pool.invalidate(tx)) -> ProcessingOutcome.Invalidated(_), - _ => new ErgoMemPool(pool.put(tx)) -> ProcessingOutcome.Accepted - ) - - case validator: TransactionValidation[ErgoTransaction@unchecked] if pool.canAccept(tx) => - // transaction validation currently works only for UtxoState, so this branch currently - // will not be triggered probably - validator.validate(tx).fold( - new ErgoMemPool(pool.invalidate(tx)) -> ProcessingOutcome.Invalidated(_), - _ => new ErgoMemPool(pool.put(tx)) -> ProcessingOutcome.Accepted - ) - case _ => - this -> ProcessingOutcome.Declined( - new Exception("Transaction validation not supported")) + if (canAccept) { + state match { + case utxo: UtxoState => + // Allow proceeded transaction to spend outputs of pooled transactions. + utxo.withTransactions(getAll).validate(tx).fold( + new ErgoMemPool(pool.invalidate(tx)) -> ProcessingOutcome.Invalidated(_), + _ => new ErgoMemPool(pool.put(tx)) -> ProcessingOutcome.Accepted + ) + case validator: TransactionValidation[ErgoTransaction@unchecked] => + // transaction validation currently works only for UtxoState, so this branch currently + // will not be triggered probably + validator.validate(tx).fold( + new ErgoMemPool(pool.invalidate(tx)) -> ProcessingOutcome.Invalidated(_), + _ => new ErgoMemPool(pool.put(tx)) -> ProcessingOutcome.Accepted + ) + case _ => + this -> ProcessingOutcome.Declined( + new Exception("Transaction validation not supported")) + } + } else { + this -> ProcessingOutcome.Declined( + new Exception(s"Pool can not accept transaction ${tx.id}, it is invalidated earlier or pool is full")) } } else { this -> ProcessingOutcome.Declined( diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala index 4ad7bfd319..1ebb6cbfc2 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletActor.scala @@ -8,6 +8,7 @@ import org.ergoplatform.ErgoBox._ import org.ergoplatform._ import org.ergoplatform.modifiers.ErgoFullBlock import org.ergoplatform.modifiers.mempool.{ErgoBoxSerializer, ErgoTransaction, UnsignedErgoTransaction} +import org.ergoplatform.nodeView.history.ErgoHistory.Height import org.ergoplatform.nodeView.history.{ErgoHistory, ErgoHistoryReader} import org.ergoplatform.nodeView.mempool.ErgoMemPoolReader import org.ergoplatform.nodeView.state.{ErgoStateContext, ErgoStateReader, UtxoStateReader} @@ -15,7 +16,7 @@ import org.ergoplatform.nodeView.wallet.persistence._ import org.ergoplatform.nodeView.wallet.requests.{AssetIssueRequest, ExternalSecret, PaymentRequest, TransactionGenerationRequest} import org.ergoplatform.nodeView.wallet.scanning.{Scan, ScanRequest} import org.ergoplatform.settings._ -import org.ergoplatform.utils.BoxUtils +import org.ergoplatform.utils.{BoxUtils, FileUtils} import org.ergoplatform.wallet.Constants.{PaymentsScanId, ScanId} import org.ergoplatform.wallet.TokensMap import org.ergoplatform.wallet.boxes.{BoxSelector, ChainStatus, TrackedBox} @@ -29,7 +30,7 @@ import scorex.core.utils.ScorexEncoding import scorex.crypto.hash.Digest32 import scorex.util.encode.Base16 import scorex.util.{ModifierId, ScorexLogging, idToBytes} -import sigmastate.Values.{ByteArrayConstant, IntConstant} +import sigmastate.Values.ByteArrayConstant import sigmastate.eval.Extensions._ import sigmastate.eval._ @@ -56,7 +57,6 @@ class ErgoWalletActor(settings: ErgoSettings, private var offChainRegistry: OffChainRegistry = OffChainRegistry.init(registry) private var walletVars = WalletVars.apply(storage, settings) - //todo: temporary 3.2.x collection and readers private var stateReaderOpt: Option[ErgoStateReader] = None private var mempoolReaderOpt: Option[ErgoMemPoolReader] = None @@ -120,6 +120,17 @@ class ErgoWalletActor(settings: ErgoSettings, offChainRegistry = offReg } + // expected height of a next block when the wallet is receiving a new block with the height blockHeight + private def expectedHeight(blockHeight: Height): Height = { + if (!settings.nodeSettings.isFullBlocksPruned) { + // Node has all the full blocks and applies them sequentially + walletHeight() + 1 + } else { + // Node has pruned blockchain + if (walletHeight() == 0) blockHeight else walletHeight() + 1 + } + } + private def scanLogic: Receive = { //scan mempool transaction case ScanOffChain(tx) => @@ -128,8 +139,7 @@ class ErgoWalletActor(settings: ErgoSettings, offChainRegistry = offChainRegistry.updateOnTransaction(newWalletBoxes, inputs) case ScanInThePast(blockHeight) => - val expectedHeight = walletHeight() + 1 - if (expectedHeight == blockHeight) { + if (expectedHeight(blockHeight) == blockHeight) { historyReader.bestFullBlockAt(blockHeight) match { case Some(block) => scanBlock(block) @@ -143,12 +153,12 @@ class ErgoWalletActor(settings: ErgoSettings, //scan block transactions case ScanOnChain(block) => - val expectedHeight = walletHeight() + 1 - if (expectedHeight == block.height) { + val expHeight = expectedHeight(block.height) + if (expHeight == block.height) { scanBlock(block) - } else if (expectedHeight < block.height) { - log.warn(s"Wallet: skipped blocks found starting from $expectedHeight, going back to scan them") - self ! ScanInThePast(expectedHeight) + } else if (expHeight < block.height) { + log.warn(s"Wallet: skipped blocks found starting from $expHeight, going back to scan them") + self ! ScanInThePast(expHeight) } else { log.warn(s"Wallet: block in the past reported at ${block.height}, blockId: ${block.id}") } @@ -322,6 +332,22 @@ class ErgoWalletActor(settings: ErgoSettings, walletVars = walletVars.resetProver() secretStorageOpt.foreach(_.lock()) + case RescanWallet => + // We do wallet rescan by closing the wallet's database, deleting it from the disk, + // then reopening it and sending a rescan signal. + val rescanResult = Try { + val registryFolder = WalletRegistry.registryFolder(settings) + log.info(s"Rescanning the wallet, the registry is in $registryFolder") + registry.close() + FileUtils.deleteRecursive(registryFolder) + registry = WalletRegistry.apply(settings) + self ! ScanInThePast(walletHeight()) // walletHeight() corresponds to empty wallet state now + } + rescanResult.recover { case t => + log.error("Error during rescan attempt: ", t) + } + sender() ! rescanResult + case GetWalletStatus => val status = WalletStatus(secretIsSet, walletVars.proverOpt.isDefined, changeAddress, walletHeight()) sender() ! status @@ -379,6 +405,10 @@ class ErgoWalletActor(settings: ErgoSettings, res.foreach(app => walletVars = walletVars.addScan(app)) sender() ! AddScanResponse(res) + case AddBox(box: ErgoBox, scanIds: Set[ScanId]) => + registry.updateScans(scanIds, box) + sender() ! AddBoxResponse(Success(())) + case StopTracking(scanId: ScanId, boxId: BoxId) => sender() ! StopTrackingResponse(registry.removeScan(boxId, scanId)) } @@ -441,6 +471,12 @@ class ErgoWalletActor(settings: ErgoSettings, */ private val noFilter: FilterFn = (_: TrackedBox) => true + /** + * Convert requests (to make payments or to issue an asset) to transaction outputs + * There can be only one asset issuance request in the input sequence. + * @param requests - an input sequence of requests + * @return sequence of transaction outputs or failure if inputs are incorrect + */ private def requestsToBoxCandidates(requests: Seq[TransactionGenerationRequest]): Try[Seq[ErgoBoxCandidate]] = Traverse[List].sequence { requests.toList @@ -467,7 +503,7 @@ class ErgoWalletActor(settings: ErgoSettings, val nonMandatoryRegisters = scala.Predef.Map( R4 -> ByteArrayConstant(name.getBytes("UTF-8")), R5 -> ByteArrayConstant(description.getBytes("UTF-8")), - R6 -> IntConstant(decimals) + R6 -> ByteArrayConstant(String.valueOf(decimals).getBytes("UTF-8")) ) ++ registers.getOrElse(Map()) (addressOpt orElse walletVars.publicKeyAddresses.headOption) .fold[Try[ErgoAddress]](Failure(new Exception("No address available for box locking")))(Success(_)) @@ -608,6 +644,10 @@ class ErgoWalletActor(settings: ErgoSettings, private def processUnlock(secretStorage: JsonSecretStorage): Unit = Try { val rootSecretSeq = secretStorage.secret.toSeq + if (rootSecretSeq.isEmpty) { + log.warn("Master key is not available after unlock") + } + // first, we're trying to find in the database paths written by clients prior 3.3.0 and convert them // into a new format (pubkeys with paths stored instead of paths) val oldPaths = storage.readPaths() @@ -619,11 +659,13 @@ class ErgoWalletActor(settings: ErgoSettings, oldPubKeys.foreach(storage.addKey) storage.removePaths() } - val pubKeys = storage.readAllKeys().toIndexedSeq + var pubKeys = storage.readAllKeys().toIndexedSeq //If no public keys in the database yet, add master's public key into it if (pubKeys.isEmpty) { - rootSecretSeq.foreach(s => storage.addKey(s.publicKey)) + val masterPubKey = rootSecretSeq.map(s => s.publicKey) + masterPubKey.foreach(pk => storage.addKey(pk)) + pubKeys = masterPubKey.toIndexedSeq } val secrets = pubKeys.flatMap { pk => @@ -833,6 +875,11 @@ object ErgoWalletActor { */ case object LockWallet + /** + * Rescan wallet + */ + case object RescanWallet + /** * Get wallet status */ @@ -882,6 +929,23 @@ object ErgoWalletActor { */ case class StopTrackingResponse(status: Try[Unit]) + + /** + * Add association between a scan and a box (and add the box to the database if it is not there) + * + * @param box + * @param scanIds + * + */ + case class AddBox(box: ErgoBox, scanIds: Set[ScanId]) + + /** + * Wrapper for a result of AddBox processing + * + * @param status + */ + case class AddBoxResponse(status: Try[Unit]) + def signTransaction(proverOpt: Option[ErgoProvingInterpreter], secrets: Seq[ExternalSecret], tx: UnsignedErgoTransaction, diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala index 4db4d395e4..61a64b6d7e 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletReader.scala @@ -46,6 +46,8 @@ trait ErgoWalletReader extends VaultReader { def lockWallet(): Unit = walletActor ! LockWallet + def rescanWallet(): Future[Try[Unit]] = (walletActor ? RescanWallet).mapTo[Try[Unit]] + def getWalletStatus: Future[WalletStatus] = (walletActor ? GetWalletStatus).mapTo[WalletStatus] @@ -108,4 +110,7 @@ trait ErgoWalletReader extends VaultReader { def stopTracking(scanId: ScanId, boxId: BoxId): Future[StopTrackingResponse] = (walletActor ? StopTracking(scanId, boxId)).mapTo[StopTrackingResponse] + def addBox(box: ErgoBox, scanIds: Set[ScanId]): Future[AddBoxResponse] = + (walletActor ? AddBox(box, scanIds)).mapTo[AddBoxResponse] + } diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala index 353b2a9d2e..52146d5718 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistry.scala @@ -1,6 +1,7 @@ package org.ergoplatform.nodeView.wallet.persistence import java.io.File + import org.ergoplatform.ErgoBox.BoxId import org.ergoplatform.db.HybridLDBKVStore import org.ergoplatform.modifiers.history.PreGenesisHeader @@ -12,8 +13,10 @@ import org.ergoplatform.wallet.boxes.{TrackedBox, TrackedBoxSerializer} import scorex.core.VersionTag import scorex.crypto.authds.ADKey import scorex.util.{ModifierId, ScorexLogging, idToBytes} -import Constants.{ScanId, PaymentsScanId} +import Constants.{PaymentsScanId, ScanId} +import org.ergoplatform.ErgoBox import scorex.db.LDBVersionedStore + import scala.util.{Failure, Success, Try} import org.ergoplatform.nodeView.wallet.IdUtils.encodedTokenId @@ -31,6 +34,13 @@ class WalletRegistry(store: HybridLDBKVStore)(ws: WalletSettings) extends Scorex private val keepHistory = ws.keepSpentBoxes + /** + * Close wallet registry storage + */ + def close(): Unit = { + store.close() + } + /** * Read wallet-related box with metadata * @@ -268,6 +278,27 @@ class WalletRegistry(store: HybridLDBKVStore)(ws: WalletSettings) extends Scorex } } + /** + * Updates scans of a box stored in the wallet database, + * puts the box into the database if it is not there + * + * @param scanIds + * @param box + * @return + */ + def updateScans(scanIds: Set[ScanId], box: ErgoBox): Try[Unit] = Try { + val bag0 = KeyValuePairsBag(toInsert = Seq.empty, toRemove = Seq.empty) + val (updTb, bag1) = getBox(box.id) match { + case Some(tb) => + (tb.copy(scans = scanIds), removeBox(bag0, tb)) + case None => + (TrackedBox(box, box.creationHeight, scanIds), bag0) + } + val bag2 = putBox(bag1, updTb) + store.nonVersionedRemove(bag2.toRemove) + store.nonVersionedPut(bag2.toInsert) + } + /** * Remove association between an application and a box. * Please note that in case of rollback association remains removed! @@ -310,8 +341,10 @@ object WalletRegistry { val PreGenesisStateVersion: Array[Byte] = idToBytes(PreGenesisHeader.id) + def registryFolder(settings: ErgoSettings): File = new File(s"${settings.directory}/wallet/registry") + def apply(settings: ErgoSettings): WalletRegistry = { - val dir = new File(s"${settings.directory}/wallet/registry") + val dir = registryFolder(settings) dir.mkdirs() val store = new HybridLDBKVStore(dir, settings.nodeSettings.keepVersions) diff --git a/src/main/scala/org/ergoplatform/settings/BootstrapSettings.scala b/src/main/scala/org/ergoplatform/settings/BootstrapSettings.scala deleted file mode 100644 index 15e9bc65dc..0000000000 --- a/src/main/scala/org/ergoplatform/settings/BootstrapSettings.scala +++ /dev/null @@ -1,5 +0,0 @@ -package org.ergoplatform.settings - -import scala.concurrent.duration.FiniteDuration - -case class BootstrapSettings(resourceUri: String, pollDelay: FiniteDuration) diff --git a/src/main/scala/org/ergoplatform/settings/ErgoSettings.scala b/src/main/scala/org/ergoplatform/settings/ErgoSettings.scala index f1184ee107..774c641aee 100644 --- a/src/main/scala/org/ergoplatform/settings/ErgoSettings.scala +++ b/src/main/scala/org/ergoplatform/settings/ErgoSettings.scala @@ -22,7 +22,6 @@ case class ErgoSettings(directory: String, scorexSettings: ScorexSettings, walletSettings: WalletSettings, cacheSettings: CacheSettings, - bootstrapSettingsOpt: Option[BootstrapSettings] = None, votingTargets: VotingTargets = VotingTargets.empty) { val addressEncoder = ErgoAddressEncoder(chainSettings.addressPrefix) @@ -58,7 +57,6 @@ object ErgoSettings extends ScorexLogging val networkType = NetworkType.fromString(networkTypeName) .getOrElse(throw new Error(s"Unknown `networkType = $networkTypeName`")) val nodeSettings = config.as[NodeConfigurationSettings](s"$configPath.node") - val bootstrappingSettingsOpt = config.as[Option[BootstrapSettings]](s"$configPath.bootstrap") val chainSettings = config.as[ChainSettings](s"$configPath.chain") val walletSettings = config.as[WalletSettings](s"$configPath.wallet") val cacheSettings = config.as[CacheSettings](s"$configPath.cache") @@ -78,7 +76,6 @@ object ErgoSettings extends ScorexLogging scorexSettings, walletSettings, cacheSettings, - bootstrappingSettingsOpt, votingTargets ), desiredNetworkTypeOpt diff --git a/src/main/scala/org/ergoplatform/settings/StateTypeReaders.scala b/src/main/scala/org/ergoplatform/settings/StateTypeReaders.scala index f1b9a2dead..c28cac0d0e 100644 --- a/src/main/scala/org/ergoplatform/settings/StateTypeReaders.scala +++ b/src/main/scala/org/ergoplatform/settings/StateTypeReaders.scala @@ -1,17 +1,10 @@ package org.ergoplatform.settings import com.typesafe.config.ConfigException -import net.ceedubs.ficus.readers.ValueReader import org.ergoplatform.nodeView.state._ trait StateTypeReaders { - implicit val stateTypeReader: ValueReader[StateType] = { (cfg, path) => - val typeKey = s"$path.stateType" - val typeName = cfg.getString(typeKey) - stateTypeFromString(typeName, typeKey) - } - def stateTypeFromString(typeName: String, path: String): StateType = { StateType.values.find(_.stateTypeName == typeName) .getOrElse(throw new ConfigException.BadValue(path, typeName)) diff --git a/src/main/scala/org/ergoplatform/utils/FileUtils.scala b/src/main/scala/org/ergoplatform/utils/FileUtils.scala index 9bd88a95dc..32fa3a5041 100644 --- a/src/main/scala/org/ergoplatform/utils/FileUtils.scala +++ b/src/main/scala/org/ergoplatform/utils/FileUtils.scala @@ -1,20 +1,21 @@ package org.ergoplatform.utils import java.io.File +import java.nio.file.Files +import scala.collection.JavaConverters._ +import scala.util.Try /** - * Utilities to work with OS file system - */ + * Utilities to work with OS file system + */ object FileUtils { + /** - * Perform recursive deletion of directory content. - */ - def deleteRecursive(dir: File): Unit = { - for (file <- dir.listFiles) { - if (!file.getName.startsWith(".")) { - if (file.isDirectory) deleteRecursive(file) - file.delete() - } + * Perform recursive deletion of directory content. + */ + def deleteRecursive(root: File): Unit = { + if (root.exists()) { + Files.walk(root.toPath).iterator().asScala.toSeq.reverse.foreach(path => Try(Files.delete(path))) } } diff --git a/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala b/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala index 2fae93ce12..a33ee02024 100644 --- a/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala +++ b/src/test/scala/org/ergoplatform/http/routes/WalletApiRouteSpec.scala @@ -15,6 +15,7 @@ import org.ergoplatform.utils.Stubs import org.ergoplatform.utils.generators.ErgoTransactionGenerators import org.ergoplatform.{ErgoAddress, Pay2SAddress} import org.scalatest.{FlatSpec, Matchers} +import org.ergoplatform.wallet.{Constants => WalletConstants} import scala.util.{Random, Try} import scala.concurrent.duration._ @@ -144,6 +145,13 @@ class WalletApiRouteSpec extends FlatSpec } } + it should "rescan wallet" in { + Get(prefix + "/rescan") ~> route ~> check { + status shouldBe StatusCodes.OK + } + } + + it should "derive new key according to a provided path" in { Post(prefix + "/deriveKey", Json.obj("derivationPath" -> "m/1/2".asJson)) ~> route ~> check { status shouldBe StatusCodes.OK @@ -202,8 +210,12 @@ class WalletApiRouteSpec extends FlatSpec Get(prefix + "/transactions") ~> route ~> check { status shouldBe StatusCodes.OK val response = responseAs[List[Json]] - response.size shouldBe 2 - responseAs[Seq[AugWalletTransaction]] shouldEqual WalletActorStub.walletTxs + val walletTxs = WalletActorStub.walletTxs.filter { awtx => + awtx.wtx.scanIds.exists(_ <= WalletConstants.PaymentsScanId) + } + + response.size shouldBe walletTxs.size + responseAs[Seq[AugWalletTransaction]] shouldEqual walletTxs } } diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala index c65926692c..e11ebad30c 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/persistence/WalletRegistrySpec.scala @@ -21,7 +21,7 @@ class WalletRegistrySpec with WalletGenerators with FileUtils { - implicit override val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 5, sizeRange = 10) + implicit override val generatorDrivenConfig = PropertyCheckConfiguration(minSuccessful = 4, sizeRange = 10) private val emptyBag = KeyValuePairsBag.empty private val walletBoxStatus = Set(PaymentsScanId) @@ -204,6 +204,27 @@ class WalletRegistrySpec } } + it should "update scans correctly" in { + val appId1: ScanId = ScanId @@ 21.toShort + val appId2: ScanId = ScanId @@ 22.toShort + + forAll(trackedBoxGen) { tb0 => + withHybridStore(10) { store => + val tb1 = tb0.copy(scans = Set(appId1, appId2), spendingHeightOpt = None, spendingTxIdOpt = None) + + val reg = new WalletRegistry(store)(ws) + WalletRegistry.putBox(emptyBag, tb1).transact(store) + reg.getBox(tb1.box.id).get.scans shouldBe Set(appId1, appId2) + reg.unspentBoxes(appId1).length shouldBe 1 + reg.unspentBoxes(appId2).length shouldBe 1 + reg.updateScans(Set(appId1), tb1.box) + reg.getBox(tb1.box.id).get.scans shouldBe Set(appId1) + reg.unspentBoxes(appId1).length shouldBe 1 + reg.unspentBoxes(appId2).length shouldBe 0 + } + } + } + it should "remove application from a box correctly" in { val appId: ScanId = ScanId @@ 20.toShort @@ -220,7 +241,7 @@ class WalletRegistrySpec } - it should "remove application and then rollback - one app" in { + it should "remove box-scan correspondence and then rollback - one app" in { val scanId: ScanId = ScanId @@ 20.toShort forAll(trackedBoxGen) { tb0 => @@ -239,7 +260,7 @@ class WalletRegistrySpec } } - it should "remove application and then rollback - multiple apps" in { + it should "remove box-scan correspondence and then rollback - multiple apps" in { val scanId: ScanId = ScanId @@ 20.toShort forAll(trackedBoxGen) { tb0 => diff --git a/src/test/scala/org/ergoplatform/utils/Stubs.scala b/src/test/scala/org/ergoplatform/utils/Stubs.scala index 01685d81d2..056c215611 100644 --- a/src/test/scala/org/ergoplatform/utils/Stubs.scala +++ b/src/test/scala/org/ergoplatform/utils/Stubs.scala @@ -16,7 +16,7 @@ import org.ergoplatform.nodeView.wallet._ import org.ergoplatform.nodeView.wallet.persistence.WalletDigest import org.ergoplatform.sanity.ErgoSanity.HT import org.ergoplatform.settings.Constants.HashLength -import org.ergoplatform.wallet.Constants.{ScanId, PaymentsScanId} +import org.ergoplatform.wallet.Constants.{PaymentsScanId, ScanId} import org.ergoplatform.settings._ import org.ergoplatform.utils.generators.{ChainGenerator, ErgoGenerators, ErgoTransactionGenerators} import org.ergoplatform.wallet.boxes.{ChainStatus, TrackedBox} @@ -24,6 +24,7 @@ import org.ergoplatform.wallet.interpreter.ErgoProvingInterpreter import org.ergoplatform.wallet.secrets.DerivationPath import org.ergoplatform.P2PKAddress import org.ergoplatform.nodeView.wallet.scanning.Scan +import org.scalacheck.Gen import scorex.core.app.Version import scorex.core.network.NetworkController.ReceivableMessages.GetConnectedPeers import scorex.core.network.peer.PeerManager.ReceivableMessages.{GetAllPeers, GetBlacklistedPeers} @@ -154,6 +155,8 @@ trait Stubs extends ErgoGenerators with ErgoTestHelpers with ChainGenerator with case LockWallet => () + case RescanWallet => sender ! Success(()) + case GetWalletStatus => sender() ! WalletStatus(true, true, None, ErgoHistory.GenesisHeight) case _: CheckSeed => sender() ! true @@ -246,7 +249,8 @@ trait Stubs extends ErgoGenerators with ErgoTestHelpers with ChainGenerator with spendingTxIdOpt = Some(modifierIdGen.sample.get) ) ) - val walletTxs: Seq[AugWalletTransaction] = Seq(augWalletTransactionGen.sample.get, augWalletTransactionGen.sample.get) + val walletTxs: Seq[AugWalletTransaction] = + Gen.listOf(augWalletTransactionGen).sample.get def props(): Props = Props(new WalletActorStub)