From a0c6f5caff6db8c829a57a2fe735ecf5c69443c9 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Thu, 27 Jun 2024 00:04:49 +0200 Subject: [PATCH 1/3] Support the update of mint URL if it had to be changed by mint operator --- package.json | 2 +- src/components/Icon.tsx | 3 +- src/i18n_messages/en.json | 4 +- src/models/Mint.ts | 41 ++++++++++--- src/models/MintsStore.ts | 6 +- src/models/ProofsStore.ts | 14 +++++ src/models/RootStore.ts | 2 +- src/models/helpers/setupRootStore.ts | 23 ++++++- src/screens/MintsScreen.tsx | 91 +++++++++++++++++++++++----- src/utils/utils.ts | 9 +++ 10 files changed, 161 insertions(+), 34 deletions(-) diff --git a/package.json b/package.json index c6d6b45d..45adb703 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minibits_wallet", - "version": "0.1.8-beta.29", + "version": "0.1.8-beta.30", "private": true, "scripts": { "android:clean": "cd android && ./gradlew clean", diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 286996ab..6adb31a7 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -91,13 +91,14 @@ import { faCircleQuestion } from '@fortawesome/free-solid-svg-icons/faCircleQues import { faEnvelope } from '@fortawesome/free-solid-svg-icons/faEnvelope' import { faCircleArrowUp } from "@fortawesome/free-solid-svg-icons/faCircleArrowUp" import { faCircleArrowDown } from "@fortawesome/free-solid-svg-icons/faCircleArrowDown" +import { faGlobe } from "@fortawesome/free-solid-svg-icons/faGlobe" export type IconTypes = keyof typeof iconRegistry // TODO remove need for manual iconregistry? // would be best to just import all of them, i guess, or figure out something smart -export const iconRegistry = { faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown } +export const iconRegistry = { faAddressCard, faAddressBook, faWallet, faQrcode, faClipboard, faSliders, faCoins, faEllipsisVertical, faEllipsis, faArrowUp, faArrowDown, faArrowLeft, faXmark, faInfoCircle, faBug, faCheckCircle, faArrowTurnUp, faArrowTurnDown, faPencil, faTags, faShareFromSquare, faRotate, faCode, faBan, faCircle, faPaperPlane, faBolt, faArrowUpFromBracket, faArrowRightToBracket, faPlus, faShieldHalved, faCloudArrowUp, faPaintbrush, faCopy, faBurst, faUserShield, faLock, faLockOpen, faTriangleExclamation, faDownload, faUpload, faRecycle, faListUl, faExpand, faFingerprint, faWandMagicSparkles, faCircleUser, faComment, faKey, faCircleNodes, faBullseye, faEyeSlash, faUpRightFromSquare, faShareNodes, faPaste, faKeyboard, faMoneyBill1, faGears, faTag, faBank, faChevronDown, faChevronUp, faCircleExclamation, faCircleQuestion, faEnvelope, faTwitter, faTelegramPlane, faDiscord, faGithub, faReddit, faCircleArrowUp, faCircleArrowDown, faGlobe } interface IconProps extends TouchableOpacityProps { diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json index 10ac57d1..8fe3796c 100644 --- a/src/i18n_messages/en.json +++ b/src/i18n_messages/en.json @@ -70,7 +70,8 @@ "success": "Success!", "copySuccessParam": "Copied %{param}", "hide": "Hide", - "show": "Show" + "show": "Show", + "update": "Update" }, "confirmBackupRecovery": "Do you really want to recover ecash from the backup?", "confirmMintRemoval": "Do you really want to remove %{hostname} - %{shortname} from the wallet?", @@ -164,6 +165,7 @@ "addMint": "Add mint", "addMintMinibits": "Add Minibits mint", "addMintUrl": "Add mint URL", + "updateMintUrl": "Update mint URL", "blockMint": "Block receiving from this mint", "copy": "Copy mint URL", "invalidUrl": "This is not a valid URL", diff --git a/src/models/Mint.ts b/src/models/Mint.ts index a69039d3..81d3eea5 100644 --- a/src/models/Mint.ts +++ b/src/models/Mint.ts @@ -8,6 +8,7 @@ import { MINIBITS_MINT_URL } from '@env' import AppError, { Err } from '../utils/AppError' import { MintUnit } from '../services/wallet/currency' import { getRootStore } from './helpers/getRootStore' +import { generateId } from '../utils/utils' // used as a helper type across app /* export type Balance = { @@ -53,7 +54,8 @@ export type MintProofsCounter = { */ export const MintModel = types .model('Mint', { - mintUrl: types.identifier, + id: types.optional(types.identifier, generateId(8)), + mintUrl: types.string, hostname: types.maybe(types.string), shortname: types.maybe(types.string), units: types.array(types.frozen()), @@ -130,6 +132,14 @@ export const MintModel = types const counter = self.proofsCounters.find(c => c.inFlightTid === tId) return counter as MintProofsCounter | undefined }, + validateURL(url: string) { + try { + new URL(url) + return true + } catch (e) { + return false + } + }, })) .actions(self => ({ getProofsCounterByUnit: flow(function* getProofsCounterByUnit(unit: MintUnit) { @@ -140,14 +150,6 @@ export const MintModel = types const counter = self.getOrCreateProofsCounter(keys.id, unit) return counter }), - validateURL(url: string) { - try { - new URL(url) - return true - } catch (e) { - return false - } - }, setHostname() { try { self.hostname = new URL(self.mintUrl).hostname @@ -155,6 +157,27 @@ export const MintModel = types return false } }, + setMintUrl(url: string) { + if(self.validateURL(url)) { + const mintsStore = getRootStore(self).mintsStore + + if(!mintsStore.alreadyExists(url)) { + + const proofsStore = getRootStore(self).proofsStore + proofsStore.updateMintUrl(self.mintUrl, url) // update mintUrl on mint's proofs + self.mintUrl = url + + return true + } + + throw new AppError(Err.VALIDATION_ERROR, 'Mint URL already exists.', {url}) + } else { + throw new AppError(Err.VALIDATION_ERROR, 'Invalid Mint URL.', {url}) + } + }, + setId() { // migration + self.id = generateId(8) + }, setShortname: flow(function* setShortname() { // get name from URL as a fallback const lastSlashIndex = self.mintUrl.lastIndexOf('/') diff --git a/src/models/MintsStore.ts b/src/models/MintsStore.ts index e1b633d3..b3343763 100644 --- a/src/models/MintsStore.ts +++ b/src/models/MintsStore.ts @@ -13,7 +13,7 @@ import { import { MintClient } from '../services' import AppError, { Err } from '../utils/AppError' import { MintKeyset } from '@cashu/cashu-ts' -import { getRootStore } from './helpers/getRootStore' + import { MintUnit, MintUnits } from '../services/wallet/currency' export type MintsByHostname = { @@ -90,10 +90,6 @@ import { MintUnit, MintUnits } from '../services/wallet/currency' if(!activeKeysets || activeKeysets.length === 0) { throw new AppError(Err.VALIDATION_ERROR, 'Mint has no active keysets and is not operational', {mintUrl}) } - - const newMint: Mint = { - mintUrl, - } for(const keyset of activeKeysets) { if(keyset.active === true) { diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index 98e618f0..d7d8e768 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -200,6 +200,20 @@ export const ProofsStoreModel = types proofs.replace(proofs.filter(proof => !proofsToRemove.some(removed => removed.secret === proof.secret))) }, + updateMintUrl(currentMintUrl: string, updatedMintUrl: string) { + log.trace('[proofStore.updateMintUrl] start') + for (const proof of self.proofs) { + if(proof.mintUrl === currentMintUrl) { + proof.setMintUrl(updatedMintUrl) + } + } + + for (const proof of self.pendingProofs) { + if(proof.mintUrl === currentMintUrl) { + proof.setMintUrl(updatedMintUrl) + } + } + }, })) .views(self => ({ get proofsCount() { diff --git a/src/models/RootStore.ts b/src/models/RootStore.ts index 30a4fbd5..c9b3feb7 100644 --- a/src/models/RootStore.ts +++ b/src/models/RootStore.ts @@ -8,7 +8,7 @@ import {PaymentRequestsStoreModel} from './PaymentRequestsStore' import {ProofsStoreModel} from './ProofsStore' import {RelaysStoreModel} from './RelaysStore' -export const rootStoreModelVersion = 14 // Update this if model changes require migrations defined in setupRootStore.ts +export const rootStoreModelVersion = 15 // Update this if model changes require migrations defined in setupRootStore.ts /** * A RootStore model. diff --git a/src/models/helpers/setupRootStore.ts b/src/models/helpers/setupRootStore.ts index 67430bfc..07bb43ef 100644 --- a/src/models/helpers/setupRootStore.ts +++ b/src/models/helpers/setupRootStore.ts @@ -25,6 +25,7 @@ import AppError, { Err } from '../../utils/AppError' import { MINIBITS_NIP05_DOMAIN, MINIBITS_RELAY_URL } from '@env' import { LogLevel } from '../../services/log/logTypes' import { MintStatus } from '../Mint' +import { generateId } from '../../utils/utils' /** * The key we'll be saving our state as within storage. @@ -262,7 +263,27 @@ async function _runMigrations(rootStore: RootStore) { } catch (e: any) { log.warn('[setupRootStore] Migration error', {message: e.name}) } - } + } + + if(currentVersion < 15) { + log.trace(`Starting rootStore migrations from version v${currentVersion} -> v15`) + try { + + for (const mint of mintsStore.allMints) { + try { + mint.setId() + } catch (e: any) { + log.warn('[_runMigrations]', e.message) + continue + } + } + + log.info(`Completed rootStore migrations to the version v${rootStoreModelVersion}`) + rootStore.setVersion(rootStoreModelVersion) + } catch (e: any) { + log.warn('[setupRootStore] Migration error', {message: e.name}) + } + } } catch (e: any) { throw new AppError( diff --git a/src/screens/MintsScreen.tsx b/src/screens/MintsScreen.tsx index 8655471e..14c2827f 100644 --- a/src/screens/MintsScreen.tsx +++ b/src/screens/MintsScreen.tsx @@ -24,10 +24,13 @@ import {Mint} from '../models/Mint' import {useStores} from '../models' import {useHeader} from '../utils/useHeader' import {log} from '../services/logService' -import AppError from '../utils/AppError' +import AppError, { Err } from '../utils/AppError' import {translate} from '../i18n' import {MintListItem} from './Mints/MintListItem' import { SvgXml } from 'react-native-svg' +import { getSnapshot, isStateTreeNode } from 'mobx-state-tree' +import { MintKeyset } from '@cashu/cashu-ts' +import { MintClient } from '../services' @@ -93,10 +96,10 @@ export const MintsScreen: FC> = observer(funct } if (mintsStore.alreadyExists(mintUrl)) { - const msg = translate('mintsScreen.mintExists') - log.trace(msg) - setInfo(msg) - return + const msg = translate('mintsScreen.mintExists') + log.trace(msg) + setInfo(msg) + return } try { @@ -109,23 +112,60 @@ export const MintsScreen: FC> = observer(funct } finally { setMintUrl('') setIsLoading(false) + toggleAddMintModal() } } const updateMint = async function () { - if (!selectedMint) {return} + if (!selectedMint) {return} - try { - setIsLoading(true) - await mintsStore.updateMint(selectedMint.mintUrl) - setInfo(translate("mintSettingsUpdated")) - } catch (e: any) { + try { + setIsLoading(true) + await mintsStore.updateMint(selectedMint.mintUrl) + setInfo(translate("mintSettingsUpdated")) + } catch (e: any) { + handleError(e) + } finally { + onMintUnselect() + } + } + + + const updateMintUrlStart = async function () { + if (!selectedMint) {return} + toggleAddMintModal() // open + } + + + const updateMintUrl = async function () { + if (!selectedMint) {return} + try { + if (isStateTreeNode(selectedMint)) { // update URL of existing mint + // checks if mint is reachable on new url, if it the same mint by checking keysets and syncs local data + if(mintsStore.alreadyExists(mintUrl)) { + throw new AppError(Err.VALIDATION_ERROR, 'Mint with this URL already exists.') + } + + setIsLoading(true) + const activeKeysets: MintKeyset[] = await MintClient.getMintKeysets(mintUrl) + const matchingKeyset = activeKeysets.find(keyset => selectedMint.keysets?.some(k => k === keyset.id)) + + if(!matchingKeyset) { + throw new AppError(Err.VALIDATION_ERROR, 'No keyset match, provided URL likely points to different mint.') + } + + selectedMint.setMintUrl!(mintUrl) + } + } catch (e: any) { handleError(e) } finally { + setMintUrl('') setIsLoading(false) + onMintUnselect() // close + toggleAddMintModal() // close } - } + } const addDefaultMint = async function () { @@ -343,6 +383,13 @@ export const MintsScreen: FC> = observer(funct bottomSeparator={true} style={{paddingHorizontal: spacing.medium}} /> + > = observer(funct @@ -397,7 +444,19 @@ export const MintsScreen: FC> = observer(funct /> -