From 512a748e71c9386138bb277be71fcbae43079078 Mon Sep 17 00:00:00 2001 From: Duddino Date: Mon, 25 Nov 2024 15:51:29 +0100 Subject: [PATCH 1/5] Fix 455 (#473) ** AMENDED ** This is a copy of 27b17a914e1d6357bc42714076cf6694d58b2548 with the cypress playback removed. Those had to be removed, due to their big sizes. * Fix private mode on non-private wallets * Add e2e tests * Fix eslint warning * Commit this * Now it works please --------- Co-authored-by: Alessandro Rezzi --- cypress/e2e/private_mode.cy.js | 34 +++++++++++++++++ cypress/support/commands.js | 15 ++++++++ scripts/alerts/Alerts.vue | 1 + scripts/composables/use_wallet.js | 59 +++++++++++++++++------------ scripts/dashboard/WalletBalance.vue | 5 ++- 5 files changed, 89 insertions(+), 25 deletions(-) create mode 100644 cypress/e2e/private_mode.cy.js diff --git a/cypress/e2e/private_mode.cy.js b/cypress/e2e/private_mode.cy.js new file mode 100644 index 000000000..b2bf61352 --- /dev/null +++ b/cypress/e2e/private_mode.cy.js @@ -0,0 +1,34 @@ +describe('public/private mode tests', () => { + beforeEach(() => { + cy.clearDb(); + cy.visit('/'); + cy.waitForLoading().should('be.visible'); + cy.playback('GET', /(xpub|address|getshielddata)/, { + matching: { ignores: ['hostname', 'port'] }, + }).as('sync'); + cy.setExplorer(0); + cy.setNode(1); + cy.goToTab('dashboard'); + cy.importWallet( + 'hawk crash art bottom rookie surprise grit giant fitness entire course spray' + ); + cy.encryptWallet('123456'); + + cy.waitForSync(); + cy.togglePrivateMode(); + }); + + it('switches back to public mode when not available', () => { + // We should be in private mode here + cy.get('[data-testid="shieldModePrefix"]').should('exist'); + cy.deleteWallet(); + // When importing a non shield capable wallet, we should be in public mode + cy.importWallet('DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb'); + cy.get('[data-testid="shieldModePrefix"]').should('not.exist'); + }); + + it('remembers private mode', () => { + cy.visit('/'); + cy.get('[data-testid="shieldModePrefix"]').should('exist'); + }); +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c7dbb46af..5047cf624 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -82,3 +82,18 @@ Cypress.Commands.add('setExplorer', (explorerNameOrIndex) => { cy.goToTab('settings'); cy.get('#explorer').select(explorerNameOrIndex); }); +Cypress.Commands.add('setNode', (nodeNameOrIndex) => { + cy.goToTab('settings'); + cy.get('#node').select(nodeNameOrIndex); +}); + +Cypress.Commands.add('togglePrivateMode', () => { + cy.goToTab('dashboard'); + cy.get('#publicPrivateText').click(); +}); + +Cypress.Commands.add('waitForSync', () => { + cy.contains('[data-testid="alerts"]', 'Sync Finished!', { + timeout: 1000 * 60 * 5, + }); +}); diff --git a/scripts/alerts/Alerts.vue b/scripts/alerts/Alerts.vue index f474d1189..9b8ad5e8a 100644 --- a/scripts/alerts/Alerts.vue +++ b/scripts/alerts/Alerts.vue @@ -52,6 +52,7 @@ watch(alerts, () => {
{ // For now we'll just import the existing one // const wallet = new Wallet(); - // Public/Private Mode will be loaded from disk after 'import-wallet' is emitted - const publicMode = ref(true); - watch(publicMode, (publicMode) => { - doms.domNavbar.classList.toggle('active', !publicMode); - doms.domLightBackground.style.opacity = publicMode ? '1' : '0'; - // Depending on our Receive type, flip to the opposite type. - // i.e: from `address` to `shield`, `shield contact` to `address`, etc - // This reduces steps for someone trying to grab their opposite-type address, which is the primary reason to mode-toggle. - const arrFlipTypes = [ - RECEIVE_TYPES.CONTACT, - RECEIVE_TYPES.ADDRESS, - RECEIVE_TYPES.SHIELD, - ]; - if (arrFlipTypes.includes(cReceiveType)) { - guiToggleReceiveType( - publicMode ? RECEIVE_TYPES.ADDRESS : RECEIVE_TYPES.SHIELD - ); - } - - // Save the mode state to DB - togglePublicMode(publicMode); - }); - const isImported = ref(wallet.isLoaded()); const isViewOnly = ref(wallet.isViewOnly()); const isSynced = ref(wallet.isSynced); @@ -120,6 +97,40 @@ export const useWallet = defineStore('wallet', () => { return res; } ); + + const _publicMode = ref(true); + // Public/Private Mode will be loaded from disk after 'import-wallet' is emitted + const publicMode = computed({ + get() { + // If the wallet is not shield capable, always return true + if (!hasShield.value) return true; + return _publicMode.value; + }, + + set(newValue) { + _publicMode.value = newValue; + const publicMode = _publicMode.value; + doms.domNavbar.classList.toggle('active', !publicMode); + doms.domLightBackground.style.opacity = publicMode ? '1' : '0'; + // Depending on our Receive type, flip to the opposite type. + // i.e: from `address` to `shield`, `shield contact` to `address`, etc + // This reduces steps for someone trying to grab their opposite-type address, which is the primary reason to mode-toggle. + const arrFlipTypes = [ + RECEIVE_TYPES.CONTACT, + RECEIVE_TYPES.ADDRESS, + RECEIVE_TYPES.SHIELD, + ]; + if (arrFlipTypes.includes(cReceiveType)) { + guiToggleReceiveType( + publicMode ? RECEIVE_TYPES.ADDRESS : RECEIVE_TYPES.SHIELD + ); + } + + // Save the mode state to DB + togglePublicMode(publicMode); + }, + }); + const isCreatingTransaction = () => createAndSendTransaction.isLocked(); getEventEmitter().on('toggle-network', async () => { diff --git a/scripts/dashboard/WalletBalance.vue b/scripts/dashboard/WalletBalance.vue index eddbc11a3..e7f08ea73 100644 --- a/scripts/dashboard/WalletBalance.vue +++ b/scripts/dashboard/WalletBalance.vue @@ -376,7 +376,10 @@ function restoreWallet() {  S- S-{{ ticker }}  From e8a01d69963d226de42f184c3b67a693f3a02452 Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Tue, 26 Nov 2024 14:11:08 +0100 Subject: [PATCH 2/5] Integration test for getHistoricalTxs() (#477) * test: Add integration test for historical txs * Add spacing to tests --- tests/integration/wallet/sync.spec.js | 128 ++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/tests/integration/wallet/sync.spec.js b/tests/integration/wallet/sync.spec.js index 541b491b7..709b38aec 100644 --- a/tests/integration/wallet/sync.spec.js +++ b/tests/integration/wallet/sync.spec.js @@ -13,6 +13,7 @@ import { import { refreshChainData } from '../../../scripts/global.js'; import { COIN } from '../../../scripts/chain_params.js'; import { flushPromises } from '@vue/test-utils'; +import { HistoricalTxType } from '../../../scripts/historical_tx.js'; vi.mock('../../../scripts/network/network_manager.js'); @@ -222,7 +223,134 @@ describe('Wallet sync tests', () => { await mineBlocks(1); expect(walletHD.getCurrentAddress()).toBe(newAddress); }); + it('correctly updates historical txs', async () => { + // Keep track of previous historical txs + let prevIds = walletHD.getHistoricalTxs().map((hTx) => { + return hTx.id; + }); + /** + * @type {HistoricalTx[]} + */ + let diffCache = [].concat(walletHD.getHistoricalTxs()); + + // 1) Receive a transaction to the current address + let addr = walletHD.getCurrentAddress(); + let blockHeight = getNetwork().getBlockCount() + 1; + await createAndSendTransaction(walletLegacy, addr, 0.01 * 10 ** 8); + + // Sanity check: Before mining the block the history is unchanged (receiving from external wallet) + getHistDiff(walletHD.getHistoricalTxs(), prevIds, 0); + checkHistPersistence(walletHD.getHistoricalTxs(), diffCache); + + // Mine and assert that a new tx has been added + await mineBlocks(1); + let diff = getHistDiff(walletHD.getHistoricalTxs(), prevIds, 1); + checkHistPersistence(walletHD.getHistoricalTxs(), diffCache); + prevIds.push(diff[0].id); + diffCache.push(diff[0]); + checkHistDiff(diff[0], { + blockHeight, + isToSelf: false, + type: HistoricalTxType.RECEIVED, + receivers: [addr, walletLegacy.getCurrentAddress()], + }); + + // 2) This time send a transaction (To external wallet) + blockHeight = getNetwork().getBlockCount() + 1; + addr = walletLegacy.getCurrentAddress(); + await createAndSendTransaction(walletHD, addr, 0.5 * 10 ** 8); + + // Since walletHD created the tx, it must already be in its history... even without mining a block + diff = getHistDiff(walletHD.getHistoricalTxs(), prevIds, 1); + checkHistPersistence(walletHD.getHistoricalTxs(), diffCache); + checkHistDiff(diff[0], { + blockHeight: -1, // pending block height + isToSelf: false, + type: HistoricalTxType.SENT, + receivers: [addr], + }); + + await mineBlocks(1); + diff = getHistDiff(walletHD.getHistoricalTxs(), prevIds, 1); + checkHistPersistence(walletHD.getHistoricalTxs(), diffCache); + checkHistDiff(diff[0], { + blockHeight, + isToSelf: false, + type: HistoricalTxType.SENT, + receivers: [walletLegacy.getCurrentAddress()], + }); + diffCache.push(diff[0]); + prevIds.push(diff[0].id); + + // 3) Create a self transaction + blockHeight = getNetwork().getBlockCount() + 1; + addr = walletHD.getCurrentAddress(); + await createAndSendTransaction(walletHD, addr, 0.2 * 10 ** 8); + + diff = getHistDiff(walletHD.getHistoricalTxs(), prevIds, 1); + checkHistPersistence(walletHD.getHistoricalTxs(), diffCache); + checkHistDiff(diff[0], { + blockHeight: -1, // pending block height + isToSelf: true, + type: HistoricalTxType.SENT, + receivers: [addr], + }); + + await mineBlocks(1); + diff = getHistDiff(walletHD.getHistoricalTxs(), prevIds, 1); + checkHistPersistence(walletHD.getHistoricalTxs(), diffCache); + checkHistDiff(diff[0], { + blockHeight, + isToSelf: true, + type: HistoricalTxType.SENT, + receivers: [addr], + }); + }); afterAll(() => { vi.clearAllMocks(); }); + + /** + * @param {HistoricalTx[]} historicalTxs + * @param {string[]} txIds + * @param {number} expectedLength - expected size of the diff + * @returns {HistoricalTx[]} the history difference + */ + function getHistDiff(historicalTxs, txIds, expectedLength) { + const diff = historicalTxs.filter((hTx) => { + return !txIds.includes(hTx.id); + }); + expect(diff.length).toBe(expectedLength); + return diff; + } + + /** + * @typedef {Object} SimplifiedHistoricalTx + * @property {number} blockHeight + * @property {HistoricalTxType} type + * @property {boolean} isToSelf + * @property {string[]} receivers - a SUBSET of the receivers of a historical tx + */ + + /** + * @param {HistoricalTx} historicalTx + * @param {SimplifiedHistoricalTx} exp + */ + function checkHistDiff(historicalTx, exp) { + expect(historicalTx.blockHeight).toStrictEqual(exp.blockHeight); + expect(historicalTx.type).toStrictEqual(exp.type); + expect(historicalTx.isToSelf).toStrictEqual(exp.isToSelf); + for (let r of exp.receivers) { + expect(historicalTx.receivers.includes(r)).toBeTruthy(); + } + } + /** + * @param {HistoricalTx[]} historicalTxs + * @param {HistoricalTx[]} diffCache + */ + function checkHistPersistence(historicalTxs, diffCache) { + for (let hTx of diffCache) { + expect(historicalTxs.includes(hTx)).toBeTruthy(); + } + } }); From 4d9258a5e210b6b262392a43ba7acc7790211602 Mon Sep 17 00:00:00 2001 From: BreadJS <83626012+BreadJS@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:24:23 +0100 Subject: [PATCH 3/5] Notification revamp (#468) * [CSS] Update styles * [VUE] New alert layout * [VUE] Update hideAlert name * [CSS] Badge count & and pass count to Alert.vue * [CSS] Update for info and warning icons * Add 'action' data to Alert constructor * Prettier * Testing: hook some of the new functionality in to the frontend NOT finished, and contains some testing code. * [VUE+CSS] Update second button * testing: revert commit used for quicker testing * feat: add ability to run 'actions' from alerts * Prettier * feat: add "Open In Explorer" button to Tx sent Notifications * tests: fix createAlert test * Prettier * tests: test all constructor arguments for createAlert * Update scripts/alerts/Alert.vue * Prettier --------- Co-authored-by: JSKitty Co-authored-by: Alessandro Rezzi Co-authored-by: Duddino --- assets/style/style.css | 122 +++++++++++++++++++++++------- scripts/alerts/Alert.vue | 35 ++++++++- scripts/alerts/Alerts.vue | 20 ++++- scripts/alerts/alert.js | 41 ++++++++-- scripts/composables/use_alerts.js | 12 ++- scripts/global.js | 14 +++- tests/unit/alert.spec.js | 17 ++++- 7 files changed, 215 insertions(+), 46 deletions(-) diff --git a/assets/style/style.css b/assets/style/style.css index 4ae39fa52..ad0f13031 100644 --- a/assets/style/style.css +++ b/assets/style/style.css @@ -2286,7 +2286,7 @@ a { position: fixed; z-index: 10000; right: 15px; - bottom: 0px; + top: 100px; display: flex; flex-direction: column; } @@ -3593,57 +3593,125 @@ select.form-control option { } } +.notifyButtonFirst { + border-bottom-right-radius:0px!important; + margin-right:1px!important; + padding: 0px 21px; +} + +.notifyButtonSecond { + border-bottom-left-radius:0px!important; + margin-left:1px!important; + width:100%; + text-transform: uppercase; +} + +.btn-notification-close { + color: #D5C9EC; + border: 2px solid #8529ED; + border-radius: 0px; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + margin-left: -1px; + margin-bottom: -1px; + margin-right: -1px; + background: rgb(30,20,49); + background: linear-gradient(0deg, rgba(30,20,49,1) 0%, rgba(44,16,79,1) 100%); + font-weight: 500; +} + +.btn-notification-close:hover { + border: 2px solid #8529ED; + color: #c7bbdd; +} + +.btn-notification-action { + color: #D5C9EC; + border-radius: 0px; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + margin-left: -1px; + margin-bottom: -1px; + margin-right: -1px; + background: rgb(30,20,49); + background: linear-gradient(0deg, #741EEA 0%, #9231EE 100%); + font-weight: 500; +} + +.btn-notification-action:hover { + color: #c7bbdd; +} + +.notifyWrapper .notifyBadgeCount { + position: absolute; + right: 0px; + border: 2px solid #9A21FF; + background-color: #431180; + border-radius: 100px; + width: 24px; + height: 24px; + display: flex; + justify-content: center; + align-items: center; + font-size: 13px; + font-weight: 500; +} + .notifyWrapper { opacity: 1; z-index: 999999; - background-color: #320044; - border-radius: 5px; - display: inline-flex; - align-items: stretch; - border: 1px solid #9F00F9; - cursor: pointer; + background-color: #260a47; + border-radius: 11px; + border-bottom-left-radius: 20px; + border-bottom-right-radius: 20px; + border: 1px solid #51199f; margin-bottom: 15px; opacity: 0; transition: all 0.250s ease-in-out; } .notifyWrapper .notifyIcon { - padding: 20px 11px; border-top-left-radius: 5px; border-bottom-left-radius: 5px; - margin-top: -1px; - margin-left: -1px; - margin-bottom: -1px; + border-top-right-radius: 10px; + border-bottom-right-radius: 10px; display: flex; + height: 45px; + margin-top: 17px; + margin-left: -5px; + width: 45px; + justify-content: center; align-items: center; } .notifyWrapper .notify-warning { - background-color: #630808; - border-top: 1px solid #FF0000; - border-left: 1px solid #FF0000; - border-bottom: 1px solid #FF0000; + background: rgb(138,0,29); + background: linear-gradient(0deg, rgba(138,0,29,1) 0%, rgba(181,0,0,1) 100%); + border: 1px solid #FF1C50; } .notifyWrapper .notify-info { - background-color: #084363; - border-top: 1px solid #0095ff; - border-left: 1px solid #0095ff; - border-bottom: 1px solid #0095ff; + background: rgb(47,18,83); + background: linear-gradient(341deg, rgba(47,18,83,1) 0%, rgba(123,101,157,1) 100%); + border: 1px solid #906DB1; } .notifyWrapper .notify-success { - background-color: #1c6308; - border-top: 1px solid #1aff00; - border-left: 1px solid #1aff00; - border-bottom: 1px solid #1aff00; + background: rgb(46,82,0); + background: linear-gradient(341deg, rgba(46,82,0,1) 0%, rgba(95,168,0,1) 100%); + border: 1px solid #69BB00; } .notifyWrapper .notifyText { - padding-left: 11px; - padding-right: 17px; - padding-top: 14px; - padding-bottom: 14px; + padding-left: 14px; + padding-right: 40px; + padding-top: 19px; + padding-bottom: 19px; + color: #8c7aa8; +} + +.notifyWrapper .notifyText b { + color:#DCD1F3; } .sliderStyle .arrow { diff --git a/scripts/alerts/Alert.vue b/scripts/alerts/Alert.vue index efc3971a2..ee4dcd582 100644 --- a/scripts/alerts/Alert.vue +++ b/scripts/alerts/Alert.vue @@ -4,6 +4,8 @@ import { computed, toRefs } from 'vue'; const props = defineProps({ message: String, level: String, + notificationCount: Number, + actionName: String, }); const { message, level } = toRefs(props); @@ -25,14 +27,39 @@ const icon = computed(() => { diff --git a/scripts/alerts/Alerts.vue b/scripts/alerts/Alerts.vue index 9b8ad5e8a..05965465d 100644 --- a/scripts/alerts/Alerts.vue +++ b/scripts/alerts/Alerts.vue @@ -11,15 +11,17 @@ watch(alerts, () => { let count = 1; const pushAlert = () => { if (previousAlert) { - const countStr = count === 1 ? '' : ` (x${count})`; const timeout = previousAlert.created + previousAlert.timeout - Date.now(); const show = timeout > 0; if (!show) return; const alert = ref({ ...previousAlert, - message: `${previousAlert.message}${countStr}`, + message: `${previousAlert.message}`, show, + count, + actionName: previousAlert.actionName, + actionFunc: previousAlert.actionFunc, // Store original message so we can use it as key. // This skips the animation in case of multiple errors original: previousAlert.message, @@ -45,6 +47,15 @@ watch(alerts, () => { pushAlert(); foldedAlerts.value = res; }); + +/** + * Run an 'action' connected to an alert + * @param {import('./alert.js').Alert} cAlert - The caller alert which is running an action + */ +function runAction(cAlert) { + cAlert.actionFunc(); + cAlert.show = false; +} diff --git a/scripts/dashboard/RestoreWallet.vue b/scripts/dashboard/RestoreWallet.vue index adcbbc84a..5cb98fd0b 100644 --- a/scripts/dashboard/RestoreWallet.vue +++ b/scripts/dashboard/RestoreWallet.vue @@ -5,14 +5,17 @@ import Password from '../Password.vue'; import { ALERTS, translation } from '../i18n.js'; import { Database } from '../database.js'; import { decrypt } from '../aes-gcm'; +import { ParsedSecret } from '../parsed_secret'; import { useAlerts } from '../composables/use_alerts.js'; + const { createAlert } = useAlerts(); const props = defineProps({ show: Boolean, reason: String, + wallet: Object, }); -const { show, reason } = toRefs(props); +const { show, reason, wallet } = toRefs(props); const emit = defineEmits(['close', 'import']); const password = ref(''); const passwordInput = ref(null); @@ -24,12 +27,27 @@ watch(show, (show) => { if (!show) password.value = ''; }); +async function importWif(wif, extsk) { + const secret = await ParsedSecret.parse(wif); + if (secret.masterKey) { + await wallet.value.setMasterKey({ mk: secret.masterKey, extsk }); + if (wallet.value.hasShield && !extsk) { + createAlert( + 'warning', + 'Could not decrypt sk even if password is correct, please contact a developer' + ); + } + createAlert('success', ALERTS.WALLET_UNLOCKED, 1500); + } +} + async function submit() { const db = await Database.getInstance(); const account = await db.getAccount(); const wif = await decrypt(account.encWif, password.value); const extsk = await decrypt(account.encExtsk, password.value); if (wif) { + await importWif(wif, extsk); emit('import', wif, extsk); } else { createAlert('warning', ALERTS.INVALID_PASSWORD); diff --git a/scripts/dashboard/WalletButtons.vue b/scripts/dashboard/WalletButtons.vue index 0b76559b0..c6c48c1da 100644 --- a/scripts/dashboard/WalletButtons.vue +++ b/scripts/dashboard/WalletButtons.vue @@ -8,13 +8,13 @@ import pAddressBook from '../../assets/icons/icon-address-book.svg'; import pGift from '../../assets/icons/icon-gift.svg'; import { useNetwork } from '../composables/use_network.js'; import { useWallet } from '../composables/use_wallet.js'; +import { getBlockbookUrl } from '../utils.js'; const wallet = useWallet(); const network = useNetwork(); function getWalletUrl() { - const urlPart = wallet.isHD ? '/xpub/' : '/address/'; - return network.explorerUrl + urlPart + wallet.getKeyToExport(); + return getBlockbookUrl(network.explorerUrl, wallet.getKeyToExport()); } diff --git a/scripts/global.js b/scripts/global.js index 7f5797b0b..3338589b0 100644 --- a/scripts/global.js +++ b/scripts/global.js @@ -1,7 +1,5 @@ -import { COutpoint } from './transaction.js'; import { TransactionBuilder } from './transaction_builder.js'; -import Masternode from './masternode.js'; -import { ALERTS, tr, start as i18nStart, translation } from './i18n.js'; +import { ALERTS, start as i18nStart, translation } from './i18n.js'; import { wallet, hasEncryptedWallet, Wallet } from './wallet.js'; import { getNetwork } from './network/network_manager.js'; import { @@ -15,16 +13,15 @@ import { confirmPopup, sanitizeHTML } from './misc.js'; import { cChainParams, COIN } from './chain_params.js'; import { sleep } from './utils.js'; import { registerWorker } from './native.js'; -import { Address6 } from 'ip-address'; import { getEventEmitter } from './event_bus.js'; -import { Database } from './database.js'; import { checkForUpgrades } from './changelog.js'; -import { FlipDown } from './flipdown.js'; import { createApp } from 'vue'; import Dashboard from './dashboard/Dashboard.vue'; import Alerts from './alerts/Alerts.vue'; import { loadDebug, debugLog, DebugTopics, debugError } from './debug.js'; import Stake from './stake/Stake.vue'; +import MasternodeComponent from './masternode/Masternode.vue'; +import Governance from './governance/Governance.vue'; import { createPinia } from 'pinia'; import { cOracle } from './prices.js'; @@ -50,6 +47,8 @@ const pinia = createPinia(); export const dashboard = createApp(Dashboard).use(pinia).mount('#DashboardTab'); createApp(Stake).use(pinia).mount('#StakingTab'); +createApp(MasternodeComponent).use(pinia).mount('#Masternode'); +createApp(Governance).use(pinia).mount('#Governance'); createApp(SideNavbar).use(pinia).mount('#SideNavbar'); createApp(Alerts).use(pinia).mount('#Alerts'); @@ -78,53 +77,6 @@ export async function start() { 'walletBreakdownLegend' ), domGenHardwareWallet: document.getElementById('generateHardwareWallet'), - //GOVERNANCE ELEMENTS - domGovTab: document.getElementById('governanceTab'), - domGovProposalsTable: document.getElementById('proposalsTable'), - domGovProposalsTableBody: document.getElementById('proposalsTableBody'), - domTotalGovernanceBudget: document.getElementById( - 'totalGovernanceBudget' - ), - domTotalGovernanceBudgetValue: document.getElementById( - 'totalGovernanceBudgetValue' - ), - domAllocatedGovernanceBudget: document.getElementById( - 'allocatedGovernanceBudget' - ), - domAllocatedGovernanceBudgetValue: document.getElementById( - 'allocatedGovernanceBudgetValue' - ), - domAllocatedGovernanceBudget2: document.getElementById( - 'allocatedGovernanceBudget2' - ), - domAllocatedGovernanceBudgetValue2: document.getElementById( - 'allocatedGovernanceBudgetValue2' - ), - domGovProposalsContestedTable: document.getElementById( - 'proposalsContestedTable' - ), - domGovProposalsContestedTableBody: document.getElementById( - 'proposalsContestedTableBody' - ), - //MASTERNODE ELEMENTS - domCreateMasternode: document.getElementById('createMasternode'), - domControlMasternode: document.getElementById('controlMasternode'), - domAccessMasternode: document.getElementById('accessMasternode'), - domMnAccessMasternodeText: document.getElementById( - 'accessMasternodeText' - ), - domMnCreateType: document.getElementById('mnCreateType'), - domMnTextErrors: document.getElementById('mnTextErrors'), - domMnIP: document.getElementById('mnIP'), - domMnTxId: document.getElementById('mnTxId'), - domMnPrivateKey: document.getElementById('mnPrivateKey'), - domMnDashboard: document.getElementById('mnDashboard'), - domMnProtocol: document.getElementById('mnProtocol'), - domMnStatus: document.getElementById('mnStatus'), - domMnNetType: document.getElementById('mnNetType'), - domMnNetIP: document.getElementById('mnNetIP'), - domMnLastSeen: document.getElementById('mnLastSeen'), - domEncryptWalletLabel: document.getElementById('encryptWalletLabel'), domEncryptPasswordCurrent: document.getElementById( 'changePassword-current' @@ -175,11 +127,6 @@ export async function start() { domConfirmModalCancelButton: document.getElementById( 'confirmModalCancelButton' ), - - masternodeLegacyAccessText: - 'Access the masternode linked to this address
Note: the masternode MUST have been already created (however it can be online or offline)
If you want to create a new masternode access with a HD wallet', - masternodeHDAccessText: - "Access your masternodes if you have any! If you don't you can create one", // Aggregate menu screens and links for faster switching arrDomScreens: document.getElementsByClassName('tabcontent'), arrDomScreenLinks: document.getElementsByClassName('tablinks'), @@ -204,7 +151,6 @@ export async function start() { domWalletSettingsBtn: document.getElementById('settingsWalletBtn'), domDisplaySettingsBtn: document.getElementById('settingsDisplayBtn'), domVersion: document.getElementById('version'), - domFlipdown: document.getElementById('flipdown'), domTestnetToggler: document.getElementById('testnetToggler'), domAdvancedModeToggler: document.getElementById('advancedModeToggler'), domAutoLockModeToggler: document.getElementById('autoLockModeToggler'), @@ -294,11 +240,6 @@ async function refreshPriceDisplay() { function subscribeToNetworkEvents() { getEventEmitter().on('new-block', (block) => { debugLog(DebugTopics.GLOBAL, `New block detected! ${block}`); - - // If it's open: update the Governance Dashboard - if (doms.domGovTab.classList.contains('active')) { - updateGovernanceTab(); - } }); getEventEmitter().on('transaction-sent', (success, result) => { @@ -326,15 +267,6 @@ function subscribeToNetworkEvents() { }); } -// WALLET STATE DATA - -let isTestnetLastState = cChainParams.current.isTestnet; - -/** - * @type {FlipDown | null} - */ -let governanceFlipdown = null; - /** * Open a UI 'tab' menu, and close all other tabs, intended for frontend use * @param {Event} evt - The click event target @@ -357,12 +289,6 @@ export function openTab(evt, tabName) { // Close the navbar if it's not already closed if (!doms.domNavbarToggler.className.includes('collapsed')) doms.domNavbarToggler.click(); - - if (tabName === 'Governance') { - updateGovernanceTab(); - } else if (tabName === 'Masternode') { - updateMasternodeTab(); - } } /** @@ -396,25 +322,9 @@ export function optimiseCurrencyLocale(nAmount) { return { nValue, cLocale }; } -/** - * //TODO: remove in Governance VUE PR - * Open the Explorer in a new tab for the current wallet, or a specific address - * @param {string?} strAddress - Optional address to open, if void, the master key is used - */ -export async function openExplorer(strAddress = '') { - const network = useNetwork(); - const toExport = wallet.getKeyToExport(); - if (wallet.isLoaded() && wallet.isHD() && !strAddress) { - window.open(network.explorerUrl + '/xpub/' + toExport, '_blank'); - } else { - window.open(network.explorerUrl + '/address/' + toExport, '_blank'); - } -} - async function loadImages() { const images = [ ['mpw-main-logo', import('../assets/logo.png')], - ['plus-icon', import('../assets/icons/icon-plus.svg')], ['plus-icon2', import('../assets/icons/icon-plus.svg')], ['plus-icon3', import('../assets/icons/icon-plus.svg')], ['del-wallet-icon', import('../assets/icons/icon-bin.svg')], @@ -506,214 +416,6 @@ export function toClipboard(source, caller) { }, 1000); } -export async function govVote(hash, voteCode) { - if ( - (await confirmPopup({ - title: ALERTS.CONFIRM_POPUP_VOTE, - html: ALERTS.CONFIRM_POPUP_VOTE_HTML, - })) === true - ) { - const database = await Database.getInstance(); - const cMasternode = await database.getMasternode(); - if (cMasternode) { - if ((await cMasternode.getStatus()) !== 'ENABLED') { - createAlert('warning', ALERTS.MN_NOT_ENABLED, 6000); - return; - } - const result = await cMasternode.vote(hash.toString(), voteCode); //1 yes 2 no - if (result.includes('Voted successfully')) { - //good vote - cMasternode.storeVote(hash.toString(), voteCode); - await updateGovernanceTab(); - createAlert('success', ALERTS.VOTE_SUBMITTED, 6000); - } else if (result.includes('Error voting :')) { - //If you already voted return an alert - createAlert('warning', ALERTS.VOTED_ALREADY, 6000); - } else if (result.includes('Failure to verify signature.')) { - //wrong masternode private key - createAlert('warning', ALERTS.VOTE_SIG_BAD, 6000); - } else { - //this could be everything - debugError(DebugTopics.GOVERNANCE, result); - createAlert('warning', ALERTS.INTERNAL_ERROR, 6000); - } - } else { - createAlert('warning', ALERTS.MN_ACCESS_BEFORE_VOTE, 6000); - } - } -} - -/** - * Start a Masternode via a signed network broadcast - * @param {boolean} fRestart - Whether this is a Restart or a first Start - */ -export async function startMasternode(fRestart = false) { - const database = await Database.getInstance(); - const cMasternode = await database.getMasternode(wallet.getMasterKey()); - if (cMasternode) { - if ( - wallet.isViewOnly() && - !(await restoreWallet(translation.walletUnlockMNStart)) - ) - return; - if (await cMasternode.start()) { - const strMsg = fRestart ? ALERTS.MN_RESTARTED : ALERTS.MN_STARTED; - createAlert('success', strMsg, 4000); - } else { - const strMsg = fRestart - ? ALERTS.MN_RESTART_FAILED - : ALERTS.MN_START_FAILED; - createAlert('warning', strMsg, 4000); - } - } -} - -export async function destroyMasternode() { - const database = await Database.getInstance(); - const cMasternode = await database.getMasternode(wallet.getMasterKey()); - if (cMasternode) { - // Unlock the coin and update the balance - wallet.unlockCoin( - new COutpoint({ - txid: cMasternode.collateralTxId, - n: cMasternode.outidx, - }) - ); - - database.removeMasternode(wallet.getMasterKey()); - createAlert('success', ALERTS.MN_DESTROYED, 5000); - updateMasternodeTab(); - } -} - -/** - * Takes an ip address and adds the port. - * If it's a tor address, ip.onion:port will be used (e.g. expyuzz4wqqyqhjn.onion:12345) - * If it's an IPv4 address, ip:port will be used, (e.g. 127.0.0.1:12345) - * If it's an IPv6 address, [ip]:port will be used, (e.g. [::1]:12345) - * @param {String} ip - Ip address with or without port - * @returns {String} - */ -function parseIpAddress(ip) { - // IPv4 or tor without port - if (ip.match(/\d+\.\d+\.\d+\.\d+/) || ip.match(/\w+\.onion/)) { - return `${ip}:${cChainParams.current.MASTERNODE_PORT}`; - } - - // IPv4 or tor with port - if (ip.match(/\d+\.\d+\.\d+\.\d+:\d+/) || ip.match(/\w+\.onion:\d+/)) { - return ip; - } - - // IPv6 without port - if (Address6.isValid(ip)) { - return `[${ip}]:${cChainParams.current.MASTERNODE_PORT}`; - } - - const groups = /\[(.*)\]:\d+/.exec(ip); - if (groups !== null && groups.length > 1) { - // IPv6 with port - if (Address6.isValid(groups[1])) { - return ip; - } - } - - // If we haven't returned yet, the address was invalid. - return null; -} - -export async function importMasternode() { - const mnPrivKey = doms.domMnPrivateKey.value; - const address = parseIpAddress(doms.domMnIP.value); - if (!address) { - createAlert('warning', ALERTS.MN_BAD_IP, 5000); - return; - } - if (!mnPrivKey) { - createAlert('warning', ALERTS.MN_BAD_PRIVKEY, 5000); - return; - } - - let collateralTxId; - let outidx; - let collateralPrivKeyPath; - doms.domMnIP.value = ''; - doms.domMnPrivateKey.value = ''; - - if (!wallet.isHD()) { - // Find the first UTXO matching the expected collateral size - const cCollaUTXO = wallet.getMasternodeUTXOs()[0]; - const balance = wallet.balance; - // If there's no valid UTXO, exit with a contextual message - if (!cCollaUTXO) { - if (balance < cChainParams.current.collateralInSats) { - // Not enough balance to create an MN UTXO - const amount = - (cChainParams.current.collateralInSats - balance) / COIN; - const ticker = cChainParams.current.TICKER; - createAlert( - 'warning', - tr(ALERTS.MN_NOT_ENOUGH_COLLAT, [ - { amount: amount }, - { ticker: ticker }, - ]), - 10000 - ); - } else { - // Balance is capable of a masternode, just needs to be created - // TODO: this UX flow is weird, is it even possible? perhaps we can re-design this entire function accordingly - const amount = cChainParams.current.collateralInSats / COIN; - const ticker = cChainParams.current.TICKER; - createAlert( - 'warning', - tr(ALERTS.MN_ENOUGH_BUT_NO_COLLAT, [ - { amount }, - { ticker }, - ]), - 10000 - ); - } - return; - } - - collateralTxId = cCollaUTXO.outpoint.txid; - outidx = cCollaUTXO.outpoint.n; - collateralPrivKeyPath = 'legacy'; - } else { - const path = doms.domMnTxId.value; - let masterUtxo; - const utxos = wallet.getMasternodeUTXOs(); - for (const u of utxos) { - if ( - u.value === cChainParams.current.collateralInSats && - wallet.getPath(u.script) === path - ) { - masterUtxo = u; - } - } - - // sanity check: - if (masterUtxo.value !== cChainParams.current.collateralInSats) { - return createAlert('warning', ALERTS.MN_COLLAT_NOT_SUITABLE, 10000); - } - collateralTxId = masterUtxo.outpoint.txid; - outidx = masterUtxo.outpoint.n; - collateralPrivKeyPath = path; - } - doms.domMnTxId.value = ''; - - const cMasternode = new Masternode({ - walletPrivateKeyPath: collateralPrivKeyPath, - mnPrivateKey: mnPrivKey, - collateralTxId: collateralTxId, - outidx: outidx, - addr: address, - }); - - await refreshMasternodeData(cMasternode, true); - await updateMasternodeTab(); -} - export async function accessOrImportWallet() { // Hide and Reset the Vanity address input @@ -791,943 +493,6 @@ export async function restoreWallet(strReason = '') { return await dashboard.restoreWallet(strReason); } -/** A lock to prevent rendering the Governance Dashboard multiple times */ -let fRenderingGovernance = false; - -/** - * Fetch Governance data and re-render the Governance UI - */ -export async function updateGovernanceTab() { - if (fRenderingGovernance) return; - fRenderingGovernance = true; - - // Setup the Superblock countdown (if not already done), no need to block thread with await, either. - if (isTestnetLastState !== cChainParams.current.isTestnet) { - // Reset flipdown - governanceFlipdown = null; - doms.domFlipdown.innerHTML = ''; - } - - // Update governance counter when testnet/mainnet has been switched - if (!governanceFlipdown && blockCount > 0) { - getNetwork() - .getNextSuperblock() - .then((nSuperblock) => { - // The estimated time to the superblock (using the block target and remaining blocks) - const nTimestamp = - Date.now() / 1000 + (nSuperblock - blockCount) * 60; - governanceFlipdown = new FlipDown(nTimestamp).start(); - }); - isTestnetLastState = cChainParams.current.isTestnet; - } - - // Fetch all proposals from the network - const arrProposals = await getNetwork().getProposals(); - - /* Sort proposals into two categories - - Standard (Proposal is either new with <100 votes, or has a healthy vote count) - - Contested (When a proposal may be considered spam, malicious, or simply highly contestable) - */ - const arrStandard = arrProposals.filter( - (a) => a.Yeas + a.Nays < 100 || a.Ratio > 0.25 - ); - const arrContested = arrProposals.filter( - (a) => a.Yeas + a.Nays >= 100 && a.Ratio <= 0.25 - ); - - // Render Proposals - await Promise.all([ - renderProposals(arrStandard, false), - renderProposals(arrContested, true), - ]); - - // Remove lock - fRenderingGovernance = false; -} - -/** - * @typedef {Object} ProposalCache - * @property {number} nSubmissionHeight - The submission height of the proposal. - * @property {string} txid - The transaction ID of the proposal (string). - * @property {boolean} fFetching - Indicates whether the proposal is currently being fetched or not. - */ - -/** - * An array of Proposal Finalisation caches - * @type {Array} - */ -const arrProposalFinalisationCache = []; - -/** - * Asynchronously wait for a Proposal Tx to confirm, then cache the height. - * - * Do NOT await unless you want to lock the thread for a long time. - * @param {ProposalCache} cProposalCache - The proposal cache to wait for - * @returns {Promise} Returns `true` once the block height is cached - */ -async function waitForSubmissionBlockHeight(cProposalCache) { - let nHeight = null; - - // Wait in a permanent throttled loop until we successfully fetch the block - const cNet = getNetwork(); - while (true) { - // If a proposal is already fetching, then consequtive calls will be rejected - cProposalCache.fFetching = true; - - // Attempt to fetch the submission Tx (may not exist yet!) - let cTx = null; - try { - cTx = await cNet.getTxInfo(cProposalCache.txid); - } catch (_) {} - - if (!cTx || !cTx.blockHeight) { - // Didn't get the TX, throttle the thread by sleeping for a bit, then try again. - await sleep(30000); - } else { - nHeight = cTx.blockHeight; - break; - } - } - - // Update the proposal finalisation cache - cProposalCache.nSubmissionHeight = nHeight; - - return true; -} - -/** - * Create a Status String for a proposal's finalisation status - * @param {ProposalCache} cPropCache - The proposal cache to check - * @returns {string} The string status, for display purposes - */ -function getProposalFinalisationStatus(cPropCache) { - // Confirmations left until finalisation, by network consensus - const nConfsLeft = - cPropCache.nSubmissionHeight + - cChainParams.current.proposalFeeConfirmRequirement - - blockCount; - - if (cPropCache.nSubmissionHeight === 0 || blockCount === 0) { - return translation.proposalFinalisationConfirming; - } else if (nConfsLeft > 0) { - return ( - nConfsLeft + - ' block' + - (nConfsLeft === 1 ? '' : 's') + - ' ' + - translation.proposalFinalisationRemaining - ); - } else if (Math.abs(nConfsLeft) >= cChainParams.current.budgetCycleBlocks) { - return translation.proposalFinalisationExpired; - } else { - return translation.proposalFinalisationReady; - } -} - -/** - * - * @param {Object} cProposal - A local proposal to add to the cache tracker - * @returns {ProposalCache} - The finalisation cache object pointer of the local proposal - */ -function addProposalToFinalisationCache(cProposal) { - // If it exists, return the existing cache - /** @type ProposalCache */ - let cPropCache = arrProposalFinalisationCache.find( - (a) => a.txid === cProposal.mpw.txid - ); - if (cPropCache) return cPropCache; - - // Create a new cache - cPropCache = { - nSubmissionHeight: 0, - txid: cProposal.mpw.txid, - fFetching: false, - }; - arrProposalFinalisationCache.push(cPropCache); - - // Return the object 'pointer' in the array for further updating - return cPropCache; -} - -/** - * Render Governance proposal objects to a given Proposal category - * @param {Array} arrProposals - The proposals to render - * @param {boolean} fContested - The proposal category - */ -async function renderProposals(arrProposals, fContested) { - // Set the total budget - doms.domTotalGovernanceBudget.innerText = ( - cChainParams.current.maxPayment / COIN - ).toLocaleString('en-gb'); - - // Update total budget in user's currency - const nPrice = cOracle.getCachedPrice(strCurrency); - const nCurrencyValue = (cChainParams.current.maxPayment / COIN) * nPrice; - const { nValue, cLocale } = optimiseCurrencyLocale(nCurrencyValue); - doms.domTotalGovernanceBudgetValue.innerHTML = - nValue.toLocaleString('en-gb', cLocale) + - ' ' + - strCurrency.toUpperCase() + - ''; - - // Select the table based on the proposal category - const domTable = fContested - ? doms.domGovProposalsContestedTableBody - : doms.domGovProposalsTableBody; - - // Render the proposals in the relevent table - const database = await Database.getInstance(); - const cMasternode = await database.getMasternode(); - - if (!fContested) { - const localProposals = - (await database.getAccount())?.localProposals?.map((p) => { - return { - Name: p.name, - URL: p.url, - PaymentAddress: p.address, - MonthlyPayment: p.monthlyPayment / COIN, - RemainingPaymentCount: p.nPayments, - TotalPayment: p.nPayments * (p.monthlyPayment / COIN), - Yeas: 0, - Nays: 0, - local: true, - Ratio: 0, - IsEstablished: false, - mpw: p, - }; - }) || []; - arrProposals = localProposals.concat(arrProposals); - } - arrProposals = await Promise.all( - arrProposals.map(async (p) => { - return { - YourVote: - cMasternode && p.Hash - ? await cMasternode.getVote(p.Name, p.Hash) - : null, - ...p, - }; - }) - ); - - // Fetch the Masternode count for proposal status calculations - const cMasternodes = await getNetwork().getMasternodeCount(); - - let totalAllocatedAmount = 0; - - // Wipe the current table and start rendering proposals - let i = 0; - domTable.innerHTML = ''; - for (const cProposal of arrProposals) { - const domRow = domTable.insertRow(); - - const domStatus = domRow.insertCell(); - domStatus.classList.add('governStatusCol'); - if (!fContested) { - domStatus.setAttribute( - 'onclick', - `if(document.getElementById('governMob${i}').classList.contains('d-none')) { document.getElementById('governMob${i}').classList.remove('d-none'); } else { document.getElementById('governMob${i}').classList.add('d-none'); }` - ); - } else { - domStatus.setAttribute( - 'onclick', - `if(document.getElementById('governMobCon${i}').classList.contains('d-none')) { document.getElementById('governMobCon${i}').classList.remove('d-none'); } else { document.getElementById('governMobCon${i}').classList.add('d-none'); }` - ); - } - - // Add border radius to last row - if (arrProposals.length - 1 === i) { - domStatus.classList.add('bblr-7p'); - } - - // Net Yes calculation - const { Yeas, Nays } = cProposal; - const nNetYes = Yeas - Nays; - const nNetYesPercent = (nNetYes / cMasternodes.enabled) * 100; - - // Proposal Status calculation - const nRequiredVotes = cMasternodes.enabled / 10; - const nMonthlyPayment = parseInt(cProposal.MonthlyPayment); - - // Initial state is assumed to be "Not enough votes" - let strStatus = translation.proposalFailing; - let strFundingStatus = translation.proposalNotFunded; - let strColourClass = 'No'; - - // Proposal Status calculations - if (nNetYes < nRequiredVotes) { - // Scenario 1: Not enough votes, default scenario - } else if (!cProposal.IsEstablished) { - // Scenario 2: Enough votes, but not established - strFundingStatus = translation.proposalTooYoung; - } else if ( - nMonthlyPayment + totalAllocatedAmount > - cChainParams.current.maxPayment / COIN - ) { - // Scenario 3: Enough votes, and established, but over-allocating the budget - strStatus = translation.proposalPassing; - strFundingStatus = translation.proposalOverBudget; - strColourClass = 'OverAllocated'; - } else { - // Scenario 4: Enough votes, and established - strStatus = translation.proposalPassing; - strFundingStatus = translation.proposalFunded; - strColourClass = 'Yes'; - - // Allocate this with the budget - totalAllocatedAmount += nMonthlyPayment; - } - - // Funding Status and allocation calculations - if (cProposal.local) { - // Check the finalisation cache - const cPropCache = addProposalToFinalisationCache(cProposal); - if (!cPropCache.fFetching) { - waitForSubmissionBlockHeight(cPropCache).then( - updateGovernanceTab - ); - } - const strLocalStatus = getProposalFinalisationStatus(cPropCache); - const finalizeButton = document.createElement('button'); - finalizeButton.className = 'pivx-button-small '; - finalizeButton.innerHTML = ''; - - if ( - strLocalStatus === translation.proposalFinalisationReady || - strLocalStatus === translation.proposalFinalisationExpired - ) { - finalizeButton.addEventListener('click', async () => { - const result = await Masternode.finalizeProposal( - cProposal.mpw - ); - - const deleteProposal = async () => { - // Fetch Account - const account = await database.getAccount(); - - // Find index of Account local proposal to remove - const nProposalIndex = account.localProposals.findIndex( - (p) => p.txid === cProposal.mpw.txid - ); - - // If found, remove the proposal and update the account with the modified localProposals array - if (nProposalIndex > -1) { - // Remove our proposal from it - account.localProposals.splice(nProposalIndex, 1); - - // Update the DB - await database.updateAccount(account, true); - } - }; - - if (result.ok) { - deleteProposal(); - // Create a prompt showing the finalisation success, vote hash, and further details - confirmPopup({ - title: translation.PROPOSAL_FINALISED + ' 🚀', - html: `

${ - translation.popupProposalFinalisedNote - }

${ - translation.popupProposalVoteHash - }
${sanitizeHTML( - result.hash - )}

${ - translation.popupProposalFinalisedSignoff - } 👋

`, - hideConfirm: true, - }); - updateGovernanceTab(); - } else { - if (result.err === 'unconfirmed') { - createAlert( - 'warning', - ALERTS.PROPOSAL_UNCONFIRMED, - 5000 - ); - } else if (result.err === 'invalid') { - createAlert( - 'warning', - ALERTS.PROPOSAL_EXPIRED, - 5000 - ); - deleteProposal(); - updateGovernanceTab(); - } else { - createAlert( - 'warning', - ALERTS.PROPOSAL_FINALISE_FAIL - ); - } - } - }); - } else { - finalizeButton.style.opacity = 0.5; - finalizeButton.style.cursor = 'default'; - } - - domStatus.innerHTML = ` - - ${strLocalStatus}
-
- - - `; - domStatus.appendChild(finalizeButton); - } else { - domStatus.innerHTML = ` - - ${strStatus}
- (${strFundingStatus})
-
- - ${nNetYesPercent.toFixed(1)}%
- ${translation.proposalNetYes} -
- - - `; - } - - // Name, Payment Address and URL hyperlink - const domNameAndURL = domRow.insertCell(); - domNameAndURL.style = 'vertical-align: middle;'; - - // IMPORTANT: Sanitise all of our HTML or a rogue server or malicious proposal could perform a cross-site scripting attack - domNameAndURL.innerHTML = `${sanitizeHTML( - cProposal.Name - )}
- ${sanitizeHTML( - cProposal.PaymentAddress.slice(0, 10) + '...' - )}`; - - // Convert proposal amount to user's currency - const nProposalValue = nMonthlyPayment * nPrice; - const { nValue } = optimiseCurrencyLocale(nProposalValue); - const strProposalCurrency = nValue.toLocaleString('en-gb', cLocale); - - // Payment Schedule and Amounts - const domPayments = domRow.insertCell(); - domPayments.classList.add('for-desktop'); - domPayments.style = 'vertical-align: middle;'; - domPayments.innerHTML = `${sanitizeHTML( - nMonthlyPayment.toLocaleString('en-gb', ',', '.') - )} ${ - cChainParams.current.TICKER - }
- ${strProposalCurrency} ${strCurrency.toUpperCase()}
- - ${sanitizeHTML( - cProposal['RemainingPaymentCount'] - )} ${ - translation.proposalPaymentsRemaining - } ${sanitizeHTML( - parseInt(cProposal.TotalPayment).toLocaleString('en-gb', ',', '.') - )} ${cChainParams.current.TICKER} ${ - translation.proposalPaymentTotal - }`; - - // Vote Counts and Consensus Percentages - const domVoteCounters = domRow.insertCell(); - domVoteCounters.classList.add('for-desktop'); - domVoteCounters.style = 'vertical-align: middle;'; - - const nLocalPercent = cProposal.Ratio * 100; - domVoteCounters.innerHTML = `${parseFloat( - nLocalPercent - ).toLocaleString( - 'en-gb', - { minimumFractionDigits: 0, maximumFractionDigits: 1 }, - ',', - '.' - )}%
-
${sanitizeHTML( - Yeas - )}
/ -
${sanitizeHTML( - Nays - )}
- `; - - // Voting Buttons for Masternode owners (MNOs) - let voteBtn; - if (cProposal.local) { - const domVoteBtns = domRow.insertCell(); - domVoteBtns.classList.add('for-desktop'); - domVoteBtns.style = 'vertical-align: middle;'; - voteBtn = ''; - } else { - let btnYesClass = 'pivx-button-small govYesBtnMob'; - let btnNoClass = - 'pivx-button-outline pivx-button-outline-small govNoBtnMob'; - if (cProposal.YourVote) { - if (cProposal.YourVote === 1) { - btnYesClass += ' pivx-button-big-yes-gov'; - } else { - btnNoClass += ' pivx-button-big-no-gov'; - } - } - - const domVoteBtns = domRow.insertCell(); - domVoteBtns.style = - 'padding-top: 30px; vertical-align: middle; display: flex; justify-content: center; align-items: center;'; - const domNoBtn = document.createElement('div'); - domNoBtn.className = btnNoClass; - domNoBtn.style.width = 'fit-content'; - domNoBtn.innerHTML = `${translation.no}`; - domNoBtn.onclick = () => govVote(cProposal.Hash, 2); - - const domYesBtn = document.createElement('button'); - domYesBtn.className = btnYesClass; - domYesBtn.innerText = translation.yes; - domYesBtn.onclick = () => govVote(cProposal.Hash, 1); - - // Add border radius to last row - if (arrProposals.length - 1 === i) { - domVoteBtns.classList.add('bbrr-7p'); - } - - domVoteBtns.classList.add('for-desktop'); - domVoteBtns.appendChild(domNoBtn); - domVoteBtns.appendChild(domYesBtn); - - domNoBtn.setAttribute( - 'onclick', - `MPW.govVote('${cProposal.Hash}', 2)` - ); - domYesBtn.setAttribute( - 'onclick', - `MPW.govVote('${cProposal.Hash}', 1);` - ); - - domYesBtn.setAttribute('style', `height:36px;`); - voteBtn = domNoBtn.outerHTML + domYesBtn.outerHTML; - } - - // Create extended row for mobile - const mobileDomRow = domTable.insertRow(); - const mobileExtended = mobileDomRow.insertCell(); - mobileExtended.style = 'vertical-align: middle;'; - if (!fContested) { - mobileExtended.id = `governMob${i}`; - } else { - mobileExtended.id = `governMobCon${i}`; - } - mobileExtended.colSpan = '2'; - mobileExtended.classList.add('text-left'); - mobileExtended.classList.add('d-none'); - mobileExtended.classList.add('for-mobile'); - mobileExtended.innerHTML = ` -
-
- ${translation.govTablePayment} -
-
- ${sanitizeHTML( - nMonthlyPayment.toLocaleString('en-gb', ',', '.') - )} ${ - cChainParams.current.TICKER - } ${strProposalCurrency} - - ${sanitizeHTML( - cProposal['RemainingPaymentCount'] - )} ${translation.proposalPaymentsRemaining} ${sanitizeHTML( - parseInt(cProposal.TotalPayment).toLocaleString('en-gb', ',', '.') - )} ${cChainParams.current.TICKER} ${ - translation.proposalPaymentTotal - } -
-
-
-
-
- ${translation.govTableVotes} -
-
- ${parseFloat(nLocalPercent).toLocaleString( - 'en-gb', - { minimumFractionDigits: 0, maximumFractionDigits: 1 }, - ',', - '.' - )}% -
${sanitizeHTML( - Yeas - )}
/ -
${sanitizeHTML( - Nays - )}
-
-
-
-
-
- ${translation.govTableVote} -
-
- ${voteBtn} -
-
`; - - i++; - } - - // Show allocated budget - if (!fContested) { - const strAlloc = sanitizeHTML( - totalAllocatedAmount.toLocaleString('en-gb') - ); - doms.domAllocatedGovernanceBudget.innerHTML = strAlloc; - doms.domAllocatedGovernanceBudget2.innerHTML = strAlloc; - - // Update allocated budget in user's currency - const nCurrencyValue = totalAllocatedAmount * nPrice; - const { nValue } = optimiseCurrencyLocale(nCurrencyValue); - const strAllocCurrency = - nValue.toLocaleString('en-gb', cLocale) + - ' ' + - strCurrency.toUpperCase() + - ''; - doms.domAllocatedGovernanceBudgetValue.innerHTML = strAllocCurrency; - doms.domAllocatedGovernanceBudgetValue2.innerHTML = strAllocCurrency; - } -} - -export async function updateMasternodeTab() { - //TODO: IN A FUTURE ADD MULTI-MASTERNODE SUPPORT BY SAVING MNs with which you logged in the past. - // Ensure a wallet is loaded - doms.domMnTextErrors.innerHTML = ''; - doms.domAccessMasternode.style.display = 'none'; - doms.domCreateMasternode.style.display = 'none'; - doms.domMnDashboard.style.display = 'none'; - - if (!wallet.isLoaded()) { - doms.domMnTextErrors.innerHTML = - 'Please ' + - ((await hasEncryptedWallet()) ? 'unlock' : 'import') + - ' your COLLATERAL WALLET first.'; - return; - } - - if (!wallet.isSynced) { - doms.domMnTextErrors.innerHTML = - 'Your wallet is empty or still loading, re-open the tab in a few seconds!'; - return; - } - - const database = await Database.getInstance(); - - let cMasternode = await database.getMasternode(); - // If the collateral is missing (spent, or switched wallet) then remove the current MN - if (cMasternode) { - if ( - !wallet.isCoinLocked( - new COutpoint({ - txid: cMasternode.collateralTxId, - n: cMasternode.outidx, - }) - ) - ) { - database.removeMasternode(); - cMasternode = null; - } - } - - doms.domControlMasternode.style.display = cMasternode ? 'block' : 'none'; - - // first case: the wallet is not HD and it is not hardware, so in case the wallet has collateral the user can check its status and do simple stuff like voting - if (!wallet.isHD()) { - doms.domMnAccessMasternodeText.innerHTML = - doms.masternodeLegacyAccessText; - doms.domMnTxId.style.display = 'none'; - // Find the first UTXO matching the expected collateral size - const cCollaUTXO = wallet.getMasternodeUTXOs()[0]; - - const balance = wallet.balance; - if (cMasternode) { - await refreshMasternodeData(cMasternode); - doms.domMnDashboard.style.display = ''; - } else if (cCollaUTXO) { - doms.domMnTxId.style.display = 'none'; - doms.domAccessMasternode.style.display = 'block'; - } else if (balance < cChainParams.current.collateralInSats) { - // The user needs more funds - doms.domMnTextErrors.innerHTML = - 'You need ' + - (cChainParams.current.collateralInSats - balance) / COIN + - ' more ' + - cChainParams.current.TICKER + - ' to create a Masternode!'; - } else { - // The user has the funds, but not an exact collateral, prompt for them to create one - doms.domCreateMasternode.style.display = 'flex'; - doms.domMnTxId.style.display = 'none'; - doms.domMnTxId.innerHTML = ''; - } - } else { - doms.domMnTxId.style.display = 'none'; - doms.domMnTxId.innerHTML = ''; - doms.domMnAccessMasternodeText.innerHTML = doms.masternodeHDAccessText; - - // First UTXO for each address in HD - const mapCollateralPath = new Map(); - - // Aggregate all valid Masternode collaterals into a map of Path <--> Collateral - for (const cUTXO of wallet.getMasternodeUTXOs()) { - mapCollateralPath.set(wallet.getPath(cUTXO.script), cUTXO); - } - const fHasCollateral = mapCollateralPath.size > 0; - // If there's no loaded MN, but valid collaterals, display the configuration screen - if (!cMasternode && fHasCollateral) { - doms.domMnTxId.style.display = 'block'; - doms.domAccessMasternode.style.display = 'block'; - - for (const [key] of mapCollateralPath) { - const option = document.createElement('option'); - option.value = key; - option.innerText = wallet.getAddressFromPath(key); - doms.domMnTxId.appendChild(option); - } - } - - // If there's no collateral found, display the creation UI - if (!fHasCollateral && !cMasternode) - doms.domCreateMasternode.style.display = 'flex'; - - // If we a loaded Masternode, display the Dashboard - if (cMasternode) { - // Refresh the display - refreshMasternodeData(cMasternode); - doms.domMnDashboard.style.display = ''; - } - } -} - -async function refreshMasternodeData(cMasternode, fAlert = false) { - const cMasternodeData = await cMasternode.getFullData(); - - debugLog(DebugTopics.GLOBAL, ' ---- NEW MASTERNODE DATA (Debug Mode) ----'); - debugLog(DebugTopics.GLOBAL, cMasternodeData); - debugLog(DebugTopics.GLOBAL, '---- END MASTERNODE DATA (Debug Mode) ----'); - - // If we have MN data available, update the dashboard - if (cMasternodeData && cMasternodeData.status !== 'MISSING') { - doms.domMnTextErrors.innerHTML = ''; - doms.domMnProtocol.innerText = `(${sanitizeHTML( - cMasternodeData.version - )})`; - doms.domMnStatus.innerText = sanitizeHTML(cMasternodeData.status); - doms.domMnNetType.innerText = sanitizeHTML( - cMasternodeData.network.toUpperCase() - ); - doms.domMnNetIP.innerText = cMasternode.addr; - doms.domMnLastSeen.innerText = new Date( - cMasternodeData.lastseen * 1000 - ).toLocaleTimeString(); - } - - if (cMasternodeData.status === 'MISSING') { - doms.domMnTextErrors.innerHTML = - 'Masternode is currently OFFLINE'; - if ( - wallet.isHardwareWallet() || - !wallet.isViewOnly() || - (await restoreWallet(translation.walletUnlockCreateMN)) - ) { - createAlert('warning', ALERTS.MN_OFFLINE_STARTING, 6000); - // try to start the masternode - const started = await cMasternode.start(); - if (started) { - doms.domMnTextErrors.innerHTML = ALERTS.MN_STARTED; - createAlert('success', ALERTS.MN_STARTED_ONLINE_SOON, 6000); - const database = await Database.getInstance(); - await database.addMasternode(cMasternode); - wallet.lockCoin( - new COutpoint({ - txid: cMasternode.collateralTxId, - n: cMasternode.outidx, - }) - ); - } else { - doms.domMnTextErrors.innerHTML = ALERTS.MN_START_FAILED; - createAlert('warning', ALERTS.MN_START_FAILED, 6000); - } - } - } else if ( - cMasternodeData.status === 'ENABLED' || - cMasternodeData.status === 'PRE_ENABLED' - ) { - if (fAlert) - createAlert( - 'success', - `${ALERTS.MN_STATUS_IS} ${sanitizeHTML( - cMasternodeData.status - )} `, - 6000 - ); - const database = await Database.getInstance(); - await database.addMasternode(cMasternode); - wallet.lockCoin( - new COutpoint({ - txid: cMasternode.collateralTxId, - n: cMasternode.outidx, - }) - ); - } else if (cMasternodeData.status === 'REMOVED') { - const state = cMasternodeData.status; - doms.domMnTextErrors.innerHTML = tr(ALERTS.MN_STATE, [ - { state: state }, - ]); - if (fAlert) - createAlert( - 'warning', - tr(ALERTS.MN_STATE, [{ state: state }]), - 6000 - ); - } else { - // connection problem - doms.domMnTextErrors.innerHTML = ALERTS.MN_CANT_CONNECT; - if (fAlert) createAlert('warning', ALERTS.MN_CANT_CONNECT, 6000); - } - - // Return the data in case the caller needs additional context - return cMasternodeData; -} - -export async function createProposal() { - // Must have a wallet - if (!wallet.isLoaded()) { - return createAlert('warning', ALERTS.PROPOSAL_IMPORT_FIRST, 4500); - } - // Wallet must be encrypted - if (!(await hasEncryptedWallet())) { - return createAlert( - 'warning', - tr(translation.popupProposalEncryptFirst, [ - { button: translation.secureYourWallet }, - ]), - 4500 - ); - } - // Wallet must be unlocked - if ( - wallet.isViewOnly() && - !(await restoreWallet(translation.walletUnlockProposal)) - ) { - return; - } - // Must have enough funds - if (wallet.balance * COIN < cChainParams.current.proposalFee) { - return createAlert('warning', ALERTS.PROPOSAL_NOT_ENOUGH_FUNDS, 4500); - } - - // Create the popup, wait for the user to confirm or cancel - const fConfirmed = await confirmPopup({ - title: `

${translation.popupCreateProposal}

- ${ - translation.popupCreateProposalCost - } ${cChainParams.current.proposalFee / COIN} ${ - cChainParams.current.TICKER - }`, - html: `
-

Proposal name

-
- -

URL

-
- -

Duration in cycles

-
- -

${ - cChainParams.current.TICKER - } per cycle

- ${ - !fAdvancedMode ? '
' : '' - } - -

Proposal Address

- -
`, - wideModal: true, - }); - - // If the user cancelled, then we return - if (!fConfirmed) return; - - const strTitle = document.getElementById('proposalTitle').value.trim(); - const strUrl = document.getElementById('proposalUrl').value.trim(); - const numCycles = parseInt( - document.getElementById('proposalCycles').value.trim() - ); - const numPayment = parseInt( - document.getElementById('proposalPayment').value.trim() - ); - - // If Advanced Mode is enabled and an address is given, use the provided address, otherwise, generate a new one - const strAddress = - document.getElementById('proposalAddress').value.trim() || - wallet.getNewChangeAddress(); - const nextSuperblock = await getNetwork().getNextSuperblock(); - const proposal = { - name: strTitle, - url: strUrl, - nPayments: numCycles, - start: nextSuperblock, - address: strAddress, - monthlyPayment: numPayment * COIN, - }; - - const isValid = Masternode.isValidProposal(proposal); - if (!isValid.ok) { - createAlert( - 'warning', - `${ALERTS.PROPOSAL_INVALID_ERROR} ${isValid.err}`, - 7500 - ); - return; - } - - const hash = Masternode.createProposalHash(proposal); - const { ok, txid } = await createAndSendTransaction({ - address: hash, - amount: cChainParams.current.proposalFee, - isProposal: true, - }); - if (ok) { - proposal.txid = txid; - const database = await Database.getInstance(); - - // Fetch our Account, add the proposal to it - const account = await database.getAccount(); - account.localProposals.push(proposal); - - // Update the DB - await database.updateAccount(account); - createAlert('success', translation.PROPOSAL_CREATED, 10000); - updateGovernanceTab(); - } -} - export async function refreshChainData() { const cNet = getNetwork(); // Fetch block count diff --git a/scripts/governance/BudgetAllocated.vue b/scripts/governance/BudgetAllocated.vue new file mode 100644 index 000000000..15fa858d6 --- /dev/null +++ b/scripts/governance/BudgetAllocated.vue @@ -0,0 +1,67 @@ + + + diff --git a/scripts/governance/Flipdown.vue b/scripts/governance/Flipdown.vue new file mode 100644 index 000000000..c6051f589 --- /dev/null +++ b/scripts/governance/Flipdown.vue @@ -0,0 +1,34 @@ + + + diff --git a/scripts/governance/Governance.vue b/scripts/governance/Governance.vue new file mode 100644 index 000000000..345403c8e --- /dev/null +++ b/scripts/governance/Governance.vue @@ -0,0 +1,298 @@ + + + diff --git a/scripts/governance/LocalProposalStatus.vue b/scripts/governance/LocalProposalStatus.vue new file mode 100644 index 000000000..a43f20a1f --- /dev/null +++ b/scripts/governance/LocalProposalStatus.vue @@ -0,0 +1,64 @@ + + diff --git a/scripts/governance/MobileProposalPayment.vue b/scripts/governance/MobileProposalPayment.vue new file mode 100644 index 000000000..4a1c741db --- /dev/null +++ b/scripts/governance/MobileProposalPayment.vue @@ -0,0 +1,60 @@ + + + diff --git a/scripts/governance/MobileProposalRow.vue b/scripts/governance/MobileProposalRow.vue new file mode 100644 index 000000000..f3945b963 --- /dev/null +++ b/scripts/governance/MobileProposalRow.vue @@ -0,0 +1,34 @@ + + + diff --git a/scripts/governance/MobileProposalVote.vue b/scripts/governance/MobileProposalVote.vue new file mode 100644 index 000000000..236b21e7d --- /dev/null +++ b/scripts/governance/MobileProposalVote.vue @@ -0,0 +1,28 @@ + + + diff --git a/scripts/governance/MobileProposalVotes.vue b/scripts/governance/MobileProposalVotes.vue new file mode 100644 index 000000000..9a878c8f9 --- /dev/null +++ b/scripts/governance/MobileProposalVotes.vue @@ -0,0 +1,39 @@ + + diff --git a/scripts/governance/MonthlyBudget.vue b/scripts/governance/MonthlyBudget.vue new file mode 100644 index 000000000..5a13a0a47 --- /dev/null +++ b/scripts/governance/MonthlyBudget.vue @@ -0,0 +1,79 @@ + + + diff --git a/scripts/governance/ProposalCreateModal.vue b/scripts/governance/ProposalCreateModal.vue new file mode 100644 index 000000000..3b7a5fa17 --- /dev/null +++ b/scripts/governance/ProposalCreateModal.vue @@ -0,0 +1,172 @@ + + + diff --git a/scripts/governance/ProposalName.vue b/scripts/governance/ProposalName.vue new file mode 100644 index 000000000..580627fd5 --- /dev/null +++ b/scripts/governance/ProposalName.vue @@ -0,0 +1,44 @@ + + diff --git a/scripts/governance/ProposalPayment.vue b/scripts/governance/ProposalPayment.vue new file mode 100644 index 000000000..4a004e80d --- /dev/null +++ b/scripts/governance/ProposalPayment.vue @@ -0,0 +1,54 @@ + + diff --git a/scripts/governance/ProposalRow.vue b/scripts/governance/ProposalRow.vue new file mode 100644 index 000000000..cb4e5d5a1 --- /dev/null +++ b/scripts/governance/ProposalRow.vue @@ -0,0 +1,124 @@ + + + diff --git a/scripts/governance/ProposalStatus.vue b/scripts/governance/ProposalStatus.vue new file mode 100644 index 000000000..d40200cda --- /dev/null +++ b/scripts/governance/ProposalStatus.vue @@ -0,0 +1,93 @@ + + + diff --git a/scripts/governance/ProposalVotes.vue b/scripts/governance/ProposalVotes.vue new file mode 100644 index 000000000..c1311b1ae --- /dev/null +++ b/scripts/governance/ProposalVotes.vue @@ -0,0 +1,34 @@ + + + diff --git a/scripts/governance/ProposalsTable.vue b/scripts/governance/ProposalsTable.vue new file mode 100644 index 000000000..b88c0db84 --- /dev/null +++ b/scripts/governance/ProposalsTable.vue @@ -0,0 +1,138 @@ + + + diff --git a/scripts/governance/status.js b/scripts/governance/status.js new file mode 100644 index 000000000..32cd03b42 --- /dev/null +++ b/scripts/governance/status.js @@ -0,0 +1,48 @@ +import { cChainParams, COIN } from '../chain_params.js'; + +/** + * @enum {number} + */ +export const reasons = { + NOT_FUNDED: 0, + TOO_YOUNG: 1, + OVER_BUDGET: 2, +}; + +export class ProposalValidator { + /** + * @type{number} Number of ENABLED masternodes + */ + #nMasternodes; + + #allocatedBudget = 0; + + constructor(nMasternodes) { + this.#nMasternodes = nMasternodes; + } + + /** + * Must be called in order of proposal for correct overbudget calculation + * @returns {{passing: boolean, reason?: reasons}} + */ + validate(proposal) { + const { Yeas, Nays } = proposal; + const netYes = Yeas - Nays; + + const requiredVotes = this.#nMasternodes / 10; + if (netYes < requiredVotes) { + return { passing: false, reason: reasons.NOT_FUNDED }; + } else if (!proposal.IsEstablished) { + return { passing: false, reason: reasons.TOO_YOUNG }; + } else if ( + this.#allocatedBudget + proposal.MonthlyPayment > + cChainParams.current.maxPayment / COIN + ) { + return { passing: false, reason: reasons.OVER_BUDGET }; + } else { + // Proposal is passing, add monthly payment to allocated budget + this.#allocatedBudget += proposal.MonthlyPayment; + return { passing: true }; + } + } +} diff --git a/scripts/index.js b/scripts/index.js index a3cec5857..1076d6b3c 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -18,14 +18,8 @@ export { toClipboard, restoreWallet, playMusic, - openExplorer, doms, - importMasternode, - destroyMasternode, - startMasternode, - createProposal, switchSettings, - govVote, } from './global.js'; export { wallet, getNewAddress } from './wallet.js'; export { @@ -37,7 +31,6 @@ export { toggleAutoLockWallet, changePassword, } from './settings.js'; -export { createMasternode } from './legacy.js'; export { promoConfirm, setPromoMode, diff --git a/scripts/masternode.js b/scripts/masternode.js index 90b8f737b..1e1d5593e 100644 --- a/scripts/masternode.js +++ b/scripts/masternode.js @@ -41,9 +41,7 @@ export default class Masternode { static sessionVotes = []; async _getWalletPrivateKey() { - return await wallet - .getMasterKey() - .getPrivateKey(this.walletPrivateKeyPath); + return wallet.getMasterKey().getPrivateKey(this.walletPrivateKeyPath); } /** @@ -328,7 +326,7 @@ export default class Masternode { */ async start() { const message = await this.broadcastMessageToHex(); - return (await getNetwork().start(message)).includes( + return (await getNetwork().startMasternode(message)).includes( 'Masternode broadcast sent' ); } @@ -524,7 +522,7 @@ export default class Masternode { * @param {Number} options.start - Superblock of when the proposal is going to start * @param {String} options.address - Base58 encoded PIVX address * @param {Number} options.monthlyPayment - Payment amount per cycle in satoshi - * @returns {boolean} If the proposal is valid + * @returns {{ok:boolean, err: string}} If the proposal is valid */ static isValidProposal({ name, diff --git a/scripts/masternode/CreateMasternode.vue b/scripts/masternode/CreateMasternode.vue new file mode 100644 index 000000000..6d8d551c3 --- /dev/null +++ b/scripts/masternode/CreateMasternode.vue @@ -0,0 +1,216 @@ + + + diff --git a/scripts/masternode/Masternode.vue b/scripts/masternode/Masternode.vue new file mode 100644 index 000000000..879d20d87 --- /dev/null +++ b/scripts/masternode/Masternode.vue @@ -0,0 +1,177 @@ + + + diff --git a/scripts/masternode/MasternodeController.vue b/scripts/masternode/MasternodeController.vue new file mode 100644 index 000000000..c1979b9f5 --- /dev/null +++ b/scripts/masternode/MasternodeController.vue @@ -0,0 +1,161 @@ + + + diff --git a/scripts/masternode/NewMasternodeList.vue b/scripts/masternode/NewMasternodeList.vue new file mode 100644 index 000000000..ac2e59111 --- /dev/null +++ b/scripts/masternode/NewMasternodeList.vue @@ -0,0 +1,204 @@ + + + diff --git a/scripts/misc.js b/scripts/misc.js index fa83b5ba2..f5697d05e 100644 --- a/scripts/misc.js +++ b/scripts/misc.js @@ -5,6 +5,7 @@ import bs58 from 'bs58'; import { BIP21_PREFIX, cChainParams } from './chain_params.js'; import { dSHA256 } from './utils.js'; import { verifyPubkey, verifyBech32 } from './encoding.js'; +import { Address6 } from 'ip-address'; // Base58 Encoding Map export const MAP_B58 = @@ -400,3 +401,45 @@ export function isEmpty(val) { (typeof val === 'object' && Object.keys(val).length === 0) ); } + +/** + * Takes an ip address and adds the port. + * If it's a tor address, ip.onion:port will be used (e.g. expyuzz4wqqyqhjn.onion:12345) + * If it's an IPv4 address, ip:port will be used, (e.g. 127.0.0.1:12345) + * If it's an IPv6 address, [ip]:port will be used, (e.g. [::1]:12345) + * @param {String} ip - Ip address with or without port + * @returns {String} + */ +export function parseIpAddress(ip) { + // IPv4 or tor without port + if (ip.match(/\d+\.\d+\.\d+\.\d+/) || ip.match(/\w+\.onion/)) { + return `${ip}:${cChainParams.current.MASTERNODE_PORT}`; + } + + // IPv4 or tor with port + if (ip.match(/\d+\.\d+\.\d+\.\d+:\d+/) || ip.match(/\w+\.onion:\d+/)) { + return ip; + } + + // IPv6 without port + if (Address6.isValid(ip)) { + return `[${ip}]:${cChainParams.current.MASTERNODE_PORT}`; + } + + const groups = /\[(.*)\]:\d+/.exec(ip); + if (groups !== null && groups.length > 1) { + // IPv6 with port + if (Address6.isValid(groups[1])) { + return ip; + } + } + + // If we haven't returned yet, the address was invalid. + return null; +} + +export function numberToCurrency(number, price) { + return (number * price).toLocaleString('en-gb', ',', '.', { + style: 'currency', + }); +} diff --git a/scripts/network/network.js b/scripts/network/network.js index a0767dfba..c6aa314bd 100644 --- a/scripts/network/network.js +++ b/scripts/network/network.js @@ -197,7 +197,10 @@ export class RPCNodeNetwork extends Network { } async getBestBlockHash() { - return await this.#callRPC('/getbestblockhash', true); + return (await this.#callRPC('/getbestblockhash', true)).replaceAll( + '"', + '' + ); } async sendTransaction(hex) { diff --git a/scripts/settings.js b/scripts/settings.js index d5301979e..a23c71e31 100644 --- a/scripts/settings.js +++ b/scripts/settings.js @@ -1,7 +1,6 @@ import { doms, updateLogOutButton, - updateGovernanceTab, dashboard, refreshChainData, } from './global.js'; @@ -416,7 +415,6 @@ export async function toggleTestnet( // Make sure we have the correct number of blocks before loading any wallet await refreshChainData(); getEventEmitter().emit('toggle-network'); - await updateGovernanceTab(); } export function toggleDebug(newValue = !debug) { diff --git a/scripts/stake/Stake.vue b/scripts/stake/Stake.vue index 9b3da89ca..2295730d8 100644 --- a/scripts/stake/Stake.vue +++ b/scripts/stake/Stake.vue @@ -18,7 +18,7 @@ const { createAlert } = useAlerts(); const wallet = useWallet(); const { balance, coldBalance, price, currency, isViewOnly } = storeToRefs(wallet); -const { advancedMode, displayDecimals } = useSettings(); +const { advancedMode, displayDecimals } = storeToRefs(useSettings()); const showUnstake = ref(false); const showStake = ref(false); const coldStakingAddress = ref(''); @@ -106,20 +106,6 @@ async function restoreWallet(strReason) { ); }); } - -async function importWif(wif, extsk) { - const secret = await ParsedSecret.parse(wif); - if (secret.masterKey) { - await wallet.setMasterKey({ mk: secret.masterKey, extsk }); - if (wallet.hasShield && !extsk) { - createAlert( - 'warning', - 'Could not decrypt sk even if password is correct, please contact a developer' - ); - } - createAlert('success', ALERTS.WALLET_UNLOCKED, 1500); - } -} diff --git a/scripts/utils.js b/scripts/utils.js index 80478cfef..db2343757 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -110,3 +110,13 @@ export function getRandomInt(N) { export function getRandomElement(arr) { return arr[getRandomInt(arr.length)]; } + +/** + * @param {string} blockbookUrl - Blockbook base URL + * @param {string} address + * @returns {string} URL to blockbook address + */ +export function getBlockbookUrl(blockbookUrl, address) { + const urlPart = address.startsWith('xpub') ? 'xpub' : 'address'; + return `${blockbookUrl.replace(/\/$/, '')}/${urlPart}/${address}`; +} diff --git a/test_setup.js b/test_setup.js index 7e0f9a19d..c1e7cc966 100644 --- a/test_setup.js +++ b/test_setup.js @@ -1,7 +1,19 @@ +import { vi } from 'vitest'; +import 'fake-indexeddb/auto'; + // We need to attach the component to a HTML, // or .isVisible() function does not work document.body.innerHTML = `
-
+
+
+
+
`; + +// Mock indexDB +vi.stubGlobal('indexedDB', new IDBFactory()); + +// eslint-disable-next-line +process.env.TZ = 'UTC'; diff --git a/tests/components/StakeBalance.test.js b/tests/components/StakeBalance.test.js index d0d25383e..f289b35f6 100644 --- a/tests/components/StakeBalance.test.js +++ b/tests/components/StakeBalance.test.js @@ -11,7 +11,7 @@ describe('stake balance tests', () => { return message; } ); - return vi.clearAllMocks(); + return vi.clearAllMocks; }); function mount(props) { return vueMount(StakeBalance, { diff --git a/tests/components/governance/BudgetAllocated.test.js b/tests/components/governance/BudgetAllocated.test.js new file mode 100644 index 000000000..6aafdcf44 --- /dev/null +++ b/tests/components/governance/BudgetAllocated.test.js @@ -0,0 +1,24 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import BudgetAllocated from '../../../scripts/governance/BudgetAllocated.vue'; + +describe('BudgetAllocated component tests', () => { + it('displays information correctly', () => { + const wrapper = mount(BudgetAllocated, { + props: { currency: 'usd', price: 1.5, allocatedBudget: 10_000 }, + }); + expect( + wrapper.find('[data-testid="allocatedGovernanceBudget"]').text() + ).toBe('10,000'); + expect( + wrapper + .find('[data-testid="allocatedGovernanceBudgetValue"]') + .text() + ).toBe('15,000'); // 1.5 * 10000 + expect( + wrapper + .find('[data-testid="allocatedGovernanceBudgetCurrency"]') + .text() + ).toBe('USD'); // currency to upper case + }); +}); diff --git a/tests/components/governance/Flipdown.test.js b/tests/components/governance/Flipdown.test.js new file mode 100644 index 000000000..f08749a62 --- /dev/null +++ b/tests/components/governance/Flipdown.test.js @@ -0,0 +1,25 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { flushPromises, mount } from '@vue/test-utils'; +import FlipDownComponent from '../../../scripts/governance/Flipdown.vue'; +import { FlipDown } from '../../../scripts/flipdown.js'; +vi.mock('../../../scripts/flipdown.js', () => { + const FlipDown = vi.fn(); + FlipDown.prototype.start = vi.fn(); + return { FlipDown }; +}); + +describe('BudgetAllocated component tests', () => { + it('updates flipdown correctly', async () => { + const wrapper = mount(FlipDownComponent, { props: { timeStamp: 5 } }); + const flipdownElement = wrapper.find('[data-testid="flipdown"]'); + await flushPromises(); + const id = flipdownElement.wrapperElement.id; + expect(FlipDown).toHaveBeenCalledWith(5, id); + expect(FlipDown.prototype.start).toHaveBeenCalledOnce(); + await wrapper.setProps({ timeStamp: 10 }); + + await flushPromises(); + expect(FlipDown).toHaveBeenCalledWith(5, id); + expect(FlipDown.prototype.start).toHaveBeenCalledTimes(2); + }); +}); diff --git a/tests/components/governance/LocalProposalStatus.test.js b/tests/components/governance/LocalProposalStatus.test.js new file mode 100644 index 000000000..df7ba3d13 --- /dev/null +++ b/tests/components/governance/LocalProposalStatus.test.js @@ -0,0 +1,61 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import LocalProposalStatus from '../../../scripts/governance/LocalProposalStatus.vue'; + +vi.mock('../../../scripts/i18n.js'); + +describe('LocalProposalStatus component tests', () => { + it('displays correct information', async () => { + const wrapper = mount(LocalProposalStatus, { + props: { + proposal: { + blockHeight: 100_100, + }, + blockCount: 100_105, + }, + }); + expect(wrapper.find('[data-testid="localProposalStatus"]').text()).toBe( + '1 block proposalFinalisationRemaining' + ); + expect( + wrapper.find('[data-testid="finalizeProposalButton"]').exists() + ).toBe(false); + await wrapper.setProps({ proposal: { blockHeight: 99_999 } }); + expect(wrapper.find('[data-testid="localProposalStatus"]').text()).toBe( + 'proposalFinalisationReady' + ); + expect( + wrapper.find('[data-testid="finalizeProposalButton"]').exists() + ).toBe(true); + await wrapper.setProps({ proposal: { blockHeight: 99_998 } }); + expect(wrapper.find('[data-testid="localProposalStatus"]').text()).toBe( + 'proposalFinalisationReady' + ); + expect( + wrapper.find('[data-testid="finalizeProposalButton"]').exists() + ).toBe(true); + await wrapper.setProps({ proposal: { blockHeight: 1000 } }); + expect(wrapper.find('[data-testid="localProposalStatus"]').text()).toBe( + 'proposalFinalisationExpired' + ); + expect( + wrapper.find('[data-testid="finalizeProposalButton"]').exists() + ).toBe(false); + }); + + it('emits finalize event when button is clicked', async () => { + const wrapper = mount(LocalProposalStatus, { + props: { + proposal: { + blockHeight: 99_998, + }, + blockCount: 100_105, + }, + }); + expect(wrapper.emitted()).toStrictEqual({}); + const button = wrapper.find('[data-testid="finalizeProposalButton"]'); + await button.trigger('click'); + // One event with no args + expect(wrapper.emitted().finalizeProposal).toStrictEqual([[]]); + }); +}); diff --git a/tests/components/governance/MonthlyBudget.spec.js b/tests/components/governance/MonthlyBudget.spec.js new file mode 100644 index 000000000..0164c003a --- /dev/null +++ b/tests/components/governance/MonthlyBudget.spec.js @@ -0,0 +1,20 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import MonthlyBudget from '../../../scripts/governance/MonthlyBudget.vue'; + +describe('MonthlyBudget component tests', () => { + it('displays correctly', () => { + const wrapper = mount(MonthlyBudget, { + props: { + currency: 'usd', + price: 1.3, + }, + }); + expect(wrapper.find('[data-testid="monthlyBudgetValue"]').text()).toBe( + '561,600' + ); + expect( + wrapper.find('[data-testid="monthlyBudgetCurrency"]').text() + ).toBe('USD'); + }); +}); diff --git a/tests/components/governance/ProposalCreateModal.spec.js b/tests/components/governance/ProposalCreateModal.spec.js new file mode 100644 index 000000000..d820501ca --- /dev/null +++ b/tests/components/governance/ProposalCreateModal.spec.js @@ -0,0 +1,56 @@ +import { describe, it, expect, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import ProposalCreateModal from '../../../scripts/governance/ProposalCreateModal.vue'; + +describe('ProposalCreateModal component tests', () => { + it('hides address input when advanced mode is false', async () => { + const wrapper = mount(ProposalCreateModal, { + props: { advancedMode: true }, + }); + let address = wrapper.find('[data-testid="proposalAddress"]'); + // Address input + expect(address.isVisible()).toBe(true); + await wrapper.setProps({ advancedMode: false }); + address = wrapper.find('[data-testid="proposalAddress"]'); + expect(address.exists()).toBe(false); + }); + + it('submits correctly', async () => { + const wrapper = mount(ProposalCreateModal, { + props: { advancedMode: true }, + }); + const proposalTitle = wrapper.find('[data-testid="proposalTitle"]'); + await proposalTitle.setValue('Proposal Title'); + const url = wrapper.find('[data-testid="proposalUrl"]'); + await url.setValue('https://proposal.com/'); + const proposalCycles = wrapper.find('[data-testid="proposalCycles"]'); + await proposalCycles.setValue(3); + const proposalPayment = wrapper.find('[data-testid="proposalPayment"]'); + await proposalPayment.setValue(20); + const address = wrapper.find('[data-testid="proposalAddress"]'); + await address.setValue('DLabSomethingSomething'); + + const proposalSubmit = wrapper.find('[data-testid="proposalSubmit"]'); + await proposalSubmit.trigger('click'); + expect(wrapper.emitted().create).toStrictEqual([ + [ + 'Proposal Title', + 'https://proposal.com/', + 3, + 20, + 'DLabSomethingSomething', + ], + ]); + await wrapper.setProps({ advancedMode: false }); + + await proposalSubmit.trigger('click'); + // When advanced mode is toggled off, address should reset + expect(wrapper.emitted().create.at(-1)).toStrictEqual([ + 'Proposal Title', + 'https://proposal.com/', + 3, + 20, + '', + ]); + }); +}); diff --git a/tests/components/governance/ProposalName.spec.js b/tests/components/governance/ProposalName.spec.js new file mode 100644 index 000000000..8230f5b44 --- /dev/null +++ b/tests/components/governance/ProposalName.spec.js @@ -0,0 +1,48 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { flushPromises, mount } from '@vue/test-utils'; +import ProposalName from '../../../scripts/governance/ProposalName.vue'; +import { setActivePinia, createPinia } from 'pinia'; +describe('ProposalName component tests', () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + beforeEach(() => { + // Create test pinia instance + setActivePinia(createPinia()); + wrapper = mount(ProposalName, { + props: { + proposal: { + URL: 'https://proposal.com/', + Name: 'ProposalName', + PaymentAddress: 'Dlabsaddress', + }, + }, + }); + }); + + it('Has correct href link', async () => { + expect( + wrapper.find('[data-testid="proposalLink"]').attributes('href') + ).toBe('/address/Dlabsaddress'); + await wrapper.setProps({ + proposal: { + PaymentAddress: 'xpubdlabs', + URL: 'https://proposal.com', + Name: 'ProposalName', + }, + }); + expect( + wrapper.find('[data-testid="proposalLink"]').attributes('href') + ).toBe('/xpub/xpubdlabs'); + }); + + it('renders correctly', async () => { + expect(wrapper.find('[data-testid="proposalName"]').text()).toBe( + 'ProposalName' + ); + expect(wrapper.find('[data-testid="proposalLink"]').text()).toBe( + 'Dlabsaddre...' + ); + }); +}); diff --git a/tests/components/governance/ProposalPayment.spec.js b/tests/components/governance/ProposalPayment.spec.js new file mode 100644 index 000000000..9e9ec58ee --- /dev/null +++ b/tests/components/governance/ProposalPayment.spec.js @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import ProposalPayment from '../../../scripts/governance/ProposalPayment.vue'; +import MobileProposalPayment from '../../../scripts/governance/MobileProposalPayment.vue'; +import MobileProposalPayment from '../../../scripts/governance/MobileProposalPayment.vue'; + +vi.mock('../../../scripts/i18n.js'); +for (const { Component, name } of [ + { + Component: ProposalPayment, + name: 'ProposalPayment', + }, + { Component: MobileProposalPayment, name: 'MobileProposalPayment' }, +]) { + describe(`${name} component tests`, () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + beforeEach(() => { + wrapper = mount(Component, { + props: { + price: 1.3, + proposal: { + MonthlyPayment: 10000, + RemainingPaymentCount: 3, + TotalPayment: 30000, + }, + strCurrency: 'usd', + }, + }); + }); + it('renders correctly', () => { + expect( + wrapper.find('[data-testid="proposalMonthlyPayment"]').text() + ).toBe('10,000'); + // 1.3 * 10000 + expect(wrapper.find('[data-testid="proposalFiat"]').text()).toBe( + '13,000 USD' + ); + // Match general phrase, but ignore spacing + expect( + wrapper.find('[data-testid="governInstallments"]').text() + ).toMatch( + /3\s*proposalPaymentsRemaining\s*30,000 PIV proposalPaymentTotal/ + ); + }); + }); +} diff --git a/tests/components/governance/ProposalRow.spec.js b/tests/components/governance/ProposalRow.spec.js new file mode 100644 index 000000000..ac45ff62d --- /dev/null +++ b/tests/components/governance/ProposalRow.spec.js @@ -0,0 +1,96 @@ +import { mount } from '@vue/test-utils'; +import ProposalRow from '../../../scripts/governance/ProposalRow.vue'; +import ProposalStatus from '../../../scripts/governance/ProposalStatus.vue'; +import LocalProposalStatus from '../../../scripts/governance/LocalProposalStatus.vue'; +import ProposalName from '../../../scripts/governance/ProposalName.vue'; +import { ProposalValidator } from '../../../scripts/governance/status.js'; +import Modal from '../../../scripts/Modal.vue'; + +vi.mock('../../../scripts/i18n.js'); + +describe('ProposalRow component tests', () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + + beforeEach(() => { + const proposalValidator = new ProposalValidator(); + proposalValidator.validate = vi.fn(() => { + return { passing: true }; + }); + + const props = { + proposal: { + Name: 'LRP - JSKitty', + URL: 'https://forum.pivx.org/threads/lrp-jskitty.2126/', + Hash: '2e9196542a65d0e84ef116f485e48e40cd93b7a90a39d850536c75979f92b809', + FeeHash: + '0b7be6b9df8b65565423a5878d2b4c62830f907d3798c976be340cf55e13d65a', + BlockStart: 4449600, + BlockEnd: 4579203, + TotalPaymentCount: 3, + RemainingPaymentCount: 0, + PaymentAddress: 'DFiH7DpxYahn5Y6p91oYwRDUqCsS9PaVGu', + Ratio: 1, + Yeas: 868, + Nays: 0, + Abstains: 0, + TotalPayment: 45000, + MonthlyPayment: 15000, + IsEstablished: true, + IsValid: true, + Allotted: 15000, + }, + proposalValidator, + masternodeCount: 100, + price: 10, + strCurrency: 'USD', + localProposal: false, + }; + wrapper = mount(ProposalRow, { + props, + }); + }); + + it('renders ProposalStatus if localProposal is false', () => { + expect(wrapper.findComponent(ProposalStatus).exists()).toBe(true); + expect(wrapper.findComponent(LocalProposalStatus).exists()).toBe(false); + }); + + it('renders LocalProposalStatus if localProposal is true', async () => { + await wrapper.setProps({ localProposal: true }); + expect(wrapper.findComponent(ProposalStatus).exists()).toBe(false); + expect(wrapper.findComponent(LocalProposalStatus).exists()).toBe(true); + }); + + it('emits click event when governStatusCol is clicked', async () => { + await wrapper.find('.governStatusCol').trigger('click'); + expect(wrapper.emitted().click).toBeTruthy(); + }); + + it('emits finalizeProposal event from LocalProposalStatus', async () => { + await wrapper.setProps({ localProposal: true }); + wrapper.findComponent(LocalProposalStatus).vm.$emit('finalizeProposal'); + expect(wrapper.emitted().finalizeProposal).toBeTruthy(); + }); + + it('emits vote event with 2 when No button is clicked', async () => { + await wrapper.find('.govNoBtnMob').trigger('click'); + await wrapper + .getComponent(Modal) + .find('[data-testid="confirmVote"]') + .trigger('click'); + + expect(wrapper.emitted().vote[0]).toEqual([2]); + }); + + it('emits vote event with 1 when Yes button is clicked', async () => { + await wrapper.find('.govYesBtnMob').trigger('click'); + await wrapper + .getComponent(Modal) + .find('[data-testid="confirmVote"]') + .trigger('click'); + expect(wrapper.emitted().vote[0]).toEqual([1]); + }); +}); diff --git a/tests/components/governance/ProposalStatus.spec.js b/tests/components/governance/ProposalStatus.spec.js new file mode 100644 index 000000000..e13660bba --- /dev/null +++ b/tests/components/governance/ProposalStatus.spec.js @@ -0,0 +1,80 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount as vueMount } from '@vue/test-utils'; +import ProposalStatus from '../../../scripts/governance/ProposalStatus.vue'; +import { ProposalValidator, reasons } from '../../../scripts/governance/status'; + +vi.mock('../../../scripts/i18n.js'); + +describe('ProposalStatus component tests', () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + let proposalValidator = new ProposalValidator(100); + beforeEach(() => { + proposalValidator.reason = null; + proposalValidator.validate = vi.fn(() => { + if (proposalValidator.reason !== null) + return { passing: false, reason: proposalValidator.reason }; + return { passing: true }; + }); + }); + function mount() { + wrapper = vueMount(ProposalStatus, { + props: { + proposal: { + URL: 'https://proposal.com/', + Name: 'ProposalName', + PaymentAddress: 'Dlabsaddress', + Yeas: 90, + Nays: 30, + }, + proposalValidator, + nMasternodes: 132, + }, + }); + } + it('renders not funded proposals correctly', () => { + proposalValidator.reason = reasons.NOT_FUNDED; + mount(); + const status = wrapper.find('[data-testid="proposalStatus"]'); + expect(status.text()).toBe('proposalFailing'); + expect(status.classes()).toContain('votesNo'); + const funding = wrapper.find('[data-testid="proposalFunding"]'); + expect(funding.text()).toBe('(proposalNotFunded)'); + }); + it('renders over budget proposals correctly', () => { + proposalValidator.reason = reasons.OVER_BUDGET; + mount(); + const status = wrapper.find('[data-testid="proposalStatus"]'); + expect(status.text()).toBe('proposalFailing'); + expect(status.classes()).toContain('votesOverAllocated'); + const funding = wrapper.find('[data-testid="proposalFunding"]'); + expect(funding.text()).toBe('(proposalOverBudget)'); + }); + it('renders young proposals correctly', () => { + proposalValidator.reason = reasons.TOO_YOUNG; + mount(); + const status = wrapper.find('[data-testid="proposalStatus"]'); + expect(status.text()).toBe('proposalFailing'); + expect(status.classes()).toContain('votesNo'); + const funding = wrapper.find('[data-testid="proposalFunding"]'); + expect(funding.text()).toBe('(proposalTooYoung)'); + }); + + it('renders passing proposals correctly', () => { + mount(); + const status = wrapper.find('[data-testid="proposalStatus"]'); + expect(status.text()).toBe('proposalPassing'); + expect(status.classes()).toContain('votesYes'); + const funding = wrapper.find('[data-testid="proposalFunding"]'); + expect(funding.text()).toBe('(proposalFunded)'); + }); + it('renders percentages correctly', () => { + mount(); + // It should be (90 - 30) / 132 = 45.5% approx. + expect(wrapper.find('[data-testid="proposalPercentage"]').text()).toBe( + '45.5% proposalNetYes' + ); + }); +}); diff --git a/tests/components/governance/ProposalVotes.spec.js b/tests/components/governance/ProposalVotes.spec.js new file mode 100644 index 000000000..225bf249c --- /dev/null +++ b/tests/components/governance/ProposalVotes.spec.js @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { mount } from '@vue/test-utils'; +import ProposalVotes from '../../../scripts/governance/ProposalVotes.vue'; +import MobileProposalVotes from '../../../scripts/governance/MobileProposalVotes.vue'; + +vi.mock('../../../scripts/i18n.js'); +for (const { Component, name } of [ + { Component: ProposalVotes, name: 'ProposalVotes' }, + { Component: MobileProposalVotes, name: 'MobileProposalVotes' }, +]) { + describe(`${name} component tests`, () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + beforeEach(() => { + wrapper = mount(Component, { + props: { + proposal: { + Yeas: 32, + Nays: 54, + Ratio: 32 / (32 + 54), + }, + }, + }); + }); + + it('renders correctly', () => { + expect( + wrapper.find('[data-testid="proposalVotes"]').text() + ).toMatch(/37.2%\s*32 \/ 54/); + }); + }); +} diff --git a/tests/components/masternode/CreateMasternode.spec.js b/tests/components/masternode/CreateMasternode.spec.js new file mode 100644 index 000000000..eab211311 --- /dev/null +++ b/tests/components/masternode/CreateMasternode.spec.js @@ -0,0 +1,117 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { mount } from '@vue/test-utils'; +import CreateMasternode from '../../../scripts/masternode/CreateMasternode.vue'; + +vi.mock('../../../scripts/i18n.js'); + +describe('CreateMasternode component tests', () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + beforeEach(() => { + wrapper = mount(CreateMasternode, { + props: { + synced: true, + balance: 11000, + possibleUTXOs: [], + }, + }); + }); + + it('displays correct error when not synced', async () => { + await wrapper.setProps({ synced: false }); + const errorElement = wrapper.find('[data-testid="error"]'); + expect(errorElement.text()).toBe('MN_UNLOCK_WALLET'); + }); + + it('displays correct error when balance is too low', async () => { + await wrapper.setProps({ balance: 9999.99 }); + const errorElement = wrapper.find('[data-testid="error"]'); + expect(errorElement.text()).toBe( + 'MN_NOT_ENOUGH_COLLAT amount 0.01 ticker PIV' + ); + await wrapper.setProps({ balance: 1234 }); + expect(errorElement.text()).toBe( + 'MN_NOT_ENOUGH_COLLAT amount 8766.00 ticker PIV' + ); + }); + + it('allows tx creation if no utxos are available', async () => { + const errorElement = wrapper.find('[data-testid="error"]'); + expect(errorElement.text()).toHaveLength(0); + const createMasternodeButton = wrapper.find( + '[data-testid="createMasternodeButton"]' + ); + + expect(createMasternodeButton.isVisible()).toBe(true); + await createMasternodeButton.trigger('click'); + const createMasternodeModalButton = wrapper.find( + '[data-testid="createMasternodeModalButton"]' + ); + const options = wrapper + .find('[data-testid="masternodeTypeSelection"]') + .findAll('option'); + // Create a VPS masternode + await options.at(0).setSelected(); + await createMasternodeModalButton.trigger('click'); + + // Create a third party masternode + await createMasternodeButton.trigger('click'); + await options.at(1).setSelected(); + await createMasternodeModalButton.trigger('click'); + + expect(wrapper.emitted().createMasternode).toStrictEqual([ + [{ isVPS: true }], + [{ isVPS: false }], + ]); + }); + + it('allows masternode importing when a valid UTXO is present', async () => { + await wrapper.setProps({ + possibleUTXOs: [ + { + outpoint: { + txid: 'masternodetxid', + n: 3, + }, + }, + { + outpoint: { + txid: 'masternodetxid2', + n: 5, + }, + }, + ], + }); + const privateKey = wrapper.find('[data-testid="importPrivateKey"]'); + const ipAddress = wrapper.find('[data-testid="importIpAddress"]'); + const selectUTXO = wrapper + .find('[data-testid="selectUTXO"]') + .findAll('option'); + expect(selectUTXO.map((o) => o.text()).slice(1)).toStrictEqual([ + 'masternodetxid/3', + 'masternodetxid2/5', + ]); + await privateKey.setValue('mnprivatekey'); + await ipAddress.setValue('::1'); + await selectUTXO.at(2).setSelected(); + + const importMasternodeButton = wrapper.find( + '[data-testid="importMasternodeButton"]' + ); + await importMasternodeButton.trigger('click'); + expect(wrapper.emitted().importMasternode).toStrictEqual([ + [ + 'mnprivatekey', + '::1', + { + outpoint: { + txid: 'masternodetxid2', + n: 5, + }, + }, + ], + ]); + }); +}); diff --git a/tests/components/masternode/MasternodeController.spec.js b/tests/components/masternode/MasternodeController.spec.js new file mode 100644 index 000000000..766c7e947 --- /dev/null +++ b/tests/components/masternode/MasternodeController.spec.js @@ -0,0 +1,66 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { flushPromises, mount } from '@vue/test-utils'; +import MasternodeController from '../../../scripts/masternode/MasternodeController.vue'; +import { nextTick } from 'vue'; + +vi.mock('../../../scripts/i18n.js'); + +describe('MasternodeController component tests', () => { + /** + * @type{import('@vue/test-utils').VueWrapper} + */ + let wrapper; + let fullData; + let masternode; + beforeEach(() => { + fullData = { + lastseen: 1725009728331, + status: 'ENABLED', + version: 70926, + network: 'ipv6', + }; + masternode = vi.fn(); + masternode.getFullData = vi.fn().mockReturnValue(fullData); + masternode.getStatus = vi.fn().mockReturnValue(fullData.status); + masternode.addr = '::1'; + wrapper = mount(MasternodeController, { + props: { + masternode, + }, + }); + + return vi.clearAllMocks; + }); + + it('renders data correctly', () => { + expect(wrapper.find('[data-testid="mnProtocol"]').text()).toBe('70926'); + expect(wrapper.find('[data-testid="mnStatus"]').text()).toBe('ENABLED'); + expect(wrapper.find('[data-testid="mnNetType"]').text()).toBe('IPV6'); + expect(wrapper.find('[data-testid="mnIp"]').text()).toBe('::1'); + expect(wrapper.find('[data-testid="mnLastSeen"]').text()).toBe( + '9:22:08 AM' + ); + }); + + it('emits start when mn goes missing', async () => { + fullData.status = 'MISSING'; + masternode = vi.fn(); + masternode.getFullData = vi.fn().mockReturnValue(fullData); + masternode.getStatus = vi.fn().mockReturnValue(fullData.status); + masternode.addr = '::1'; + await wrapper.setProps({ masternode }); + + await flushPromises(); + expect(wrapper.emitted().start).toStrictEqual([[{ restart: false }]]); + }); + + it('emits restart event on button click', async () => { + await wrapper.find('[data-testid="restartButton"]').trigger('click'); + expect(wrapper.emitted().start).toStrictEqual([[{ restart: true }]]); + }); + + it('emits destroy event on button click', async () => { + await wrapper.find('[data-testid="destroyButton"]').trigger('click'); + expect(wrapper.emitted().destroy).toHaveLength(1); + }); +}); diff --git a/tests/unit/proposal_validator/dummy_proposals.json b/tests/unit/proposal_validator/dummy_proposals.json new file mode 100644 index 000000000..861228682 --- /dev/null +++ b/tests/unit/proposal_validator/dummy_proposals.json @@ -0,0 +1,555 @@ +[ + { + "Name": "LRP - JSKitty", + "URL": "https://forum.pivx.org/threads/lrp-jskitty.2126/", + "Hash": "2e9196542a65d0e84ef116f485e48e40cd93b7a90a39d850536c75979f92b809", + "FeeHash": "0b7be6b9df8b65565423a5878d2b4c62830f907d3798c976be340cf55e13d65a", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "DFiH7DpxYahn5Y6p91oYwRDUqCsS9PaVGu", + "Ratio": 1, + "Yeas": 868, + "Nays": 0, + "Abstains": 0, + "TotalPayment": 45000, + "MonthlyPayment": 15000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 15000, + "valid": true + }, + { + "Name": "LRP - Labs Infra", + "URL": "https://forum.pivx.org/threads/lrp-labs-infra.2129/", + "Hash": "ddd5dd8a0f63b066ab3feadd8d50b29daffb2b27b569831aa21ee75ea928c7a3", + "FeeHash": "e869fecb384221161318fda9fcb784bf54bc09857bebe3fa88b5f66546f7a2f4", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb", + "Ratio": 0.9988304093567252, + "Yeas": 854, + "Nays": 1, + "Abstains": 0, + "TotalPayment": 6600, + "MonthlyPayment": 2200, + "IsEstablished": true, + "IsValid": true, + "Allotted": 2200, + "valid": true + }, + { + "Name": "Young proposal", + "URL": "https://needtobe18todrink.com", + "Hash": "ddd5dd8a0f63b066ab3feadd8d50b29daffb2b27b569831aa21ee75ea928c7a3", + "FeeHash": "e869fecb384221161318fda9fcb784bf54bc09857bebe3fa88b5f66546f7a2f4", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb", + "Ratio": 0.9988304093567252, + "Yeas": 854, + "Nays": 1, + "Abstains": 0, + "TotalPayment": 6600, + "MonthlyPayment": 2200, + "IsEstablished": false, + "IsValid": true, + "Allotted": 2200, + "valid": false, + "reason": 1 + }, + { + "Name": "Greedy", + "URL": "https://greedneverends.com", + "Hash": "ddd5dd8a0f63b066ab3feadd8d50b29daffb2b27b569831aa21ee75ea928c7a3", + "FeeHash": "e869fecb384221161318fda9fcb784bf54bc09857bebe3fa88b5f66546f7a2f4", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb", + "Ratio": 0.9988304093567252, + "Yeas": 854, + "Nays": 1, + "Abstains": 0, + "TotalPayment": 6600, + "MonthlyPayment": 22000000000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 2200, + "valid": false, + "reason": 2 + }, + { + "Name": "LRP - Luke", + "URL": "https://forum.pivx.org/threads/lrp-luke.2181/", + "Hash": "624e886a45f536385633c0bb93ae578c1054bf8cbbe800aab8d149d4d84e993b", + "FeeHash": "7e717c8608a9d640dd5b595a06ccc7f6a86decb6c6617733d965c4818a15c442", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DNkAdmxHiGvYvMV3Cvtb4FFYhJMUTc8oR2", + "Ratio": 1, + "Yeas": 826, + "Nays": 0, + "Abstains": 0, + "TotalPayment": 60000, + "MonthlyPayment": 20000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 20000, + "valid": true + }, + { + "Name": "LRP - Web Developer", + "URL": "https://forum.pivx.org/threads/lrp-web-developer.2185/", + "Hash": "beb64db7e80aacc561754cc93cfa9e04fe7eccf387b24cf8133847339488766a", + "FeeHash": "b8c094a484e7d549a724652361fd0b97637b9ecca1b82a9c9fca252ed2cc9e93", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DDS5v6mPqUN3rdzkp3268LV7TKjHCZqy8c", + "Ratio": 1, + "Yeas": 825, + "Nays": 0, + "Abstains": 0, + "TotalPayment": 52500, + "MonthlyPayment": 17500, + "IsEstablished": true, + "IsValid": true, + "Allotted": 17500, + "valid": true + }, + { + "Name": "PIVX-Turkish-Jun24", + "URL": "https://forum.pivx.org/threads/pivx-turkey-promotion-work.1970/", + "Hash": "45dd2ad597751ff2e7f10ddf09546ac8d6f6188e4c6242fb52ffcd875c4406df", + "FeeHash": "838a2a3edd639d3c8f457faf828778409ce34d15217c8cb898a6e170ff05a35d", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "D5Av2FN6jmmpiFvbYxDNhDx4wUg9onbQWt", + "Ratio": 0.983510011778563, + "Yeas": 835, + "Nays": 14, + "Abstains": 0, + "TotalPayment": 9600, + "MonthlyPayment": 3200, + "IsEstablished": true, + "IsValid": true, + "Allotted": 3200, + "valid": true + }, + { + "Name": "BusinessDev06", + "URL": "https://forum.pivx.org/threads/lobd3-jeffrey.2170/", + "Hash": "20a356497b1da74018cd727c8fb4841423473356f097e43c5343376684f29163", + "FeeHash": "1fb7681c100b7a29b3c7a59c2feb0ac7dee63b4f2b9b33b22eff87a8be6bdfab", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DHfMc81XkPwkF11vW1hhhFLNPqgBBp87BL", + "Ratio": 0.9975669099756691, + "Yeas": 820, + "Nays": 2, + "Abstains": 0, + "TotalPayment": 90000, + "MonthlyPayment": 30000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 30000, + "valid": true + }, + { + "Name": "MKT-Leander-Aug-oct", + "URL": "https://forum.pivx.org/threads/mkt-leander-aug-oct.2173/", + "Hash": "9f3ab51c484bab24c706b10563ddb20c788db837226f15acd3d2f47df7bbaa67", + "FeeHash": "0b9a23d1bdf38cfc06e003a3379ce378748354d091f2ff898ee430384fab4fc1", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DPiVXPykDKQi3PLVbcfekKMXv1E1o3FxtA", + "Ratio": 0.9975550122249389, + "Yeas": 816, + "Nays": 2, + "Abstains": 0, + "TotalPayment": 30000, + "MonthlyPayment": 10000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 10000, + "valid": true + }, + { + "Name": "Labs x Cryptech", + "URL": "https://forum.pivx.org/threads/labs-x-cryptech-partnership.2258/", + "Hash": "9cf7068fdf547dd0e623d0d6de47fd389f283dbdb52fa8d411f8d3593c666a8a", + "FeeHash": "c1d75c2f0c37d024a152561757d6431303cf02739798b5217a2065ed8c1c1ac4", + "BlockStart": 4536000, + "BlockEnd": 4579201, + "TotalPaymentCount": 1, + "RemainingPaymentCount": 0, + "PaymentAddress": "DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb", + "Ratio": 1, + "Yeas": 807, + "Nays": 0, + "Abstains": 0, + "TotalPayment": 7500, + "MonthlyPayment": 7500, + "IsEstablished": true, + "IsValid": true, + "Allotted": 7500, + "valid": true + }, + { + "Name": "LRP - Duddino", + "URL": "https://forum.pivx.org/threads/lrp-duddino.2267/", + "Hash": "727f5b9ac1d08be65e7cd92f93d0c11f0cc30fba98f1287eccf66c8793523b25", + "FeeHash": "ccf0ecd3d41443be58e9037c08e2f8bfa71698de7eae77b739441963bd0649c0", + "BlockStart": 4536000, + "BlockEnd": 4665603, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 2, + "PaymentAddress": "DShxa9sykpVUYBe2VKZfq9dzE8f2yBbtmg", + "Ratio": 1, + "Yeas": 805, + "Nays": 0, + "Abstains": 0, + "TotalPayment": 90000, + "MonthlyPayment": 30000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 30000, + "valid": true + }, + { + "Name": "PIVXxArmada1", + "URL": "https://forum.pivx.org/threads/pivx-x-armada-marketmaking.2261/", + "Hash": "76e6d2e760a38ac07136e9fa954fd96f0bb57d79218117ad92ef006cf06e8f3b", + "FeeHash": "2fc558ce11226755eb9379a7217c3cd2e0839ff288d7693fe047747ad4d06fb8", + "BlockStart": 4536000, + "BlockEnd": 4579201, + "TotalPaymentCount": 1, + "RemainingPaymentCount": 0, + "PaymentAddress": "DMZYkebbyAX6YVEwd92w79QRarzUZfcWN8", + "Ratio": 0.9925093632958801, + "Yeas": 795, + "Nays": 6, + "Abstains": 0, + "TotalPayment": 160100, + "MonthlyPayment": 160100, + "IsEstablished": true, + "IsValid": true, + "Allotted": 160100, + "valid": true + }, + { + "Name": "rigden bastyon s 005", + "URL": "https://forum.pivx.org/threads/rigden-bastyon-s-005.2176/", + "Hash": "d847802a536b6fe94ab092fec5d226a6da63c2fb67cef6b818d8def044d7831b", + "FeeHash": "9e7253369c689cf038146b0923af0c2cf05f02d13468b5780faa32a3ee70ac15", + "BlockStart": 4492800, + "BlockEnd": 4579202, + "TotalPaymentCount": 2, + "RemainingPaymentCount": 0, + "PaymentAddress": "D9i9e44gQhoto8wKxtfCpiyPSDgaMycXYK", + "Ratio": 0.9694656488549618, + "Yeas": 762, + "Nays": 24, + "Abstains": 0, + "TotalPayment": 6100, + "MonthlyPayment": 3050, + "IsEstablished": true, + "IsValid": true, + "Allotted": 3050, + "valid": true + }, + { + "Name": "CSJune24", + "URL": "https://forum.pivx.org/threads/content-strategist.2143/", + "Hash": "2e6138acc85de01f2692387af0eaeb9d6f34d72c067260f29f08464688c62f92", + "FeeHash": "a713cf8f916908ea85200348947623a53062590787479aadd66b909fc30610e7", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "D7BAAZFABG1CgYQ8ykfhtDqbumt8M9hZes", + "Ratio": 0.7780320366132724, + "Yeas": 680, + "Nays": 194, + "Abstains": 0, + "TotalPayment": 16893, + "MonthlyPayment": 5631, + "IsEstablished": true, + "IsValid": true, + "Allotted": 5631, + "valid": true + }, + { + "Name": "PIVX-Spanish-Jun24", + "URL": "http://bit.ly/4c07hc9", + "Hash": "cbb60c7d114f58ea09de58f1a8b55e1950a2f8ff436a7fbb29b0113b795d991d", + "FeeHash": "a953aa86b560299f0f762e40f697ab2bc2f78581318da768873bbc7527d32f02", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "DLXRPKz2Y4oPihGhALSK9LuSBzFBqXP5eM", + "Ratio": 0.7733644859813084, + "Yeas": 662, + "Nays": 194, + "Abstains": 0, + "TotalPayment": 9000, + "MonthlyPayment": 3000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 3000, + "valid": true + }, + { + "Name": "LRP - Meerkat", + "URL": "https://forum.pivx.org/threads/lrp-meerkat.2244/", + "Hash": "43e9a0488cd6aded1e546622e8264586f780bc320088b88a50f1a7c56a7e17d6", + "FeeHash": "8e08a57e87105f50baa3f6e2de2cb66a6a5a2e9072f6533abc0b16921b83d216", + "BlockStart": 4536000, + "BlockEnd": 4665603, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 2, + "PaymentAddress": "DBaWgpEKnV8Gr7CNFBWwjAJZqK3irdZkUM", + "Ratio": 0.7619631901840491, + "Yeas": 621, + "Nays": 194, + "Abstains": 0, + "TotalPayment": 60000, + "MonthlyPayment": 20000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 20000, + "valid": true + }, + { + "Name": "LRP - Rushali", + "URL": "https://forum.pivx.org/threads/lrp-rushali.2274/", + "Hash": "c7c48583b860a587a22d96d9ca97ad9a3a3485e75e342d3c3ba76aa22a647c3a", + "FeeHash": "b9847c8aaff341a431c9980ebd4242870fbe032aa118d7d85a5f7ead9e3d2c0d", + "BlockStart": 4536000, + "BlockEnd": 4665603, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 2, + "PaymentAddress": "DQHajggqx1XXZPWuchWBB5jS4pmiT3LNJN", + "Ratio": 0.7593052109181141, + "Yeas": 612, + "Nays": 194, + "Abstains": 0, + "TotalPayment": 37500, + "MonthlyPayment": 12500, + "IsEstablished": true, + "IsValid": true, + "Allotted": 12500, + "valid": true + }, + { + "Name": "Marketing-RedByrd", + "URL": "https://forum.pivx.org/threads/redbyrd-proposal.2262/", + "Hash": "f15819fdc70a486c78f692c233058ec0700e50efb51ba84f8939d02669dc938a", + "FeeHash": "6156e67c38fee8f5b72cc465283083664385a5e10017b40b1b7bd2f4838c3370", + "BlockStart": 4536000, + "BlockEnd": 4579201, + "TotalPaymentCount": 1, + "RemainingPaymentCount": 0, + "PaymentAddress": "D5zWMZAdJo5ssPHD3KFKmPVRvXBZiegCri", + "Ratio": 0.758364312267658, + "Yeas": 612, + "Nays": 195, + "Abstains": 0, + "TotalPayment": 21052, + "MonthlyPayment": 21052, + "IsEstablished": true, + "IsValid": true, + "Allotted": 21052, + "valid": true + }, + { + "Name": "Liquid369-Aug-Oct24", + "URL": "https://bit.ly/4crOCVZ", + "Hash": "55f737cb493bb1db985dcd74ee18799ad17b95ec8b51a53858d138c863098f06", + "FeeHash": "60afeaa98ff2d8f13a8032d46e802e18ba53d99551ad61d193723b480741eaea", + "BlockStart": 4536000, + "BlockEnd": 4665603, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 2, + "PaymentAddress": "D66LAo1cQmKJ3JoqeuSRrTL6hycLx7kKso", + "Ratio": 0.7568238213399504, + "Yeas": 610, + "Nays": 196, + "Abstains": 0, + "TotalPayment": 90000, + "MonthlyPayment": 30000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 30000, + "valid": true + }, + { + "Name": "Marketing-Hawtch", + "URL": "https://forum.pivx.org/threads/marketing-hawtch.2256/", + "Hash": "160144845b119cf06804e403034c4ee3e9386ef46ee93401a6ca92b275adff38", + "FeeHash": "4deb8b70a36d897e021b636026816bd7ddedb01b94da98d74e74403a1b869e37", + "BlockStart": 4536000, + "BlockEnd": 4579201, + "TotalPaymentCount": 1, + "RemainingPaymentCount": 0, + "PaymentAddress": "D5hLZRKeqeoxSw1N4hTgXMFerDh2XBAB5H", + "Ratio": 0.7558859975216853, + "Yeas": 610, + "Nays": 197, + "Abstains": 0, + "TotalPayment": 22200, + "MonthlyPayment": 22200, + "IsEstablished": true, + "IsValid": true, + "Allotted": 22200, + "valid": true + }, + { + "Name": "CoreDevOps-Fuzz-Q324", + "URL": "https://bit.ly/3S3L3hj", + "Hash": "26d42146866d8fa3875739b22fc22e2318df3d3c81d28c5545f651624170796e", + "FeeHash": "2adc33692e3843b37a05741040d1c139baa5fb040e9ecca8944339fd6c60a4e2", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DBh9o9uRGohcDKpeiEyRiwtmTaTL3xDdev", + "Ratio": 0.6473317865429234, + "Yeas": 558, + "Nays": 304, + "Abstains": 0, + "TotalPayment": 45000, + "MonthlyPayment": 15000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 15000, + "valid": true + }, + { + "Name": "PIVX-Spanish-Boost24", + "URL": "https://bit.ly/3VuLa7P", + "Hash": "1b3bba7407941584f9ddd854f98efdab0ca3135e02faf41a9bad2514b2d4de2f", + "FeeHash": "1d89c7fa90a81569fa5b3528e8e3053f7f5ba70f40ac25c4cbfffe9bfeefb14d", + "BlockStart": 4449600, + "BlockEnd": 4579203, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 0, + "PaymentAddress": "DF7V8BsfUT4qHbqC9vQ6so9KS4618P6c35", + "Ratio": 0.5729411764705883, + "Yeas": 487, + "Nays": 363, + "Abstains": 0, + "TotalPayment": 12000, + "MonthlyPayment": 4000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 0, + "valid": false, + "reason": 0 + }, + { + "Name": "PivxAmbassadorProgrm", + "URL": "https://l24.im/0PgVB", + "Hash": "ca5f5193d12d881016eb02792f78c8b056772ab5c6237c7d7b09c153f5437e38", + "FeeHash": "44bcf8492022e7df1dd73e7cdf2fb616eeb20d4729e0e49347bdb7662bed90a6", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DPSsJgQiuLnx2Vkk9C4Kqp8aC6a7CXfDph", + "Ratio": 0.5345238095238095, + "Yeas": 449, + "Nays": 391, + "Abstains": 0, + "TotalPayment": 72171, + "MonthlyPayment": 24057, + "IsEstablished": true, + "IsValid": true, + "Allotted": 0, + "valid": false, + "reason": 0 + }, + { + "Name": "TurkeyMarketingCamp", + "URL": "https://l24.im/pxHY7", + "Hash": "cbc76960343537913443361eab6737975df9ab4b7aa4a0169c26f85087b77313", + "FeeHash": "09f18b8dc5790367a1f032fd8243e1b9646ef922accdbeda2cacb1b1855a4f48", + "BlockStart": 4536000, + "BlockEnd": 4579201, + "TotalPaymentCount": 1, + "RemainingPaymentCount": 0, + "PaymentAddress": "DCBVNBCBsVW5p21q4Jqo7Aq66JAu45vm8D", + "Ratio": 0.0379746835443038, + "Yeas": 6, + "Nays": 152, + "Abstains": 0, + "TotalPayment": 3800, + "MonthlyPayment": 3800, + "IsEstablished": true, + "IsValid": true, + "Allotted": 0, + "valid": false, + "reason": 0 + }, + { + "Name": "PIVX.poker", + "URL": "https://forum.pivx.org/threads/pivx-poker-supplementz.2024/", + "Hash": "07a8366a6ef49d372877d2f12b5e9d39d5ec6937290b022c3268b04e497492c7", + "FeeHash": "9fc5cf508f3cbf020a4b2c72c9bda3af07eb9208564c78daa049d871419e83d8", + "BlockStart": 4363200, + "BlockEnd": 4622406, + "TotalPaymentCount": 6, + "RemainingPaymentCount": 1, + "PaymentAddress": "DTCbXC1pvfYLPZLTfYoJwbXQowPMYodt2u", + "Ratio": 0.2908458864426419, + "Yeas": 251, + "Nays": 612, + "Abstains": 0, + "TotalPayment": 30000, + "MonthlyPayment": 5000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 0, + "valid": false, + "reason": 0 + }, + { + "Name": "nnmfnwl7 dexsetup 01", + "URL": "https://forum.pivx.org/threads/nnmfnwl7-dexsetup-001.2199/", + "Hash": "1d92f6d0593fff8dcbfe6ddd2da3709964cff14411af96c7b8852393a045b365", + "FeeHash": "09e3e75f97122bbd0b68e4eb41a6448c3ce15ccfd6f1cca65a4519dad688d594", + "BlockStart": 4492800, + "BlockEnd": 4622403, + "TotalPaymentCount": 3, + "RemainingPaymentCount": 1, + "PaymentAddress": "DE6KsnGuNGbdN7BcLJusybkEbjHzEn3RL6", + "Ratio": 0.01988071570576541, + "Yeas": 10, + "Nays": 493, + "Abstains": 0, + "TotalPayment": 27000, + "MonthlyPayment": 9000, + "IsEstablished": true, + "IsValid": true, + "Allotted": 0, + "valid": false, + "reason": 0 + } +] diff --git a/tests/unit/proposal_validator/proposal_validator.spec.js b/tests/unit/proposal_validator/proposal_validator.spec.js new file mode 100644 index 000000000..abf740014 --- /dev/null +++ b/tests/unit/proposal_validator/proposal_validator.spec.js @@ -0,0 +1,14 @@ +import { describe, it, expect } from 'vitest'; +import { ProposalValidator } from '../../../scripts/governance/status'; +import proposals from './dummy_proposals.json'; + +describe('Proposal validator tests', () => { + it('correctly validates proposals', () => { + const proposalValidator = new ProposalValidator(2000); + for (const proposal of proposals) { + const { passing, reason } = proposalValidator.validate(proposal); + expect(passing).toBe(proposal.valid); + expect(reason).toBe(proposal.reason); + } + }); +}); From 3407e2e70468c22bb6b971392452cd330ca3f28d Mon Sep 17 00:00:00 2001 From: Alessandro Rezzi Date: Mon, 2 Dec 2024 10:21:27 +0100 Subject: [PATCH 5/5] test: Increase the legacy mainnet wallet balance (#478) --- scripts/network/__mocks__/network_manager.js | 5 +++- tests/unit/wallet/transactions.spec.js | 30 +++++++++++--------- tests/utils/test_utils.js | 6 +++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/scripts/network/__mocks__/network_manager.js b/scripts/network/__mocks__/network_manager.js index f5c7f6d98..293d5f43d 100644 --- a/scripts/network/__mocks__/network_manager.js +++ b/scripts/network/__mocks__/network_manager.js @@ -45,7 +45,10 @@ class TestNetwork { // tx_1 provides a spendable balance of 0.1 * 10^8 satoshi const tx_1 = '010000000138f9c8ac1b4c6ad54e487206daad1fd12bae510dd70fbd2dc928f617ef6b4a47010000006a47304402200d01891ac5dc6d25452cadbe7ba7edd98143631cc2922da45dde94919593b222022066ef14c01c165a1530e2acf74bcd8648d7151b555fa0bfd0222c33e983091678012102033a004cb71693e809c45e1e491bc797654fa0c012be9dd46401ce8368beb705ffffffff02d02c5d05000000001976a91463fa54dad2e215ec21c54ff45a195d49b570b97988ac80969800000000001976a914f49b25384b79685227be5418f779b98a6be4c73888ac00000000'; - return [Transaction.fromHex(tx_1)]; + // tx_2 provides a spendable balance of 10000 PIVs + const tx_2 = + '010000000198bb641ffb74cf14e4f3e3c329fa7f97d605ca1b89d2c61a1e6d728da49342e2020000006a4730440220437d04f65dbc1e23cab38c1a6d2d2b4905ccfdea9d6e33429af8d58d3a689f41022039830fcae7545d8b5964b7c078b3d158e72e01642d274f9c3b38639d3ed292b9012103109bab9e66c51cb5da0dcc200fa7a336b8a3890489026ba7bbdf8ad379cf0251ffffffff020010a5d4e80000001976a914f49b25384b79685227be5418f779b98a6be4c73888ac228ff73e5d0100001976a914f8e77779f1787490e1459f4bde3afde5c057f6f888ac00000000'; + return [Transaction.fromHex(tx_2), Transaction.fromHex(tx_1)]; } else if ( addr === 'xpub6DVaPT3irDwUth7Y6Ff137FgA9jjvsmA2CHD7g2vjvvuMiNSUJRs9F8jSoPpXpc1s7ohR93dNAuzR5T2oPZFDTn7G2mHKYtpwtk7krNZmnV' diff --git a/tests/unit/wallet/transactions.spec.js b/tests/unit/wallet/transactions.spec.js index 338926cf6..2b2d0673d 100644 --- a/tests/unit/wallet/transactions.spec.js +++ b/tests/unit/wallet/transactions.spec.js @@ -1,6 +1,9 @@ import { Wallet } from '../../../scripts/wallet.js'; import { Mempool } from '../../../scripts/mempool.js'; -import { setUpLegacyMainnetWallet } from '../../utils/test_utils'; +import { + legacyMainnetInitialBalance, + setUpLegacyMainnetWallet, +} from '../../utils/test_utils'; import { describe, it, vi, afterAll, expect } from 'vitest'; import { COutpoint, @@ -34,6 +37,7 @@ async function checkFees(wallet, tx, feesPerBytes) { const nBytes = (await wallet.sign(tx)).serialize().length / 2; expect(fees).toBeGreaterThanOrEqual(feesPerBytes * nBytes); expect(fees).toBeLessThanOrEqual((feesPerBytes + 1) * nBytes); + return fees; } describe('Wallet transaction tests', () => { let wallet; @@ -198,11 +202,11 @@ describe('Wallet transaction tests', () => { it('creates a tx with max balance', async () => { const tx = wallet.createTransaction( 'SR3L4TFUKKGNsnv2Q4hWTuET2a4vHpm1b9', - 0.1 * 10 ** 8, + legacyMainnetInitialBalance(), { isDelegation: true } ); expect(tx.version).toBe(1); - expect(tx.vin).toHaveLength(1); + expect(tx.vin).toHaveLength(2); expect(tx.vin[0]).toStrictEqual( new CTxIn({ outpoint: new COutpoint({ @@ -213,13 +217,13 @@ describe('Wallet transaction tests', () => { }) ); expect(tx.vout).toHaveLength(1); + const fees = await checkFees(wallet, tx, MIN_FEE_PER_BYTE); expect(tx.vout[0]).toStrictEqual( new CTxOut({ script: '76a97b63d114291a25b5b4d1802e0611e9bf724a1e57d9210e826714f49b25384b79685227be5418f779b98a6be4c7386888ac', - value: 9997810, // 0.1 PIV - fee + value: legacyMainnetInitialBalance() - fees, }) ); - await checkFees(wallet, tx, MIN_FEE_PER_BYTE); }); it('creates a t->s tx correctly', () => { @@ -251,16 +255,15 @@ describe('Wallet transaction tests', () => { it('it does not insert dust change', async () => { // The tipical output has 34 bytes, so a 200 satoshi change is surely going to be dust - // a P2PKH with 1 input and 1 output will have more or less 190 bytes in size and 1900 satoshi of fees - // Finally 0.1*10**8 is the value of the UTXO we are spending (0.1 PIVs) - const value = 0.1 * 10 ** 8 - 1900 - 200; + // a P2PKH with 2 inputs and 1 output will have more or less 346 bytes in size and 3460 satoshi of fees + const value = legacyMainnetInitialBalance() - 3460 - 200; const tx = wallet.createTransaction( 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', value, { subtractFeeFromAmt: false } ); expect(tx.version).toBe(1); - expect(tx.vin).toHaveLength(1); + expect(tx.vin).toHaveLength(2); expect(tx.vin[0]).toStrictEqual( new CTxIn({ outpoint: new COutpoint({ @@ -319,23 +322,24 @@ describe('Wallet transaction tests', () => { }); it('throws when balance is insufficient', () => { + const value = legacyMainnetInitialBalance() + 1; expect(() => wallet.createTransaction( 'SR3L4TFUKKGNsnv2Q4hWTuET2a4vHpm1b9', - 20 * 10 ** 8, + value, { isDelegation: true } ) ).toThrow(/not enough balance/i); expect(() => wallet.createTransaction( 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', - 20 * 10 ** 8 + value ) ).toThrow(/not enough balance/i); expect(() => wallet.createTransaction( 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', - 50 * 10 ** 8, + value, { useShieldInputs: true } ) ).toThrow(/not enough balance/i); @@ -352,7 +356,7 @@ describe('Wallet transaction tests', () => { expect(() => wallet.createTransaction( 'DLabsktzGMnsK5K9uRTMCF6NoYNY6ET4Bb', - 0.1 * 10 ** 8, + legacyMainnetInitialBalance(), { subtractFeeFromAmt: false } ) ).toThrow(/not enough balance/i); diff --git a/tests/utils/test_utils.js b/tests/utils/test_utils.js index 7667fb532..f3be196cd 100644 --- a/tests/utils/test_utils.js +++ b/tests/utils/test_utils.js @@ -50,6 +50,10 @@ async function setUpWallet(masterKey, includeShield) { expect(wallet.isSyncing).toBeFalsy(); return wallet; } + +export function legacyMainnetInitialBalance() { + return 10 ** 7 + 10 ** 12; +} /** * Creates a mainnet wallet with a legacy master key and a spendable UTXO and a dummy PIVXShield * @returns {Promise} @@ -59,7 +63,7 @@ export async function setUpLegacyMainnetWallet() { const wallet = await setUpWallet(getLegacyMainnet(), true); // sanity check on the balance - expect(wallet.balance).toBe(0.1 * 10 ** 8); + expect(wallet.balance).toBe(legacyMainnetInitialBalance()); expect(wallet.coldBalance).toBe(0); expect(wallet.immatureBalance).toBe(0);