Fix: Donation for custom wallet address throws error when paying from the same wallet

This commit is contained in:
minibits-cash
2024-12-16 00:34:58 +01:00
parent eb2838ec8f
commit 3ebe27a210
8 changed files with 105 additions and 153 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.10-beta.11",
"version": "0.1.10-beta.12",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+6 -2
View File
@@ -152,9 +152,13 @@ export type WalletStackParamList = {
unit: MintUnit,
encodedInvoice?: string,
paymentRequest?: PaymentRequest,
lnurlParams?: LNURLPayParams & {address?: string},
lnurlParams?: LNURLPayParams & {address?: string},
fixedAmount?: number,
comment?: string
paymentOption?: SendOption,
mintUrl?: string,
mintUrl?: string,
isDonation?: boolean,
donationForName?: string
}
Topup: {
unit: MintUnit,
+43 -101
View File
@@ -1,6 +1,6 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'
import {Text as RNText, TextStyle, View, ViewStyle, InteractionManager, TextInput, ScrollView } from 'react-native'
import {Text as RNText, TextStyle, View, ViewStyle, TextInput } from 'react-native'
import {colors, spacing, typography, useThemeColor} from '../../theme'
import {BottomModal, Button, Card, ErrorModal, Icon, InfoModal, ListItem, Loading, Screen, Text} from '../../components'
import {useStores} from '../../models'
@@ -17,16 +17,17 @@ import { CurrencyAmount } from '../Wallet/CurrencyAmount'
import { CurrencyCode } from '../../services/wallet/currency'
import { QRCodeBlock } from '../Wallet/QRCode'
import { MintBalance } from '../../models/Mint'
import { MintListItem } from '../Mints/MintListItem'
import Clipboard from '@react-native-clipboard/clipboard'
import { round, roundUp } from '../../utils/number'
import { roundUp } from '../../utils/number'
import { LnurlClient } from '../../services/lnurlService'
const DEFAULT_DONATION_AMOUNT = 500
const DONATION_LNURL_ADDRESS = 'minibits@minibits.cash'
export const OwnName = observer(function (props: {navigation: any, pubkey: string}) {
// const navigation = useNavigation()
const ownNameInputRef = useRef<TextInput>(null)
const {proofsStore, walletProfileStore, userSettingsStore, mintsStore} = useStores()
const {proofsStore, walletProfileStore} = useStores()
const {pubkey, navigation} = props
const [ownName, setOwnName] = useState<string>('')
@@ -34,8 +35,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
const [selectedBalance, setSelectedBalance] = useState<MintBalance | undefined>(undefined)
const [donationAmount, setDonationAmount] = useState(DEFAULT_DONATION_AMOUNT)
const [donationInvoice, setDonationInvoice] = useState<{payment_hash: string, payment_request: string} | undefined>(undefined)
const [isLoading, setIsLoading] = useState(false)
const [isQRcodeVisible, setIsQRCodeVisible] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isChecked, setIsChecked] = useState(false)
// const [isNameInputEnabled, setIsNameInputEnabled] = useState(true)
const [isInvoicePaid, setIsInvoicePaid] = useState<boolean>(false)
@@ -80,7 +80,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
checkDonationPaid,
{
interval: 2 * 1000, // every 2s to make it responsive.
maxPolls: 120,
maxPolls: 60,
maxErrors: 10
})
.then(() => log.trace('[checkDonationPaid]', 'Polling completed'))
@@ -126,8 +126,7 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
setInfo('')
setIsLoading(false)
setDonationInvoice(undefined)
setDonationAmount(DEFAULT_DONATION_AMOUNT)
setIsQRCodeVisible(false)
setDonationAmount(DEFAULT_DONATION_AMOUNT)
setIsInvoicePaid(false)
}
@@ -179,20 +178,35 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
const onCreateDonation = async function () {
try {
setIsLoading(true)
const memo = `Donation for ${ownName+MINIBITS_NIP05_DOMAIN}`
const invoice = await MinibitsClient.createDonation(
donationAmount,
memo,
pubkey
)
if(invoice) {
setDonationInvoice(invoice)
const feeReserve = roundUp(donationAmount / 100, 0)
const comment = `Donation for ${ownName+MINIBITS_NIP05_DOMAIN}`
const feeReserve = roundUp(donationAmount / 100, 0)
if(!selectedBalance || donationAmount >= selectedBalance.balances['sat']! + feeReserve) {
setIsQRCodeVisible(true)
}
if(selectedBalance && selectedBalance.balances['sat']! + feeReserve >= donationAmount) {
const addressParamsResult = await LnurlClient.getLnurlAddressParams(DONATION_LNURL_ADDRESS) // throws
return navigation.navigate('WalletNavigator', {
screen: 'Transfer',
params: {
lnurlParams: addressParamsResult.lnurlParams,
paymentOption: SendOption.LNURL_PAY,
fixedAmount: donationAmount,
unit: 'sat',
comment,
mintUrl: selectedBalance.mintUrl,
isDonation: true,
donationForName: ownName
},
})
} else {
const invoice = await MinibitsClient.createDonation(
donationAmount,
comment,
pubkey
)
setDonationInvoice(invoice)
}
setIsLoading(false)
@@ -211,22 +225,6 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
}
}
const onPayDonation = async function () {
try {
return navigation.navigate('WalletNavigator', {
screen: 'Transfer',
params: {
encodedInvoice: donationInvoice?.payment_request,
unit: userSettingsStore.preferredUnit || 'sat',
paymentOption: SendOption.DONATION
},
})
} catch (e: any) {
handleError(e)
}
}
// poll handler
const checkDonationPaid = async function (): Promise<void> {
try {
@@ -331,70 +329,13 @@ export const OwnName = observer(function (props: {navigation: any, pubkey: strin
text={translate("contactsScreen.ownName.payToGetOwnName", { name: ownName+MINIBITS_NIP05_DOMAIN })}
style={[$supportText, {color: hint}]}
/>
{isQRcodeVisible && (
<QRCodeBlock
qrCodeData={donationInvoice.payment_request}
title='Lightning invoice to pay'
type='Bolt11Invoice'
size={270}
/>
)}
{(!!selectedBalance && selectedBalance.balances['sat']! > donationAmount) ? (
<>
<ListItem
text='Invoice'
subText={donationInvoice.payment_request.slice(0, 20) + '...'}
RightComponent={
<CurrencyAmount
amount={donationAmount}
currencyCode={CurrencyCode.SAT}
size='medium'
/>
}
topSeparator={true}
bottomSeparator={true}
leftIcon='faBolt'
onPress={onCopyInvoice}
/>
<Text style={[$supportText, {color: hint}]} text={`Pay from`} />
<MintListItem
mint={mintsStore.findByUrl(selectedBalance.mintUrl)!}
mintBalance={selectedBalance}
selectedUnit='sat'
isSelectable={true}
isSelected={true}
separator='both'
/>
<View style={$buttonContainer}>
<Button
preset="default"
style={{marginRight: spacing.small}}
tx='contactsScreen.ownName.ctaPay'
onPress={onPayDonation}
/>
<Button
preset="secondary"
tx='common.cancel'
onPress={resetState}
/>
</View>
</>
) : (
<View>
<Text
size='xs'
style={{textAlign: 'center', margin: spacing.medium}}
tx="contactsScreen.ownName.insufficient"
/>
<View style={$buttonContainer}>
<Button
preset="secondary"
tx='common.cancel'
onPress={resetState}
/>
</View>
</View>
)}
<QRCodeBlock
qrCodeData={donationInvoice.payment_request}
title='Lightning invoice to pay'
type='Bolt11Invoice'
size={270}
/>
</>
) : (
<>
@@ -553,6 +494,7 @@ const $ownNameInput: TextStyle = {
const $ownNameDomain: TextStyle = {
marginRight: spacing.small,
marginLeft: -spacing.small,
borderTopRightRadius: spacing.extraSmall,
borderBottomRightRadius: spacing.extraSmall,
padding: spacing.extraSmall,
+4 -4
View File
@@ -1,8 +1,8 @@
import {observer} from 'mobx-react-lite'
import React, {FC, useState, useCallback, useEffect} from 'react'
import {Alert, TextStyle, View, ViewStyle} from 'react-native'
import {TextStyle, View, ViewStyle} from 'react-native'
import {spacing, useThemeColor, colors} from '../theme'
import {AppStackScreenProps, SettingsStackScreenProps, WalletStackScreenProps} from '../navigation'
import {AppStackScreenProps} from '../navigation'
import {
Button,
Icon,
@@ -21,7 +21,7 @@ import { SyncStateTaskResult, WalletTask } from '../services/walletService'
import EventEmitter from '../utils/eventEmitter'
import { translate } from '../i18n'
export enum RecoveryOption {
/* export enum RecoveryOption {
SEND_TOKEN = 'SEND_TOKEN',
PASTE_OR_SCAN_INVOICE = 'PASTE_OR_SCAN_INVOICE',
SHOW_TOKEN = 'SHOW_TOKEN',
@@ -29,7 +29,7 @@ export enum RecoveryOption {
LNURL_PAY = 'LNURL_PAY',
LNURL_ADDRESS = 'LNURL_ADDRESS',
DONATION = 'DONATION',
}
} */
export const RecoveryOptionsScreen: FC<AppStackScreenProps<'RecoveryOptions'>> = observer(
function RecoveryOptionsScreen({route, navigation}) {
+1 -1
View File
@@ -70,7 +70,7 @@ export enum SendOption {
PAY_PAYMENT_REQUEST = 'PAY_PAYMENT_REQUEST',
LNURL_PAY = 'LNURL_PAY',
LNURL_ADDRESS = 'LNURL_ADDRESS',
DONATION = 'DONATION',
// DONATION = 'DONATION',
}
if (Platform.OS === 'android' &&
+48 -42
View File
@@ -65,7 +65,7 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
const amountInputRef = useRef<TextInput>(null)
const lnurlCommentInputRef = useRef<TextInput>(null)
const {proofsStore, mintsStore, paymentRequestsStore, walletStore} = useStores()
const {proofsStore, mintsStore, paymentRequestsStore, walletStore, walletProfileStore} = useStores()
// const {walletStore} = nonPersistedStores
const isInternetReachable = useIsInternetReachable()
@@ -83,6 +83,7 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
const [lnurlDescription, setLnurlDescription] = useState('')
const [lnurlPayCommentAllowed, setLnurlPayCommentAllowed] = useState(0)
const [lnurlPayComment, setLnurlPayComment] = useState('')
const [donationForName, setDonationForName] = useState<string | undefined>()
const [availableMintBalances, setAvailableMintBalances] = useState<MintBalance[]>([])
const [mintBalanceToTransferFrom, setMintBalanceToTransferFrom] = useState<MintBalance | undefined>()
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
@@ -90,6 +91,7 @@ export const TransferScreen: FC<WalletStackScreenProps<'Transfer'>> = observer(
const [info, setInfo] = useState('')
const [error, setError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState(false)
const [isAmountEditable, setIsAmountEditable] = useState(true)
const [isNotEnoughFunds, setIsNotEnoughFunds] = useState(false)
const [isInvoiceDonation, setIsInvoiceDonation] = useState(false)
const [isTransferTaskSentToQueue, setIsTransferTaskSentToQueue] = useState(false)
@@ -181,13 +183,27 @@ useFocusEffect(
const handleLnurlPay = async () => {
try {
const {lnurlParams, unit} = route.params
// amountSats allows to default amount to be paid to a lightning address
const {lnurlParams, unit, fixedAmount, comment, isDonation, donationForName} = route.params
log.trace('[handleLnurlPay] start', {unit})
if (!lnurlParams) {
throw new AppError(Err.VALIDATION_ERROR, translate('missingLNURLParamsError'))
}
if(fixedAmount && fixedAmount > 0) {
setIsAmountEditable(false)
}
if(comment) {
setLnurlPayComment(comment)
}
if(isDonation) {
setIsInvoiceDonation(true)
setDonationForName(donationForName)
}
const metadata = lnurlParams.decodedMetadata
@@ -208,6 +224,7 @@ useFocusEffect(
break
}
}
if ('commentAllowed' in lnurlParams && lnurlParams.commentAllowed > 0) {
setLnurlPayCommentAllowed(lnurlParams.commentAllowed)
}
@@ -222,16 +239,16 @@ useFocusEffect(
}
}
const amountSats = roundUp(lnurlParams.minSendable / 1000, 0)
const defaultAmount = fixedAmount || roundUp(lnurlParams.minSendable / 1000, 0)
// Set minSendable into amountToTransfer in unit currency
if (unit !== 'sat') {
const rate = await walletStore.getExchangeRate(getCurrency(unit).code)
const amountUnit = convertToFromSats(amountSats, CurrencyCode.SAT, rate)
const defaultAmountUnit = convertToFromSats(defaultAmount, CurrencyCode.SAT, rate)
log.trace('[handleLnurlPay] minSendable conversion from SAT', {amountSats, rate, amountUnit})
log.trace('[handleLnurlPay] minSendable conversion from SAT', {defaultAmount, rate, defaultAmountUnit})
setAmountToTransfer(`${numbro(amountUnit / getCurrency(unit).precision)
setAmountToTransfer(`${numbro(defaultAmountUnit / getCurrency(unit).precision)
.format({
thousandSeparated: true,
mantissa: getCurrency(unit).mantissa
@@ -239,7 +256,7 @@ useFocusEffect(
)
} else {
setAmountToTransfer(`${numbro(amountSats)
setAmountToTransfer(`${numbro(defaultAmount)
.format({
thousandSeparated: true,
mantissa: 0
@@ -253,26 +270,6 @@ useFocusEffect(
}
}
const handleDonation = () => {
try {
const {encodedInvoice} = route.params
if (!encodedInvoice) {
throw new AppError(Err.VALIDATION_ERROR, 'Missing donation invoice.')
}
if(unit !== 'sat') {
throw new AppError(Err.VALIDATION_ERROR, `Donations can currently be paid only with ${CurrencyCode.SAT} balances.`)
}
log.trace('[handleDonation]', {encodedInvoice})
setIsInvoiceDonation(true)
onEncodedInvoice(encodedInvoice)
} catch (e: any) {
handleError(e)
}
}
if(paymentOption && paymentOption === SendOption.PASTE_OR_SCAN_INVOICE) {
handleInvoice()
@@ -286,10 +283,6 @@ useFocusEffect(
handleLnurlPay()
}
if(paymentOption && paymentOption === SendOption.DONATION) {
handleDonation()
}
}, [route.params?.paymentOption]),
)
@@ -400,11 +393,20 @@ useEffect(() => {
}
} else {
if(!isInvoiceDonation) { // Donation has own polling to avoid paying with test ecash and triggers own ResultModal on paid invoice
setResultModalInfo({
status,
message,
})
if(isInvoiceDonation && donationForName) {
await walletProfileStore.updateName(donationForName)
setResultModalInfo({
status,
message: `Donation for ${donationForName} has been successfully paid and your wallet address has been updated. Thank you!`,
})
} else {
setResultModalInfo({
status,
message,
})
}
// update related paymentRequest status if exists
@@ -421,9 +423,7 @@ useEffect(() => {
setFinalFee(finalFee)
}
if(!isInvoiceDonation || error) {
toggleResultModal()
}
toggleResultModal()
}
// Subscribe to the task result event
@@ -450,8 +450,10 @@ const resetState = function () {
setMintBalanceToTransferFrom(undefined)
setTransactionStatus(undefined)
setInfo('')
setDonationForName(undefined)
setError(undefined)
setIsLoading(false)
setIsLoading(false)
setIsAmountEditable(true)
setIsInvoiceDonation(false)
setIsTransferTaskSentToQueue(false)
setIsResultModalVisible(false)
@@ -559,7 +561,10 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
navigation.setParams({encodedInvoice: undefined})
navigation.setParams({paymentRequest: undefined})
navigation.setParams({lnurlParams: undefined})
navigation.setParams({paymentOption: undefined})
navigation.setParams({paymentOption: undefined})
navigation.setParams({fixedAmount: undefined})
navigation.setParams({isDonation: undefined})
navigation.setParams({donationForName: undefined})
const invoice = LightningUtils.decodeInvoice(encoded)
const {amount, expiry, description, timestamp} = LightningUtils.getInvoiceData(invoice)
@@ -572,6 +577,7 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
if(!isInternetReachable) setInfo(translate('common.offlinePretty'));
setIsAmountEditable(false)
setEncodedInvoice(encoded)
setInvoice(invoice)
setInvoiceExpiry(expiresAt)
@@ -715,7 +721,7 @@ const amountInputColor = useThemeColor('amountInput')
maxLength={9}
keyboardType="numeric"
selectTextOnFocus={true}
editable={encodedInvoice ? false : true}
editable={isAmountEditable ? true : false}
/>
{encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? (
+1 -1
View File
@@ -202,7 +202,7 @@ const navigateWithIncomingData = async function (
return navigation.navigate('Transfer', {
lnurlParams: addressParamsResult.lnurlParams,
paymentOption: SendOption.LNURL_PAY,
paymentOption: SendOption.LNURL_PAY,
unit,
mintUrl
})
+1 -1
View File
@@ -101,7 +101,7 @@ const getLnurlAddressParams = async (lnurlAddress: string) => {
headers,
})
log.trace(`Got LNURL address params from ${domain}`, lnurlParams, 'getLnurlAddressParams')
log.trace(`[getLnurlAddressParams] Got LNURL address params from ${domain}`, lnurlParams)
if(lnurlParams.status && lnurlParams.status === 'ERROR') {
throw new AppError(Err.CONNECTION_ERROR, lnurlParams.reason, {domain, caller: 'getLnurlAddressParams'})