diff --git a/.gitignore b/.gitignore
index fb8dcc84..bc52320a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -67,4 +67,6 @@ yarn-error.log
.metro-health-check*
# testing
-/coverage
\ No newline at end of file
+/coverage
+
+minibits.inlang/cache
\ No newline at end of file
diff --git a/minibits.inlang/.gitignore b/minibits.inlang/.gitignore
new file mode 100644
index 00000000..5e465967
--- /dev/null
+++ b/minibits.inlang/.gitignore
@@ -0,0 +1 @@
+cache
\ No newline at end of file
diff --git a/minibits.inlang/settings.json b/minibits.inlang/settings.json
index 2e7f4338..18ab7a59 100644
--- a/minibits.inlang/settings.json
+++ b/minibits.inlang/settings.json
@@ -8,7 +8,7 @@
"modules": [
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-empty-pattern@latest/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/message-lint-rule-without-source@latest/dist/index.js",
- "https://cdn.jsdelivr.net/gh/KraXen72/inlang-t-function-jsx-hybrid-matcher/dist/index.js",
+ "https://cdn.jsdelivr.net/gh/KraXen72/inlang-t-function-jsx-hybrid-matcher/dist/index.js",
"https://cdn.jsdelivr.net/npm/@inlang/plugin-json@latest/dist/index.js"
],
"plugin.inlang.json": {
@@ -22,6 +22,11 @@
"preferredTfuncName": "translate",
"recognizedTfuncNames": [
"translate"
+ ],
+ "recognizedJSXAttributes": [
+ "tx",
+ "subTx",
+ "contentTx"
]
}
}
\ No newline at end of file
diff --git a/src/components/AvatarHeader.tsx b/src/components/AvatarHeader.tsx
index bdc705f2..a3d747b9 100644
--- a/src/components/AvatarHeader.tsx
+++ b/src/components/AvatarHeader.tsx
@@ -58,7 +58,7 @@ export const AvatarHeader = observer(function (props: AvatarHeaderProps) {
)}
{props.heading && }
- {props.text && }
+ {props.text && }
{props.children}
)
diff --git a/src/components/BottomModal.tsx b/src/components/BottomModal.tsx
index 8871e37e..7ed66993 100644
--- a/src/components/BottomModal.tsx
+++ b/src/components/BottomModal.tsx
@@ -110,7 +110,7 @@ export function BottomModal(props: ModalProps) {
isVisible = true,
onBackdropPress,
onBackButtonPress,
- backdropOpacity = 0.45,
+ backdropOpacity = 0.25,
content,
contentTx,
contentTxOptions,
@@ -216,19 +216,17 @@ export function BottomModal(props: ModalProps) {
}
const $outerContainerBase: ViewStyle = {
- flex: 1,
- justifyContent: 'flex-end',
- alignItems: 'center',
- width: '100%',
- margin: 0,
+ flex: 1,
+ justifyContent: 'flex-end',
+ alignItems: 'center',
+ width: '100%',
+ margin: 0
}
const $innerContainerBase: ViewStyle = {
- width: '100%',
- alignItems: 'center',
- borderTopLeftRadius: spacing.small,
- borderTopRightRadius: spacing.small,
- padding: spacing.small
-}
-
-
+ width: '100%',
+ alignItems: 'center',
+ borderTopLeftRadius: spacing.small,
+ borderTopRightRadius: spacing.small,
+ padding: spacing.small
+}
\ No newline at end of file
diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx
index b67e8f0d..4c62dd5a 100644
--- a/src/components/Loading.tsx
+++ b/src/components/Loading.tsx
@@ -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}) {
- return (
-
-
- {props.statusMessage && ()}
-
- )
-}
\ No newline at end of file
+export function Loading(props: ViewProps & { statusMessage?: string, textStyle?: TextStyle, shiftedUp?: boolean }) {
+ return (
+
+
+ {props.statusMessage && ()}
+
+ )
+}
+
+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)
\ No newline at end of file
diff --git a/src/components/MemoInputCard.tsx b/src/components/MemoInputCard.tsx
new file mode 100644
index 00000000..4a8b565b
--- /dev/null
+++ b/src/components/MemoInputCard.tsx
@@ -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((props, memoInputRef) => {
+ const {
+ memo,
+ setMemo,
+ disabled = true,
+ onMemoDone,
+ onMemoEndEditing = () => {},
+ maxLength = 200,
+ } = props
+ return (
+
+ setMemo(memo)}
+ onEndEditing={onMemoEndEditing}
+ value={`${memo}`}
+ style={$memoInput}
+ maxLength={maxLength}
+ keyboardType="default"
+ selectTextOnFocus={true}
+ placeholder={translate('sendScreen.memo')}
+ editable={!disabled}
+ />
+
+
+ }
+ />
+ )
+})
+
+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,
+}
diff --git a/src/i18n_messages/en.json b/src/i18n_messages/en.json
index 8fe3796c..928ab294 100644
--- a/src/i18n_messages/en.json
+++ b/src/i18n_messages/en.json
@@ -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",
diff --git a/src/i18n_messages/sk.json b/src/i18n_messages/sk.json
index 7a4a5056..0b709418 100644
--- a/src/i18n_messages/sk.json
+++ b/src/i18n_messages/sk.json
@@ -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",
diff --git a/src/screens/BackupScreen.tsx b/src/screens/BackupScreen.tsx
index 97118fd5..e65e2dde 100644
--- a/src/screens/BackupScreen.tsx
+++ b/src/screens/BackupScreen.tsx
@@ -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,
diff --git a/src/screens/Contacts/OwnName.tsx b/src/screens/Contacts/OwnName.tsx
index 686add6c..d0fe4fcf 100644
--- a/src/screens/Contacts/OwnName.tsx
+++ b/src/screens/Contacts/OwnName.tsx
@@ -466,7 +466,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
}
/>
)}
- {isLoading && }
+ {isLoading && }
> = fu
@@ -300,11 +299,11 @@ const $buttonContainer: ViewStyle = {
const $addressInput: TextStyle = {
- textAlignVertical: 'top' ,
- borderRadius: spacing.extraSmall,
- padding: spacing.extraSmall,
- alignSelf: 'stretch',
- height: 120,
+ textAlignVertical: 'top' ,
+ borderRadius: spacing.extraSmall,
+ padding: spacing.extraSmall,
+ alignSelf: 'stretch',
+ height: 120,
}
const $bottomContainer: ViewStyle = {
diff --git a/src/screens/MintInfoScreen.tsx b/src/screens/MintInfoScreen.tsx
index 20534eff..11e2269f 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, 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
+ }
+}
+
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> = 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 })
diff --git a/src/screens/MintsScreen.tsx b/src/screens/MintsScreen.tsx
index 14c2827f..00f4d82e 100644
--- a/src/screens/MintsScreen.tsx
+++ b/src/screens/MintsScreen.tsx
@@ -88,13 +88,6 @@ export const MintsScreen: FC> = 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)
diff --git a/src/screens/PaymentRequests/IncomingRequests.tsx b/src/screens/PaymentRequests/IncomingRequests.tsx
index 873c0174..6907ae8d 100644
--- a/src/screens/PaymentRequests/IncomingRequests.tsx
+++ b/src/screens/PaymentRequests/IncomingRequests.tsx
@@ -77,8 +77,8 @@ export const IncomingRequests = observer(function (props: {
style={$card}
/>
)}
- {isLoading && }
+ {isLoading && }
{error && }
{info && }
diff --git a/src/screens/PictureScreen.tsx b/src/screens/PictureScreen.tsx
index a1daa3d7..b2c6bce4 100644
--- a/src/screens/PictureScreen.tsx
+++ b/src/screens/PictureScreen.tsx
@@ -133,7 +133,7 @@ const $unselected: ViewStyle = {
const $contentContainer: TextStyle = {
padding: spacing.small,
-
+ minHeight: spacing.huge,
}
const $picturesContainer: TextStyle = {
diff --git a/src/screens/SendScreen.tsx b/src/screens/SendScreen.tsx
index 3e238de1..da384ae0 100644
--- a/src/screens/SendScreen.tsx
+++ b/src/screens/SendScreen.tsx
@@ -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' &&
@@ -610,43 +611,16 @@ export const SendScreen: FC> = observer(
- {!encodedTokenToSend && (
-
- setMemo(memo)}
- onEndEditing={onMemoEndEditing}
- value={`${memo}`}
- style={$memoInput}
- maxLength={200}
- keyboardType="default"
- selectTextOnFocus={true}
- placeholder="Memo for recipient"
- editable={
- transactionStatus === TransactionStatus.PENDING
- ? false
- : true
- }
- />
-
-
- }
- />
- )}
-
+ {!encodedTokenToSend && (
+
+ )}
{isMintSelectorVisible && (
>
/>
}
style={{maxHeight: spacing.screenHeight * 0.66}}
- />
- {isLoading && }
+ />
+ {isLoading && }
> = observer(
function TransferScreen({route, navigation}) {
const amountInputRef = useRef(null)
+ const lnurlCommentInputRef = useRef(null)
+
const {proofsStore, mintsStore, paymentRequestsStore, transactionsStore} = useStores()
const isInternetReachable = useIsInternetReachable()
@@ -80,6 +84,8 @@ export const TransferScreen: FC> = observer(
const [finalFee, setFinalFee] = useState(0)
const [memo, setMemo] = useState('')
const [lnurlDescription, setLnurlDescription] = useState('')
+ const [lnurlPayCommentAllowed, setLnurlPayCommentAllowed] = useState(0)
+ const [lnurlPayComment, setLnurlPayComment] = useState('')
const [availableMintBalances, setAvailableMintBalances] = useState([])
const [mintBalanceToTransferFrom, setMintBalanceToTransferFrom] = useState()
const [transactionStatus, setTransactionStatus] = useState()
@@ -94,23 +100,23 @@ export const TransferScreen: FC> = observer(
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus; title?: string, message: string} | undefined>()
-useEffect(() => {
+ useEffect(() => {
const focus = () => {
- if(route.params?.paymentOption === SendOption.LNURL_PAY) {
- amountInputRef && amountInputRef.current
- ? amountInputRef.current.focus()
- : false
- }
+ if (route.params?.paymentOption === SendOption.LNURL_PAY) {
+ amountInputRef && amountInputRef.current
+ ? amountInputRef.current.focus()
+ : false
+ }
}
-
- const timer = setTimeout(() => focus(), 100)
-
+
+ const timer = setTimeout(() => focus(), 100)
+
return () => {
- clearTimeout(timer)
+ clearTimeout(timer)
}
-}, [])
-
+ }, [])
+// TODO: fix indentation in this component
useEffect(() => {
const setUnitAndMint = () => {
@@ -203,9 +209,12 @@ useFocusEffect(
break
}
}
+ if ('commentAllowed' in lnurlParams && lnurlParams.commentAllowed > 0) {
+ setLnurlPayCommentAllowed(lnurlParams.commentAllowed)
+ }
if(desc) {
- setLnurlDescription(desc)
+ setLnurlDescription(desc)
}
if(address) {
@@ -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 precision = getCurrency(unit).precision
const amount = round(toNumber(amountToTransfer) * precision, 0)
if (!amount || amount === 0) {
@@ -444,7 +460,7 @@ const onAmountEndEditing = async function () {
return;
}
- if (lnurlPayParams.maxSendable && amount > lnurlPayParams.maxSendable / 1000 ) {
+ if (lnurlPayParams.maxSendable && amount > lnurlPayParams.maxSendable / 1000) {
infoMessage(translate("payCommon.maximumPay", {
amount: roundDown(lnurlPayParams.maxSendable / 1000, 0),
currency: CurrencyCode.SATS
@@ -453,23 +469,32 @@ const onAmountEndEditing = async function () {
}
if (lnurlPayParams.payerData) {
- infoMessage(translate("transferScreen.LUD18unsupported"))
+ infoMessage(translate("transferScreen.LUD18unsupported"))
}
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
@@ -522,36 +550,36 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
}
const transfer = async function () {
- try {
- if(!meltQuote) {
- throw new AppError(Err.VALIDATION_ERROR, 'Missing quote to initiate transfer transaction')
- }
-
- if (!mintBalanceToTransferFrom) {
- setInfo(translate("transferScreen.selectMintFrom"))
- return;
- }
-
- setIsLoading(true)
- setIsTransferTaskSentToQueue(true)
-
-
- log.trace('[transfer]', {isInvoiceDonation})
-
- const amountToTransferInt = round(toNumber(amountToTransfer) * getCurrency(unit).precision, 0)
-
- WalletTask.transfer(
- mintBalanceToTransferFrom,
- amountToTransferInt,
- unit,
- meltQuote,
- memo,
- invoiceExpiry as Date,
- encodedInvoice,
- )
- } catch (e: any) {
-
+ try {
+ if(!meltQuote) {
+ throw new AppError(Err.VALIDATION_ERROR, 'Missing quote to initiate transfer transaction')
}
+
+ if (!mintBalanceToTransferFrom) {
+ setInfo(translate("transferScreen.selectMintFrom"))
+ return;
+ }
+
+ setIsLoading(true)
+ setIsTransferTaskSentToQueue(true)
+
+
+ log.trace('[transfer]', {isInvoiceDonation})
+
+ const amountToTransferInt = round(toNumber(amountToTransfer) * getCurrency(unit).precision, 0)
+
+ WalletTask.transfer(
+ mintBalanceToTransferFrom,
+ amountToTransferInt,
+ unit,
+ meltQuote,
+ memo,
+ invoiceExpiry as Date,
+ encodedInvoice,
+ )
+ } catch (e: any) {
+ handleError(e)
+ }
}
@@ -567,218 +595,268 @@ 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 (
-
-
+
+
+
+ setAmountToTransfer(amount)}
+ // onEndEditing={onAmountEndEditing}
+ value={amountToTransfer}
+ style={$amountInput}
+ maxLength={9}
+ keyboardType="numeric"
+ selectTextOnFocus={true}
+ editable={encodedInvoice ? false : true}
+ />
+
+ {encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
+
+ ) : (
+
+ )}
+
+
+
+ {transactionStatus !== TransactionStatus.COMPLETED && (
+
+
+ }
+ style={$item}
+ />
+ {lnurlPayComment && encodedInvoice && (
+
+ }
+ style={$item}
+ />
+ )}
+ >
+ }
+ />
+ )}
+ {!encodedInvoice && transactionStatus !== TransactionStatus.COMPLETED && lnurlPayCommentAllowed > 0 && (
+
+ )}
+ {!encodedInvoice && transactionStatus !== TransactionStatus.COMPLETED && (
+
+
+
+
+
+ )}
+ {availableMintBalances.length > 0 &&
+ transactionStatus !== TransactionStatus.COMPLETED && (
+
+ )}
+ {transaction && transactionStatus === TransactionStatus.COMPLETED && (
+
+
+ {transaction?.memo && (
+
+ )}
+
+
+ >
+ }
/>
-
-
- setAmountToTransfer(amount)}
- onEndEditing={onAmountEndEditing}
- value={amountToTransfer}
- style={$amountInput}
- maxLength={9}
- keyboardType="numeric"
- selectTextOnFocus={true}
- editable={
- encodedInvoice ? false : true
- }
- />
-
- {encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
-
- ) : (
-
- )}
-
+ )}
+ {transactionStatus === TransactionStatus.COMPLETED && (
+
+
+
+
-
- {transactionStatus !== TransactionStatus.COMPLETED && (
-
- }
- style={$item}
- />
- }
+ )}
+ {isLoading && }
+ {error && }
+ {info && }
+
+
+ {resultModalInfo &&
+ transactionStatus === TransactionStatus.COMPLETED && (
+ <>
+
- )}
- {availableMintBalances.length > 0 &&
- transactionStatus !== TransactionStatus.COMPLETED && (
-
- )}
- {transaction && transactionStatus === TransactionStatus.COMPLETED && (
-
-
- {transaction?.memo && (
-
- )}
-
-
- >
- }
- />
- )}
- {transactionStatus === TransactionStatus.COMPLETED && (
-
-
-
-
+
+
+ >
)}
- {isLoading && }
- {error && }
- {info && }
-
-
- {resultModalInfo &&
- transactionStatus === TransactionStatus.COMPLETED && (
- <>
-
-
-
- >
- )}
- {resultModalInfo &&
- transactionStatus === TransactionStatus.REVERTED && (
- <>
-
-
-
-
- >
- )}
- {resultModalInfo &&
- transactionStatus === TransactionStatus.ERROR && (
- <>
-
-
-
-
- >
- )}
- {resultModalInfo &&
- transactionStatus === TransactionStatus.PENDING && (
- <>
-
-
-
- >
- )}
- >
-
- }
- onBackButtonPress={toggleResultModal}
- onBackdropPress={toggleResultModal}
- />
-
+ {resultModalInfo &&
+ transactionStatus === TransactionStatus.REVERTED && (
+ <>
+
+
+
+
+ >
+ )}
+ {resultModalInfo &&
+ transactionStatus === TransactionStatus.ERROR && (
+ <>
+
+
+
+
+ >
+ )}
+ {resultModalInfo &&
+ transactionStatus === TransactionStatus.PENDING && (
+ <>
+
+
+
+ >
+ )}
+ >
+ }
+ 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,
diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx
index 6b580d5f..dc1a41b3 100644
--- a/src/screens/WalletScreen.tsx
+++ b/src/screens/WalletScreen.tsx
@@ -298,12 +298,12 @@ export const WalletScreen: FC = 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')
diff --git a/src/services/cashuMintClient.ts b/src/services/cashuMintClient.ts
index 83ea764a..1287d6ad 100644
--- a/src/services/cashuMintClient.ts
+++ b/src/services/cashuMintClient.ts
@@ -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 {
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,16 +295,17 @@ 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: e.message,
- mintUrl,
- caller: 'MintClient.sendFromMint',
- proofsToSendFrom,
-
- }
+ Err.MINT_ERROR,
+ message,
+ {
+ message: e.message,
+ mintUrl,
+ caller: 'MintClient.sendFromMint',
+ proofsToSendFrom,
+ }
)
}
}
@@ -322,13 +331,15 @@ 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',
- mintUrl
+ message: e.message,
+ caller: 'CashuMintClient.getSpentOrPendingProofsFromMint',
+ mintUrl
}
)
}
@@ -353,13 +364,15 @@ 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',
- request: {mintUrl, unit, encodedInvoice},
+ message: e.message,
+ caller: 'getLightningMeltQuote',
+ request: {mintUrl, unit, encodedInvoice},
}
)
}
@@ -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,14 +446,16 @@ 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: e.message,
- caller: 'getBolt11MintQuote',
- mintUrl,
- }
+ Err.MINT_ERROR,
+ message,
+ {
+ message: e.message,
+ caller: 'getBolt11MintQuote',
+ mintUrl,
+ }
)
}
}
@@ -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,16 +597,17 @@ const getMintInfo = async function (
log.trace('[getMintInfo]', {info})
return info
} catch (e: any) {
- throw new AppError(
- Err.MINT_ERROR,
- 'The mint could not return mint information.',
- {
- message: e.message,
- caller: 'getMintInfo',
- mintUrl,
-
- }
- )
+ let message = 'The mint could not return mint information.';
+ if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
+ throw new AppError(
+ Err.MINT_ERROR,
+ message,
+ {
+ message: e.message,
+ caller: 'getMintInfo',
+ mintUrl
+ }
+ )
}
}
diff --git a/src/services/lnurlService.ts b/src/services/lnurlService.ts
index 88805bf1..04b2e32e 100644
--- a/src/services/lnurlService.ts
+++ b/src/services/lnurlService.ts
@@ -123,27 +123,27 @@ 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, {
- method,
- headers,
+ const invoiceResult: any = await MinibitsClient.fetchApi(strUrl, {
+ method,
+ headers,
})
if(invoiceResult.status && invoiceResult.status === 'ERROR') {
- throw new AppError(Err.CONNECTION_ERROR, invoiceResult.reason, {domain: lnurlParams.domain, caller: 'getLnurlParams'})
+ throw new AppError(Err.CONNECTION_ERROR, invoiceResult.reason, {domain: lnurlParams.domain, caller: 'getLnurlParams'})
}
if(invoiceResult.pr) {
- return invoiceResult.pr as string
+ return invoiceResult.pr as string
}
throw new AppError(Err.CONNECTION_ERROR, 'Could not get lightning invoice from the LNURL provider', {domain: lnurlParams.domain, caller: 'getLnurlParams'})
diff --git a/src/services/wallet/transferTask.ts b/src/services/wallet/transferTask.ts
index ccce7337..ae977cdf 100644
--- a/src/services/wallet/transferTask.ts
+++ b/src/services/wallet/transferTask.ts
@@ -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(),
})