From 8560a736db2b0f6296864c3a24da02608c65f64f Mon Sep 17 00:00:00 2001 From: KraXen72 Date: Mon, 22 Jul 2024 15:44:59 +0200 Subject: [PATCH] improve contacts, fix typedefs, v4 decode (#85) --- .gitignore | 3 +- package.json | 4 +- src/screens/MintInfoScreen.tsx | 84 ++++++++++++-------------------- src/screens/ScanScreen.tsx | 2 +- src/services/cashu/cashuUtils.ts | 23 +++++---- 5 files changed, 48 insertions(+), 68 deletions(-) diff --git a/.gitignore b/.gitignore index bc52320a..810f0c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -69,4 +69,5 @@ yarn-error.log # testing /coverage -minibits.inlang/cache \ No newline at end of file +minibits.inlang/cache +.mise.toml \ No newline at end of file diff --git a/package.json b/package.json index a2f4e55c..1c78ea7c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "postinstall": "patch-package --exclude 'nothing'" }, "dependencies": { - "@cashu/cashu-ts": "minibits-cash/cashu-ts#1.0.0-minibits", + "@cashu/cashu-ts": "KraXen72/cashu-ts-minibits#8345e93ee408e1aa8fde79b7c35badc07ba60694", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.5.2", @@ -99,6 +99,6 @@ "typescript": "5.0.4" }, "engines": { - "node": ">=18" + "node": ">=18 || 20 || 22" } } diff --git a/src/screens/MintInfoScreen.tsx b/src/screens/MintInfoScreen.tsx index 9698d4d4..6bd8bf8f 100644 --- a/src/screens/MintInfoScreen.tsx +++ b/src/screens/MintInfoScreen.tsx @@ -1,5 +1,5 @@ import React, { FC, useEffect, useMemo, useState } from 'react' -import { GetInfoResponse as _GetInfoResponse, SwapMethod } from '@cashu/cashu-ts' +import { GetInfoResponse, NUT15Entry, NUT17Entry, SwapMethod } from '@cashu/cashu-ts' import { isObj } from '@cashu/cashu-ts/src/utils' import Clipboard from '@react-native-clipboard/clipboard' import { observer } from 'mobx-react-lite' @@ -33,17 +33,6 @@ import AppError, { Err } from '../utils/AppError' import { useHeader } from '../utils/useHeader' import { CurrencySign } from './Wallet/CurrencySign' import { SvgXml } from 'react-native-svg' -import { isArray } from 'util' - -// cashu-ts currently does not type NUT15 (multipath payments) correctly -// relevant issue: https://github.com/cashubtc/cashu-ts/issues/142 -// nut-15 spec: https://github.com/cashubtc/nuts/blob/main/15.md -type NUT15Entry = { method: string, unit: string, mpp: boolean } -type GetInfoResponse = _GetInfoResponse & { - nuts: { - 15: Array - } -} if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { @@ -195,10 +184,21 @@ function NutsCard(props: {info: GetInfoResponse}) { nutsSimple.push(['15', (info as NUT15Entry[])[0]?.mpp ?? false]) continue; } + // bug: nutshell 0.15.3 returns NUT17Entry[], instead of { supported: NUT17Entry[] } + // see https://github.com/cashubtc/nutshell/issues/588 + if (nut === '17' && Array.isArray(info) && (info as NUT17Entry[]).length > 0) { + nutsSimple.push(['17', (info as NUT17Entry[]).some(m => m.unit === 'sat')]) + continue; + } + // proper nut-17 response handling (not implemented in nutshell yet) + if (nut === '17' && 'supported' in info && Array.isArray(info.supported) && info.supported.length > 0) { + nutsSimple.push(['17', info.supported.some(m => m.unit === 'sat')]) + continue; + } if ('disabled' in info && info.disabled === false) { // detailed supportedNutsDetailed.push([nut, info]) - } else if ('supported' in info) { // simple - nutsSimple.push([nut, info.supported]) + } else if ('supported' in info && typeof info.supported !== 'undefined') { // simple + nutsSimple.push([nut, !!info.supported]) } else { nutsSimple.push([nut, false]) // fallback or detailed but disabled nut } @@ -235,65 +235,41 @@ function NutsCard(props: {info: GetInfoResponse}) { function ContactCard(props: { info: GetInfoResponse, popupMessage: (msg: string) => void }) { const textDim = useThemeColor('textDim') + type Contact = {method: string, info: string} - type Contact = [string, string] - type NewContact = {method: string, info: string} - - let contacts: Array = [] - let newContacts: Array = [] - let mayBeContacts = props.info.contact - - if(mayBeContacts.length > 0) { - if(mayBeContacts[0].method) { - newContacts = mayBeContacts - } - - if(Array.isArray(mayBeContacts[0])) { - contacts = mayBeContacts + let contacts: Contact[] = [] + for (const contact of props.info.contact) { + if (Array.isArray(contact) && contact.length === 2) { + contacts.push({ method: contact[0], info: contact[1] }) + } else if ('method' in contact && 'info' in contact) { + contacts.push(contact) } } - - log.trace({newContacts, contacts}) return ( ) : ( <> - {contacts.map(([platform, user]: Contact, index) => ( + {contacts.map(({ method, info }, index) => ( } - RightComponent={} + LeftComponent={} + RightComponent={} topSeparator={index !== 0} onLongPress={() => { - Clipboard.setString(user) - props.popupMessage(translate('common.copySuccessParam', { param: user })) - }} - /> - ))} - {newContacts.map((c: NewContact, index) => ( - } - RightComponent={} - topSeparator={index !== 0} - onLongPress={() => { - Clipboard.setString(c.info) - props.popupMessage(translate('common.copySuccessParam', { param: c.info })) + Clipboard.setString(info) + props.popupMessage(translate('common.copySuccessParam', { param: info })) }} /> ))} @@ -408,7 +384,7 @@ export const MintInfoScreen: FC> = observer const mint = mintsStore.findByUrl(route.params.mintUrl) if (mint) { - const info: _GetInfoResponse = await MintClient.getMintInfo(mint.mintUrl) + const info: GetInfoResponse = await MintClient.getMintInfo(mint.mintUrl) mint.setStatus(MintStatus.ONLINE) if(info.name && info.name !== mint.shortname) { await mint.setShortname() diff --git a/src/screens/ScanScreen.tsx b/src/screens/ScanScreen.tsx index 24e219c4..cb418930 100644 --- a/src/screens/ScanScreen.tsx +++ b/src/screens/ScanScreen.tsx @@ -181,7 +181,7 @@ export const ScanScreen: FC> = function ScanScree default: try { // generic scan button on wallet screen - const incomingData = IncomingParser.findAndExtract(incoming) + const incomingData = IncomingParser.findAndExtract(incoming) return IncomingParser.navigateWithIncomingData(incomingData, navigation, unit) } catch (e: any) { e.name = Err.VALIDATION_ERROR diff --git a/src/services/cashu/cashuUtils.ts b/src/services/cashu/cashuUtils.ts index fc1ab54a..4c76691d 100644 --- a/src/services/cashu/cashuUtils.ts +++ b/src/services/cashu/cashuUtils.ts @@ -10,30 +10,33 @@ import {TokenEntry} from '../../models/TokenEntry' import {Proof} from '../../models/Proof' import { log } from '../logService' - - const findEncodedCashuToken = function (content: string) { - const words = content.split(/\s+|\n+/) // Split text into words - const maybeToken = words.find(word => word.includes("cashuA")) - return maybeToken || null + const words = content.split(/\s+|\n+/) // Split text into words + const maybeToken = words.find(word => CASHU_TOKEN_PREFIXES.some(pref => word.includes(pref))) + return maybeToken || null } -const cashuUriPrefixes = [ +const CASHU_URI_PREFIXES = [ 'https://wallet.nutstash.app/#', 'https://wallet.cashu.me/?token=', 'web+cashu://', 'cashu://', 'cashu:' ] +const CASHU_TOKEN_PREFIXES = [ + 'cashuA', + 'cashuB' +] + export function validCashuAToken(text: string) { - for (const prefix of cashuUriPrefixes) { + for (const prefix of CASHU_URI_PREFIXES) { if (text && text.startsWith(prefix)) { text = text.slice(prefix.length) break // necessary } } - return text && text.startsWith('cashuA') + return text && CASHU_TOKEN_PREFIXES.some(pref => text.startsWith(pref)) } const extractEncodedCashuToken = function (maybeToken: string): string { @@ -43,12 +46,12 @@ const extractEncodedCashuToken = function (maybeToken: string): string { let encodedToken: string | undefined = undefined let decoded: Token | undefined = undefined - if (maybeToken && maybeToken.startsWith('cashuA')) { + if (maybeToken && CASHU_TOKEN_PREFIXES.some(pref => maybeToken.startsWith(pref))) { decoded = decodeToken(maybeToken) // throws return maybeToken } - for (const prefix of cashuUriPrefixes) { + for (const prefix of CASHU_URI_PREFIXES) { if (maybeToken && maybeToken.startsWith(prefix)) { encodedToken = maybeToken.slice(prefix.length) break // necessary