mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-08-12 09:23:20 +00:00
Merge branch 'main' of https://github.com/minibits-cash/minibits_wallet
This commit is contained in:
@@ -41,7 +41,7 @@ Receive ecash
|
||||
- [x] Receive ecash from another wallet over NOSTR message sent to minibits.cash address
|
||||
- [x] Receive ecash in person while being offline, redeem later (MVP version)
|
||||
- [x] Realtime and encrypted push notifications on receive to minibits.cash lightning address
|
||||
- [ ] Display or send cashu payment requests
|
||||
- [x] Display or send cashu payment requests [✨ New!]
|
||||
|
||||
Send ecash
|
||||
- [x] Share ecash token to send through another app
|
||||
@@ -143,7 +143,7 @@ Minibits wallet is in early beta and available as of now only for Android device
|
||||
- [x] Download it from Google Play
|
||||
- [x] Join testing program on Google Play to get early releases to test (Submit your email to get an invite on [Minibits.cash](https://www.minibits.cash))
|
||||
- [x] Download .apk file from Releases page and install it on your phone
|
||||
- [ ] Try on Testflight
|
||||
- [x] Try on Testflight
|
||||
- [ ] Download from AppStore
|
||||
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
019708fa-f620-71c5-a06c-f71503d945bd
|
||||
01976662-f1fb-757e-9929-8c0796d93c51
|
||||
@@ -31,7 +31,7 @@
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Release"
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "minibits_wallet",
|
||||
"version": "0.2.2-beta.14",
|
||||
"version": "0.2.2-beta.18",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"android:clean": "cd android && ./gradlew clean",
|
||||
@@ -20,7 +20,7 @@
|
||||
"clean": "npx react-native-clean-project"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cashu/cashu-ts": "minibits-cash/cashu-ts#ec5ff88db34d47911e2fa3b97109243b60eee646",
|
||||
"@cashu/cashu-ts": "minibits-cash/cashu-ts#b3ba36757d84ec03f1064029db02308eb3a09cc4",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.7.2",
|
||||
|
||||
@@ -161,27 +161,27 @@ export function BottomModal(props: ModalProps) {
|
||||
FooterTextProps?.style,
|
||||
]
|
||||
|
||||
const statusBarOnModalOpen = useThemeColor('statusBarOnModalOpen')
|
||||
// const statusBarOnModalOpen = useThemeColor('statusBarOnModalOpen')
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
statusBarTranslucent={false}
|
||||
statusBarTranslucent={true}
|
||||
avoidKeyboard={true}
|
||||
onBackdropPress={onBackdropPress}
|
||||
onBackButtonPress={onBackButtonPress}
|
||||
backdropOpacity={backdropOpacity}
|
||||
style={[$outerContainerBase]}
|
||||
//coverScreen={true}
|
||||
// coverScreen={true}
|
||||
{...otherProps}
|
||||
>
|
||||
<StatusBar backgroundColor={isVisible ? statusBarOnModalOpen : undefined} />
|
||||
{/*<StatusBar backgroundColor={isVisible ? statusBarOnModalOpen : undefined} />*/}
|
||||
<View style={[$innerContainerBase, $innerContainerStyle]}>
|
||||
{HeadingComponent ||
|
||||
(isHeadingPresent && (
|
||||
<Text
|
||||
weight="bold"
|
||||
weight="medium"
|
||||
text={heading}
|
||||
tx={headingTx}
|
||||
txOptions={headingTxOptions}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Text } from './Text'
|
||||
import { spacing } from '../theme'
|
||||
|
||||
export function Loading(props: ViewProps & { statusMessage?: string, textStyle?: TextStyle, shiftedUp?: boolean }) {
|
||||
const statusBarOnModalOpen = useThemeColor('statusBarOnLoading')
|
||||
//const statusBarOnModalOpen = useThemeColor('statusBarOnLoading')
|
||||
const loadingIndicator = useThemeColor('loadingIndicator')
|
||||
return (
|
||||
<View style={[StyleSheet.absoluteFillObject, $loading(props?.shiftedUp ?? false), props.style]}>
|
||||
<StatusBar backgroundColor={props.style && props.style.backgroundColor ? props.style.backgroundColor : statusBarOnModalOpen } />
|
||||
{/*<StatusBar backgroundColor={props.style && props.style.backgroundColor ? props.style.backgroundColor : statusBarOnModalOpen } />*/}
|
||||
<ActivityIndicator color={loadingIndicator} animating size="large" />
|
||||
{props.statusMessage && (<Text style={[{opacity: 1}, props.textStyle]} text={props.statusMessage}/>)}
|
||||
</View>
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
"amount": {
|
||||
"requested": "Requested amount",
|
||||
"topup": "Topup amount",
|
||||
"withdraw": "Withdraw amount"
|
||||
"withdraw": "Withdraw amount",
|
||||
"send": "Amount to send"
|
||||
},
|
||||
"attention": "Attention!",
|
||||
"backupScreen": {
|
||||
@@ -65,6 +66,7 @@
|
||||
"paste": "Paste",
|
||||
"pasteEcash": "Paste ecash",
|
||||
"pasteEcashToken": "Paste ecash token",
|
||||
"createCashuPaymentRequest": "Create payment request",
|
||||
"qr": "QR Code",
|
||||
"reconnect": "Reconnect",
|
||||
"reset": "Reset",
|
||||
@@ -243,7 +245,7 @@
|
||||
"nsecPrivatePublicKeyMismatchError": "Provided private key does not match your new profile public key.",
|
||||
"numberOfBackedupProofs": "Number of stored proofs",
|
||||
"onDeviceInfo": "On-device information",
|
||||
"pasteEcashTokenDesc": "Paste ecash token you want to receive.",
|
||||
"pasteEcashTokenDesc": "Ecash token you want to receive.",
|
||||
"payCommon": {
|
||||
"amountToPayLabel": "Amount to pay",
|
||||
"amountZeroOrNegative": "Amount should be positive number.",
|
||||
@@ -269,6 +271,7 @@
|
||||
"requestPayment": "Request payment",
|
||||
"send": "Send",
|
||||
"withdraw": "Withdraw"
|
||||
|
||||
},
|
||||
"payerMemo": "Memo for the payer",
|
||||
"paymentRequestScreen": {
|
||||
@@ -478,13 +481,18 @@
|
||||
"sendRequest": "Send request",
|
||||
"invoiceCreatedDescParam": "Your selected mint balance to top up is %{mintUrl}"
|
||||
},
|
||||
"cashuPaymentRequest": {
|
||||
"to": "Receive to",
|
||||
"create": "Create request"
|
||||
},
|
||||
"tranDetailScreen": {
|
||||
"amount": "Amount",
|
||||
"paymentId": "Payment ID",
|
||||
"balanceAfter": "Balance after this transaction",
|
||||
"claim": "Claim",
|
||||
"createdAt": "Created at",
|
||||
"expiresAt": "Expires at",
|
||||
"id": "ID",
|
||||
"id": "Transaction ID",
|
||||
"invoice": "Lightning invoice to pay",
|
||||
"isOffline": "Redeem online",
|
||||
"lightningFee": "Lightning network fee",
|
||||
@@ -537,7 +545,8 @@
|
||||
"prepared": "Prepared",
|
||||
"reverted": "Reverted",
|
||||
"spent": "Spent",
|
||||
"unspent": "Unspent"
|
||||
"unspent": "Unspent",
|
||||
"recovered": "Recovered"
|
||||
},
|
||||
"tapToRedeem": "Tap to redeem",
|
||||
"unknown": "Unknown transaction",
|
||||
|
||||
@@ -37,6 +37,10 @@ import { MINIBITS_NIP05_DOMAIN } from '@env'
|
||||
const c = self.contacts.find(c => c.npub === npub)
|
||||
return c ? c : undefined
|
||||
},
|
||||
findByLud16: (lud16: string) => {
|
||||
const c = self.contacts.find(c => c.lud16 === lud16)
|
||||
return c ? c : undefined
|
||||
},
|
||||
alreadyExists(pubkey: string) {
|
||||
return self.contacts.some(m => m.pubkey === pubkey) ? true : false
|
||||
},
|
||||
|
||||
@@ -28,7 +28,7 @@ export const PaymentRequestModel = types
|
||||
status: types.frozen<PaymentRequestStatus>(),
|
||||
mint: types.maybe(types.string),
|
||||
mintQuote: types.maybe(types.string),
|
||||
mintUnit: types.maybe(types.frozen<MintUnit>()), // TODO migration to set default unit for existing PRs
|
||||
mintUnit: types.maybe(types.frozen<MintUnit>()),
|
||||
mintMethod: types.optional(types.frozen<MintMethod>(), MintMethod.BOLT11),
|
||||
amountToTopup: types.maybe(types.number),
|
||||
encodedInvoice: types.string,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Database } from '../services'
|
||||
|
||||
export interface TransactionRecord {
|
||||
id: number
|
||||
paymentId?: string | null
|
||||
type: TransactionType
|
||||
amount: number
|
||||
fee: number
|
||||
@@ -37,6 +38,7 @@ export enum TransactionType {
|
||||
RECEIVE = 'RECEIVE',
|
||||
RECEIVE_OFFLINE = 'RECEIVE_OFFLINE',
|
||||
RECEIVE_NOSTR = 'RECEIVE_NOSTR', // not used
|
||||
RECEIVE_BY_PAYMENT_REQUEST = 'RECEIVE_BY_PAYMENT_REQUEST',
|
||||
TOPUP = 'TOPUP',
|
||||
TRANSFER = 'TRANSFER',
|
||||
NWC_TRANSFER = 'NWC_TRANSFER',
|
||||
@@ -58,6 +60,7 @@ export enum TransactionStatus {
|
||||
export const TransactionModel = types
|
||||
.model('Transaction', {
|
||||
id: types.identifierNumber,
|
||||
paymentId: types.maybe(types.maybeNull(types.string)),
|
||||
type: types.frozen<TransactionType>(),
|
||||
amount: types.integer,
|
||||
fee: types.optional(types.integer, 0),
|
||||
@@ -108,6 +111,11 @@ export const TransactionModel = types
|
||||
self.fee = fee
|
||||
log.trace('[setFee]', 'Transaction fee updated', {id: self.id, fee})
|
||||
},
|
||||
setPaymentId(paymentId: string) {
|
||||
Database.updatePaymentId(self.id!, paymentId)
|
||||
self.paymentId = paymentId
|
||||
log.trace('[setPaymentId]', 'Transaction paymentId updated', {id: self.id, paymentId})
|
||||
},
|
||||
setReceivedAmount(amount: number) {
|
||||
Database.updateReceivedAmount(self.id!, amount)
|
||||
self.amount = amount
|
||||
|
||||
@@ -110,7 +110,36 @@ import {
|
||||
}
|
||||
|
||||
return transaction
|
||||
},
|
||||
},
|
||||
findByPaymentId(id: string) {
|
||||
let transaction = self.transactionsMap.get(id)
|
||||
|
||||
// Search the db and add if tx is not in the state
|
||||
// Search always to retrieve full tokens in tx detail screen
|
||||
if(!transaction) {
|
||||
const dbTransaction = Database.getTransactionByPaymentId(id)
|
||||
|
||||
if(dbTransaction) {
|
||||
const createdAt = new Date(dbTransaction.createdAt)
|
||||
const inStoreTransaction = {...dbTransaction, createdAt}
|
||||
const {id} = dbTransaction
|
||||
|
||||
// Shorten for performance reasons
|
||||
if(inStoreTransaction.inputToken && inStoreTransaction.inputToken.length > 0) {
|
||||
inStoreTransaction.inputToken = inStoreTransaction.inputToken?.slice(0, 40)
|
||||
}
|
||||
|
||||
if(inStoreTransaction.outputToken && inStoreTransaction.outputToken.length > 0) {
|
||||
inStoreTransaction.outputToken = inStoreTransaction.outputToken?.slice(0, 40)
|
||||
}
|
||||
|
||||
self.transactionsMap.set(id, inStoreTransaction)
|
||||
transaction = self.transactionsMap.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
return transaction
|
||||
},
|
||||
pruneRecentByUnit(unit: MintUnit) {
|
||||
const unitCount = self.countRecentByUnit(unit)
|
||||
|
||||
|
||||
@@ -470,7 +470,9 @@ export const WalletStoreModel = types
|
||||
return {proofs, swapFeePaid}
|
||||
|
||||
} catch (e: any) {
|
||||
if(!e.message.toLowerCase().includes('timeout')) {
|
||||
if(!e.message.toLowerCase().includes('timeout') &&
|
||||
!e.message.toLowerCase().includes('network request failed')) {
|
||||
// remove in-flight request only if it was not a timeout or network error
|
||||
currentCounter.removeInFlightRequest(transactionId)
|
||||
}
|
||||
throw new AppError(
|
||||
@@ -555,9 +557,11 @@ export const WalletStoreModel = types
|
||||
}
|
||||
|
||||
} catch (e: any) {
|
||||
if(!e.message.toLowerCase().includes('timeout')) {
|
||||
currentCounter.removeInFlightRequest(transactionId)
|
||||
}
|
||||
if(!e.message.toLowerCase().includes('timeout') &&
|
||||
!e.message.toLowerCase().includes('network request failed')) {
|
||||
// remove in-flight request only if it was not a timeout or network error
|
||||
currentCounter.removeInFlightRequest(transactionId)
|
||||
}
|
||||
|
||||
let message = 'Swap to prepare ecash to send has failed.'
|
||||
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
|
||||
@@ -732,9 +736,11 @@ export const WalletStoreModel = types
|
||||
return proofs
|
||||
|
||||
} catch (e: any) {
|
||||
if(!e.message.toLowerCase().includes('timeout')) {
|
||||
if(!e.message.toLowerCase().includes('timeout') &&
|
||||
!e.message.toLowerCase().includes('network request failed')) {
|
||||
// remove in-flight request only if it was not a timeout or network error
|
||||
currentCounter.removeInFlightRequest(transactionId)
|
||||
}
|
||||
}
|
||||
|
||||
let message = 'Error on request to mint new ecash.'
|
||||
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
|
||||
@@ -841,7 +847,9 @@ export const WalletStoreModel = types
|
||||
return meltResponse
|
||||
|
||||
} catch (e: any) {
|
||||
if(!e.message.toLowerCase().includes('timeout')) {
|
||||
if(!e.message.toLowerCase().includes('timeout') &&
|
||||
!e.message.toLowerCase().includes('network request failed')) {
|
||||
// remove in-flight request only if it was not a timeout or network error
|
||||
currentCounter.removeInFlightRequest(transactionId)
|
||||
}
|
||||
|
||||
@@ -858,6 +866,81 @@ export const WalletStoreModel = types
|
||||
)
|
||||
}
|
||||
}),
|
||||
checkLightningMeltQuote: flow(function* checkLightningMeltQuote(
|
||||
mintUrl: string,
|
||||
quote: string,
|
||||
) {
|
||||
try {
|
||||
const cashuMint: CashuMint = yield self.getMint(mintUrl)
|
||||
const quoteResponse: MeltQuoteResponse = yield cashuMint.checkMeltQuote(
|
||||
quote
|
||||
)
|
||||
|
||||
log.info('[checkLightningMeltQuote]', {quoteResponse})
|
||||
|
||||
return quoteResponse
|
||||
|
||||
} catch (e: any) {
|
||||
let message = 'The mint could not return the state of a melt quote.'
|
||||
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
|
||||
throw new AppError(
|
||||
Err.MINT_ERROR,
|
||||
message,
|
||||
{
|
||||
message: e.message,
|
||||
caller: 'checkLightningMeltQuote',
|
||||
mintUrl,
|
||||
}
|
||||
)
|
||||
}
|
||||
}),
|
||||
recoverMeltQuoteChange: flow(function* recoverMeltQuoteChange(
|
||||
mintUrl: string,
|
||||
meltQuote: MeltQuoteResponse,
|
||||
) {
|
||||
try {
|
||||
const mintInstance = self.getMintModelInstance(mintUrl)
|
||||
|
||||
if(!mintInstance) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint instance', {mintUrl})
|
||||
}
|
||||
|
||||
const cashuWallet = yield self.getWallet(
|
||||
mintUrl,
|
||||
'sat',
|
||||
{
|
||||
withSeed: true,
|
||||
}
|
||||
)
|
||||
|
||||
const currentCounter = mintInstance.getProofsCounterByKeysetId!(cashuWallet.keysetId)
|
||||
|
||||
const {change}: MeltProofsResponse = yield cashuWallet.recoverMeltQuoteChange(
|
||||
meltQuote,
|
||||
{
|
||||
keysetId: cashuWallet.keysetId,
|
||||
counter: currentCounter.counter
|
||||
}
|
||||
)
|
||||
|
||||
log.info('[recoverMeltQuoteChange]', {change})
|
||||
|
||||
return change
|
||||
|
||||
} catch (e: any) {
|
||||
let message = 'The mint could not return change from a melt quote.'
|
||||
if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions;
|
||||
throw new AppError(
|
||||
Err.MINT_ERROR,
|
||||
message,
|
||||
{
|
||||
message: e.message,
|
||||
caller: 'recoverMeltQuoteChange',
|
||||
mintUrl,
|
||||
}
|
||||
)
|
||||
}
|
||||
}),
|
||||
restore: flow(function* restore(
|
||||
mintUrl: string,
|
||||
seed: Uint8Array,
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
OwnKeysScreen,
|
||||
TokenReceiveScreen,
|
||||
NwcScreen,
|
||||
CashuPaymentRequestScreen
|
||||
} from "../screens"
|
||||
import { colors, spacing, typography } from "../theme"
|
||||
|
||||
@@ -56,7 +57,7 @@ const $tabBarLabel: TextStyle = {
|
||||
flex: 1,
|
||||
}
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const WalletStack = createNativeStackNavigator({
|
||||
screenOptions: {
|
||||
headerShown: false,
|
||||
@@ -70,7 +71,8 @@ const WalletStack = createNativeStackNavigator({
|
||||
LightningPay: LightningPayScreen,
|
||||
PaymentRequests: PaymentRequestsScreen,
|
||||
Transfer: TransferScreen,
|
||||
Topup: TopupScreen,
|
||||
Topup: TopupScreen,
|
||||
CashuPaymentRequest: CashuPaymentRequestScreen,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -81,7 +83,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
const TransactionsStack = createNativeStackNavigator({
|
||||
screenOptions: {
|
||||
headerShown: false,
|
||||
@@ -100,6 +102,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const ContactsStack = createNativeStackNavigator({
|
||||
screenOptions: {
|
||||
headerShown: false,
|
||||
@@ -121,6 +124,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const SettingsStack = createNativeStackNavigator({
|
||||
screenOptions: {
|
||||
headerShown: false,
|
||||
|
||||
@@ -0,0 +1,624 @@
|
||||
import React, { useRef, useState, useEffect } from "react"
|
||||
import { observer } from "mobx-react-lite"
|
||||
import { StackActions, StaticScreenProps, useNavigation } from "@react-navigation/native"
|
||||
import QuickCrypto from 'react-native-quick-crypto'
|
||||
import { View, TextInput, LayoutAnimation, Keyboard, ViewStyle, TextStyle } from "react-native"
|
||||
import { spacing, useThemeColor, typography, colors } from "../theme"
|
||||
import { useStores } from "../models"
|
||||
import { MintBalanceSelector } from "./Mints/MintBalanceSelector"
|
||||
import { CurrencyAmount } from "./Wallet/CurrencyAmount"
|
||||
import EventEmitter from '../utils/eventEmitter'
|
||||
import { getCurrency, MintUnit, CurrencyCode, convertToFromSats } from "../services/wallet/currency"
|
||||
import { round, toNumber } from "../utils/number"
|
||||
import { translate } from "../i18n"
|
||||
import AppError, { Err } from "../utils/AppError"
|
||||
import { infoMessage } from "../utils/utils"
|
||||
import numbro from "numbro"
|
||||
import {
|
||||
Screen,
|
||||
Card,
|
||||
Button,
|
||||
Loading,
|
||||
InfoModal,
|
||||
ErrorModal,
|
||||
BottomModal,
|
||||
Text,
|
||||
} from "../components"
|
||||
import useIsInternetReachable from "../utils/useIsInternetReachable"
|
||||
import {HANDLE_RECEIVED_EVENT_TASK, log, TransactionTaskResult, WalletTask } from "../services"
|
||||
import { QRCodeBlock } from "./Wallet/QRCode"
|
||||
import { TranItem } from "./TranDetailScreen"
|
||||
import { Transaction, TransactionStatus } from "../models/Transaction"
|
||||
import { CASHU_PAYMENT_REQUEST_TASK } from "../services/wallet/cashuPaymentRequestTask"
|
||||
import { ResultModalInfo } from "./Wallet/ResultModalInfo"
|
||||
import { MintHeader } from "./Mints/MintHeader"
|
||||
import { verticalScale } from "@gocodingnow/rn-size-matters"
|
||||
|
||||
type Props = StaticScreenProps<{
|
||||
unit: MintUnit,
|
||||
mintUrl?: string,
|
||||
}>
|
||||
|
||||
export const CashuPaymentRequestScreen = observer(function CashuPaymentRequestScreen({ route }: Props) {
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
const navigation = useNavigation()
|
||||
const {
|
||||
proofsStore,
|
||||
mintsStore,
|
||||
walletStore,
|
||||
userSettingsStore,
|
||||
} = useStores()
|
||||
|
||||
const amountInputRef = useRef<TextInput>(null)
|
||||
const memoInputRef = useRef<TextInput>(null)
|
||||
|
||||
const [amountToRequest, setAmountToRequest] = useState<string>("0")
|
||||
const [unit, setUnit] = useState<MintUnit>("sat")
|
||||
const [memo, setMemo] = useState("")
|
||||
const [availableMintBalances, setAvailableMintBalances] = useState<any[]>([])
|
||||
const [mintBalanceToReceiveTo, setMintBalanceToReceiveTo] = useState<any | undefined>(undefined)
|
||||
const [transactionStatus, setTransactionStatus] = useState<TransactionStatus | undefined>()
|
||||
const [transactionId, setTransactionId] = useState<number | undefined>()
|
||||
const [transaction, setTransaction] = useState<Transaction | undefined>()
|
||||
const [isCashuPaymentRequestTaskSentToQueue, setIsCashuPaymentRequestTaskSentToQueue] = useState(false)
|
||||
const [isMintSelectorVisible, setIsMintSelectorVisible] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [info, setInfo] = useState("")
|
||||
const [encodedPaymentRequest, setEncodedPaymentRequest] = useState<string | undefined>()
|
||||
const [resultModalInfo, setResultModalInfo] = useState<
|
||||
{status: TransactionStatus; title?: string; message: string} | undefined
|
||||
>()
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const focus = () => {
|
||||
amountInputRef && amountInputRef.current
|
||||
? amountInputRef.current.focus()
|
||||
: false
|
||||
}
|
||||
const timer = setTimeout(() => focus(), 400)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const setUnitAndMint = () => {
|
||||
try {
|
||||
const {unit, mintUrl} = route.params
|
||||
if (!unit) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
translate('missingMintUnitRouteParamsError')
|
||||
)
|
||||
}
|
||||
|
||||
setUnit(unit)
|
||||
|
||||
if (mintUrl) {
|
||||
const mintBalance = proofsStore.getMintBalance(mintUrl)
|
||||
setMintBalanceToReceiveTo(mintBalance)
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
setUnitAndMint()
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handlePayReqTaskResult = async (result: TransactionTaskResult) => {
|
||||
log.trace('handlePayReqTaskResult event handler triggered')
|
||||
|
||||
setIsLoading(false)
|
||||
setIsMintSelectorVisible(false)
|
||||
|
||||
const {status, id} = result.transaction as Transaction
|
||||
setTransactionStatus(status) // Should be PENDING
|
||||
setTransactionId(id)
|
||||
|
||||
if (result.encodedCashuPaymentRequest) {
|
||||
setEncodedPaymentRequest(result.encodedCashuPaymentRequest)
|
||||
}
|
||||
|
||||
if (result.error) {
|
||||
setResultModalInfo({
|
||||
status: result.transaction?.status as TransactionStatus,
|
||||
title: result.error.params?.message
|
||||
? result.error.message
|
||||
: 'Error',
|
||||
message: result.error.params?.message || result.error.message,
|
||||
})
|
||||
setIsResultModalVisible(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Subscribe to the payment request creation event
|
||||
if( isCashuPaymentRequestTaskSentToQueue) {
|
||||
EventEmitter.on(`ev_${CASHU_PAYMENT_REQUEST_TASK}_result`, handlePayReqTaskResult)
|
||||
}
|
||||
|
||||
// Unsubscribe on component unmount
|
||||
return () => {
|
||||
EventEmitter.off(`ev_${CASHU_PAYMENT_REQUEST_TASK}_result`, handlePayReqTaskResult)
|
||||
}
|
||||
}, [isCashuPaymentRequestTaskSentToQueue])
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const handleReceivedPayReqPayloadTaskResult = (result: TransactionTaskResult) => {
|
||||
log.trace('[handleReceivedPayReqPayloadTaskResult] event handler triggered', {transactionId, result})
|
||||
|
||||
if (!transactionId) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter and handle events only for this cashuPaymentRequest transactionId
|
||||
if (result.transaction?.id === transactionId) {
|
||||
// Show result modal only on completed payment
|
||||
if (result.transaction.status !== TransactionStatus.COMPLETED) {
|
||||
return
|
||||
}
|
||||
|
||||
log.trace(
|
||||
'[handleReceivedPayReqPayloadTaskResult]',
|
||||
'Payment request has been paid and new proofs received',
|
||||
)
|
||||
|
||||
setResultModalInfo({
|
||||
status: result.transaction.status,
|
||||
message: result.message,
|
||||
})
|
||||
|
||||
setTransactionStatus(TransactionStatus.COMPLETED)
|
||||
setTransaction(result.transaction)
|
||||
setIsResultModalVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
if(transactionId) {
|
||||
EventEmitter.on(
|
||||
`ev_${HANDLE_RECEIVED_EVENT_TASK}_result`,
|
||||
handleReceivedPayReqPayloadTaskResult,
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
EventEmitter.off(
|
||||
`ev_${HANDLE_RECEIVED_EVENT_TASK}_result`,
|
||||
handleReceivedPayReqPayloadTaskResult,
|
||||
)
|
||||
}
|
||||
}, [transactionId])
|
||||
|
||||
|
||||
const toggleResultModal = () =>
|
||||
setIsResultModalVisible(previousState => !previousState)
|
||||
|
||||
const onAmountEndEditing = () => {
|
||||
log.trace("[onAmountEndEditing] called")
|
||||
|
||||
if(isCashuPaymentRequestTaskSentToQueue) {
|
||||
log.trace('[onAmountEndEditing] Request task already sent to queue, ignoring further edits')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const precision = getCurrency(unit).precision
|
||||
const mantissa = getCurrency(unit).mantissa
|
||||
const amountNum = round(toNumber(amountToRequest) * precision, 0)
|
||||
|
||||
if (!amountNum || amountNum <= 0) {
|
||||
infoMessage(translate("payCommon.amountZeroOrNegative"))
|
||||
return
|
||||
}
|
||||
|
||||
const balances = proofsStore.getMintBalancesWithUnit(unit)
|
||||
if (balances.length === 0) {
|
||||
infoMessage(
|
||||
translate("topup.missingMintAddFirst"),
|
||||
translate("topup.missingMintAddFirstDesc"),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
setAmountToRequest(
|
||||
`${numbro(amountToRequest).format({
|
||||
thousandSeparated: true,
|
||||
mantissa,
|
||||
})}`,
|
||||
)
|
||||
setAvailableMintBalances(balances)
|
||||
if (!mintBalanceToReceiveTo) setMintBalanceToReceiveTo(balances[0])
|
||||
LayoutAnimation.easeInEaseOut()
|
||||
|
||||
setIsMintSelectorVisible(true)
|
||||
|
||||
} catch (e: any) {
|
||||
setError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const onMemoEndEditing = () => {
|
||||
log.trace("[onMemoEndEditing] called")
|
||||
LayoutAnimation.easeInEaseOut()
|
||||
if (availableMintBalances.length > 0) {
|
||||
setIsMintSelectorVisible(true)
|
||||
}
|
||||
}
|
||||
|
||||
const onMemoDone = () => {
|
||||
if (parseInt(amountToRequest) > 0) {
|
||||
memoInputRef.current?.blur()
|
||||
amountInputRef.current?.blur()
|
||||
onMemoEndEditing()
|
||||
} else {
|
||||
amountInputRef.current?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const onMintBalanceSelect = (balance: any) => {
|
||||
setMintBalanceToReceiveTo(balance)
|
||||
}
|
||||
|
||||
const onMintBalanceCancel = () => {
|
||||
setIsMintSelectorVisible(false)
|
||||
}
|
||||
|
||||
const onMintBalanceConfirm = async () => {
|
||||
if (!mintBalanceToReceiveTo) return
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const mintUrl = mintBalanceToReceiveTo.mintUrl
|
||||
const mint = mintsStore.findByUrl(mintUrl)
|
||||
if (!mint) throw new AppError(Err.NOTFOUND_ERROR, "Mint not found")
|
||||
|
||||
setIsCashuPaymentRequestTaskSentToQueue(true)
|
||||
|
||||
const amountInt = round(
|
||||
toNumber(amountToRequest) * getCurrency(unit).precision,
|
||||
0,
|
||||
)
|
||||
|
||||
WalletTask.cashuPaymentRequestQueue(
|
||||
mintBalanceToReceiveTo,
|
||||
amountInt,
|
||||
unit,
|
||||
memo,
|
||||
)
|
||||
|
||||
} catch (e: any) {
|
||||
setError(e)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
setIsMintSelectorVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
setAmountToRequest("0")
|
||||
setMemo("")
|
||||
setMintBalanceToReceiveTo(undefined)
|
||||
setEncodedPaymentRequest(undefined)
|
||||
setIsResultModalVisible(false)
|
||||
}
|
||||
|
||||
const gotoWallet = () => {
|
||||
resetState()
|
||||
navigation.dispatch(
|
||||
StackActions.popToTop()
|
||||
)
|
||||
}
|
||||
|
||||
const handleError = function (e: AppError): void {
|
||||
setIsLoading(false)
|
||||
setError(e)
|
||||
}
|
||||
|
||||
const headerBg = useThemeColor("header")
|
||||
const placeholderTextColor = useThemeColor("textDim")
|
||||
const amountInputColor = useThemeColor("amountInput")
|
||||
const inputText = useThemeColor("text")
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle')
|
||||
|
||||
const getConvertedAmount = function () {
|
||||
if (!walletStore.exchangeRate) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const precision = getCurrency(unit).precision
|
||||
return convertToFromSats(
|
||||
round(toNumber(amountToRequest) * precision, 0) || 0,
|
||||
getCurrency(unit).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = function () {
|
||||
return (
|
||||
walletStore.exchangeRate &&
|
||||
(userSettingsStore.exchangeCurrency === getCurrency(unit).code ||
|
||||
unit === 'sat') &&
|
||||
getConvertedAmount() !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
<MintHeader
|
||||
mint={
|
||||
mintBalanceToReceiveTo
|
||||
? mintsStore.findByUrl(mintBalanceToReceiveTo?.mintUrl)
|
||||
: undefined
|
||||
}
|
||||
unit={unit}
|
||||
/>
|
||||
<View style={[$headerContainer, { backgroundColor: headerBg }]}>
|
||||
<View style={$amountContainer}>
|
||||
<TextInput
|
||||
ref={amountInputRef}
|
||||
onChangeText={setAmountToRequest}
|
||||
onEndEditing={onAmountEndEditing}
|
||||
value={amountToRequest}
|
||||
style={[$amountInput, { color: amountInputColor }]}
|
||||
maxLength={9}
|
||||
keyboardType="numeric"
|
||||
editable={
|
||||
transactionStatus === TransactionStatus.PENDING ? false : true
|
||||
}
|
||||
selectTextOnFocus={true}
|
||||
returnKeyType={"done"}
|
||||
/>
|
||||
{isConvertedAmountVisible() && (
|
||||
<CurrencyAmount
|
||||
amount={getConvertedAmount() ?? 0}
|
||||
currencyCode={unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
||||
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
||||
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
|
||||
size='small'
|
||||
containerStyle={{justifyContent: 'center'}}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
text={translate("amount.requested")}
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: "center",
|
||||
marginTop: isConvertedAmountVisible()
|
||||
? -spacing.extraSmall
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
{!encodedPaymentRequest && (
|
||||
<Card
|
||||
style={$memoCard}
|
||||
ContentComponent={
|
||||
<View style={$memoContainer}>
|
||||
<TextInput
|
||||
ref={memoInputRef}
|
||||
onChangeText={setMemo}
|
||||
onEndEditing={onMemoEndEditing}
|
||||
value={memo}
|
||||
style={[$memoInput, { color: inputText }]}
|
||||
maxLength={200}
|
||||
keyboardType="default"
|
||||
selectTextOnFocus={true}
|
||||
placeholder={translate("payerMemo")}
|
||||
placeholderTextColor={placeholderTextColor}
|
||||
/>
|
||||
<Button
|
||||
preset="secondary"
|
||||
style={$memoButton}
|
||||
text="Done"
|
||||
onPress={onMemoDone}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isMintSelectorVisible && (
|
||||
<MintBalanceSelector
|
||||
mintBalances={availableMintBalances}
|
||||
selectedMintBalance={mintBalanceToReceiveTo}
|
||||
unit={unit}
|
||||
title={'Mint to receive'}
|
||||
confirmTitle={translate("cashuPaymentRequest.create")}
|
||||
onMintBalanceSelect={onMintBalanceSelect}
|
||||
onCancel={onMintBalanceCancel}
|
||||
onMintBalanceConfirm={onMintBalanceConfirm}
|
||||
/>
|
||||
)}
|
||||
{transactionStatus === TransactionStatus.PENDING &&
|
||||
encodedPaymentRequest && (
|
||||
<>
|
||||
<QRCodeBlock
|
||||
qrCodeData={encodedPaymentRequest}
|
||||
title='Cashu Payment request'
|
||||
type='PaymentRequest'
|
||||
size={270}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{transaction && transactionStatus === TransactionStatus.COMPLETED && (
|
||||
<>
|
||||
<Card
|
||||
style={{padding: spacing.medium}}
|
||||
ContentComponent={
|
||||
<>
|
||||
<TranItem
|
||||
label="cashuPaymentRequest.to"
|
||||
isFirst={true}
|
||||
value={
|
||||
mintsStore.findByUrl(transaction.mint)
|
||||
?.shortname as string
|
||||
}
|
||||
/>
|
||||
{transaction.memo && (
|
||||
<TranItem
|
||||
label="receiverMemo"
|
||||
value={transaction?.memo as string}
|
||||
/>
|
||||
)}
|
||||
<TranItem
|
||||
label="transactionCommon.feePaid"
|
||||
value={transaction.fee || 0}
|
||||
unit={unit}
|
||||
isCurrency={true}
|
||||
/>
|
||||
<TranItem
|
||||
label="tranDetailScreen.status"
|
||||
value={transaction.status as string}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx='common.close'
|
||||
onPress={gotoWallet}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
<BottomModal
|
||||
isVisible={isResultModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<>
|
||||
{resultModalInfo &&
|
||||
transactionStatus === TransactionStatus.COMPLETED && (
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon="faCheckCircle"
|
||||
iconColor={colors.palette.success200}
|
||||
title={translate('common.success')}
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx='common.close'
|
||||
onPress={gotoWallet}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
{resultModalInfo &&
|
||||
transactionStatus === TransactionStatus.ERROR && (
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon="faTriangleExclamation"
|
||||
iconColor={colors.palette.angry500}
|
||||
title={resultModalInfo?.title || translate('topup.failed')}
|
||||
message={resultModalInfo?.message}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx='common.close'
|
||||
onPress={toggleResultModal}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleResultModal}
|
||||
onBackdropPress={toggleResultModal}
|
||||
/>
|
||||
{error && <ErrorModal error={error} />}
|
||||
{info && <InfoModal message={info} />}
|
||||
{isLoading && <Loading />}
|
||||
</Screen>
|
||||
)
|
||||
})
|
||||
|
||||
const $screen: ViewStyle = {
|
||||
flex: 1,
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: "center",
|
||||
padding: spacing.extraSmall,
|
||||
paddingTop: 0,
|
||||
height: spacing.screenHeight * 0.2,
|
||||
}
|
||||
|
||||
const $amountContainer: ViewStyle = {}
|
||||
|
||||
const $amountInput: TextStyle = {
|
||||
borderRadius: spacing.small,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
fontSize: 48,
|
||||
fontFamily: typography.primary?.medium,
|
||||
textAlign: "center",
|
||||
color: "white",
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
flex: 1,
|
||||
padding: spacing.extraSmall,
|
||||
marginTop: -spacing.extraLarge * 1.5,
|
||||
}
|
||||
|
||||
const $memoCard: ViewStyle = {
|
||||
marginBottom: spacing.small,
|
||||
minHeight: 80,
|
||||
}
|
||||
|
||||
const $memoContainer: ViewStyle = {
|
||||
flex: 1,
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
}
|
||||
|
||||
const $memoInput: TextStyle = {
|
||||
flex: 1,
|
||||
borderRadius: spacing.small,
|
||||
fontSize: 16,
|
||||
textAlignVertical: "center",
|
||||
marginRight: spacing.small,
|
||||
}
|
||||
|
||||
const $memoButton: ViewStyle = {
|
||||
maxHeight: 50,
|
||||
}
|
||||
|
||||
const $bottomModal: ViewStyle = {
|
||||
alignItems: "center",
|
||||
paddingVertical: spacing.large,
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
}
|
||||
|
||||
const $bottomContainer: ViewStyle = {
|
||||
// position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: spacing.medium,
|
||||
alignSelf: 'stretch',
|
||||
// opacity: 0,
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import {ResultModalInfo} from './Wallet/ResultModalInfo'
|
||||
import {MintListItem} from './Mints/MintListItem'
|
||||
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
||||
import { verticalScale } from '@gocodingnow/rn-size-matters'
|
||||
import { CurrencyCode, MintUnit, getCurrency } from "../services/wallet/currency"
|
||||
import { CurrencyCode, MintUnit, convertToFromSats, getCurrency } from "../services/wallet/currency"
|
||||
import { MintHeader } from './Mints/MintHeader'
|
||||
import { round, toNumber } from '../utils/number'
|
||||
import numbro from 'numbro'
|
||||
@@ -36,6 +36,7 @@ import { TranItem } from './TranDetailScreen'
|
||||
import { translate } from '../i18n'
|
||||
import { Token, getDecodedToken } from '@cashu/cashu-ts'
|
||||
import { RECEIVE_OFFLINE_PREPARE_TASK, RECEIVE_TASK } from '../services/wallet/receiveTask'
|
||||
import { CurrencyAmount } from './Wallet/CurrencyAmount'
|
||||
|
||||
export enum ReceiveOption {
|
||||
SEND_PAYMENT_REQUEST = 'SEND_PAYMENT_REQUEST',
|
||||
@@ -51,7 +52,7 @@ type Props = StaticScreenProps<{
|
||||
export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
|
||||
const navigation = useNavigation()
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
const {mintsStore, walletStore} = useStores()
|
||||
const {mintsStore, walletStore, userSettingsStore} = useStores()
|
||||
|
||||
const [token, setToken] = useState<Token | undefined>()
|
||||
const [encodedToken, setEncodedToken] = useState<string | undefined>()
|
||||
@@ -177,6 +178,7 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
|
||||
|
||||
const onEncodedToken = async function (encoded: string) {
|
||||
try {
|
||||
//@ts-ignore
|
||||
navigation.setParams({encodedToken: undefined})
|
||||
|
||||
const decoded = getDecodedToken(encoded)
|
||||
@@ -291,6 +293,30 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
|
||||
const iconColor = useThemeColor('textDim')
|
||||
const amountInputColor = useThemeColor('amountInput')
|
||||
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle')
|
||||
|
||||
const getConvertedAmount = function () {
|
||||
if (!walletStore.exchangeRate) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const precision = getCurrency(unit).precision
|
||||
return convertToFromSats(
|
||||
round(toNumber(amountToReceive) * precision, 0) || 0,
|
||||
getCurrency(unit).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = function () {
|
||||
return (
|
||||
walletStore.exchangeRate &&
|
||||
(userSettingsStore.exchangeCurrency === getCurrency(unit).code ||
|
||||
unit === 'sat') &&
|
||||
getConvertedAmount() !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Screen preset="auto" contentContainerStyle={$screen}>
|
||||
<MintHeader
|
||||
@@ -306,8 +332,22 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
|
||||
editable={false}
|
||||
/>
|
||||
</View>
|
||||
{isConvertedAmountVisible() && (
|
||||
<CurrencyAmount
|
||||
amount={getConvertedAmount() ?? 0}
|
||||
currencyCode={unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
||||
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
||||
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
|
||||
size='small'
|
||||
containerStyle={{justifyContent: 'center'}}
|
||||
/>
|
||||
)}
|
||||
{isP2PKLocked ? (
|
||||
<View style={{flexDirection: 'row', alignItems: 'center'}}>
|
||||
<View style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}>
|
||||
<Icon
|
||||
containerStyle={$iconLockContainer}
|
||||
icon={toNumber(receivedAmount) > 0 ? "faLockOpen" : "faLock"}
|
||||
@@ -316,13 +356,13 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
|
||||
/>
|
||||
{isP2PKLockedToWallet ? (
|
||||
<Text
|
||||
size='sm'
|
||||
size='xs'
|
||||
tx={toNumber(receivedAmount) > 0 ? "receiveScreen.received" : "receiveScreen.lockedToWalletPK"}
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
size='sm'
|
||||
size='xs'
|
||||
tx={toNumber(receivedAmount) > 0 ? "receiveScreen.received" : "receiveScreen.lockedToUnknownPK"}
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
/>
|
||||
@@ -330,9 +370,13 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) {
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
size='sm'
|
||||
size='xs'
|
||||
tx={toNumber(receivedAmount) > 0 ? "receiveScreen.received" : "receiveScreen.toReceive"}
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -49,14 +49,20 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [info, setInfo] = useState('')
|
||||
const [mintBalanceToMintFrom, setMintBalanceToMintFrom] = useState<MintBalance>(mintBalancesRef.current[0])
|
||||
const [mintBalanceToRecoverFrom, setMintBalanceToRecoverFrom] = useState<MintBalance>(mintBalancesRef.current[0])
|
||||
const [mintQuote, setMintQuote] = useState<string>('')
|
||||
const [meltQuote, setMeltQuote] = useState<string>('')
|
||||
const [mintedAmount, setMintedAmount] = useState<number>(0)
|
||||
const [meltChangeAmount, setMeltChangeAmount] = useState<number>(0)
|
||||
const [isSyncStateSentToQueue, setIsSyncStateSentToQueue] = useState<boolean>(false)
|
||||
const [isNotificationModalVisible, setIsNotificationModalVisible] = useState(false)
|
||||
const [isNotificationModalVisible, setIsNotificationModalVisible] = useState(false)
|
||||
const [isMintQuoteRecoveryStarted, setIsMintQuoteRecoveryStarted] = useState(false)
|
||||
const [isMeltQuoteRecoveryStarted, setIsMeltQuoteRecoveryStarted] = useState(false)
|
||||
const [isMintSelectorModalVisible, setIsMintSelectorModalVisible] = useState(false)
|
||||
const [isMintQuoteModalVisible, setIsMintQuoteModalVisible] = useState(false)
|
||||
const [isMintQuoteResultModalVisible, setIsMintQuoteResultModalVisible] = useState(false)
|
||||
const [isMintQuoteModalVisible, setIsMintQuoteModalVisible] = useState(false)
|
||||
const [isMeltQuoteModalVisible, setIsMeltQuoteModalVisible] = useState(false)
|
||||
const [isMintQuoteResultModalVisible, setIsMintQuoteResultModalVisible] = useState(false)
|
||||
const [isMeltQuoteResultModalVisible, setIsMeltQuoteResultModalVisible] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -100,9 +106,15 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
setIsMintQuoteModalVisible(previousState => !previousState)
|
||||
|
||||
|
||||
const toggleMeltQuoteModal = () =>
|
||||
setIsMeltQuoteModalVisible(previousState => !previousState)
|
||||
|
||||
|
||||
const toggleMintQuoteResultModal = () =>
|
||||
setIsMintQuoteResultModalVisible(previousState => !previousState)
|
||||
|
||||
const toggleMeltQuoteResultModal = () =>
|
||||
setIsMeltQuoteResultModalVisible(previousState => !previousState)
|
||||
|
||||
const gotoSeedRecovery = function () {
|
||||
//@ts-ignore
|
||||
@@ -156,19 +168,30 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
}
|
||||
|
||||
const onMintBalanceSelect = function (balance: MintBalance) {
|
||||
setMintBalanceToMintFrom(balance)
|
||||
setMintBalanceToRecoverFrom(balance)
|
||||
}
|
||||
|
||||
const onMintBalanceConfirm = function () {
|
||||
toggleMintSelectorModal()
|
||||
|
||||
if(mintBalanceToMintFrom) {
|
||||
if(setMintBalanceToRecoverFrom) {
|
||||
if(Platform.OS === 'ios') {
|
||||
setTimeout(() => {
|
||||
toggleMintQuoteModal()
|
||||
if(isMeltQuoteRecoveryStarted) {
|
||||
toggleMeltQuoteModal()
|
||||
}
|
||||
if(isMintQuoteRecoveryStarted) {
|
||||
toggleMintQuoteModal()
|
||||
}
|
||||
|
||||
}, 500)
|
||||
} else {
|
||||
toggleMintQuoteModal()
|
||||
if(isMeltQuoteRecoveryStarted) {
|
||||
toggleMeltQuoteModal()
|
||||
}
|
||||
if(isMintQuoteRecoveryStarted) {
|
||||
toggleMintQuoteModal()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -177,20 +200,29 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
toggleMintSelectorModal()
|
||||
}
|
||||
|
||||
const onPasteQuote = async function () {
|
||||
const onPasteMintQuote = async function () {
|
||||
const quote = await Clipboard.getString()
|
||||
if (!quote || quote.length !== 40) {
|
||||
setInfo('Invalid mint quote')
|
||||
return
|
||||
}
|
||||
setMintQuote(quote)
|
||||
}
|
||||
}
|
||||
|
||||
const onPasteMeltQuote = async function () {
|
||||
const quote = await Clipboard.getString()
|
||||
if (!quote || quote.length !== 40) {
|
||||
setInfo('Invalid melt quote')
|
||||
return
|
||||
}
|
||||
setMeltQuote(quote)
|
||||
}
|
||||
|
||||
const onMintEcashFromQuote = async function () {
|
||||
setIsLoading(true)
|
||||
toggleMintQuoteModal()
|
||||
try {
|
||||
if(!mintBalanceToMintFrom) {
|
||||
if(!mintBalanceToRecoverFrom) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Mint is not selected.')
|
||||
}
|
||||
|
||||
@@ -199,7 +231,7 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
}
|
||||
|
||||
const result = await WalletTask.recoverMintQuote({
|
||||
mintUrl: mintBalanceToMintFrom.mintUrl,
|
||||
mintUrl: mintBalanceToRecoverFrom.mintUrl,
|
||||
mintQuote
|
||||
})
|
||||
|
||||
@@ -208,6 +240,7 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
if(result.recoveredAmount > 0) {
|
||||
setMintedAmount(result.recoveredAmount)
|
||||
toggleMintQuoteResultModal()
|
||||
resetState()
|
||||
} else {
|
||||
throw new AppError(Err.MINT_ERROR, 'Could not mint ecash from provided mint quote.')
|
||||
}
|
||||
@@ -216,17 +249,74 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
}
|
||||
}
|
||||
|
||||
const onRecoverChangeFromQuote = async function () {
|
||||
setIsLoading(true)
|
||||
toggleMeltQuoteModal()
|
||||
try {
|
||||
if(!mintBalanceToRecoverFrom) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Mint is not selected.')
|
||||
}
|
||||
|
||||
if(meltQuote.length !== 40) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Melt quote must have 40 characters.')
|
||||
}
|
||||
|
||||
const result = await WalletTask.recoverMeltQuoteChange({
|
||||
mintUrl: mintBalanceToRecoverFrom.mintUrl,
|
||||
meltQuote
|
||||
})
|
||||
|
||||
setIsLoading(false)
|
||||
|
||||
if(result.recoveredAmount > 0) {
|
||||
setMeltChangeAmount(result.recoveredAmount)
|
||||
toggleMeltQuoteResultModal()
|
||||
resetState()
|
||||
} else {
|
||||
throw new AppError(Err.MINT_ERROR, 'Could not recover any change from provided melt quote.')
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
const handleError = function (e: AppError): void {
|
||||
log.error(e.name, e.message)
|
||||
setIsLoading(false)
|
||||
resetState()
|
||||
setError(e)
|
||||
}
|
||||
|
||||
const startMeltQuoteRecovery = function(): void {
|
||||
setIsMeltQuoteRecoveryStarted(true)
|
||||
toggleMintSelectorModal()
|
||||
|
||||
}
|
||||
|
||||
const startMintQuoteRecovery = function(): void {
|
||||
setIsMintQuoteRecoveryStarted(true)
|
||||
toggleMintSelectorModal()
|
||||
}
|
||||
|
||||
const resetState = function () {
|
||||
setIsLoading(false)
|
||||
setError(undefined)
|
||||
setInfo('')
|
||||
setMintedAmount(0)
|
||||
setMeltChangeAmount(0)
|
||||
setMintQuote('')
|
||||
setMeltQuote('')
|
||||
setIsSyncStateSentToQueue(false)
|
||||
setIsMintQuoteRecoveryStarted(false)
|
||||
setIsMeltQuoteRecoveryStarted(false)
|
||||
setMintBalanceToRecoverFrom(mintBalancesRef.current[0])
|
||||
}
|
||||
|
||||
const headerBg = useThemeColor('header')
|
||||
const inputBg = useThemeColor('background')
|
||||
const mintsModalBg = useThemeColor('background')
|
||||
const inputText = useThemeColor('text')
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
<View style={[$headerContainer, {backgroundColor: headerBg}]}>
|
||||
@@ -326,10 +416,11 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
style={$card}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
label='Experimental tools'
|
||||
style={[$card, {marginBottom: spacing.huge * 2}]}
|
||||
HeadingComponent={
|
||||
<>
|
||||
<ListItem
|
||||
text="Recover mint quote"
|
||||
subText="Retry to mint ecash from an already paid mint quote."
|
||||
leftIcon='faCoins'
|
||||
@@ -338,18 +429,36 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Button
|
||||
onPress={toggleMintSelectorModal}
|
||||
onPress={startMintQuoteRecovery}
|
||||
text='Start'
|
||||
preset='secondary'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
text="Recover melt quote change"
|
||||
subText="Retry to receive back unspent ecash from a completed Lightning payment."
|
||||
leftIcon='faArrowTurnDown'
|
||||
leftIconColor={colors.palette.iconGreyBlue400}
|
||||
leftIconInverse={true}
|
||||
topSeparator={true}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Button
|
||||
onPress={startMeltQuoteRecovery}
|
||||
text='Start'
|
||||
preset='secondary'
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
style={$item}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
{isLoading && <Loading />}
|
||||
{error && <ErrorModal error={error} />}
|
||||
@@ -389,7 +498,7 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
confirmTitle={'Confirm'}
|
||||
collapsible={false}
|
||||
onMintBalanceSelect={onMintBalanceSelect}
|
||||
selectedMintBalance={mintBalanceToMintFrom}
|
||||
selectedMintBalance={mintBalanceToRecoverFrom}
|
||||
onCancel={onMintBalanceCancel}
|
||||
onMintBalanceConfirm={onMintBalanceConfirm}
|
||||
/>
|
||||
@@ -418,7 +527,38 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
tx={'common.paste'}
|
||||
preset='secondary'
|
||||
style={$pasteButton}
|
||||
onPress={onPasteQuote}
|
||||
onPress={onPasteMintQuote}
|
||||
/>
|
||||
</View>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
|
||||
<Button onPress={onMintEcashFromQuote} text='Mint ecash' />
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
onBackButtonPress={toggleMintQuoteModal}
|
||||
onBackdropPress={toggleMintQuoteModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isMintQuoteModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<View style={$quoteContainer}>
|
||||
<Text text='Enter mint quote' />
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={mintQuoteInputRef}
|
||||
onChangeText={(quote) => setMintQuote(quote)}
|
||||
value={mintQuote}
|
||||
autoCapitalize='none'
|
||||
keyboardType='default'
|
||||
maxLength={40}
|
||||
selectTextOnFocus={true}
|
||||
style={[$quoteInput, {backgroundColor: inputBg, color: inputText}]}
|
||||
/>
|
||||
<Button
|
||||
tx={'common.paste'}
|
||||
preset='secondary'
|
||||
style={$pasteButton}
|
||||
onPress={onPasteMintQuote}
|
||||
/>
|
||||
</View>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
|
||||
@@ -451,13 +591,66 @@ export const RecoveryOptionsScreen = observer(function RecoveryOptionsScreen({ r
|
||||
onBackButtonPress={toggleMintQuoteResultModal}
|
||||
onBackdropPress={toggleMintQuoteResultModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isMeltQuoteModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<View style={$quoteContainer}>
|
||||
<Text text='Enter melt quote' />
|
||||
<View style={{flexDirection: 'row', alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={mintQuoteInputRef}
|
||||
onChangeText={(quote) => setMeltQuote(quote)}
|
||||
value={meltQuote}
|
||||
autoCapitalize='none'
|
||||
keyboardType='default'
|
||||
maxLength={40}
|
||||
selectTextOnFocus={true}
|
||||
style={[$quoteInput, {backgroundColor: inputBg, color: inputText}]}
|
||||
/>
|
||||
<Button
|
||||
tx={'common.paste'}
|
||||
preset='secondary'
|
||||
style={$pasteButton}
|
||||
onPress={onPasteMeltQuote}
|
||||
/>
|
||||
</View>
|
||||
<View style={[$buttonContainer, {marginTop: spacing.medium}]}>
|
||||
<Button onPress={onRecoverChangeFromQuote} text='Recover change' />
|
||||
</View>
|
||||
</View>
|
||||
}
|
||||
onBackButtonPress={toggleMintQuoteModal}
|
||||
onBackdropPress={toggleMintQuoteModal}
|
||||
/>
|
||||
<BottomModal
|
||||
isVisible={isMeltQuoteResultModalVisible ? true : false}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ResultModalInfo
|
||||
icon="faCheckCircle"
|
||||
iconColor={colors.palette.success200}
|
||||
title="Success!"
|
||||
message={`Successfully recovered ${meltChangeAmount} SAT from the provided melt quote.`}
|
||||
/>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset="secondary"
|
||||
tx={'common.close'}
|
||||
onPress={toggleMeltQuoteResultModal}
|
||||
/>
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
onBackButtonPress={toggleMeltQuoteResultModal}
|
||||
onBackdropPress={toggleMeltQuoteResultModal}
|
||||
/>
|
||||
</Screen>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const $screen: ViewStyle = {
|
||||
flex: 1,
|
||||
//flex: 1,
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
@@ -467,7 +660,7 @@ const $headerContainer: TextStyle = {
|
||||
}
|
||||
|
||||
const $contentContainer: TextStyle = {
|
||||
flex: 1,
|
||||
//flex: 1,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
padding: spacing.extraSmall,
|
||||
// alignItems: 'center',
|
||||
|
||||
+90
-29
@@ -48,7 +48,7 @@ import { Proof } from '../models/Proof'
|
||||
import { Contact, ContactType } from '../models/Contact'
|
||||
import { getImageSource, infoMessage } from '../utils/utils'
|
||||
import { verticalScale } from '@gocodingnow/rn-size-matters'
|
||||
import { CurrencyCode, MintUnit, MintUnits, formatCurrency, getCurrency } from "../services/wallet/currency"
|
||||
import { CurrencyCode, MintUnit, MintUnits, convertToFromSats, formatCurrency, getCurrency } from "../services/wallet/currency"
|
||||
import { MintHeader } from './Mints/MintHeader'
|
||||
import { MintBalanceSelector } from './Mints/MintBalanceSelector'
|
||||
import { round, toNumber } from '../utils/number'
|
||||
@@ -96,7 +96,8 @@ export const SendScreen = observer(function SendScreen({ route }: Props) {
|
||||
mintsStore,
|
||||
relaysStore,
|
||||
walletStore,
|
||||
contactsStore
|
||||
contactsStore,
|
||||
userSettingsStore
|
||||
} = useStores()
|
||||
|
||||
const amountInputRef = useRef<TextInput>(null)
|
||||
@@ -961,8 +962,31 @@ const pubkeyInputRef = useRef<TextInput>(null) // Initialize pubkeyInputRef
|
||||
navigation.navigate('Scan', {
|
||||
unit: unitRef.current,
|
||||
mintUrl: mintBalanceToSendFrom.mintUrl
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle')
|
||||
|
||||
const getConvertedAmount = function () {
|
||||
if (!walletStore.exchangeRate) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const precision = getCurrency(unitRef.current).precision
|
||||
return convertToFromSats(
|
||||
round(toNumber(amountToSend) * precision, 0) || 0,
|
||||
getCurrency(unitRef.current).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = function () {
|
||||
return (
|
||||
walletStore.exchangeRate &&
|
||||
(userSettingsStore.exchangeCurrency === getCurrency(unitRef.current).code ||
|
||||
unitRef.current === 'sat') &&
|
||||
getConvertedAmount() !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -989,17 +1013,23 @@ const pubkeyInputRef = useRef<TextInput>(null) // Initialize pubkeyInputRef
|
||||
}
|
||||
returnKeyType={'done'}
|
||||
/>
|
||||
<Text
|
||||
size='sm'
|
||||
text="Amount to send"
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
/>
|
||||
{lockedPubkey && (
|
||||
{isConvertedAmountVisible() && (
|
||||
<CurrencyAmount
|
||||
amount={getConvertedAmount() ?? 0}
|
||||
currencyCode={unitRef.current === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
||||
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
||||
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
|
||||
size='small'
|
||||
containerStyle={{justifyContent: 'center'}}
|
||||
/>
|
||||
)}
|
||||
{lockedPubkey ? (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
justifyContent: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
@@ -1008,12 +1038,22 @@ const pubkeyInputRef = useRef<TextInput>(null) // Initialize pubkeyInputRef
|
||||
color={amountInputColor}
|
||||
/>
|
||||
<Text
|
||||
size='xxs'
|
||||
size='xs'
|
||||
text="Locked"
|
||||
style={{color: amountInputColor, marginLeft: spacing.tiny}}
|
||||
/>
|
||||
|
||||
</View>
|
||||
) : (
|
||||
<Text
|
||||
size='xs'
|
||||
tx='amount.send'
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
@@ -1028,7 +1068,7 @@ const pubkeyInputRef = useRef<TextInput>(null) // Initialize pubkeyInputRef
|
||||
onMemoEndEditing={onMemoEndEditing}
|
||||
/>
|
||||
)}
|
||||
{isMintSelectorVisible && (
|
||||
{isMintSelectorVisible && !encodedTokenToSend && (
|
||||
<MintBalanceSelector
|
||||
mintBalances={availableMintBalances}
|
||||
selectedMintBalance={mintBalanceToSendFrom as MintBalance}
|
||||
@@ -1387,6 +1427,7 @@ const SelectProofsBlock = observer(function (props: {
|
||||
|
||||
const {proofsStore} = useStores()
|
||||
const hintColor = useThemeColor('textDim')
|
||||
const statusColor = useThemeColor('header')
|
||||
|
||||
|
||||
const onCancel = function () {
|
||||
@@ -1396,13 +1437,48 @@ const SelectProofsBlock = observer(function (props: {
|
||||
|
||||
return (
|
||||
<View style={$bottomModal}>
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
alignSelf: 'center',
|
||||
marginTop: spacing.tiny,
|
||||
paddingHorizontal: spacing.tiny,
|
||||
borderRadius: spacing.tiny,
|
||||
backgroundColor: colors.palette.primary200,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
text={'OFFLINE MODE'}
|
||||
style={[
|
||||
{
|
||||
color: statusColor,
|
||||
fontSize: 10,
|
||||
fontFamily: typography.primary?.light,
|
||||
padding: 0,
|
||||
lineHeight: 16,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
<Text text='Select ecash to send' style={{marginTop: spacing.large}}/>
|
||||
<Text
|
||||
text='You can only send exact ecash denominations while you are offline.'
|
||||
style={{color: hintColor, paddingHorizontal: spacing.small, textAlign: 'center'}}
|
||||
size='xs'
|
||||
/>
|
||||
<CurrencyAmount
|
||||
amount={CashuUtils.getProofsAmount(props.selectedProofs)}
|
||||
mintUnit={props.unit}
|
||||
size='extraLarge'
|
||||
containerStyle={{marginTop: spacing.large, marginBottom: spacing.small, alignItems: 'center'}}
|
||||
/>
|
||||
<View style={{maxHeight: spacing.screenHeight * 0.4}}>
|
||||
<View style={{
|
||||
maxHeight: spacing.screenHeight * 0.45,
|
||||
borderWidth: 1,
|
||||
borderColor: hintColor,
|
||||
borderRadius: spacing.medium,
|
||||
marginTop: spacing.small
|
||||
}}>
|
||||
<FlatList<Proof>
|
||||
data={proofsStore.getByMint(props.mintBalanceToSendFrom.mintUrl, {isPending: false, unit: props.unit})}
|
||||
renderItem={({ item }) => {
|
||||
@@ -1424,21 +1500,6 @@ const SelectProofsBlock = observer(function (props: {
|
||||
/>
|
||||
</View>
|
||||
<View style={[$bottomContainer, {marginTop: spacing.extraLarge}]}>
|
||||
<View style={[$buttonContainer, {marginBottom: spacing.medium}]}>
|
||||
<CurrencyAmount
|
||||
amount={CashuUtils.getProofsAmount(props.selectedProofs)}
|
||||
mintUnit={props.unit}
|
||||
|
||||
size='large'
|
||||
/>
|
||||
<Button
|
||||
preset={'tertiary'}
|
||||
onPress={() => props.toggleIsLockedToPubkey()}
|
||||
LeftAccessory={() => <Icon icon={props.isLockedToPubkey ? 'faLock' : 'faLock'}/>}
|
||||
text={props.isLockedToPubkey ? 'Locked' : 'Lock'}
|
||||
style={{minWidth: 80}}
|
||||
/>
|
||||
</View>
|
||||
<View style={[$buttonContainer]}>
|
||||
<Button
|
||||
text="Create token"
|
||||
|
||||
+116
-102
@@ -4,6 +4,7 @@ import {
|
||||
View,
|
||||
TextStyle,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
} from 'react-native'
|
||||
import {colors, spacing, useThemeColor} from '../theme'
|
||||
import {log} from '../services/logService'
|
||||
@@ -32,6 +33,13 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
const tokenInputRef = useRef<TextInput>(null)
|
||||
const {mintsStore} = useStores()
|
||||
|
||||
// New: controls visibility of token input
|
||||
const [showTokenInput, setShowTokenInput] = useState(false)
|
||||
const [encodedToken, setEncodedToken] = useState<string | undefined>(undefined)
|
||||
const [unit, setUnit] = useState<MintUnit>('sat')
|
||||
const [mint, setMint] = useState<Mint | undefined>(undefined)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
async function autoPaste(setter: (text: string) => void, sideEffect: () => void) {
|
||||
const clipboard = (await Clipboard.getString()).trim();
|
||||
if (clipboard.length === 0) return
|
||||
@@ -40,6 +48,8 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
const resultFromClipboard = IncomingParser.findAndExtract(clipboard, IncomingDataType.CASHU)
|
||||
setter(resultFromClipboard.encoded)
|
||||
sideEffect()
|
||||
// Show input if autopaste sets encodedToken
|
||||
setShowTokenInput(true)
|
||||
} catch (e: any) {
|
||||
return
|
||||
}
|
||||
@@ -70,12 +80,12 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
return () => {}
|
||||
}, [])
|
||||
|
||||
|
||||
const [encodedToken, setEncodedToken] = useState<string | undefined>(undefined)
|
||||
const [unit, setUnit] = useState<MintUnit>('sat')
|
||||
const [mint, setMint] = useState<Mint | undefined>(undefined)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
|
||||
// If encodedToken is set (by autopaste), show input
|
||||
useEffect(() => {
|
||||
if (encodedToken && encodedToken.length > 0) {
|
||||
setShowTokenInput(true)
|
||||
}
|
||||
}, [encodedToken])
|
||||
|
||||
const onPaste = async function() {
|
||||
const clipboard = await Clipboard.getString()
|
||||
@@ -86,9 +96,9 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
|
||||
setEncodedToken(clipboard)
|
||||
tokenInputRef.current?.blur()
|
||||
setShowTokenInput(true)
|
||||
}
|
||||
|
||||
|
||||
const gotoScan = async function () {
|
||||
tokenInputRef.current?.blur()
|
||||
//@ts-ignore
|
||||
@@ -98,17 +108,13 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/* const gotoContacts = function () {
|
||||
const gotoCashuPaymentRequest = async function () {
|
||||
//@ts-ignore
|
||||
navigation.navigate('ContactsNavigator', {
|
||||
screen: 'Contacts',
|
||||
params: {
|
||||
paymentOption: SendOption.LNURL_ADDRESS
|
||||
}
|
||||
navigation.navigate('CashuPaymentRequest', {
|
||||
mintUrl: mint?.mintUrl,
|
||||
unit
|
||||
})
|
||||
} */
|
||||
|
||||
}
|
||||
|
||||
const onConfirm = async function() {
|
||||
if(!encodedToken) {
|
||||
@@ -126,7 +132,6 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const gotoTopup = async function () {
|
||||
//@ts-ignore
|
||||
navigation.navigate('Topup', {
|
||||
@@ -135,7 +140,6 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const handleError = function(e: AppError): void {
|
||||
setError(e)
|
||||
}
|
||||
@@ -146,7 +150,8 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
const inputText = useThemeColor('text')
|
||||
const contactIcon = useThemeColor('button')
|
||||
const headerBg = useThemeColor('header')
|
||||
const headerTitle = useThemeColor('headerTitle')
|
||||
const headerTitle = useThemeColor('headerTitle')
|
||||
const mainButtonColor = useThemeColor('card')
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
@@ -162,86 +167,72 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
/>
|
||||
</View>
|
||||
<View style={$contentContainer}>
|
||||
<Card
|
||||
HeadingComponent={
|
||||
<ListItem
|
||||
leftIcon='faMoneyBill1'
|
||||
leftIconColor={colors.palette.iconViolet300}
|
||||
tx="common.pasteEcashToken"
|
||||
bottomSeparator={true}
|
||||
/* RightComponent={
|
||||
<Button
|
||||
preset='tertiary'
|
||||
LeftAccessory={() => <Icon color={contactIcon} containerStyle={{paddingVertical: 0}} icon='faAddressBook' />}
|
||||
onPress={gotoContacts}
|
||||
text='Contacts'
|
||||
textStyle={{fontSize: 12, color: contactIcon}}
|
||||
/>
|
||||
}*/
|
||||
/>
|
||||
}
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
<Text
|
||||
size='xs'
|
||||
style={{color: hintText, padding: spacing.extraSmall}}
|
||||
tx="pasteEcashTokenDesc"
|
||||
/>
|
||||
<View style={{alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={tokenInputRef}
|
||||
onChangeText={data => setEncodedToken(data)}
|
||||
value={encodedToken}
|
||||
autoCapitalize='none'
|
||||
keyboardType='default'
|
||||
maxLength={5000}
|
||||
numberOfLines={4}
|
||||
multiline={true}
|
||||
selectTextOnFocus={true}
|
||||
style={[$addressInput, {backgroundColor: inputBg, color: inputText}]}
|
||||
<ListItem
|
||||
leftIcon='faMoneyBill1'
|
||||
tx="common.pasteEcashToken"
|
||||
bottomSeparator={showTokenInput}
|
||||
onPress={() => setShowTokenInput((v) => !v)}
|
||||
/>
|
||||
</View>
|
||||
{!!encodedToken && encodedToken?.length > 1 ? (
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset='default'
|
||||
tx='common.confirm'
|
||||
onPress={onConfirm}
|
||||
style={{marginLeft: spacing.small}}
|
||||
LeftAccessory={() => <Icon icon='faCheckCircle' color='white'/>}
|
||||
{/* Token Input */}
|
||||
{showTokenInput && (
|
||||
<View style={{paddingVertical: spacing.extraSmall}}>
|
||||
<Text
|
||||
size='xs'
|
||||
style={{color: hintText, padding: spacing.extraSmall}}
|
||||
tx="pasteEcashTokenDesc"
|
||||
/>
|
||||
<View style={{alignItems: 'center', marginTop: spacing.small}}>
|
||||
<TextInput
|
||||
ref={tokenInputRef}
|
||||
onChangeText={data => setEncodedToken(data)}
|
||||
value={encodedToken}
|
||||
autoCapitalize='none'
|
||||
keyboardType='default'
|
||||
maxLength={5000}
|
||||
numberOfLines={3}
|
||||
multiline={true}
|
||||
selectTextOnFocus={true}
|
||||
style={[$addressInput, {backgroundColor: inputBg, color: inputText}]}
|
||||
/>
|
||||
</View>
|
||||
{!!encodedToken && encodedToken?.length > 1 ? (
|
||||
<View style={[$buttonContainer, {marginTop: spacing.small}]}>
|
||||
<Button
|
||||
preset='default'
|
||||
tx='common.confirm'
|
||||
onPress={onConfirm}
|
||||
// style={{marginLeft: spacing.small}}
|
||||
LeftAccessory={() => <Icon icon='faCheckCircle' color='white'/>}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[$buttonContainer, {marginTop: spacing.small}]}>
|
||||
<Button
|
||||
preset='secondary'
|
||||
tx={'common.paste'}
|
||||
onPress={onPaste}
|
||||
LeftAccessory={() => (
|
||||
<Icon icon='faPaste'/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset='secondary'
|
||||
tx={'common.paste'}
|
||||
onPress={onPaste}
|
||||
LeftAccessory={() => (
|
||||
<Icon icon='faPaste'/>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
preset='secondary'
|
||||
tx='common.scan'
|
||||
onPress={gotoScan}
|
||||
style={{marginLeft: spacing.small}}
|
||||
LeftAccessory={() => {
|
||||
return(
|
||||
<SvgXml
|
||||
width={spacing.medium}
|
||||
height={spacing.medium}
|
||||
xml={ScanIcon}
|
||||
fill={scanIcon}
|
||||
style={{marginHorizontal: spacing.extraSmall}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
)}
|
||||
{/* Create Payment Request */}
|
||||
<ListItem
|
||||
leftIcon='faQrcode'
|
||||
tx="common.createCashuPaymentRequest"
|
||||
onPress={gotoCashuPaymentRequest}
|
||||
topSeparator={true}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
style={$card}
|
||||
//style={{marginBottom: spacing.medium}}
|
||||
/>
|
||||
<Button
|
||||
tx="tokenReceiveScreen.topupWithLightning"
|
||||
@@ -263,8 +254,27 @@ export const TokenReceiveScreen = function TokenReceiveScreen({ route }: Props)
|
||||
marginTop: spacing.medium
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
</View>
|
||||
<View style={$bottomContainer}>
|
||||
<View style={$buttonContainer}>
|
||||
<Button
|
||||
preset='tertiary'
|
||||
LeftAccessory={() => (
|
||||
<SvgXml
|
||||
width={spacing.medium}
|
||||
height={spacing.medium}
|
||||
xml={ScanIcon}
|
||||
fill={scanIcon}
|
||||
style={{marginHorizontal: spacing.extraSmall}}
|
||||
/>
|
||||
)}
|
||||
onPress={gotoScan}
|
||||
style={{backgroundColor: mainButtonColor}}
|
||||
text='Scan'
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{error && <ErrorModal error={error} />}
|
||||
</Screen>
|
||||
)
|
||||
@@ -275,10 +285,9 @@ const $screen: ViewStyle = {
|
||||
}
|
||||
|
||||
const $contentContainer: ViewStyle = {
|
||||
// flex: 1,
|
||||
marginTop: -spacing.extraLarge * 2,
|
||||
padding: spacing.extraSmall,
|
||||
// alignItems: 'center',
|
||||
flex: 1
|
||||
}
|
||||
|
||||
const $headerContainer: TextStyle = {
|
||||
@@ -288,29 +297,34 @@ const $headerContainer: TextStyle = {
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
marginTop: spacing.large,
|
||||
//marginTop: spacing.large,
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
}
|
||||
|
||||
|
||||
const $addressInput: TextStyle = {
|
||||
textAlignVertical: 'top' ,
|
||||
borderRadius: spacing.extraSmall,
|
||||
padding: spacing.extraSmall,
|
||||
alignSelf: 'stretch',
|
||||
height: 120,
|
||||
height: verticalScale(70),
|
||||
}
|
||||
|
||||
|
||||
const $card: ViewStyle = {
|
||||
marginBottom: 0,
|
||||
}
|
||||
|
||||
const $bottomContainer: ViewStyle = {
|
||||
position: 'absolute',
|
||||
/*position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flex: 1,
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: spacing.medium,
|
||||
alignSelf: 'stretch',
|
||||
marginBottom: spacing.medium,*/
|
||||
alignSelf: 'center',
|
||||
// opacity: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -50,10 +50,13 @@ import {LnurlClient, LnurlWithdrawResult} from '../services/lnurlService'
|
||||
import {
|
||||
verticalScale,
|
||||
} from '@gocodingnow/rn-size-matters'
|
||||
|
||||
import { CurrencyAmount } from './Wallet/CurrencyAmount'
|
||||
import {
|
||||
CurrencyCode,
|
||||
MintUnit,
|
||||
getCurrency,
|
||||
convertToFromSats
|
||||
} from '../services/wallet/currency'
|
||||
import {MintHeader} from './Mints/MintHeader'
|
||||
import useIsInternetReachable from '../utils/useIsInternetReachable'
|
||||
@@ -83,6 +86,8 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
walletProfileStore,
|
||||
transactionsStore,
|
||||
relaysStore,
|
||||
walletStore,
|
||||
userSettingsStore
|
||||
} = useStores()
|
||||
const amountInputRef = useRef<TextInput>(null)
|
||||
const memoInputRef = useRef<TextInput>(null)
|
||||
@@ -304,7 +309,9 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
}
|
||||
|
||||
// Subscribe to the 'sendCompleted' event
|
||||
EventEmitter.on(`ev_${TOPUP_TASK}_result`, handleTopupTaskResult)
|
||||
if(isTopupTaskSentToQueue) {
|
||||
EventEmitter.on(`ev_${TOPUP_TASK}_result`, handleTopupTaskResult)
|
||||
}
|
||||
|
||||
// Unsubscribe from the 'sendCompleted' event on component unmount
|
||||
return () => {
|
||||
@@ -346,10 +353,12 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
}
|
||||
}
|
||||
|
||||
EventEmitter.on(
|
||||
`ev_${HANDLE_PENDING_TOPUP_TASK}_result`,
|
||||
handlePendingTopupTaskResult,
|
||||
)
|
||||
if(transactionId) {
|
||||
EventEmitter.on(
|
||||
`ev_${HANDLE_PENDING_TOPUP_TASK}_result`,
|
||||
handlePendingTopupTaskResult,
|
||||
)
|
||||
}
|
||||
|
||||
return () => {
|
||||
EventEmitter.off(
|
||||
@@ -367,6 +376,13 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
setIsResultModalVisible(previousState => !previousState)
|
||||
|
||||
const onAmountEndEditing = function () {
|
||||
log.trace("[onAmountEndEditing] called")
|
||||
|
||||
if(isTopupTaskSentToQueue) {
|
||||
log.trace('[onAmountEndEditing] Topup task already sent to queue, ignoring further edits')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const precision = getCurrency(unit).precision
|
||||
const mantissa = getCurrency(unit).mantissa
|
||||
@@ -648,6 +664,30 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
const placeholderTextColor = useThemeColor('textDim')
|
||||
const inputText = useThemeColor('text')
|
||||
const amountInputColor = useThemeColor('amountInput')
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle')
|
||||
|
||||
const getConvertedAmount = function () {
|
||||
if (!walletStore.exchangeRate) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const precision = getCurrency(unit).precision
|
||||
return convertToFromSats(
|
||||
round(toNumber(amountToTopup) * precision, 0) || 0,
|
||||
getCurrency(unit).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = function () {
|
||||
return (
|
||||
walletStore.exchangeRate &&
|
||||
(userSettingsStore.exchangeCurrency === getCurrency(unit).code ||
|
||||
unit === 'sat') &&
|
||||
getConvertedAmount() !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
@@ -674,11 +714,25 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
transactionStatus === TransactionStatus.PENDING ? false : true
|
||||
}
|
||||
returnKeyType={'done'}
|
||||
/>
|
||||
/>
|
||||
{isConvertedAmountVisible() && (
|
||||
<CurrencyAmount
|
||||
amount={getConvertedAmount() ?? 0}
|
||||
currencyCode={unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
||||
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
||||
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
|
||||
size='small'
|
||||
containerStyle={{justifyContent: 'center'}}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
size="sm"
|
||||
size="xs"
|
||||
text={getAmountTitle()}
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -751,7 +805,7 @@ export const TopupScreen = observer(function TopupScreen({ route }: Props) {
|
||||
gotoContacts={gotoContacts}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
)}
|
||||
{transaction && transactionStatus === TransactionStatus.COMPLETED && (
|
||||
<Card
|
||||
style={{padding: spacing.medium}}
|
||||
|
||||
@@ -216,7 +216,7 @@ export const TranDetailScreen = observer(function TranDetailScreen({ route }: Pr
|
||||
if (!transaction) { return '' }
|
||||
|
||||
switch (transaction?.type) {
|
||||
case TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE:
|
||||
case TransactionType.RECEIVE || TransactionType.RECEIVE_OFFLINE || TransactionType.RECEIVE_BY_PAYMENT_REQUEST:
|
||||
return `+${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
||||
case TransactionType.SEND:
|
||||
return `-${formatCurrency(transaction.amount, getCurrency(transaction.unit).code)}`
|
||||
@@ -232,6 +232,7 @@ export const TranDetailScreen = observer(function TranDetailScreen({ route }: Pr
|
||||
const colorScheme = useColorScheme()
|
||||
const headerTitle = useThemeColor('headerTitle')
|
||||
const inputText = useThemeColor('text')
|
||||
const statusColor = useThemeColor('header')
|
||||
|
||||
return (
|
||||
<Screen contentContainerStyle={$screen} preset="fixed">
|
||||
@@ -253,7 +254,32 @@ export const TranDetailScreen = observer(function TranDetailScreen({ route }: Pr
|
||||
preset="heading"
|
||||
text={getFormattedAmount()}
|
||||
style={[$tranAmount, {color: headerTitle}]}
|
||||
/>
|
||||
/>
|
||||
{transaction.status !== TransactionStatus.COMPLETED && (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
alignSelf: 'center',
|
||||
marginTop: spacing.tiny,
|
||||
paddingHorizontal: spacing.tiny,
|
||||
borderRadius: spacing.tiny,
|
||||
backgroundColor: colors.palette.primary200,
|
||||
},
|
||||
]}>
|
||||
<Text
|
||||
text={transaction.status as string}
|
||||
style={[
|
||||
{
|
||||
color: statusColor,
|
||||
fontSize: 10,
|
||||
fontFamily: typography.primary?.light,
|
||||
padding: 0,
|
||||
lineHeight: 16,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<ScrollView style={$contentContainer}>
|
||||
<Card
|
||||
@@ -298,7 +324,7 @@ export const TranDetailScreen = observer(function TranDetailScreen({ route }: Pr
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{transaction.type === TransactionType.RECEIVE && (
|
||||
{(transaction.type === TransactionType.RECEIVE || transaction.type === TransactionType.RECEIVE_BY_PAYMENT_REQUEST) && (
|
||||
<ReceiveInfoBlock
|
||||
transaction={transaction}
|
||||
isDataParsable={isDataParsable}
|
||||
@@ -405,7 +431,8 @@ export const TranDetailScreen = observer(function TranDetailScreen({ route }: Pr
|
||||
].includes(transaction.status)
|
||||
) && (
|
||||
<Card
|
||||
label='Token tracking'
|
||||
label='Token tracking'
|
||||
style={$dataCard}
|
||||
ContentComponent={
|
||||
<>
|
||||
<ListItem
|
||||
@@ -722,10 +749,12 @@ const ReceiveInfoBlock = function (props: {
|
||||
isCurrency={true}
|
||||
isFirst={true}
|
||||
/>
|
||||
{transaction.memo && (
|
||||
<TranItem
|
||||
label="tranDetailScreen.memoFromSender"
|
||||
value={transaction.memo as string}
|
||||
/>
|
||||
)}
|
||||
{transaction.sentFrom && (
|
||||
<>
|
||||
{profilePicture ? (
|
||||
@@ -823,6 +852,12 @@ const ReceiveInfoBlock = function (props: {
|
||||
label="tranDetailScreen.createdAt"
|
||||
value={(transaction.createdAt as Date).toLocaleString()}
|
||||
/>
|
||||
{transaction.paymentId && (
|
||||
<TranItem
|
||||
label="tranDetailScreen.paymentId"
|
||||
value={transaction.paymentId as string}
|
||||
/>
|
||||
)}
|
||||
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
||||
</>
|
||||
}
|
||||
@@ -1037,6 +1072,12 @@ const ReceiveOfflineInfoBlock = function (props: {
|
||||
label="tranDetailScreen.createdAt"
|
||||
value={(transaction.createdAt as Date).toLocaleString()}
|
||||
/>
|
||||
{transaction.paymentId && (
|
||||
<TranItem
|
||||
label="tranDetailScreen.paymentId"
|
||||
value={transaction.paymentId as string}
|
||||
/>
|
||||
)}
|
||||
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
||||
</>
|
||||
}
|
||||
@@ -1503,7 +1544,13 @@ const TopupInfoBlock = function (props: {
|
||||
label="tranDetailScreen.expiresAt"
|
||||
value={(new Date(paymentRequest.expiresAt!)).toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
{transaction.paymentId && (
|
||||
<TranItem
|
||||
label="tranDetailScreen.paymentId"
|
||||
value={transaction.paymentId as string}
|
||||
/>
|
||||
)}
|
||||
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
||||
</>
|
||||
}
|
||||
@@ -1748,6 +1795,12 @@ const TransferInfoBlock = function (props: {
|
||||
label="tranDetailScreen.createdAt"
|
||||
value={(transaction.createdAt as Date).toLocaleString()}
|
||||
/>
|
||||
{transaction.paymentId && (
|
||||
<TranItem
|
||||
label="tranDetailScreen.paymentId"
|
||||
value={transaction.paymentId as string}
|
||||
/>
|
||||
)}
|
||||
<TranItem label="tranDetailScreen.id" value={`${transaction.id}`} />
|
||||
</>
|
||||
}
|
||||
@@ -2003,7 +2056,7 @@ const $contentContainer: TextStyle = {
|
||||
const $tranAmount: TextStyle = {
|
||||
fontSize: verticalScale(48),
|
||||
lineHeight: verticalScale(48),
|
||||
marginLeft: -20,
|
||||
//marginLeft: -20,
|
||||
}
|
||||
|
||||
const $actionCard: ViewStyle = {
|
||||
|
||||
@@ -62,6 +62,14 @@ export const TransactionListItem = observer(function (props: TransactionListProp
|
||||
} else {
|
||||
return tx.memo ? tx.memo : translate('transactionCommon.youReceived')
|
||||
}
|
||||
case TransactionType.RECEIVE_BY_PAYMENT_REQUEST:
|
||||
if (tx.sentFrom) {
|
||||
if (!tx.memo || tx.memo.includes('Sent from Minibits')) {
|
||||
return translate('transactionCommon.from', {sender: getProfileName(tx.sentFrom)})
|
||||
}
|
||||
} else {
|
||||
return tx.memo ? tx.memo : translate('transactionCommon.youReceived')
|
||||
}
|
||||
case TransactionType.RECEIVE_OFFLINE:
|
||||
if (tx.sentFrom) {
|
||||
if (!tx.memo || tx.memo.includes('Sent from Minibits')) {
|
||||
@@ -116,6 +124,8 @@ export const TransactionListItem = observer(function (props: TransactionListProp
|
||||
return translate('transactionCommon.status.completed')
|
||||
}
|
||||
}
|
||||
case TransactionStatus.RECOVERED:
|
||||
return timeAgo + translate('transactionCommon.status.recovered')
|
||||
case TransactionStatus.DRAFT:
|
||||
return timeAgo + translate('transactionCommon.status.draft')
|
||||
case TransactionStatus.ERROR:
|
||||
@@ -155,7 +165,7 @@ export const TransactionListItem = observer(function (props: TransactionListProp
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faClock" size={spacing.medium} color={txErrorColor}/>)
|
||||
}
|
||||
|
||||
if([TransactionType.RECEIVE, TransactionType.TOPUP].includes(tx.type)) {
|
||||
if([TransactionType.RECEIVE, TransactionType.TOPUP, TransactionType.RECEIVE_BY_PAYMENT_REQUEST].includes(tx.type)) {
|
||||
if(tx.profile) {
|
||||
const profilePicture = getProfilePicture(tx.profile)
|
||||
if(profilePicture) {
|
||||
@@ -169,6 +179,15 @@ export const TransactionListItem = observer(function (props: TransactionListProp
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnDown" size={spacing.medium} color={txPendingColor}/>)
|
||||
}
|
||||
|
||||
if([TransactionType.SEND, TransactionType.TRANSFER].includes(tx.type)) {
|
||||
if(tx.profile) {
|
||||
const profilePicture = getProfilePicture(tx.profile)
|
||||
if(profilePicture) {
|
||||
return profilePicture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (<Icon containerStyle={$txIconContainer} icon="faArrowTurnUp" size={spacing.medium} color={txSendColor}/>)
|
||||
}
|
||||
|
||||
@@ -224,9 +243,9 @@ export const TransactionListItem = observer(function (props: TransactionListProp
|
||||
LeftComponent={getLeftIcon(tx)}
|
||||
RightComponent={
|
||||
<View style={$txContainer}>
|
||||
{([TransactionType.RECEIVE, TransactionType.RECEIVE_OFFLINE].includes(tx.type)) && (
|
||||
{([TransactionType.RECEIVE, TransactionType.RECEIVE_OFFLINE, TransactionType.RECEIVE_BY_PAYMENT_REQUEST].includes(tx.type)) && (
|
||||
<>
|
||||
{[TransactionStatus.COMPLETED].includes(tx.status) && (
|
||||
{[TransactionStatus.COMPLETED, TransactionStatus.RECOVERED].includes(tx.status) && (
|
||||
<CurrencyAmount
|
||||
amount={tx.amount}
|
||||
mintUnit={tx.unit}
|
||||
|
||||
@@ -50,6 +50,7 @@ import useIsInternetReachable from '../utils/useIsInternetReachable'
|
||||
import { translate } from '../i18n'
|
||||
import { MemoInputCard } from '../components/MemoInputCard'
|
||||
import { TRANSFER_TASK } from '../services/wallet/transferTask'
|
||||
import { CurrencyAmount } from './Wallet/CurrencyAmount'
|
||||
|
||||
type Props = StaticScreenProps<{
|
||||
unit: MintUnit,
|
||||
@@ -69,7 +70,15 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props)
|
||||
const amountInputRef = useRef<TextInput>(null)
|
||||
const lnurlCommentInputRef = useRef<TextInput>(null)
|
||||
|
||||
const {proofsStore, mintsStore, paymentRequestsStore, walletStore, walletProfileStore} = useStores()
|
||||
const {
|
||||
proofsStore,
|
||||
mintsStore,
|
||||
paymentRequestsStore,
|
||||
walletStore,
|
||||
walletProfileStore,
|
||||
contactsStore,
|
||||
userSettingsStore
|
||||
} = useStores()
|
||||
// const {walletStore} = nonPersistedStores
|
||||
|
||||
const isInternetReachable = useIsInternetReachable()
|
||||
@@ -377,9 +386,15 @@ useEffect(() => {
|
||||
setTransaction(transaction)
|
||||
|
||||
if(transaction && lnurlPayParams && lnurlPayParams.address) {
|
||||
transaction.setSentTo(
|
||||
lnurlPayParams.address as string
|
||||
)
|
||||
|
||||
const profile = contactsStore.findByLud16(lnurlPayParams.address)
|
||||
|
||||
if (profile) {
|
||||
transaction.setSentTo(profile.nip05 || profile.name)
|
||||
transaction.setProfile(JSON.stringify(profile))
|
||||
} else {
|
||||
transaction.setSentTo(lnurlPayParams.address)
|
||||
}
|
||||
}
|
||||
|
||||
if (error) { // This handles timed out pending payments
|
||||
@@ -583,12 +598,19 @@ const onEncodedInvoice = async function (encoded: string, paymentRequestDesc: st
|
||||
log.trace("[onEncodedInvoice] start", {mintUrl, unit})
|
||||
|
||||
try {
|
||||
//@ts-ignore
|
||||
navigation.setParams({encodedInvoice: undefined})
|
||||
//@ts-ignore
|
||||
navigation.setParams({paymentRequest: undefined})
|
||||
//@ts-ignore
|
||||
navigation.setParams({lnurlParams: undefined})
|
||||
//@ts-ignore
|
||||
navigation.setParams({paymentOption: undefined})
|
||||
//@ts-ignore
|
||||
navigation.setParams({fixedAmount: undefined})
|
||||
//@ts-ignore
|
||||
navigation.setParams({isDonation: undefined})
|
||||
//@ts-ignore
|
||||
navigation.setParams({donationForName: undefined})
|
||||
|
||||
const invoice = LightningUtils.decodeInvoice(encoded)
|
||||
@@ -716,6 +738,30 @@ const headerBg = useThemeColor('header')
|
||||
const iconColor = useThemeColor('textDim')
|
||||
const amountInputColor = useThemeColor('amountInput')
|
||||
|
||||
const convertedAmountColor = useThemeColor('headerSubTitle')
|
||||
|
||||
const getConvertedAmount = function () {
|
||||
if (!walletStore.exchangeRate) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const precision = getCurrency(unit).precision
|
||||
return convertToFromSats(
|
||||
round(toNumber(amountToTransfer) * precision, 0) || 0,
|
||||
getCurrency(unit).code,
|
||||
walletStore.exchangeRate
|
||||
)
|
||||
}
|
||||
|
||||
const isConvertedAmountVisible = function () {
|
||||
return (
|
||||
walletStore.exchangeRate &&
|
||||
(userSettingsStore.exchangeCurrency === getCurrency(unit).code ||
|
||||
unit === 'sat') &&
|
||||
getConvertedAmount() !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Screen preset="fixed" contentContainerStyle={$screen}>
|
||||
@@ -749,11 +795,27 @@ const amountInputColor = useThemeColor('amountInput')
|
||||
finalFee={finalFee}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
size="sm"
|
||||
tx="payCommon.amountToPayLabel"
|
||||
style={{color: amountInputColor, textAlign: 'center'}}
|
||||
/>
|
||||
<>
|
||||
{isConvertedAmountVisible() && (
|
||||
<CurrencyAmount
|
||||
amount={getConvertedAmount() ?? 0}
|
||||
currencyCode={unit === 'sat' ? userSettingsStore.exchangeCurrency : CurrencyCode.SAT}
|
||||
symbolStyle={{color: convertedAmountColor, marginTop: spacing.tiny, fontSize: verticalScale(10)}}
|
||||
amountStyle={{color: convertedAmountColor, lineHeight: spacing.small}}
|
||||
size='small'
|
||||
containerStyle={{justifyContent: 'center'}}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
tx="payCommon.amountToPayLabel"
|
||||
style={{
|
||||
color: amountInputColor,
|
||||
textAlign: 'center',
|
||||
marginTop: isConvertedAmountVisible() ? -spacing.extraSmall : undefined
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CashuUtils, CashuProof } from "../../services/cashu/cashuUtils"
|
||||
import { log } from "../../services"
|
||||
import { Token, getDecodedToken, getEncodedToken } from '@cashu/cashu-ts';
|
||||
|
||||
export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC' | 'PUBKEY'
|
||||
export type QRCodeBlockTypes = 'EncodedV3Token' | 'EncodedV4Token' | 'Bolt11Invoice' | 'URL' | 'NWC' | 'PUBKEY' | 'PaymentRequest'
|
||||
|
||||
const ANIMATED_QR_FRAGMENT_LENGTH = 150
|
||||
const ANIMATED_QR_INTERVAL = 250
|
||||
|
||||
@@ -1085,7 +1085,7 @@ const $screen: ViewStyle = {
|
||||
const $headerContainer: TextStyle = {
|
||||
alignItems: 'center',
|
||||
// padding: spacing.tiny,
|
||||
height: spacing.screenHeight * 0.28,
|
||||
height: spacing.screenHeight * 0.27,
|
||||
}
|
||||
|
||||
const $tabContainer: TextStyle = {
|
||||
|
||||
@@ -17,6 +17,7 @@ export * from './TransferScreen'
|
||||
export * from './TopupScreen'
|
||||
export * from './LightningPayScreen'
|
||||
export * from './TokenReceiveScreen'
|
||||
export * from './CashuPaymentRequestScreen'
|
||||
|
||||
// ContactsNavigator
|
||||
export * from './ContactsScreen'
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
Token,
|
||||
Proof as CashuProof,
|
||||
PaymentRequest as CashuPaymentRequest,
|
||||
PaymentRequestPayload,
|
||||
} from '@cashu/cashu-ts'
|
||||
import AppError, {Err} from '../../utils/AppError'
|
||||
import { getDecodedToken } from '@cashu/cashu-ts'
|
||||
@@ -28,7 +29,7 @@ const CASHU_TOKEN_PREFIXES = [
|
||||
'cashuB'
|
||||
]
|
||||
|
||||
const CASHU_REQUEST_PREFIXES = [
|
||||
const CASHU_PAYMENT_REQUEST_PREFIXES = [
|
||||
'creqA',
|
||||
]
|
||||
|
||||
@@ -41,11 +42,31 @@ const findEncodedCashuToken = function (content: string) {
|
||||
|
||||
const findEncodedCashuPaymentRequest = function (content: string) {
|
||||
const words = content.split(/\s+|\n+/) // Split text into words
|
||||
const maybeRequest = words.find(word => CASHU_REQUEST_PREFIXES.some(pref => word.includes(pref)))
|
||||
const maybeRequest = words.find(word => CASHU_PAYMENT_REQUEST_PREFIXES.some(pref => word.includes(pref)))
|
||||
return maybeRequest || null
|
||||
}
|
||||
|
||||
|
||||
const findEncodedCashuPaymentRequestPayload = function (content: string) {
|
||||
try {
|
||||
const decoded = JSON.parse(content)
|
||||
|
||||
if(decoded &&
|
||||
decoded.mint &&
|
||||
decoded.unit &&
|
||||
Array.isArray(decoded.proofs) &&
|
||||
decoded.proofs.length > 0) {
|
||||
return decoded as PaymentRequestPayload
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
} catch (e: any) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const extractEncodedCashuToken = function (maybeToken: string): string {
|
||||
|
||||
log.trace('[extractEncodedCashuToken] Extract token from', {maybeToken})
|
||||
@@ -84,7 +105,7 @@ const extractEncodedCashuPaymentRequest = function (maybeRequest: string): strin
|
||||
let encodedRequest: string | undefined = undefined
|
||||
let decoded: CashuPaymentRequest | undefined = undefined
|
||||
|
||||
if (maybeRequest && CASHU_REQUEST_PREFIXES.some(pref => maybeRequest.startsWith(pref))) {
|
||||
if (maybeRequest && CASHU_PAYMENT_REQUEST_PREFIXES.some(pref => maybeRequest.startsWith(pref))) {
|
||||
decoded = decodePaymentRequest(maybeRequest) // throws
|
||||
return maybeRequest
|
||||
}
|
||||
@@ -322,6 +343,7 @@ const isTokenP2PKLocked = function (token: Token) {
|
||||
export const CashuUtils = {
|
||||
findEncodedCashuToken,
|
||||
findEncodedCashuPaymentRequest,
|
||||
findEncodedCashuPaymentRequestPayload,
|
||||
extractEncodedCashuToken,
|
||||
extractEncodedCashuPaymentRequest,
|
||||
getProofsAmount,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { NostrClient } from './nostrService'
|
||||
export enum IncomingDataType {
|
||||
CASHU = 'CASHU',
|
||||
CASHU_PAYMENT_REQUEST = 'CASHU_PAYMENT_REQUEST',
|
||||
CASHU_PAYMENT_REQUEST_PAYLOAD = 'CASHU_PAYMENT_REQUEST_PAYLOAD',
|
||||
INVOICE = 'INVOICE',
|
||||
LNURL = 'LNURL',
|
||||
LNURL_ADDRESS = 'LNURL_ADDRESS',
|
||||
@@ -44,6 +45,12 @@ const findAndExtract = function (
|
||||
type: expectedType,
|
||||
encoded
|
||||
}
|
||||
case IncomingDataType.CASHU_PAYMENT_REQUEST_PAYLOAD:
|
||||
encoded = incomingData // stringified JSON payload
|
||||
return {
|
||||
type: expectedType,
|
||||
encoded
|
||||
}
|
||||
case IncomingDataType.INVOICE:
|
||||
encoded = LightningUtils.extractEncodedLightningInvoice(incomingData)
|
||||
return {
|
||||
@@ -115,6 +122,19 @@ const findAndExtract = function (
|
||||
}
|
||||
}
|
||||
|
||||
const maybeCashuPaymentRequestPayload = CashuUtils.findEncodedCashuPaymentRequestPayload(incomingData)
|
||||
|
||||
if(maybeCashuPaymentRequestPayload) {
|
||||
log.trace('Got maybeCashuPaymentRequestPayload', maybeCashuPaymentRequestPayload, 'findAndExtract')
|
||||
|
||||
const encoded = incomingData // to be aligned with other cases we pass encoded (stringified) JSON payload
|
||||
|
||||
return {
|
||||
type: IncomingDataType.CASHU_PAYMENT_REQUEST_PAYLOAD,
|
||||
encoded
|
||||
}
|
||||
}
|
||||
|
||||
const maybeInvoice = LightningUtils.findEncodedLightningInvoice(incomingData)
|
||||
|
||||
if(maybeInvoice) {
|
||||
|
||||
@@ -57,7 +57,13 @@ const extractEncodedLightningInvoice = function (maybeInvoice: string) {
|
||||
return encodedInvoice
|
||||
}
|
||||
|
||||
if (maybeInvoice && maybeInvoice.toLowerCase().startsWith('bitcoin:' || 'http')) {
|
||||
if (
|
||||
maybeInvoice &&
|
||||
(
|
||||
maybeInvoice.toLowerCase().startsWith('bitcoin:') ||
|
||||
maybeInvoice.toLowerCase().startsWith('http')
|
||||
)
|
||||
) {
|
||||
const url = new URL(maybeInvoice.toLowerCase())
|
||||
// Use URLSearchParams to get the value of the "lightning" parameter
|
||||
encodedInvoice = url.searchParams.get("lightning") as string
|
||||
|
||||
@@ -8,7 +8,7 @@ import { finalizeEvent, validateEvent } from 'nostr-tools/pure'
|
||||
import { normalizeURL } from 'nostr-tools/utils'
|
||||
import { encrypt, decrypt } from 'nostr-tools/nip04'
|
||||
import { wrapEvent, unwrapEvent } from 'nostr-tools/nip59'
|
||||
import { neventEncode as nostrNeventEncode, npubEncode, decode as nip19Decode } from 'nostr-tools/nip19'
|
||||
import { neventEncode as nostrNeventEncode, npubEncode, decode as nip19Decode, nprofileEncode } from 'nostr-tools/nip19'
|
||||
import {SimplePool} from 'nostr-tools/pool'
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils'
|
||||
import { PrivateDirectMessage, Metadata } from 'nostr-tools/kinds'
|
||||
@@ -158,6 +158,21 @@ const decodeNprofile = function (nprofile: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const encodeNprofile = function (pubkey: string, relays: string[] = []): string {
|
||||
try {
|
||||
const encoded = nprofileEncode({
|
||||
pubkey,
|
||||
relays
|
||||
})
|
||||
|
||||
log.trace('[encodeNprofile]', {encoded})
|
||||
|
||||
return encoded
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
/* const maybeConvertNpub = function (key: string) {
|
||||
// Check and convert npub to P2PK
|
||||
if (key && key.startsWith("npub1")) {
|
||||
@@ -562,6 +577,7 @@ export const NostrClient = { // TODO split helper functions to separate module
|
||||
getHexkey,
|
||||
neventEncode,
|
||||
decodeNprofile,
|
||||
encodeNprofile,
|
||||
// maybeConvertNpub,
|
||||
encryptNip04,
|
||||
decryptNip04,
|
||||
|
||||
+59
-1
@@ -15,7 +15,7 @@ import {BackupProof} from '../models/Proof'
|
||||
|
||||
let _db: QuickSQLiteConnection
|
||||
|
||||
const _dbVersion = 19 // Update this if db changes require migrations
|
||||
const _dbVersion = 20 // Update this if db changes require migrations
|
||||
|
||||
const getInstance = function () {
|
||||
if (!_db) {
|
||||
@@ -47,6 +47,7 @@ const _createOrUpdateSchema = function (db: QuickSQLiteConnection) {
|
||||
[
|
||||
`CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
paymentId TEXT,
|
||||
type TEXT,
|
||||
amount INTEGER,
|
||||
unit TEXT,
|
||||
@@ -133,6 +134,15 @@ const _runMigrations = function (db: QuickSQLiteConnection) {
|
||||
log.info(`Prepared database migrations from ${currentVersion} -> 19`)
|
||||
}
|
||||
|
||||
if (currentVersion < 20) {
|
||||
migrationQueries.push([
|
||||
`ALTER TABLE transactions
|
||||
ADD COLUMN paymentId TEXT`,
|
||||
])
|
||||
|
||||
log.info(`Prepared database migrations from ${currentVersion} -> 20`)
|
||||
}
|
||||
|
||||
// Update db version as a part of migration sqls
|
||||
migrationQueries.push([
|
||||
`INSERT OR REPLACE INTO dbversion (id, version, createdAt)
|
||||
@@ -370,6 +380,25 @@ const getTransactionById = function (id: number) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const getTransactionByPaymentId = function (id: string) {
|
||||
try {
|
||||
const query = `
|
||||
SELECT * FROM transactions WHERE paymentId = ?
|
||||
`
|
||||
|
||||
const params = [id]
|
||||
|
||||
const db = getInstance()
|
||||
const {rows} = db.execute(query, params)
|
||||
|
||||
return rows?.item(0) as TransactionRecord
|
||||
} catch (e: any) {
|
||||
throw new AppError(Err.DATABASE_ERROR, 'Transaction not found', e.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const addTransactionAsync = async function (tx: Transaction): Promise<TransactionRecord> {
|
||||
try {
|
||||
const {type, amount, fee, unit, data, memo, mint, status} = tx
|
||||
@@ -522,6 +551,33 @@ const updateBalanceAfter = function (
|
||||
}
|
||||
|
||||
|
||||
const updatePaymentId = function (id: number, paymentId: string) {
|
||||
try {
|
||||
const query = `
|
||||
UPDATE transactions
|
||||
SET paymentId = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
const params = [paymentId, id]
|
||||
|
||||
const db = getInstance()
|
||||
db.execute(query, params)
|
||||
|
||||
log.debug('[updatePaymentId]', 'Transaction paymentId updated in the database', {id, paymentId})
|
||||
|
||||
const updatedTx = getTransactionById(id as number)
|
||||
|
||||
return updatedTx as TransactionRecord
|
||||
} catch (e: any) {
|
||||
throw new AppError(
|
||||
Err.DATABASE_ERROR,
|
||||
'Could not update transaction paymentId in the database',
|
||||
e.message,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const updateFee = function (id: number, fee: number) {
|
||||
try {
|
||||
const query = `
|
||||
@@ -1132,6 +1188,7 @@ export const Database = {
|
||||
cleanAll,
|
||||
getTransactionsCount,
|
||||
getTransactionById,
|
||||
getTransactionByPaymentId,
|
||||
getRecentTransactionsByUnit,
|
||||
getTransactions,
|
||||
addTransactionAsync,
|
||||
@@ -1140,6 +1197,7 @@ export const Database = {
|
||||
updateStatusesAsync,
|
||||
updateBalanceAfter,
|
||||
updateFee,
|
||||
updatePaymentId,
|
||||
updateReceivedAmount,
|
||||
updateNote,
|
||||
updateSentFrom,
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import {rootStoreInstance} from '../../models'
|
||||
import { TransactionTaskResult, WalletTask } from '../walletService'
|
||||
import { MintBalance } from '../../models/Mint'
|
||||
import { poller } from '../../utils/poller'
|
||||
import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../../models/Transaction'
|
||||
import { log } from '../logService'
|
||||
import { Contact } from '../../models/Contact'
|
||||
import { LightningUtils } from '../lightning/lightningUtils'
|
||||
import { getSnapshot, isStateTreeNode } from 'mobx-state-tree'
|
||||
import { WalletUtils } from './utils'
|
||||
import { MintUnit } from './currency'
|
||||
import { NostrClient, NostrEvent } from '../nostrService'
|
||||
import { Err } from '../../utils/AppError'
|
||||
import {
|
||||
CashuMint,
|
||||
CashuWallet,
|
||||
MintQuoteResponse,
|
||||
PaymentRequest as CashuPaymentRequest,
|
||||
PaymentRequestTransport,
|
||||
PaymentRequestTransportType
|
||||
} from '@cashu/cashu-ts'
|
||||
import QuickCrypto from 'react-native-quick-crypto'
|
||||
|
||||
const {
|
||||
transactionsStore,
|
||||
walletProfileStore,
|
||||
paymentRequestsStore,
|
||||
walletStore,
|
||||
relaysStore
|
||||
} = rootStoreInstance
|
||||
|
||||
// const {walletStore} = nonPersistedStores
|
||||
|
||||
export const CASHU_PAYMENT_REQUEST_TASK = 'cashuPaymentRequestTask'
|
||||
|
||||
export const cashuPaymentRequestTask = async function (
|
||||
mintBalanceToReceiveTo: MintBalance,
|
||||
amountToReceive: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
) : Promise<TransactionTaskResult> {
|
||||
log.info('[cashuPaymentRequestTask]', {mintBalanceToReceiveTo})
|
||||
log.info('[cashuPaymentRequestTask]', {amountToReceive, unit})
|
||||
|
||||
// create draft transaction
|
||||
const transactionData: TransactionData[] = [
|
||||
{
|
||||
status: TransactionStatus.DRAFT,
|
||||
amountToReceive,
|
||||
unit,
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]
|
||||
|
||||
let transaction: Transaction | undefined = undefined
|
||||
const mintUrl = mintBalanceToReceiveTo.mintUrl
|
||||
|
||||
try {
|
||||
const newTransaction = {
|
||||
type: TransactionType.RECEIVE_BY_PAYMENT_REQUEST,
|
||||
amount: amountToReceive,
|
||||
fee: 0,
|
||||
unit,
|
||||
data: JSON.stringify(transactionData),
|
||||
memo,
|
||||
mint: mintUrl,
|
||||
status: TransactionStatus.DRAFT,
|
||||
}
|
||||
// store tx in db and in the model
|
||||
transaction = await transactionsStore.addTransaction(newTransaction)
|
||||
|
||||
const tags = [["n", "17"]]
|
||||
const transport = [
|
||||
{
|
||||
type: PaymentRequestTransportType.NOSTR,
|
||||
target: NostrClient.encodeNprofile(walletProfileStore.pubkey, relaysStore.allUrls),
|
||||
tags,
|
||||
},
|
||||
] as PaymentRequestTransport[]
|
||||
|
||||
const cashuPrId = QuickCrypto.randomBytes(4).toString("hex")
|
||||
const cashuPaymentRequest = new CashuPaymentRequest(
|
||||
transport,
|
||||
cashuPrId,
|
||||
amountToReceive,
|
||||
unit,
|
||||
[mintUrl],
|
||||
memo
|
||||
)
|
||||
|
||||
const encoded = cashuPaymentRequest.toEncodedRequest()
|
||||
|
||||
log.trace("[cashuPaymentRequestTask] Creating cashu payment request", {cashuPaymentRequest, encoded})
|
||||
|
||||
transactionData.push({
|
||||
status: TransactionStatus.PENDING,
|
||||
cashuPaymentRequest,
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.PENDING,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
transaction.setPaymentId(cashuPrId)
|
||||
|
||||
return {
|
||||
taskFunction: CASHU_PAYMENT_REQUEST_TASK,
|
||||
mintUrl,
|
||||
transaction,
|
||||
message: '',
|
||||
cashuPaymentRequest,
|
||||
encodedCashuPaymentRequest: encoded,
|
||||
} as TransactionTaskResult
|
||||
|
||||
} catch (e: any) {
|
||||
|
||||
if (transaction) {
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(e),
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
|
||||
log.error(e.name, e.message)
|
||||
|
||||
return {
|
||||
taskFunction: CASHU_PAYMENT_REQUEST_TASK,
|
||||
transaction,
|
||||
message: e.message,
|
||||
error: WalletUtils.formatError(e),
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { DEFAULT_DENOMINATION_TARGET, TransactionTaskResult } from '../walletSer
|
||||
import { WalletUtils } from './utils'
|
||||
import { MintUnit, formatCurrency, getCurrency } from './currency'
|
||||
import { MintProofsCounter } from '../../models/Mint'
|
||||
import { Token, getDecodedToken } from '@cashu/cashu-ts'
|
||||
import { PaymentRequestPayload, Token, getDecodedToken } from '@cashu/cashu-ts'
|
||||
import { getEncodedToken, getKeepAmounts } from '@cashu/cashu-ts/src/utils'
|
||||
|
||||
const {
|
||||
@@ -27,6 +27,7 @@ const {
|
||||
export const RECEIVE_TASK = 'receiveTask'
|
||||
export const RECEIVE_OFFLINE_PREPARE_TASK = 'receiveOfflinePrepareTask'
|
||||
export const RECEIVE_OFFLINE_COMPLETE_TASK = 'receiveOfflineCompleteTask'
|
||||
export const RECEIVE_BY_CASHU_PAYMENT_REQUEST_TASK = 'receiveByCashuPaymentRequestTask'
|
||||
|
||||
export const receiveTask = async function (
|
||||
token: Token,
|
||||
@@ -274,6 +275,9 @@ export const receiveOfflinePrepareTask = async function (
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export const receiveOfflineCompleteTask = async function (
|
||||
transactionId: number
|
||||
) {
|
||||
@@ -390,9 +394,152 @@ export const receiveOfflineCompleteTask = async function (
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const receiveByCashuPaymentRequestTask = async function (
|
||||
paymentRequestPayload: PaymentRequestPayload,
|
||||
): Promise<TransactionTaskResult> {
|
||||
|
||||
const transactionData: TransactionData[] = []
|
||||
let transaction: Transaction | undefined = undefined
|
||||
const unit = paymentRequestPayload.unit as MintUnit
|
||||
const mintToReceive = paymentRequestPayload.mint
|
||||
const proofsToReceive = paymentRequestPayload.proofs
|
||||
const paymentRequestId = paymentRequestPayload.id
|
||||
|
||||
try {
|
||||
if (!mintToReceive || !unit || !Array.isArray(proofsToReceive) || proofsToReceive.length === 0) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Payment request payload is invalid.',
|
||||
{paymentRequestPayload}
|
||||
)
|
||||
}
|
||||
|
||||
// Let's find transaction with related payment request
|
||||
const transaction = transactionsStore.findByPaymentId(paymentRequestId)
|
||||
|
||||
if(!transaction) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Related Payment request could not be found in the wallet.',
|
||||
{paymentRequestPayload}
|
||||
)
|
||||
}
|
||||
|
||||
const amountToReceive = CashuUtils.getProofsAmount(proofsToReceive)
|
||||
const memo = paymentRequestPayload.memo || 'PR ' + paymentRequestId
|
||||
|
||||
if(transaction.unit !== unit || transaction.amount !== amountToReceive) {
|
||||
throw new AppError(
|
||||
Err.VALIDATION_ERROR,
|
||||
'Related Payment request has different amount or unit than the incoming payment.',
|
||||
{paymentRequestPayload, expctedUnit: transaction.unit, expectedAmount: transaction.amount}
|
||||
)
|
||||
}
|
||||
|
||||
// Handle blocked mint
|
||||
const isBlocked = mintsStore.isBlocked(mintToReceive)
|
||||
|
||||
if (isBlocked) {
|
||||
transactionData.push({
|
||||
status: TransactionStatus.BLOCKED,
|
||||
message: 'Mint is blocked in your Settings, ecash has not been received.',
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.BLOCKED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_BY_CASHU_PAYMENT_REQUEST_TASK,
|
||||
transaction,
|
||||
message: `The mint ${mintToReceive} is blocked. You can unblock it in Settings.`,
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
|
||||
// TODO rework to PR payload
|
||||
const {
|
||||
receivedAmount,
|
||||
receivedProofs,
|
||||
outputToken,
|
||||
swapFeePaid,
|
||||
} = await receiveSync(
|
||||
mintToReceive,
|
||||
paymentRequestPayload,
|
||||
memo,
|
||||
transaction.id
|
||||
)
|
||||
|
||||
|
||||
// Update tx amount if full amount was not received
|
||||
if (receivedAmount !== amountToReceive) {
|
||||
transaction.setReceivedAmount(receivedAmount)
|
||||
}
|
||||
|
||||
// Finally, update completed transaction
|
||||
transactionData.push({
|
||||
status: TransactionStatus.COMPLETED,
|
||||
swapFeePaid,
|
||||
receivedAmount,
|
||||
unit,
|
||||
createdAt: new Date(),
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.COMPLETED,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
if(swapFeePaid > 0) {
|
||||
transaction.setFee(swapFeePaid)
|
||||
}
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_BY_CASHU_PAYMENT_REQUEST_TASK,
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: `Payment request ${paymentRequestId} with amount of ${formatCurrency(receivedAmount, getCurrency(unit).code)} ${getCurrency(unit).code} has been paid.`,
|
||||
receivedAmount,
|
||||
receivedProofsCount: receivedProofs.length
|
||||
} as TransactionTaskResult
|
||||
|
||||
} catch (e: any) {
|
||||
if (transaction) {
|
||||
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(e),
|
||||
errorToken: e.params?.errorToken || undefined
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
|
||||
log.error(e.name, e.message)
|
||||
|
||||
return {
|
||||
taskFunction: RECEIVE_BY_CASHU_PAYMENT_REQUEST_TASK,
|
||||
mintUrl: mintToReceive,
|
||||
transaction,
|
||||
message: e.message,
|
||||
error: WalletUtils.formatError(e),
|
||||
} as TransactionTaskResult
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const receiveSync = async function (
|
||||
mintToReceive: string,
|
||||
token: Token,
|
||||
token: Token | PaymentRequestPayload,
|
||||
memo: string,
|
||||
transactionId: number
|
||||
) {
|
||||
|
||||
@@ -133,11 +133,13 @@ export const topupTask = async function (
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
transaction.setStatus(
|
||||
TransactionStatus.PENDING,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
|
||||
transaction.setPaymentId(payment_hash)
|
||||
|
||||
if(!nwcEvent) {
|
||||
const wsMint = new CashuMint(mintUrl)
|
||||
const wsWallet = new CashuWallet(wsMint)
|
||||
|
||||
@@ -11,6 +11,7 @@ import { WalletUtils } from './utils'
|
||||
import {isBefore} from 'date-fns'
|
||||
import { MintUnit, formatCurrency, getCurrency } from './currency'
|
||||
import { NostrEvent } from '../nostrService'
|
||||
import { LightningUtils } from '../lightning/lightningUtils'
|
||||
|
||||
const {
|
||||
transactionsStore,
|
||||
@@ -77,6 +78,8 @@ export const transferTask = async function (
|
||||
// store tx in db and in the model
|
||||
transaction = await transactionsStore.addTransaction(newTransaction)
|
||||
const transactionId = transaction.id
|
||||
const paymentHash = LightningUtils.getInvoiceData(LightningUtils.decodeInvoice(encodedInvoice)).payment_hash
|
||||
transaction.setPaymentId(paymentHash)
|
||||
|
||||
if (amountToTransfer + meltQuote.fee_reserve > mintBalanceToTransferFrom.balances[unit]!) {
|
||||
throw new AppError(
|
||||
@@ -197,8 +200,6 @@ export const transferTask = async function (
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (meltResponse.quote.state === MeltQuoteState.PAID) {
|
||||
|
||||
@@ -219,7 +220,8 @@ export const transferTask = async function (
|
||||
let meltFeePaid = meltFeeReserve
|
||||
let returnedAmount = CashuUtils.getProofsAmount(meltResponse.change)
|
||||
|
||||
if(meltResponse.change.length > 0) {
|
||||
if(meltResponse.change.length > 0) {
|
||||
|
||||
WalletUtils.addCashuProofs(
|
||||
mintUrl,
|
||||
meltResponse.change,
|
||||
@@ -240,7 +242,7 @@ export const transferTask = async function (
|
||||
|
||||
totalFeePaid = totalFeePaid - returnedAmount
|
||||
lightningFeePaid = totalFeePaid - meltFeeReserve
|
||||
}
|
||||
}
|
||||
|
||||
// Save final fee in db
|
||||
if(totalFeePaid !== transaction.fee) {
|
||||
@@ -263,7 +265,7 @@ export const transferTask = async function (
|
||||
)
|
||||
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
|
||||
return {
|
||||
taskFunction: TRANSFER_TASK,
|
||||
|
||||
+243
-20
@@ -15,7 +15,7 @@ import {CashuProof, CashuUtils} from './cashu/cashuUtils'
|
||||
import {LightningUtils} from './lightning/lightningUtils'
|
||||
import AppError, {Err} from '../utils/AppError'
|
||||
import {MintBalance, MintStatus} from '../models/Mint'
|
||||
import {MeltQuoteResponse, MintQuoteState, Token, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {MeltQuoteResponse, MeltQuoteState, MintQuoteState, PaymentRequestPayload, Token, getDecodedToken, getEncodedToken} from '@cashu/cashu-ts'
|
||||
import {Mint} from '../models/Mint'
|
||||
import {pollerExists, stopPolling} from '../utils/poller'
|
||||
import { NostrClient, NostrEvent, NostrProfile } from './nostrService'
|
||||
@@ -24,7 +24,12 @@ import { PaymentRequest, PaymentRequestStatus, PaymentRequestType } from '../mod
|
||||
import { IncomingDataType, IncomingParser } from './incomingParser'
|
||||
import { Contact } from '../models/Contact'
|
||||
import { SyncQueue } from './syncQueueService'
|
||||
import { receiveTask, receiveOfflinePrepareTask, receiveOfflineCompleteTask} from './wallet/receiveTask'
|
||||
import {
|
||||
receiveTask,
|
||||
receiveOfflinePrepareTask,
|
||||
receiveOfflineCompleteTask,
|
||||
receiveByCashuPaymentRequestTask
|
||||
} from './wallet/receiveTask'
|
||||
import { sendTask } from './wallet/sendTask'
|
||||
import { topupTask } from './wallet/topupTask'
|
||||
import { transferTask } from './wallet/transferTask'
|
||||
@@ -33,9 +38,10 @@ import { WalletUtils } from './wallet/utils'
|
||||
import { NotificationService } from './notificationService'
|
||||
import { CurrencyCode, MintUnit, formatCurrency, getCurrency } from './wallet/currency'
|
||||
import { MinibitsClient } from './minibitsService'
|
||||
import { KeyChain } from './keyChain'
|
||||
import { UnsignedEvent } from 'nostr-tools'
|
||||
import { Platform } from 'react-native'
|
||||
import { cashuPaymentRequestTask } from './wallet/cashuPaymentRequestTask'
|
||||
import { sumBlindSignatures } from '@cashu/cashu-ts/src/utils'
|
||||
|
||||
/**
|
||||
* The default number of proofs per denomination to keep in a wallet.
|
||||
@@ -127,6 +133,12 @@ type WalletTaskService = {
|
||||
contactToSendTo?: Contact,
|
||||
nwcEvent?: NostrEvent
|
||||
) => Promise<void>
|
||||
cashuPaymentRequestQueue: (
|
||||
mintBalanceToReceiveTo: MintBalance,
|
||||
amountToRequest: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
) => Promise<void>
|
||||
revertQueue: (
|
||||
transaction: Transaction
|
||||
) => Promise<void>
|
||||
@@ -135,6 +147,10 @@ type WalletTaskService = {
|
||||
mintUrl: string,
|
||||
mintQuote: string
|
||||
}) => Promise<{recoveredAmount: number}>
|
||||
recoverMeltQuoteChange: (params: {
|
||||
mintUrl: string,
|
||||
meltQuote: string
|
||||
}) => Promise<{recoveredAmount: number}>
|
||||
}
|
||||
|
||||
export interface WalletTaskResult {
|
||||
@@ -542,6 +558,26 @@ const topupQueue = async function (
|
||||
}
|
||||
|
||||
|
||||
const cashuPaymentRequestQueue = async function (
|
||||
mintBalanceToReceiveTo: MintBalance,
|
||||
amountToRequest: number,
|
||||
unit: MintUnit,
|
||||
memo: string,
|
||||
): Promise<void> {
|
||||
const now = new Date().getTime()
|
||||
SyncQueue.addPrioritizedTask(
|
||||
`cashuPaymentRequestTask-${now}`,
|
||||
async () => await cashuPaymentRequestTask(
|
||||
mintBalanceToReceiveTo,
|
||||
amountToRequest,
|
||||
unit,
|
||||
memo,
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
const revertQueue = async function (
|
||||
transaction: Transaction
|
||||
@@ -1719,7 +1755,7 @@ const recoverMintQuote = async function (params: {mintUrl: string, mintQuote: st
|
||||
|
||||
switch (state) {
|
||||
case MintQuoteState.UNPAID:
|
||||
log.trace('[handlePendingTopupTask] Quote not paid', {mintUrl, mintQuote})
|
||||
log.trace('[recoverMintQuote] Quote not paid', {mintUrl, mintQuote})
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${mintQuote} is not paid`)
|
||||
case MintQuoteState.PAID:
|
||||
const invoice = LightningUtils.decodeInvoice(encodedInvoice)
|
||||
@@ -1761,7 +1797,7 @@ const recoverMintQuote = async function (params: {mintUrl: string, mintQuote: st
|
||||
} catch (e: any) {
|
||||
if(e.message.includes('outputs have already been signed before')) {
|
||||
|
||||
log.error('[handlePendingTopupTask] Increasing proofsCounter outdated values and repeating mintProofs.')
|
||||
log.error('[recoverMintQuote] Increasing proofsCounter outdated values and repeating mintProofs.')
|
||||
|
||||
proofs = (await walletStore.mintProofs(
|
||||
mintUrl as string,
|
||||
@@ -1795,14 +1831,14 @@ const recoverMintQuote = async function (params: {mintUrl: string, mintQuote: st
|
||||
|
||||
// update related tx
|
||||
const transactionDataUpdate = {
|
||||
status: TransactionStatus.COMPLETED,
|
||||
status: TransactionStatus.RECOVERED,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
// await for final status
|
||||
await transactionsStore.updateStatuses(
|
||||
[transactionId],
|
||||
TransactionStatus.COMPLETED,
|
||||
TransactionStatus.RECOVERED,
|
||||
JSON.stringify(transactionDataUpdate),
|
||||
)
|
||||
|
||||
@@ -1816,20 +1852,177 @@ const recoverMintQuote = async function (params: {mintUrl: string, mintQuote: st
|
||||
* ISSUED
|
||||
*/
|
||||
case MintQuoteState.ISSUED:
|
||||
log.trace('[handlePendingTopupTask] Quote already issued', {mintUrl, mintQuote})
|
||||
log.trace('[recoverMintQuote] Quote already issued', {mintUrl, mintQuote})
|
||||
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${mintQuote} already issued.`)
|
||||
/*
|
||||
* UNKNOWN
|
||||
*/
|
||||
default:
|
||||
log.error(`[handlePendingTopupTask] Unknown MintQuoteState`, {state})
|
||||
log.error(`[recoverMintQuote] Unknown MintQuoteState`, {state})
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${mintQuote} has unknown state ${state}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const recoverMeltQuoteChange = async function (params: {mintUrl: string, meltQuote: string}): Promise<{recoveredAmount: number}> {
|
||||
const {mintUrl, meltQuote} = params
|
||||
const mintInstance = mintsStore.findByUrl(mintUrl as string)
|
||||
const unit = 'sat'
|
||||
|
||||
if(!mintInstance || !meltQuote) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Missing mint or melt quote', {mintUrl})
|
||||
}
|
||||
|
||||
// check is quote has been paid
|
||||
const meltQuoteResponse: MeltQuoteResponse = await walletStore.checkLightningMeltQuote(mintUrl, meltQuote)
|
||||
const {quote, state} = meltQuoteResponse
|
||||
const amountToRecover = sumBlindSignatures(meltQuoteResponse.change)
|
||||
|
||||
if (quote !== meltQuote) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Returned quote is different then the one requested', {mintUrl, meltQuoteResponse, meltQuote})
|
||||
}
|
||||
|
||||
switch (state) {
|
||||
/*
|
||||
* UNPAID
|
||||
*/
|
||||
case MeltQuoteState.UNPAID:
|
||||
log.trace('[recoverMeltQuoteChange] Quote not paid', {mintUrl, meltQuote})
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${meltQuote} is not paid`)
|
||||
/*
|
||||
* PENDING
|
||||
*/
|
||||
case MeltQuoteState.PENDING:
|
||||
log.trace('[recoverMeltQuoteChange] Quote is pending', {mintUrl, meltQuote})
|
||||
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${meltQuote} is still pending.`)
|
||||
/*
|
||||
* PAID
|
||||
*/
|
||||
case MeltQuoteState.PAID:
|
||||
|
||||
if(!meltQuoteResponse.change || meltQuoteResponse.change.length === 0) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${meltQuote} has not any change to recover.`)
|
||||
}
|
||||
|
||||
// Not sure how to find related transfer transaction, so create new one
|
||||
// TODO extent transaction model to store melt quote so we can search by it
|
||||
const transactionData: TransactionData[] = [
|
||||
{
|
||||
status: TransactionStatus.DRAFT,
|
||||
amountToRecover,
|
||||
unit,
|
||||
meltQuoteToRecover: quote,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
]
|
||||
|
||||
const newTransaction = {
|
||||
type: TransactionType.RECEIVE,
|
||||
amount: amountToRecover,
|
||||
fee: 0,
|
||||
unit,
|
||||
data: JSON.stringify(transactionData),
|
||||
memo: 'Melt quote change recovery',
|
||||
mint: mintUrl,
|
||||
status: TransactionStatus.DRAFT,
|
||||
}
|
||||
// store tx in db and in the model
|
||||
const transaction = await transactionsStore.addTransaction(newTransaction)
|
||||
const transactionId = transaction.id
|
||||
|
||||
try {
|
||||
|
||||
const change = await walletStore.recoverMeltQuoteChange(
|
||||
mintUrl as string,
|
||||
meltQuoteResponse
|
||||
)
|
||||
|
||||
// Force swap with the mint to make sure that change proofs are valid
|
||||
const {proofsToSend, returnedProofs} = await walletStore.send(
|
||||
mintUrl as string,
|
||||
0,
|
||||
unit,
|
||||
change as Proof[],
|
||||
transactionId,
|
||||
{
|
||||
increaseCounterBy: change.length, //if we missed to receive the change before, counter might be outdated
|
||||
p2pk: undefined,
|
||||
}
|
||||
)
|
||||
|
||||
log.debug('[recoverMeltQuoteChange] Swapped proofs', {proofsToSend, returnedProofs})
|
||||
|
||||
|
||||
if (!returnedProofs || returnedProofs.length === 0) {
|
||||
throw new AppError(Err.VALIDATION_ERROR, 'Mint did not return any proofs.')
|
||||
}
|
||||
|
||||
const {addedAmount: recoveredAmount} = WalletUtils.addCashuProofs(
|
||||
mintUrl as string,
|
||||
returnedProofs,
|
||||
{
|
||||
unit,
|
||||
transactionId,
|
||||
isPending: false
|
||||
}
|
||||
)
|
||||
|
||||
if(amountToRecover !== recoveredAmount) {
|
||||
transaction.setReceivedAmount(recoveredAmount)
|
||||
}
|
||||
|
||||
// update related tx
|
||||
const transactionDataUpdate = {
|
||||
status: TransactionStatus.RECOVERED,
|
||||
recoveredAmount,
|
||||
createdAt: new Date(),
|
||||
}
|
||||
|
||||
// await for final status
|
||||
await transactionsStore.updateStatuses(
|
||||
[transactionId],
|
||||
TransactionStatus.RECOVERED,
|
||||
JSON.stringify(transactionDataUpdate),
|
||||
)
|
||||
|
||||
// Update tx with current total balance of topup unit/currency
|
||||
const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance!
|
||||
const outputToken = getEncodedToken({
|
||||
mint: mintUrl,
|
||||
proofs: returnedProofs,
|
||||
unit,
|
||||
})
|
||||
transaction.setBalanceAfter(balanceAfter)
|
||||
transaction.setOutputToken(outputToken)
|
||||
|
||||
return { recoveredAmount }
|
||||
|
||||
} catch (e: any) {
|
||||
transactionData.push({
|
||||
status: TransactionStatus.ERROR,
|
||||
error: WalletUtils.formatError(e),
|
||||
createdAt: new Date()
|
||||
})
|
||||
|
||||
transaction.setStatus(
|
||||
TransactionStatus.ERROR,
|
||||
JSON.stringify(transactionData),
|
||||
)
|
||||
}
|
||||
/*
|
||||
* UNKNOWN
|
||||
*/
|
||||
default:
|
||||
log.error(`[recoverMeltQuoteChange] Unknown MeltQuoteState`, {state: meltQuoteResponse.state})
|
||||
throw new AppError(Err.VALIDATION_ERROR, `Quote ${meltQuote} has unknown state ${meltQuoteResponse.state}`)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
const handleClaimQueue = async function (): Promise<void> {
|
||||
|
||||
log.info('[handleClaimQueue] start')
|
||||
@@ -2089,7 +2282,7 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
|
||||
// this is not valid for events sent from LNURL bridge, that are sent and signed by a minibits server key
|
||||
// and *** do not contain sentFrom *** // LEGACY, replaced by claim api
|
||||
let sentFromPubkey = directMessageEvent.pubkey
|
||||
let sentFrom = NostrClient.getFirstTagValue(directMessageEvent.tags, 'from')
|
||||
let sentFrom = NostrClient.getFirstTagValue(directMessageEvent.tags, 'from') as string
|
||||
let sentFromNpub = NostrClient.getNpubkey(sentFromPubkey)
|
||||
let contactFrom: Contact | undefined = undefined
|
||||
let zapSenderProfile: NostrProfile | undefined = undefined
|
||||
@@ -2195,12 +2388,7 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
|
||||
message: 'Incoming ecash token has been received.',
|
||||
proofsCount: decoded.proofs.length,
|
||||
proofsAmount: receivedAmount,
|
||||
notificationInputs: {
|
||||
event: directMessageEvent,
|
||||
decrypted: decryptedMessage,
|
||||
transaction,
|
||||
receivedAmount
|
||||
} // not used`
|
||||
transaction
|
||||
} as WalletTaskResult
|
||||
}
|
||||
|
||||
@@ -2259,13 +2447,46 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro
|
||||
paymentRequest
|
||||
} as WalletTaskResult
|
||||
}
|
||||
else if(incoming.type === IncomingDataType.CASHU_PAYMENT_REQUEST) {
|
||||
throw new AppError(Err.NOTFOUND_ERROR, 'CASHU_PAYMENT_REQUEST support is not yet implemented.', {caller: HANDLE_RECEIVED_EVENT_TASK})
|
||||
|
||||
else if(incoming.type === IncomingDataType.CASHU_PAYMENT_REQUEST_PAYLOAD) {
|
||||
const decoded: PaymentRequestPayload = JSON.parse(incoming.encoded)
|
||||
log.trace('[handleReceivedEventTask]', 'Decoded payment request payload', {decoded})
|
||||
|
||||
const {transaction, receivedAmount, message} = await receiveByCashuPaymentRequestTask(
|
||||
decoded
|
||||
)
|
||||
|
||||
// store contact or zapseder in tx details
|
||||
if(transaction && sentFrom) {
|
||||
if (contactFrom) {
|
||||
transaction.setProfile(JSON.stringify(contactFrom))
|
||||
transaction.setSentFrom(sentFrom)
|
||||
}
|
||||
|
||||
// We do it defensively only after cash is received
|
||||
// and asynchronously so we speed up queue
|
||||
_sendReceiveNotification(
|
||||
receivedAmount,
|
||||
transaction.unit,
|
||||
false,
|
||||
sentFrom,
|
||||
sentFromPicture
|
||||
) // TODO move to task result handler
|
||||
}
|
||||
|
||||
return {
|
||||
mintUrl: decoded.mint,
|
||||
taskFunction: HANDLE_RECEIVED_EVENT_TASK,
|
||||
message,
|
||||
proofsCount: decoded.proofs.length,
|
||||
proofsAmount: receivedAmount,
|
||||
transaction,
|
||||
} as WalletTaskResult
|
||||
}
|
||||
else if (incoming.type === IncomingDataType.LNURL) {
|
||||
throw new AppError(Err.NOTFOUND_ERROR, 'LNURL support is not yet implemented.', {caller: HANDLE_RECEIVED_EVENT_TASK})
|
||||
} else {
|
||||
throw new AppError(Err.NOTFOUND_ERROR, 'Received unknown message', incoming)
|
||||
throw new AppError(Err.NOTFOUND_ERROR, 'Received unknown event message', {caller: HANDLE_RECEIVED_EVENT_TASK,incoming})
|
||||
}
|
||||
} catch (e: any) {
|
||||
log.error('[handleReceivedEventTask]', e.name, e.message)
|
||||
@@ -2364,7 +2585,9 @@ export const WalletTask: WalletTaskService = {
|
||||
swapAllQueue,
|
||||
transferQueue,
|
||||
topupQueue,
|
||||
cashuPaymentRequestQueue,
|
||||
revertQueue,
|
||||
testQueue,
|
||||
recoverMintQuote
|
||||
recoverMintQuote,
|
||||
recoverMeltQuoteChange
|
||||
}
|
||||
Reference in New Issue
Block a user