Lightning: Support for payment comments, Tor mint support (#83)

Co-authored-by: minibits-cash <mail@minibits.cash>
This commit is contained in:
KraXen72
2024-06-28 09:25:03 +02:00
committed by GitHub
co-authored by minibits-cash
parent ae192570ce
commit 307e3b93c8
24 changed files with 607 additions and 425 deletions
+2
View File
@@ -68,3 +68,5 @@ yarn-error.log
# testing
/coverage
minibits.inlang/cache
+1
View File
@@ -0,0 +1 @@
cache
+5
View File
@@ -22,6 +22,11 @@
"preferredTfuncName": "translate",
"recognizedTfuncNames": [
"translate"
],
"recognizedJSXAttributes": [
"tx",
"subTx",
"contentTx"
]
}
}
+1 -1
View File
@@ -58,7 +58,7 @@ export const AvatarHeader = observer(function (props: AvatarHeaderProps) {
)}
</View>
{props.heading && <Text style={{ fontSize: 26, lineHeight: 40 }} text={props.heading} adjustsFontSizeToFit={true} numberOfLines={1} />}
{props.text && <Text preset='bold' text={props.text} style={{ color: 'white', marginBottom: spacing.small }} />}
{props.text && <Text preset='bold' text={props.text} style={{ color: 'white', marginBottom: spacing.small }} numberOfLines={2} />}
{props.children}
</View>
)
+2 -4
View File
@@ -110,7 +110,7 @@ export function BottomModal(props: ModalProps) {
isVisible = true,
onBackdropPress,
onBackButtonPress,
backdropOpacity = 0.45,
backdropOpacity = 0.25,
content,
contentTx,
contentTxOptions,
@@ -220,7 +220,7 @@ const $outerContainerBase: ViewStyle = {
justifyContent: 'flex-end',
alignItems: 'center',
width: '100%',
margin: 0,
margin: 0
}
const $innerContainerBase: ViewStyle = {
@@ -230,5 +230,3 @@ const $innerContainerBase: ViewStyle = {
borderTopRightRadius: spacing.small,
padding: spacing.small
}
+15 -10
View File
@@ -1,20 +1,25 @@
import * as React from 'react'
import { StyleSheet, View, ActivityIndicator, ViewProps, TextStyle } from 'react-native'
import { StyleSheet, View, ActivityIndicator, ViewProps, TextStyle, ColorValue, ViewStyle } from 'react-native'
import { useThemeColor } from '../theme'
import { Text } from './Text'
import { spacing } from '../theme'
export function Loading(props: ViewProps & {statusMessage?: string, textStyle?: TextStyle}) {
export function Loading(props: ViewProps & { statusMessage?: string, textStyle?: TextStyle, shiftedUp?: boolean }) {
return (
<View style={[{
flex: 1,
alignItems: 'center',
backgroundColor: useThemeColor('background'),
opacity: 0.5,
justifyContent: 'center',
zIndex: 9999,
}, StyleSheet.absoluteFillObject, props.style]}>
<View style={[StyleSheet.absoluteFillObject, $loading(useThemeColor('background'), props?.shiftedUp ?? false), props.style]}>
<ActivityIndicator color="#ccc" animating size="large" />
{props.statusMessage && (<Text style={[{opacity: 1}, props.textStyle]} text={props.statusMessage}/>)}
</View>
)
}
const $loading = (bg: ColorValue, shiftedUp = false) => ({
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: bg,
opacity: 0.5,
zIndex: 9999,
// for cards that go up in the header, the loading should cover them
marginTop: shiftedUp ? -(spacing.extraLarge * 2 + spacing.small) : 0,
} satisfies ViewStyle)
+77
View File
@@ -0,0 +1,77 @@
import React, {forwardRef} from 'react'
import {View, ViewStyle, TextInput, TextStyle} from 'react-native'
import {spacing} from '../theme'
import {Button} from './Button'
import {Card} from './Card'
import {translate} from '../i18n'
interface MemoInputProps {
memo: string
setMemo: (memo: string) => void
disabled?: boolean
onMemoDone: () => void
onMemoEndEditing?: () => void
maxLength?: number
}
export const MemoInputCard = forwardRef<TextInput, MemoInputProps>((props, memoInputRef) => {
const {
memo,
setMemo,
disabled = true,
onMemoDone,
onMemoEndEditing = () => {},
maxLength = 200,
} = props
return (
<Card
style={$memoCard}
ContentComponent={
<View style={$memoContainer}>
<TextInput
ref={memoInputRef}
onChangeText={memo => setMemo(memo)}
onEndEditing={onMemoEndEditing}
value={`${memo}`}
style={$memoInput}
maxLength={maxLength}
keyboardType="default"
selectTextOnFocus={true}
placeholder={translate('sendScreen.memo')}
editable={!disabled}
/>
<Button
preset="secondary"
style={$memoButton}
text="Done"
onPress={onMemoDone}
disabled={disabled}
/>
</View>
}
/>
)
})
const $memoContainer: ViewStyle = {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
}
const $memoCard: ViewStyle = {
marginBottom: spacing.small,
}
const $memoButton: ViewStyle = {
maxHeight: 50,
}
const $memoInput: TextStyle = {
flex: 1,
borderRadius: spacing.small,
fontSize: 16,
textAlignVertical: 'center',
marginRight: spacing.small,
}
+7 -4
View File
@@ -121,7 +121,7 @@
"copyAsEncodedTokens": "Copy as encoded tokens",
"copyContactPublicKey": "Copy contact's public key",
"copyMnemonicBackupWorkaround": "To apply backup to your existing funds, send your entire balance to yourself. Otherwise, only funds from this point onward can be restored.",
"copyProofs": "text=\"Copy proofs\"",
"copyProofs": "Copy proofs",
"decodedMissingCurrencyUnit": "Currency unit is missing in the received token. Wallet will assume token amount is in bitcoin $%{unit}. Do not continue if you are not sure this is correct.",
"deleteContact": "Delete contact",
"deleteContactDesc": "Remove this contact from your wallet.",
@@ -150,7 +150,8 @@
"lightningAddress": "Lightning address",
"lightningInvoiceSharedWaiting": "Lightning invoice has been shared, waiting to be paid by receiver.",
"lightningPayScreen": {
"onPasteEmptyClipboard": "First copy Lightning invoice, address or pay code to pay to. Then paste."
"onPasteEmptyClipboard": "First copy Lightning invoice, address or pay code to pay to. Then paste.",
"payHeading": "Pay"
},
"loadingPublicInfo": "Loading public info",
"localBackupDisabled": "Local backup is off",
@@ -398,7 +399,8 @@
"sendToContactDescription": "Send your ecash to one of your contacts stored in your contact list.",
"showOrShareToken": "Share ecash",
"showOrShareTokenDescription": "Present or share ecash token for an amount you want to send.",
"title": "Send"
"title": "Send",
"memo": "Memo for recipient"
},
"saveContactPrivate": "Save as private contact",
"saveContactPrivateDesc": "Save %{nip05} to your Private contacts so you can pay faster",
@@ -522,7 +524,8 @@
"insufficientFunds": "There is not enough balance in %{currency} to pay the invoice amount and expected fees: %{amount} %{currency}",
"LUD18unsupported": "Minibits does not yet support entering of payer identity data (LUD18).",
"noMintWithBalance": "There is no mint with %{unit} balance from which payment can be made.",
"selectMintFrom": "Select mint balance to transfer from"
"selectMintFrom": "Select mint balance to transfer from",
"requestInvoice": "Continue"
},
"updateScreen": {
"gotoPlayStore": "Visit Play Store",
+2 -1
View File
@@ -43,7 +43,8 @@
"walletLabel": "Peňaženka"
},
"transferScreen": {
"noMintWithBalance": "Neexistuje žiaden mint so zostatkom %{unit} z ktorého je možné uskutočniť platbu."
"noMintWithBalance": "Neexistuje žiaden mint so zostatkom %{unit} z ktorého je možné uskutočniť platbu.",
"requestInvoice": "Pokračovať"
},
"walletScreen": {
"sendEcash": "Poslať Ecash",
-1
View File
@@ -4,7 +4,6 @@ import {Switch, TextStyle, View, ViewStyle} from 'react-native'
import {colors, spacing, useThemeColor} from '../theme'
import {SettingsStackScreenProps} from '../navigation'
import {
Icon,
ListItem,
Screen,
Text,
+1 -2
View File
@@ -163,9 +163,8 @@ export const LightningPayScreen: FC<WalletStackScreenProps<'LightningPay'>> = fu
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
<Text
preset="heading"
text={'Pay'}
tx="lightningPayScreen.payHeading"
style={{color: 'white'}}
// style={$tranAmount}
/>
</View>
<View style={$contentContainer}>
+20 -3
View File
@@ -1,5 +1,5 @@
import React, { FC, useEffect, useMemo, useState } from 'react'
import { GetInfoResponse, SwapMethod } from '@cashu/cashu-ts'
import { GetInfoResponse as _GetInfoResponse, 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'
@@ -34,6 +34,16 @@ import { useHeader } from '../utils/useHeader'
import { CurrencySign } from './Wallet/CurrencySign'
import { SvgXml } from 'react-native-svg'
// 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<NUT15Entry>
}
}
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true)
@@ -177,6 +187,13 @@ function NutsCard(props: {info: GetInfoResponse}) {
// detailed nuts are separated from simple ones if we want to show more info abt them in the future
for (const [nut, info] of Object.entries(props.info.nuts)) {
if (nut === '15' && (info as NUT15Entry[]).length > 0) {
// see https://github.com/cashubtc/nuts/blob/main/15.md - multipath payments
// in the future, it might be nice to show for which currencies are multipath payments supported
// for example by extending NutItem
nutsSimple.push(['15', (info as NUT15Entry[])[0]?.mpp ?? false])
continue;
}
if ('disabled' in info && info.disabled === false) { // detailed
supportedNutsDetailed.push([nut, info])
} else if ('supported' in info) { // simple
@@ -357,12 +374,12 @@ export const MintInfoScreen: FC<SettingsStackScreenProps<'MintInfo'>> = 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()
}
setMintInfo(info)
setMintInfo(info as GetInfoResponse)
setMint(mint)
} else {
throw new AppError(Err.VALIDATION_ERROR, 'Could not find mint', { mintUrl: route.params.mintUrl })
-7
View File
@@ -88,13 +88,6 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = observer(funct
const addMint = async function () {
setIsAddMintVisible(false)
if(mintUrl.includes('.onion')) {
if(!userSettingsStore.isTorDaemonOn) {
setInfo('Tor support has been discontinued in version v0.1.7-beta. Minibits needs help to get Tor support back. Visit our Github for more info.')
return
}
}
if (mintsStore.alreadyExists(mintUrl)) {
const msg = translate('mintsScreen.mintExists')
log.trace(msg)
@@ -77,8 +77,8 @@ export const IncomingRequests = observer(function (props: {
style={$card}
/>
)}
{isLoading && <Loading />}
</View>
{isLoading && <Loading />}
{error && <ErrorModal error={error} />}
{info && <InfoModal message={info} />}
</Screen>
+1 -1
View File
@@ -133,7 +133,7 @@ const $unselected: ViewStyle = {
const $contentContainer: TextStyle = {
padding: spacing.small,
minHeight: spacing.huge,
}
const $picturesContainer: TextStyle = {
+7 -33
View File
@@ -64,6 +64,7 @@ import { round, toNumber } from '../utils/number'
import { QRCodeBlock } from './Wallet/QRCode'
import numbro from 'numbro'
import { TranItem } from './TranDetailScreen'
import { MemoInputCard } from '../components/MemoInputCard'
if (Platform.OS === 'android' &&
@@ -611,42 +612,15 @@ export const SendScreen: FC<WalletStackScreenProps<'Send'>> = observer(
</View>
<View style={$contentContainer}>
{!encodedTokenToSend && (
<Card
style={$memoCard}
ContentComponent={
<View style={$memoContainer}>
<TextInput
<MemoInputCard
memo={memo}
ref={memoInputRef}
onChangeText={memo => setMemo(memo)}
onEndEditing={onMemoEndEditing}
value={`${memo}`}
style={$memoInput}
maxLength={200}
keyboardType="default"
selectTextOnFocus={true}
placeholder="Memo for recipient"
editable={
transactionStatus === TransactionStatus.PENDING
? false
: true
}
/>
<Button
preset="secondary"
style={$memoButton}
text="Done"
onPress={onMemoDone}
disabled={
transactionStatus === TransactionStatus.PENDING
? true
: false
}
/>
</View>
}
setMemo={setMemo}
disabled={transactionStatus === TransactionStatus.PENDING}
onMemoDone={onMemoDone}
onMemoEndEditing={onMemoEndEditing}
/>
)}
{isMintSelectorVisible && (
<MintBalanceSelector
mintBalances={availableMintBalances}
+1 -1
View File
@@ -350,7 +350,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
}
style={{maxHeight: spacing.screenHeight * 0.66}}
/>
{isLoading && <Loading />}
{isLoading && <Loading shiftedUp={true} />}
</View>
<BottomModal
isVisible={isDeleteModalVisible ? true : false}
@@ -39,29 +39,29 @@ export const TransactionListItem = observer(function (
case TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE:
if (tx.sentFrom) {
if (!tx.memo || tx.memo.includes('Sent from Minibits')) {
return translate('transactionCommon.from', {sender: tx.sentFrom})
return translate('transactionCommon.from', {sender: tx.sentFrom}).slice(0, 30)
}
} else {
return tx.memo ? tx.memo : translate('transactionCommon.youReceived')
}
case TransactionType.SEND:
return tx.memo
return (tx.memo
? tx.memo
: tx.sentTo
? translate('transactionCommon.sentTo', {receiver: tx.sentTo})
: translate('transactionCommon.youSent')
: translate('transactionCommon.youSent')).slice(0, 30)
case TransactionType.TOPUP:
return tx.memo
return (tx.memo
? tx.memo
: tx.sentFrom
? translate('transactionCommon.receivedFrom', {sender: tx.sentFrom})
: translate('transactionCommon.youReceived')
: translate('transactionCommon.youReceived')).slice(0, 30)
case TransactionType.TRANSFER:
return tx.memo
return (tx.memo
? tx.memo
: tx.sentTo
? translate('transactionCommon.paidTo', {receiver: tx.sentTo})
: translate('transactionCommon.youPaid')
: translate('transactionCommon.youPaid')).slice(0, 30)
default:
return translate('transactionCommon.unknown')
}
+113 -27
View File
@@ -9,6 +9,7 @@ import {
ViewStyle,
FlatList,
TextInput,
Keyboard,
} from 'react-native'
import {spacing, useThemeColor, colors, typography} from '../theme'
import {WalletStackScreenProps} from '../navigation'
@@ -51,6 +52,7 @@ import numbro from 'numbro'
import { TranItem } from './TranDetailScreen'
import useIsInternetReachable from '../utils/useIsInternetReachable'
import { translate } from '../i18n'
import { MemoInputCard } from '../components/MemoInputCard'
if (
@@ -64,6 +66,8 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
function TransferScreen({route, navigation}) {
const amountInputRef = useRef<TextInput>(null)
const lnurlCommentInputRef = useRef<TextInput>(null)
const {proofsStore, mintsStore, paymentRequestsStore, transactionsStore} = useStores()
const isInternetReachable = useIsInternetReachable()
@@ -80,6 +84,8 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
const [finalFee, setFinalFee] = useState<number>(0)
const [memo, setMemo] = useState('')
const [lnurlDescription, setLnurlDescription] = useState('')
const [lnurlPayCommentAllowed, setLnurlPayCommentAllowed] = useState(0)
const [lnurlPayComment, setLnurlPayComment] = useState('')
const [availableMintBalances, setAvailableMintBalances] = useState<MintBalance[]>([])
const [mintBalanceToTransferFrom, setMintBalanceToTransferFrom] = useState<MintBalance | undefined>()
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
@@ -110,7 +116,7 @@ useEffect(() => {
}
}, [])
// TODO: fix indentation in this component
useEffect(() => {
const setUnitAndMint = () => {
@@ -203,6 +209,9 @@ useFocusEffect(
break
}
}
if ('commentAllowed' in lnurlParams && lnurlParams.commentAllowed > 0) {
setLnurlPayCommentAllowed(lnurlParams.commentAllowed)
}
if(desc) {
setLnurlDescription(desc)
@@ -272,7 +281,13 @@ useEffect(() => {
try {
log.trace('[getEstimatedFee]', {mintBalanceToTransferFrom})
if (!mintBalanceToTransferFrom || !mintBalanceToTransferFrom.balances[unit] || !encodedInvoice) {
log.trace('[getEstimatedFee]', 'Not ready... exiting')
log.trace(
'[getEstimatedFee]',
'Not ready... exiting',
mintBalanceToTransferFrom,
mintBalanceToTransferFrom?.balances[unit],
encodedInvoice,
)
return
}
@@ -412,6 +427,8 @@ const resetState = function () {
setIsTransferTaskSentToQueue(false)
setIsResultModalVisible(false)
setResultModalInfo(undefined)
setLnurlPayCommentAllowed(0)
setLnurlPayComment('')
}
const toggleResultModal = () => setIsResultModalVisible(previousState => !previousState)
@@ -421,10 +438,9 @@ const onMintBalanceSelect = function (balance: MintBalance) {
}
// Amount is editable only in case of LNURL Pay, while invoice is not yet retrieved
const onAmountEndEditing = async function () {
const onRequestLnurlInvoice = async function () { // onAmountEndEditing
try {
const precision = getCurrency(unit).precision
const mantissa = getCurrency(unit).mantissa
const amount = round(toNumber(amountToTransfer) * precision, 0)
if (!amount || amount === 0) {
@@ -459,17 +475,26 @@ const onAmountEndEditing = async function () {
setAmountToTransfer(`${numbro(amountToTransfer).format({thousandSeparated: true, mantissa: getCurrency(unit).mantissa})}`)
setIsLoading(true)
const encoded = await LnurlClient.getInvoice(lnurlPayParams, amount * 1000)
const encoded = await LnurlClient.getInvoice(lnurlPayParams, amount * 1000, lnurlPayCommentAllowed > 0 ? lnurlPayComment : void 0)
setIsLoading(false)
if (encoded) return onEncodedInvoice(encoded);
if (encoded) return onEncodedInvoice(encoded)
throw new AppError(Err.NOTFOUND_ERROR, `Could not get lightning invoice from ${lnurlPayParams.domain}`)
} catch (e: any) { handleError(e) }
}
const ensureCommentNotTooLong = async function () {
Keyboard.dismiss()
if (!lnurlPayCommentAllowed || lnurlPayComment.trim().length === 0) return;
if (lnurlPayComment.trim().length > lnurlPayCommentAllowed) {
setLnurlPayComment(lnurlPayComment.slice(0, lnurlPayCommentAllowed));
}
}
const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: string = '') {
log.trace("onEncodedInvoice")
try {
navigation.setParams({encodedInvoice: undefined})
navigation.setParams({paymentRequest: undefined})
@@ -480,9 +505,6 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice)
const expiresAt = addSeconds(new Date(timestamp as number * 1000), expiry as number)
// log.trace('Decoded invoice', invoice, 'onEncodedInvoice')
log.trace('[onEncodedInvoice] Invoice data', {amount, expiresAt, description})
if (!amount || amount === 0) {
infoMessage(translate('payCommon.amountZeroOrNegative'))
return;
@@ -494,10 +516,16 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
setInvoice(invoice)
setInvoiceExpiry(expiresAt)
if(description) {
setMemo(description)
}
if (paymentRequestDesc) {
setMemo(paymentRequestDesc)
} else if(description) {
setMemo(description)
}
if (lnurlPayComment) {
setMemo(lnurlPayComment)
}
// We need to retrieve the quote first to know how much is needed to settle invoice in selected currency unit
@@ -550,7 +578,7 @@ const transfer = async function () {
encodedInvoice,
)
} catch (e: any) {
handleError(e)
}
}
@@ -567,14 +595,17 @@ const handleError = function(e: AppError): void {
}
const headerBg = useThemeColor('header')
const feeColor = colors.palette.primary200
const iconColor = useThemeColor('textDim')
const satsColor = colors.palette.primary200
return (
<Screen preset="fixed" contentContainerStyle={$screen}>
<MintHeader
mint={mintBalanceToTransferFrom ? mintsStore.findByUrl(mintBalanceToTransferFrom?.mintUrl) : undefined}
mint={
mintBalanceToTransferFrom
? mintsStore.findByUrl(mintBalanceToTransferFrom?.mintUrl)
: undefined
}
unit={unit}
navigation={navigation}
/>
@@ -583,15 +614,13 @@ const satsColor = colors.palette.primary200
<TextInput
ref={amountInputRef}
onChangeText={amount => setAmountToTransfer(amount)}
onEndEditing={onAmountEndEditing}
// onEndEditing={onAmountEndEditing}
value={amountToTransfer}
style={$amountInput}
maxLength={9}
keyboardType="numeric"
selectTextOnFocus={true}
editable={
encodedInvoice ? false : true
}
editable={encodedInvoice ? false : true}
/>
{encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
@@ -602,7 +631,7 @@ const satsColor = colors.palette.primary200
/>
) : (
<Text
size='sm'
size="sm"
tx="payCommon.amountToPayLabel"
style={{color: 'white', textAlign: 'center'}}
/>
@@ -614,8 +643,14 @@ const satsColor = colors.palette.primary200
<Card
style={[$card, {minHeight: 50}]}
ContentComponent={
<>
<ListItem
text={lnurlPayParams?.address || memo || lnurlPayParams?.domain || translate("common.noDescPlaceholder")}
text={
lnurlPayParams?.address ||
memo ||
lnurlPayParams?.domain ||
translate('common.noDescPlaceholder')
}
subText={lnurlDescription}
LeftComponent={
<Icon
@@ -627,17 +662,53 @@ const satsColor = colors.palette.primary200
}
style={$item}
/>
{lnurlPayComment && encodedInvoice && (
<ListItem
text={lnurlPayComment}
topSeparator={true}
LeftComponent={
<Icon
containerStyle={$iconContainer}
icon="faPencil"
size={spacing.medium}
color={iconColor}
/>
}
style={$item}
/>
)}
</>
}
/>
)}
{!encodedInvoice && transactionStatus !== TransactionStatus.COMPLETED && lnurlPayCommentAllowed > 0 && (
<MemoInputCard
memo={lnurlPayComment}
setMemo={setLnurlPayComment}
ref={lnurlCommentInputRef}
onMemoDone={ensureCommentNotTooLong}
disabled={encodedInvoice ? true : false}
maxLength={lnurlPayCommentAllowed}
/>
)}
{!encodedInvoice && transactionStatus !== TransactionStatus.COMPLETED && (
<View style={$bottomContainer}>
<View style={$buttonContainer}>
<Button
tx="transferScreen.requestInvoice"
onPress={onRequestLnurlInvoice}
/>
</View>
</View>
)}
{availableMintBalances.length > 0 &&
transactionStatus !== TransactionStatus.COMPLETED && (
<MintBalanceSelector
mintBalances={availableMintBalances}
selectedMintBalance={mintBalanceToTransferFrom}
unit={unit}
title={translate("payCommon.payFrom")}
confirmTitle={translate("payCommon.payNow")}
title={translate('payCommon.payFrom')}
confirmTitle={translate('payCommon.payNow')}
onMintBalanceSelect={onMintBalanceSelect}
onCancel={onClose}
onMintBalanceConfirm={transfer}
@@ -651,7 +722,10 @@ const satsColor = colors.palette.primary200
<TranItem
label="tranDetailScreen.trasferredTo"
isFirst={true}
value={mintsStore.findByUrl(transaction.mint)?.shortname as string}
value={
mintsStore.findByUrl(transaction.mint)
?.shortname as string
}
/>
{transaction?.memo && (
<TranItem
@@ -707,7 +781,10 @@ const satsColor = colors.palette.primary200
tx={'common.close'}
onPress={() => {
if (isInvoiceDonation) {
navigation.navigate('ContactsNavigator', {screen: 'Contacts', params: {}})
navigation.navigate('ContactsNavigator', {
screen: 'Contacts',
params: {},
})
} else {
navigation.navigate('Wallet', {})
}
@@ -740,7 +817,9 @@ const satsColor = colors.palette.primary200
<ResultModalInfo
icon="faTriangleExclamation"
iconColor={colors.palette.angry500}
title={resultModalInfo?.title || translate('payCommon.failed')}
title={
resultModalInfo?.title || translate('payCommon.failed')
}
message={resultModalInfo?.message}
/>
<View style={$buttonContainer}>
@@ -773,7 +852,6 @@ const satsColor = colors.palette.primary200
</>
)}
</>
}
onBackButtonPress={toggleResultModal}
onBackdropPress={toggleResultModal}
@@ -808,6 +886,14 @@ const $headerContainer: TextStyle = {
color: 'white',
}
const $commentInput: TextStyle = {
textAlignVertical: 'top' ,
borderRadius: spacing.extraSmall,
padding: spacing.extraSmall,
alignSelf: 'stretch',
height: 120,
}
const $contentContainer: TextStyle = {
flex: 1,
padding: spacing.extraSmall,
+6 -6
View File
@@ -298,12 +298,12 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
log.trace('newMintUrl', newMintUrl)
if(newMintUrl.includes('.onion')) {
if(!userSettingsStore.isTorDaemonOn) {
setInfo('Please enable Tor daemon in Privacy settings before connecting to the mint using .onion address.')
return
}
}
// if(newMintUrl.includes('.onion')) {
// if(!userSettingsStore.isTorDaemonOn) {
// setInfo('Please enable Tor daemon in Privacy settings before connecting to the mint using .onion address.')
// return
// }
// }
if (mintsStore.alreadyExists(newMintUrl)) {
const msg = translate('walletScreen.mintExists')
+39 -18
View File
@@ -32,6 +32,14 @@ let _seed: Uint8Array | undefined = undefined
const { mintsStore } = rootStoreInstance
function isOnionMint(mintUrl: string) {
return new URL(mintUrl).hostname.endsWith('.onion')
}
const TorVPNSetupInstructions = `
Is your Tor VPN running?
Mints on Tor require a Tor VPN like Orbot. You can get it on Google Play or Github (guardianproject/orbot).
Tor can sometimes be slow, so requests might time out. If Orbot is running, try again in a bit.`
const getOrCreateMnemonic = async function (): Promise<string> {
let mnemonic: string | undefined = undefined
@@ -169,7 +177,9 @@ const getWallet = async function (
log.trace('[getWallet]', 'Returning new cashuWallet instance')
return newWallet
} catch (e: any) {
throw new AppError(Err.NETWORK_ERROR, 'Could not get keys from the mint', {message: e.message, caller: 'getWallet'})
let message = 'Could not connect to the selected mint.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(Err.NETWORK_ERROR, message, {message: e.message, caller: 'getWallet'})
}
}
@@ -188,11 +198,9 @@ const getMintKeysets = async function (mintUrl: string) {
const {keysets} = await cashuMint.getKeySets() // all
return keysets
} catch (e: any) {
throw new AppError(
Err.CONNECTION_ERROR,
`Could not connect to the selected mint.`,
{message: e.message, mintUrl},
)
let message = 'Could not connect to the selected mint.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(Err.CONNECTION_ERROR, message, { message: e.message, mintUrl })
}
}
@@ -287,15 +295,16 @@ const sendFromMint = async function (
proofsToSend: send as Proof[],
}
} catch (e: any) {
let message = 'The mint could not return signatures necessary for this transaction'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
`The mint could not return signatures necessary for this transaction`,
message,
{
message: e.message,
mintUrl,
caller: 'MintClient.sendFromMint',
proofsToSendFrom,
}
)
}
@@ -322,9 +331,11 @@ const getSpentOrPendingProofsFromMint = async function (
}
} catch (e: any) {
let message = 'Could not get response from the mint.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'Could not get response from the mint.',
message,
{
message: e.message,
caller: 'CashuMintClient.getSpentOrPendingProofsFromMint',
@@ -353,9 +364,11 @@ const getLightningMeltQuote = async function (
return lightningQuote
} catch (e: any) {
let message = 'The mint could not return the lightning quote.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'The mint could not return the lightning quote.',
message,
{
message: e.message,
caller: 'getLightningMeltQuote',
@@ -395,9 +408,11 @@ const payLightningMelt = async function (
preimage,
}
} catch (e: any) {
let message = 'Lightning payment failed.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'Lightning payment failed.',
message,
{
message: isObj(e.message) ? JSON.stringify(e.message) : e.message,
caller: 'payLightningMelt',
@@ -431,9 +446,11 @@ const getBolt11MintQuote = async function (
mintQuote,
}
} catch (e: any) {
let message = 'The mint could not return an invoice.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'The mint could not return an invoice.',
message,
{
message: e.message,
caller: 'getBolt11MintQuote',
@@ -466,9 +483,11 @@ const getBolt11MintQuoteIsPaid = async function (
isPaid
}
} catch (e: any) {
let message = 'The mint could not return the state of a mint quote.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'The mint could not return the state of a mint quote.',
message,
{
message: e.message,
caller: 'getBolt11MintQuoteIsPaid',
@@ -514,10 +533,11 @@ const mintProofs = async function (
proofs: [],
}
}
let message = 'The mint returned error on request to mint new ecash.'
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'The mint returned error on request to mint new ecash.',
message,
{
message: e.message,
caller: 'mintProofs',
@@ -577,14 +597,15 @@ const getMintInfo = async function (
log.trace('[getMintInfo]', {info})
return info
} catch (e: any) {
let message = 'The mint could not return mint information.';
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
throw new AppError(
Err.MINT_ERROR,
'The mint could not return mint information.',
message,
{
message: e.message,
caller: 'getMintInfo',
mintUrl,
mintUrl
}
)
}
+7 -7
View File
@@ -123,17 +123,17 @@ const getLnurlAddressParams = async (lnurlAddress: string) => {
const getInvoice = async(lnurlParams: LNURLPayParams, amountMsat?: number) => {
const getInvoice = async (lnurlParams: LNURLPayParams, amountMsat?: number, comment?: string) => {
if (!amountMsat || amountMsat === 0) { amountMsat = lnurlParams.minSendable }
if(!amountMsat || amountMsat === 0) {
amountMsat = lnurlParams.minSendable
}
const url = lnurlParams.callback.includes('?') ? `${lnurlParams.callback}&amount=${amountMsat}` : `${lnurlParams.callback}?amount=${amountMsat}`
const url = new URL(lnurlParams.callback)
url.searchParams.set('amount', amountMsat.toString())
if (comment) url.searchParams.set('comment', comment)
const strUrl = url.toString()
const method = 'GET'
const headers = MinibitsClient.getPublicHeaders()
const invoiceResult: any = await MinibitsClient.fetchApi(url, {
const invoiceResult: any = await MinibitsClient.fetchApi(strUrl, {
method,
headers,
})
+2 -1
View File
@@ -118,7 +118,7 @@ export const transferTask = async function (
// get locked counter values
const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit)
const {isPaid, feeSavedProofs} = await MintClient.payLightningMelt(
const {isPaid, preimage, feeSavedProofs} = await MintClient.payLightningMelt(
mintUrl,
unit,
meltQuote,
@@ -206,6 +206,7 @@ export const transferTask = async function (
transactionData.push({
status: TransactionStatus.COMPLETED,
finalFee,
preimage, // TODO add to tx details
createdAt: new Date(),
})