Support the update of mint URL if it had to be changed by mint operator

This commit is contained in:
minibits-cash
2024-06-27 00:04:49 +02:00
parent 4bd700ec38
commit a0c6f5caff
10 changed files with 161 additions and 34 deletions
+1 -1
View File
@@ -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",
+2 -1
View File
@@ -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 {
+3 -1
View File
@@ -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",
+32 -9
View File
@@ -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<MintUnit>()),
@@ -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('/')
+1 -5
View File
@@ -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) {
+14
View File
@@ -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() {
+1 -1
View File
@@ -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.
+22 -1
View File
@@ -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(
+76 -15
View File
@@ -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<SettingsStackScreenProps<'Mints'>> = 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<SettingsStackScreenProps<'Mints'>> = 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<SettingsStackScreenProps<'Mints'>> = observer(funct
bottomSeparator={true}
style={{paddingHorizontal: spacing.medium}}
/>
<ListItem
leftIcon='faGlobe'
onPress={updateMintUrlStart}
text="Update mint URL"
bottomSeparator={true}
style={{paddingHorizontal: spacing.medium}}
/>
<ListItem
leftIcon="faXmark"
onPress={removeMint}
@@ -361,7 +408,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
<View style={$bottomModal}>
<Text
preset="subheading"
tx='mintsScreen.addMintUrl'
tx={selectedMint ? 'mintsScreen.updateMintUrl' : 'mintsScreen.addMintUrl'}
// style={{marginBottom: spacing.medium, textAlign: 'center'}}
/>
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: spacing.small}}>
@@ -397,7 +444,19 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
/>
</View>
<View style={$buttonContainer}>
<Button
{selectedMint ? (
<Button
tx='common.update'
style={{
// borderTopLeftRadius: 0,
// borderBottomLeftRadius: 0,
marginRight: spacing.small,
minWidth: 80
}}
onPress={updateMintUrl}
/>
) : (
<Button
tx='common.save'
style={{
// borderTopLeftRadius: 0,
@@ -406,7 +465,9 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
minWidth: 80
}}
onPress={addMint}
/>
/>
)}
<Button
tx='common.cancel'
onPress={toggleAddMintModal}
+9
View File
@@ -1,5 +1,7 @@
import { showMessage } from "react-native-flash-message"
import { colors, spacing, useThemeColor } from "../theme"
import QuickCrypto from 'react-native-quick-crypto'
import { fromByteArray } from "react-native-quick-base64"
/**
* sleep statement.
@@ -59,4 +61,11 @@ export const warningMessage = function(message: string, description?: string) {
})
}
export const generateId = function (lengthInBytes: number) {
const random = QuickCrypto.randomBytes(lengthInBytes)
const uint8Array = new Uint8Array(random)
return Buffer.from(uint8Array).toString('hex')
}