NFC working prototype + stub for awaitable wallet tasks

This commit is contained in:
minibits-cash
2025-12-12 23:02:06 +01:00
parent 52c1d7d07e
commit 463e093cdf
9 changed files with 1098 additions and 426 deletions
+1 -1
View File
@@ -21,7 +21,7 @@
"machine-translate": "npx @inlang/cli machine translate --project minibits.inlang"
},
"dependencies": {
"@cashu/cashu-ts": "minibits-cash/cashu-ts#c85d4638c007ac8126bc83a7287a6d848fcc8153",
"@cashu/cashu-ts": "minibits-cash/cashu-ts#e97b1a4ee9a7ddbb1468302c6996a9d6d3efcb6f",
"@fortawesome/fontawesome-svg-core": "^7.1.0",
"@fortawesome/free-brands-svg-icons": "^7.1.0",
"@fortawesome/free-regular-svg-icons": "^7.1.0",
+210 -393
View File
@@ -47,6 +47,7 @@ import Animated, {
useAnimatedStyle,
Easing,
} from 'react-native-reanimated';
import { NfcService } from '../services/nfcService'
//import Animated from 'react-native-reanimated'
const ContactlessIcon = (color: ColorValue | string) => `<?xml version="1.0" encoding="utf-8"?>
@@ -113,6 +114,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const navigation = useNavigation<any>()
const { mintsStore, walletStore, proofsStore } = useStores()
const unitRef = useRef<MintUnit>('sat')
const isInternetReachable = useIsInternetReachable()
const [encodedTokenToSend, setEncodedTokenToSend] = useState<string | undefined>()
@@ -132,8 +134,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
const [resultModalInfo, setResultModalInfo] = useState<{status: TransactionStatus, title?: string, message: string} | undefined>()
const [isLoading, setIsLoading] = useState(false)
const [isSendTaskSentToQueue, setIsSendTaskSentToQueue] = useState(false)
const [isTransferTaskSentToQueue, setIsTransferTaskSentToQueue] = useState(false)
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
// NFC state
const [isNfcEnabled, setIsNfcEnabled] = useState(false)
@@ -150,17 +150,15 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
useEffect(() => {
const initNfc = async () => {
try {
const supported = await NfcManager.isSupported()
const supported = await NfcService.init() // runs NfcManager.start()
setIsNfcSupported(supported)
if (!supported) {
setNfcInfo('NFC is not supported on this device')
return
}
await NfcManager.start()
const enabled = await NfcManager.isEnabled()
const enabled = await NfcService.isEnabled()
if (!enabled) {
setNfcInfo('Please enable NFC in your device settings')
return
@@ -198,123 +196,10 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
}, [navigation, isNfcEnabled, isProcessing])
const sendTaskListenerRef = useRef<((result: TransactionTaskResult) => void) | null>(null);
const handleSendTaskResult = useCallback(
async (result: TransactionTaskResult) => {
log.debug('[NfcScreen] handleSendTaskResult triggered', { result })
try {
const { transaction, error, encodedTokenToSend } = result
// Always update core transaction state
if (transaction) {
setTransactionStatus(transaction.status)
setTransaction(transaction)
setTransactionId(transaction.id)
// Optional: link back to original payment request
if (decodedCashuPaymentRequest?.id || encodedCashuPaymentRequest) {
transaction.update({
paymentId: decodedCashuPaymentRequest?.id,
paymentRequest: encodedCashuPaymentRequest,
});
}
}
// ———————— Error Cases ————————
if (error || !transaction) {
const message = error?.params?.message || error?.message || 'Unknown error'
const title = error ? 'Payment failed' : 'Internal error'
setResultModalInfo({
status: (transaction?.status || TransactionStatus.ERROR) as TransactionStatus,
title,
message,
});
toggleResultModal()
return
}
// ———————— Missing Token ————————
if (!encodedTokenToSend) {
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Internal error',
message: 'Failed to generate ecash token',
});
toggleResultModal()
return
}
// ———————— Success Path ————————
setEncodedTokenToSend(encodedTokenToSend)
// Send over NFC to payee's POS or wallet
await writeTokenToNfcTag(encodedTokenToSend)
} catch (err: any) {
log.error('Unexpected error in handleSendTaskResult', err)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unexpected error',
message: err.message || 'Something went wrong',
})
toggleResultModal()
}
},
[
decodedCashuPaymentRequest?.id,
encodedCashuPaymentRequest,
]
)
useEffect(() => {
if (!isSendTaskSentToQueue) {
// optional: make sure no stray listener exists
if (sendTaskListenerRef.current) {
EventEmitter.off(`ev_${SEND_TASK}_result`, sendTaskListenerRef.current)
sendTaskListenerRef.current = null
}
return
}
const eventName = `ev_${SEND_TASK}_result`
// Remove previous (defensive)
if (sendTaskListenerRef.current) {
EventEmitter.off(eventName, sendTaskListenerRef.current)
}
const oneTimeHandler = (result: TransactionTaskResult) => {
// Immediately detach so it can never fire twice
EventEmitter.off(eventName, oneTimeHandler)
sendTaskListenerRef.current = null
// Forward to the stable callback
handleSendTaskResult(result)
}
sendTaskListenerRef.current = oneTimeHandler
EventEmitter.on(eventName, oneTimeHandler)
return () => {
if (sendTaskListenerRef.current) {
EventEmitter.off(eventName, sendTaskListenerRef.current)
sendTaskListenerRef.current = null
}
}
}, [isSendTaskSentToQueue, handleSendTaskResult])
// Stable handler — only recreated when dependencies actually change
const handleSyncStateResult = useCallback(
async (result: SyncStateTaskResult) => {
log.trace('[NfcScreen] handleSyncStateResult triggered', { result, transactionId });
const handleSyncStateResult = useCallback(async (result: SyncStateTaskResult) => {
log.trace('[NfcScreen.handleSyncStateResult] handleSyncStateResult triggered', { result, transactionId });
if (!transactionId) return;
@@ -322,7 +207,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
// ——— SUCCESS: Receiver claimed the ecash ———
if (completedTransactionIds?.includes(transactionId)) {
log.debug('[NfcScreen] Ecash claimed successfully by the payee.', { transactionId });
log.debug('[NfcScreen.handleSyncStateResult] Ecash claimed successfully by the payee.', { transactionId });
const amountSentInt = Math.round(
(toNumber(amountToPay || '0') * getCurrency(unitRef.current).precision)
@@ -337,6 +222,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
});
setTransactionStatus(TransactionStatus.COMPLETED);
setIsResultModalVisible(true);
setNfcInfo('Payment completed successfully!');
return;
}
@@ -345,7 +231,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
log.trace('[NfcScreen] Sync error for transaction', { transactionId });
const update = transactionStateUpdates?.find(u => u.tId === transactionId);
const message = update?.message || 'Transaction failed to complete on the mint.'
const message = update?.message || 'Transaction failed to complete on the mint side.'
setResultModalInfo({
status: TransactionStatus.ERROR,
@@ -422,262 +308,72 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
// ====================== Lightning Payment (Transfer Task) Result Handler ======================
const handleTransferTaskResult = useCallback(
async (result: TransactionTaskResult) => {
log.debug('[NfcScreen] handleTransferTaskResult triggered', {result})
setIsLoading(false)
const { transaction, error, message, finalFee } = result
// ——— Early error before transaction exists ———
if (!transaction && error) {
setTransactionStatus(TransactionStatus.ERROR)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: translate('payCommon_failed'),
message: error.message || 'Lightning payment failed',
})
toggleResultModal()
return;
}
// ——— Transaction exists ———
if (transaction) {
const { status } = transaction;
setTransactionStatus(status);
setTransaction(transaction);
if (error) {
// Pending but timed out / failed
if (status === TransactionStatus.PENDING) {
setResultModalInfo({
status,
message,
})
} else {
setResultModalInfo({
status,
title: error.params?.message ? error.message : translate('payCommon_failed'),
message: error.params?.message || error.message || 'Lightning payment failed',
})
}
} else {
// Success or settled pending
setResultModalInfo({
status,
message: message || 'Lightning payment successful!',
title: status === TransactionStatus.COMPLETED ? 'Payment sent!' : undefined,
});
}
} else {
// Fallback (shouldn't happen)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unknown error',
message: 'No transaction data received',
})
}
toggleResultModal()
},
[isTransferTaskSentToQueue],
)
// One-time listener with auto-cleanup
const transferTaskListenerRef = useRef<((r: TransactionTaskResult) => void) | null>(null);
useEffect(() => {
if (!isTransferTaskSentToQueue) {
if (transferTaskListenerRef.current) {
EventEmitter.off(`ev_${TRANSFER_TASK}_result`, transferTaskListenerRef.current)
transferTaskListenerRef.current = null
}
return
}
const eventName = `ev_${TRANSFER_TASK}_result`
// Remove any previous listener (defensive)
if (transferTaskListenerRef.current) {
EventEmitter.off(eventName, transferTaskListenerRef.current)
}
const oneTimeHandler = (result: TransactionTaskResult) => {
// Immediately detach — this event should fire only once per payment
EventEmitter.off(eventName, oneTimeHandler)
transferTaskListenerRef.current = null
handleTransferTaskResult(result)
}
transferTaskListenerRef.current = oneTimeHandler
EventEmitter.on(eventName, oneTimeHandler)
// Cleanup on unmount or when task flag resets
return () => {
if (transferTaskListenerRef.current) {
EventEmitter.off(eventName, transferTaskListenerRef.current)
transferTaskListenerRef.current = null
}
}
}, [handleTransferTaskResult])
const readNdefOnce = function () {
const cleanUp = () => {
NfcManager.setEventListener(NfcEvents.DiscoverTag, null);
NfcManager.setEventListener(NfcEvents.SessionClosed, null);
};
return new Promise<TagEvent | null>((resolve) => {
let tagFound: TagEvent | null = null;
NfcManager.setEventListener(NfcEvents.DiscoverTag, (tag: TagEvent) => {
tagFound = tag;
resolve(tagFound);
NfcManager.unregisterTagEvent().catch(() => 0);
});
NfcManager.setEventListener(NfcEvents.SessionClosed, (e: any) => {
if (e) {
log.error(Err.NFC_ERROR, 'NFC Session closed with error', e.message);
}
cleanUp()
if (!tagFound) {
resolve(null)
}
});
NfcManager.registerTagEvent();
});
}
const readTag = async function() {
try {
await NfcManager.requestTechnology([NfcTech.Ndef]);
const tag = await NfcManager.getTag();
return tag
} catch (e: any) {
// for tag reading, we don't actually need to show any error
log.error(Err.NFC_ERROR, 'Failed to read NFC tag', e.message);
} finally {
NfcManager.cancelTechnologyRequest();
}
}
const startNfcSession = async () => {
let decoded: string | undefined = undefined;
try {
log.debug('Starting NFC session for payment request reading...', {caller: 'startNfcSession'})
log.debug('[startNfcSession] Starting NFC session for payment request reading...')
const tag = await readNdefOnce()
// const tag = await readTag()
const tag = await NfcService.readNdefTag()
if (!tag) {
return
}
log.warn('Tag found', {tag});
//log.info('NFC Tag found', {tag});
const bytes = new Uint8Array(tag.ndefMessage[0]?.payload ?? [])
decoded = Ndef.text.decodePayload(bytes)
/*await NfcManager.requestTechnology(NfcTech.Ndef, {
alertMessage: 'Hold phone near the device...',
invalidateAfterFirstRead: false,
})
if(!decoded || decoded.length < 140) {
const msg = 'This NFC tag can not be processed, missing or truncated NDEF message.'
setResultModalInfo({
status: TransactionStatus.ERROR,
message: msg,
})
const tag = await NfcManager.getTag();
log.warn('Tag found', {tag});
toggleResultModal()
if(!tag || !tag.ndefMessage || tag.ndefMessage.length === 0) {
setInfo('No NDEF message found on the NFC tag')
log.error(Err.NFC_ERROR, msg, { tag })
return
}
const bytes = new Uint8Array(
tag.ndefMessage[0].payload
);
setNfcInfo('Processing payment request...')
let payload: string
log.info('NFC tag decoded', { decoded })
const decoded = Ndef.text.decodePayload(bytes);
setReadNfcData(decoded)
await handlePaymentRequest(decoded)
log.warn('NFC tag decoded', { decoded })*/
// setNfcInfo('Reading payment request...')
// Listen for tag discovery
/*NfcManager.setEventListener(NfcEvents.DiscoverTag, async (tag: any) => {
if (isProcessing) return
setIsProcessing(true)
setNfcInfo('Reading payment request...')
log.info('NFC tag discovered, reading...', { tag })
try {
const ndefMessage = tag.ndefMessage?.[0]
if (!ndefMessage) {
throw new AppError(Err.VALIDATION_ERROR, 'No NDEF message found')
}
let payload: string
// Handle both Text and URI records
if (ndefMessage.type === Ndef.RTD_TEXT) {
const decoded = Ndef.text.decodePayload(ndefMessage.payload)
payload = decoded
} else if (ndefMessage.type === Ndef.RTD_URI) {
const decoded = Ndef.uri.decodePayload(ndefMessage.payload)
payload = decoded
} else {
payload = new TextDecoder().decode(ndefMessage.payload)
}
log.debug('NFC tag read', { payload })
setReadNfcData(payload)
await handlePaymentRequest(payload)
} catch (e: any) {
handleError(e)
} finally {
setIsProcessing(false)
}
})*/
} catch (e: any) {
log.error(Err.NFC_ERROR, e.message)
log.error('[NfcPayScreen] handlePaymentRequest failed', {
error: e?.message ?? String(e),
stack: e?.stack,
decoded: decoded?.slice?.(0,200)
})
handleError(e)
} finally {
NfcManager.cancelTechnologyRequest().catch(() => {})
}
}
// handlePaymentRequest.ts (main entry)
const handlePaymentRequest = async (data: string) => {
try {
Alert.alert(data)
log.info('[handlePaymentRequest] received data via NFC', { data })
// Alert.alert(data)
log.info('[handlePaymentRequest] received data via NFC', { data })
const result = IncomingParser.findAndExtract(data)
if (!result) {
throw new AppError(Err.VALIDATION_ERROR, 'Unsupported or invalid payment request')
}
const result = IncomingParser.findAndExtract(data)
if (result.type === IncomingDataType.CASHU_PAYMENT_REQUEST) {
await handleCashuPaymentRequest(result.encoded)
} else if (result.type === IncomingDataType.INVOICE) {
await handleLightningInvoice(result.encoded)
}
} catch (e: any) {
handleError(e)
log.info('[handlePaymentRequest] parsed result', { result })
if (!result || !result.encoded) {
throw new AppError(Err.VALIDATION_ERROR, 'Unsupported or invalid payment request')
}
if (result.type === IncomingDataType.CASHU_PAYMENT_REQUEST) {
await handleCashuPaymentRequest(result.encoded)
} else if (result.type === IncomingDataType.INVOICE) {
await handleLightningInvoice(result.encoded)
}
}
@@ -718,9 +414,8 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
setMintBalanceToSendFrom(selectedBalance)
setAmountToPay(formatDisplayAmount(requiredAmount, unit))
setIsSendTaskSentToQueue(true);
WalletTask.sendQueue(
const result = await WalletTask.sendQueueAwaitable(
selectedBalance,
requiredAmount,
unit,
@@ -729,27 +424,105 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
undefined, // p2pk not supported in current Cashu PR spec
undefined
)
await handleSendTaskResult(result)
}
const handleSendTaskResult = async function (result: TransactionTaskResult) {
log.debug('[NfcScreen] handleSendTaskResult start', {transactionStatus: result.transaction?.status})
try {
const { transaction, error, encodedTokenToSend } = result
// Always update core transaction state
if (transaction) {
setTransactionStatus(transaction.status)
setTransaction(transaction)
setTransactionId(transaction.id)
// Optional: link back to original payment request
if (decodedCashuPaymentRequest?.id || encodedCashuPaymentRequest) {
transaction.update({
paymentId: decodedCashuPaymentRequest?.id,
paymentRequest: encodedCashuPaymentRequest,
});
}
}
// ———————— Error Cases ————————
if (error || !transaction) {
const message = error?.params?.message || error?.message || 'Unknown error'
const title = error ? 'Payment failed' : 'Internal error'
setResultModalInfo({
status: (transaction?.status || TransactionStatus.ERROR) as TransactionStatus,
title,
message,
});
toggleResultModal()
setNfcInfo('Payment failed')
return
}
// ———————— Missing Token ————————
if (!encodedTokenToSend) {
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Internal error',
message: 'Failed to generate ecash token',
});
toggleResultModal()
setNfcInfo('Payment failed')
return
}
// ———————— Success Path ————————
setEncodedTokenToSend(encodedTokenToSend)
// Send over NFC to payee's POS or wallet
setNfcInfo('Prepared ecash token to be sent via NFC.')
log.trace('[NfcScreen] handleSendTaskResult: Starting NFC write of an ecash token.')
await NfcService.writeNdefMessage(encodedTokenToSend)
log.trace('[NfcScreen] handleSendTaskResult: Write completed.')
setNfcInfo('Ecash token sent successfully.')
} catch (err: any) {
log.error('Error in handleSendTaskResult', {error: err, caller: 'NfcScreen.handleSendTaskResult'})
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unexpected error',
message: err.message || 'Something went wrong',
})
toggleResultModal()
}
}
const handleLightningInvoice = async (encodedInvoice: string) => {
if (!isInternetReachable) {
setInfo(translate('commonOfflinePretty'));
setInfo(translate('commonOfflinePretty'))
return
}
const decoded = LightningUtils.decodeInvoice(encodedInvoice)
const { amount: invoiceAmount, description, expiry, timestamp } = LightningUtils.getInvoiceData(decoded)
if (!invoiceAmount || invoiceAmount <= 0) {
throw new AppError(Err.VALIDATION_ERROR, 'Lightning invoice has no amount')
}
const expiresAt = addSeconds(new Date((timestamp as number) * 1000), expiry as number)
const feeReserve = Math.max(MIN_LIGHTNING_FEE, Math.round(invoiceAmount * LIGHTNING_FEE_PERCENT / 100))
const totalRequired = invoiceAmount + feeReserve;
const totalRequired = invoiceAmount + feeReserve
const balances = proofsStore.getMintBalancesWithEnoughBalance(totalRequired, unitRef.current)
if (balances.length === 0) {
setInfo(
translate('transferScreen_insufficientFunds', {
@@ -759,16 +532,17 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
)
return
}
const selectedBalance = balances[0]
setMintBalanceToSendFrom(selectedBalance)
setInfo('Creating Lightning payment quote...')
const quote = await walletStore.createLightningMeltQuote(
selectedBalance.mintUrl,
unitRef.current,
encodedInvoice
)
// Update UI
setEncodedInvoice(encodedInvoice)
setInvoice(decoded)
@@ -776,19 +550,78 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
setMeltQuote(quote)
setAmountToPay(formatDisplayAmount(quote.amount, unitRef.current))
if (description) setMemo(description)
setIsTransferTaskSentToQueue(true)
try {
setInfo('Paying Lightning invoice...')
setIsLoading(true)
const result = await WalletTask.transferQueueAwaitable(
selectedBalance,
quote.amount,
unitRef.current,
quote,
description ?? '',
expiresAt,
encodedInvoice
)
WalletTask.transferQueue(
selectedBalance,
quote.amount,
unitRef.current,
quote,
description ?? '',
expiresAt,
encodedInvoice,
undefined,
undefined
)
const { transaction, message, finalFee } = result
if (!transaction && error) {
setTransactionStatus(TransactionStatus.ERROR)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: translate('payCommon_failed'),
message: error.message || 'Lightning payment failed',
})
toggleResultModal()
return;
}
// ——— Transaction exists ———
if (transaction) {
const { status } = transaction;
setTransactionStatus(status);
setTransaction(transaction);
if (error) {
// Pending but timed out / failed
if (status === TransactionStatus.PENDING) {
setResultModalInfo({
status,
message,
})
} else {
setResultModalInfo({
status,
title: error.params?.message ? error.message : translate('payCommon_failed'),
message: error.params?.message || error.message || 'Lightning payment failed',
})
}
} else {
// Success or settled pending
setResultModalInfo({
status,
message: message || 'Lightning payment successful!',
title: status === TransactionStatus.COMPLETED ? 'Payment sent!' : undefined,
});
}
} else {
// Fallback (shouldn't happen)
setResultModalInfo({
status: TransactionStatus.ERROR,
title: 'Unknown error',
message: 'No transaction data received',
})
}
toggleResultModal()
} finally {
setIsLoading(false)
setInfo('')
}
}
@@ -859,21 +692,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
})
}
const writeTokenToNfcTag = async (token: string) => {
try {
// Re-request tech to write back
await NfcManager.requestTechnology(NfcTech.Ndef)
const textRecord = Ndef.textRecord('en', token)
const bytes = Ndef.encodeMessage([textRecord])
await NfcManager.ndefHandler.writeNdefMessage(bytes)
} catch (e: any) {
throw new AppError(Err.NFC_ERROR, 'Failed to send token', e.message)
} finally {
NfcManager.cancelTechnologyRequest().catch(() => {})
}
}
const handleError = (e: AppError) => {
setError(e)
@@ -902,7 +720,6 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
setInfo('')
setError(undefined)
setIsLoading(false)
setIsTransferTaskSentToQueue(false)
setIsResultModalVisible(false)
setResultModalInfo(undefined)
}
@@ -969,7 +786,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) {
</View>
</View>
{info && !isProcessing && <InfoModal message={info} />}
{info && <InfoModal message={info} />}
{error && <ErrorModal error={error} />}
<BottomModal
isVisible={isResultModalVisible}
+33 -8
View File
@@ -76,7 +76,8 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
} = useStores()
const appState = useRef(AppState.currentState)
const lastMintCheckRef = useRef(0)
const lastMintCheckRef = useRef<number>(0)
const lastBackgroundTimestampRef = useRef<number>(0)
const isInternetReachable = useIsInternetReachable()
const groupedMints: MintsByUnit[] = mintsStore.groupedByUnit
@@ -312,22 +313,46 @@ export const WalletScreen = observer(function WalletScreen({ route }: Props) {
}, [isInternetReachable, userSettingsStore.exchangeCurrency])
useFocusEffect(performChecks)
useFocusEffect(() => {
log.trace('[useFocusEffect] WalletScreen')
performChecks()
})
useEffect(() => {
const handleAppStateChange = (nextAppState: AppStateStatus) => {
/* const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
log.trace('[handleAppStateChange] WalletScreen active again')
performChecks()
NostrClient.reconnectToRelays().catch(e => false)
}
appState.current = nextAppState
};
}*/
const subscription = AppState.addEventListener('change', handleAppStateChange)
const subscription = AppState.addEventListener('change', (nextState) => {
if (nextState === 'background') {
lastBackgroundTimestampRef.current = Date.now()
return
}
if (nextState === 'active') {
const timeInBackground = Date.now() - lastBackgroundTimestampRef.current
if (timeInBackground < 1000) { // (usually <1s for NFC)
log.trace('[handleAppStateChange] Ignored too short background event (could be NFC tap)', { timeInBackground })
return // Ignore – this was an NFC flicker
}
// Real foreground – run your normal logic (e.g., unlock check, etc.)
log.trace('[handleAppStateChange] WalletScreen active again')
performChecks()
NostrClient.reconnectToRelays().catch(e => false)
}
})
return () => subscription.remove()
return () => {
subscription.remove() // Ensure cleanup to avoid multiple listeners
}
}, [performChecks])
-1
View File
@@ -100,7 +100,6 @@ const extractEncodedCashuToken = function (maybeToken: string): string {
const extractEncodedCashuPaymentRequest = function (maybeRequest: string): string {
log.trace('[extractEncodedCashuPaymentRequest] Extract payment request from', {maybeRequest})
let encodedRequest: string | undefined = undefined
+2 -2
View File
@@ -110,7 +110,7 @@ const findAndExtract = function (
const maybeCashuPaymentRequest = CashuUtils.findEncodedCashuPaymentRequest(incomingData)
if(maybeCashuPaymentRequest) {
log.trace('Got maybeCashuPaymentRequest', maybeCashuPaymentRequest, 'findAndExtract')
log.trace('Got maybeCashuPaymentRequest', {maybeCashuPaymentRequest, caller: 'findAndExtract'})
const encoded = CashuUtils.extractEncodedCashuPaymentRequest(maybeCashuPaymentRequest) // throws
@@ -123,7 +123,7 @@ const findAndExtract = function (
const maybeCashuPaymentRequestPayload = CashuUtils.findEncodedCashuPaymentRequestPayload(incomingData)
if(maybeCashuPaymentRequestPayload) {
log.trace('Got maybeCashuPaymentRequestPayload', maybeCashuPaymentRequestPayload, 'findAndExtract')
log.trace('Got maybeCashuPaymentRequestPayload', {maybeCashuPaymentRequestPayload, caller: 'findAndExtract'})
const encoded = incomingData // to be aligned with other cases we pass encoded (stringified) JSON payload
+76
View File
@@ -0,0 +1,76 @@
import { Platform } from 'react-native'
import NfcManager, { NfcTech, Ndef, NfcEvents, TagEvent } from 'react-native-nfc-manager'
import { log } from './logService'
import AppError, { Err } from '../utils/AppError'
const NFC_STALE_DELAY_MS = 120
const init = async function () {
const supported = await NfcManager.isSupported()
if (supported) {
await NfcManager.start()
}
return supported
}
const isEnabled = function () {
return NfcManager.isEnabled()
}
const goToNfcSetting = function () {
return NfcManager.goToNfcSetting()
}
/**
* Ensures a fresh NFC connection by cancelling any previous session
* and adding a tiny delay on Android
*/
const withFreshNfcConnection = async <T>(action: () => Promise<T>): Promise<T> => {
// Cancel any previous session
await NfcManager.cancelTechnologyRequest().catch(() => {})
log.trace('[withFreshNfcConnection] Pre-cancel completed')
if (Platform.OS === 'android') {
await new Promise(r => setTimeout(r, NFC_STALE_DELAY_MS))
}
try {
return await action() // Only requestTechnology + getTag / writeNdef
} finally {
// This now runs IMMEDIATELY after read/write
await NfcManager.cancelTechnologyRequest().catch(() => {})
log.trace('[withFreshNfcConnection] Post-cancel completed')
}
}
const readNdefTag = async () => {
return await withFreshNfcConnection(async () => {
log.trace('[readNdefTag] requesting technology')
await NfcManager.requestTechnology(NfcTech.Ndef)
log.trace('[readNdefTag] requestTechnology completed')
const tag = await NfcManager.getTag()
return tag
})
}
const writeNdefMessage = async (text: string) => {
return await withFreshNfcConnection(async () => {
await NfcManager.requestTechnology(NfcTech.Ndef)
log.trace('[writeNdefMessage] requestTechnology completed')
const bytes = Ndef.encodeMessage([Ndef.textRecord('en', text)])
await NfcManager.ndefHandler.writeNdefMessage(bytes)
})
}
export const NfcService = {
init,
isEnabled,
goToNfcSetting,
readNdefTag,
writeNdefMessage,
}
+29 -13
View File
@@ -22,31 +22,47 @@ const getSyncQueue = function () {
return _queue as TaskQueue
}
const addTask = function (taskId: TaskId, task: Promise<any> | any) {
const addTask = function <T>(
taskId: TaskId,
task: Promise<T> | (() => Promise<T>) | any
): Promise<T> {
const queue = getSyncQueue()
log.info(`Adding new task ${taskId} to the queue`)
queue
.addTask(
const promise = queue.addTask(
task,
taskId, _handleTaskStatusChange)
.then((result: WalletTaskResult | TransactionTaskResult) => _handleTaskResult(taskId, result))
taskId,
_handleTaskStatusChange
) as Promise<T>
// Side effect: handle result when it resolves (fire-and-forget)
promise.then((result: any) => {
_handleTaskResult(taskId, result as WalletTaskResult | TransactionTaskResult)
}).catch(() => {}) // Optional: ignore errors here if handled elsewhere
return promise
}
const addPrioritizedTask = function (taskId: TaskId, task: Promise<any> | any) {
const addPrioritizedTask = function <T>(
taskId: TaskId,
task: Promise<T> | (() => Promise<T>) | any
): Promise<T> {
const queue = getSyncQueue()
log.info(`Adding new high priority task ${taskId} to the queue`)
queue
.addPrioritizedTask(
const promise = queue.addPrioritizedTask(
task,
taskId, _handleTaskStatusChange)
.then((result: WalletTaskResult | TransactionTaskResult) => _handleTaskResult(taskId, result))
taskId,
_handleTaskStatusChange
) as Promise<T>
promise.then((result: any) => {
_handleTaskResult(taskId, result as WalletTaskResult | TransactionTaskResult)
}).catch(() => {})
return promise
}
// retrieve result of wallet transaction by listening to ev_taskFuncion event
+743 -4
View File
@@ -3,6 +3,7 @@ import {getSnapshot} from 'mobx-state-tree'
import { GiftWrap, EncryptedDirectMessage } from 'nostr-tools/kinds'
import {log} from './logService'
import {Proof} from '../models/Proof'
import EventEmitter from '../utils/eventEmitter'
import {
Transaction,
TransactionData,
@@ -67,6 +68,11 @@ type WalletTaskService = {
isPending: boolean
}
) => Promise<void>
syncStateWithAllMintsQueueAwaitable: (
options: {
isPending: boolean
}
) => Promise<SyncStateTaskResult>
syncStateWithMintQueue: (
options: {
proofsToSync: Proof[],
@@ -74,6 +80,13 @@ type WalletTaskService = {
isPending: boolean
}
) => Promise<void>
syncStateWithMintQueueAwaitable: (
options: {
proofsToSync: Proof[],
mintUrl: string,
isPending: boolean
}
) => Promise<SyncStateTaskResult>
syncStateWithMintTask: (
options: {
proofsToSync: Proof[],
@@ -99,21 +112,47 @@ type WalletTaskService = {
nwcEvent?: NostrEvent,
draftTransactionId?: number
) => Promise<void>
transferQueueAwaitable: (
mintBalanceToTransferFrom: MintBalance,
amountToTransfer: number,
unit: MintUnit,
meltQuote: MeltQuoteResponse,
memo: string,
invoiceExpiry: Date,
encodedInvoice: string,
nwcEvent?: NostrEvent,
draftTransactionId?: number
) => Promise<TransactionTaskResult>
receiveQueue: (
token: Token,
amountToReceive: number,
memo: string,
encodedToken: string,
) => Promise<void>
receiveQueueAwaitable: (
token: Token,
amountToReceive: number,
memo: string,
encodedToken: string,
) => Promise<TransactionTaskResult>
receiveOfflinePrepareQueue: (
token: Token,
amountToReceive: number,
memo: string,
encodedToken: string,
) => Promise<void>
receiveOfflinePrepareQueueAwaitable: (
token: Token,
amountToReceive: number,
memo: string,
encodedToken: string,
) => Promise<TransactionTaskResult>
receiveOfflineCompleteQueue: (
transactionId: number
) => Promise<void>
receiveOfflineCompleteQueueAwaitable: (
transactionId: number
) => Promise<TransactionTaskResult>
sendQueue: (
mintBalanceToSendFrom: MintBalance,
amountToSend: number,
@@ -123,6 +162,15 @@ type WalletTaskService = {
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> },
draftTransactionId?: number
) => Promise<void>
sendQueueAwaitable: (
mintBalanceToSendFrom: MintBalance,
amountToSend: number,
unit: MintUnit,
memo: string,
selectedProofs: Proof[],
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> },
draftTransactionId?: number
) => Promise<TransactionTaskResult>
swapAllQueue: () => Promise<void>
topupQueue: (
mintBalanceToTopup: MintBalance,
@@ -132,15 +180,32 @@ type WalletTaskService = {
contactToSendTo?: Contact,
nwcEvent?: NostrEvent
) => Promise<void>
topupQueueAwaitable: (
mintBalanceToTopup: MintBalance,
amountToTopup: number,
unit: MintUnit,
memo: string,
contactToSendTo?: Contact,
nwcEvent?: NostrEvent
) => Promise<TransactionTaskResult>
cashuPaymentRequestQueue: (
mintBalanceToReceiveTo: MintBalance,
amountToRequest: number,
unit: MintUnit,
memo: string,
) => Promise<void>
cashuPaymentRequestQueueAwaitable: (
mintBalanceToReceiveTo: MintBalance,
amountToRequest: number,
unit: MintUnit,
memo: string,
) => Promise<TransactionTaskResult>
revertQueue: (
transaction: Transaction
) => Promise<void>
revertQueueAwaitable: (
transaction: Transaction
) => Promise<TransactionTaskResult>
testQueue: () => Promise<void>
recoverMintQuote: (params: {
mintUrl: string,
@@ -234,6 +299,84 @@ const transferQueue = async function (
}
const transferQueueAwaitable = function (
mintBalanceToTransferFrom: MintBalance,
amountToTransfer: number,
unit: MintUnit,
meltQuote: MeltQuoteResponse,
memo: string,
invoiceExpiry: Date,
encodedInvoice: string,
nwcEvent?: NostrEvent,
draftTransactionId?: number
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `transferTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_transferTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addPrioritizedTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await transferTask(
mintBalanceToTransferFrom,
amountToTransfer,
unit,
meltQuote,
memo,
invoiceExpiry,
encodedInvoice,
nwcEvent,
draftTransactionId
)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) {
resolveOnce(result)
}
})
.catch((error) => {
rejectOnce(error)
})
// Safety timeout
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'transferQueue timed out'))
}
}, 15000)
})
}
const receiveQueue = async function (
token: Token,
amountToReceive: number,
@@ -271,6 +414,79 @@ const receiveQueue = async function (
return
}
const receiveQueueAwaitable = function (
token: Token,
amountToReceive: number,
memo: string,
encodedToken: string,
): Promise<TransactionTaskResult> {
const now = Date.now()
const proofsCount = token.proofs.length
const taskId = proofsCount > MAX_SWAP_INPUT_SIZE
? `receiveBatchTask-${now}`
: `receiveTask-${now}`
const eventName = proofsCount > MAX_SWAP_INPUT_SIZE
? `ev_receiveBatchTask_result`
: `ev_receiveTask_result`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
const taskPromise = proofsCount > MAX_SWAP_INPUT_SIZE
? receiveBatchTask(token, amountToReceive, memo, encodedToken)
: receiveTask(token, amountToReceive, memo, encodedToken)
SyncQueue.addPrioritizedTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await taskPromise
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) {
resolveOnce(result)
}
})
.catch((error) => {
rejectOnce(error)
})
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'receiveQueue timed out'))
}
}, 15000)
})
}
/*
* Receive big tokens in batches to keep mint load reasonable.
*
@@ -364,6 +580,74 @@ const receiveOfflinePrepareQueue = async function (
}
const receiveOfflinePrepareQueueAwaitable = function (
token: Token,
amountToReceive: number,
memo: string,
encodedToken: string,
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `receiveOfflinePrepareTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_receiveOfflinePrepareTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await receiveOfflinePrepareTask(
token,
amountToReceive,
memo,
encodedToken
)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) {
resolveOnce(result)
}
})
.catch((error) => {
rejectOnce(error)
})
// Timeout after 20s (offline tasks can be slower)
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Offline receive prepare timed out'))
}
}, 20000)
})
}
const receiveOfflineCompleteQueue = async function (
transactionId: number
): Promise<void> {
@@ -378,6 +662,65 @@ const receiveOfflineCompleteQueue = async function (
}
const receiveOfflineCompleteQueueAwaitable = function (
transactionId: number
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `receiveOfflineCompleteTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_receiveOfflineCompleteTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await receiveOfflineCompleteTask(transactionId)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) {
resolveOnce(result)
}
})
.catch((error) => {
rejectOnce(error)
})
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Offline receive complete timed out'))
}
}, 20000)
})
}
const sendQueue = async function (
mintBalanceToSendFrom: MintBalance,
amountToSend: number,
@@ -404,6 +747,81 @@ const sendQueue = async function (
}
const sendQueueAwaitable = function (
mintBalanceToSendFrom: MintBalance,
amountToSend: number,
unit: MintUnit,
memo: string,
selectedProofs: Proof[],
p2pk?: { pubkey: string; locktime?: number; refundKeys?: Array<string> },
draftTransactionId?: number
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `sendTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_sendTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
// Explicitly type the generic when calling
SyncQueue.addPrioritizedTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await sendTask(
mintBalanceToSendFrom,
amountToSend,
unit,
memo,
selectedProofs,
p2pk,
draftTransactionId
)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => { // Now result is correctly typed as TransactionTaskResult
if (!resolved) {
resolveOnce(result)
}
})
.catch((error) => {
rejectOnce(error)
})
// Timeout safety
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'sendQueue timed out'))
}
}, 10000)
})
}
/*
* swapAllTask sends all proofs to pending and swaps them with the mint for standard amount preference
* This decreases the total number of proofs held by the wallet. Used to optimize exported backup size.
@@ -556,6 +974,73 @@ const topupQueue = async function (
}
const topupQueueAwaitable = function (
mintBalanceToTopup: MintBalance,
amountToTopup: number,
unit: MintUnit,
memo: string,
contactToSendTo?: Contact,
nwcEvent?: NostrEvent
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `topupTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_topupTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addPrioritizedTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await topupTask(
mintBalanceToTopup,
amountToTopup,
unit,
memo,
contactToSendTo,
nwcEvent
)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) resolveOnce(result)
})
.catch(rejectOnce)
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Topup task timed out'))
}
}, 15000)
})
}
const cashuPaymentRequestQueue = async function (
mintBalanceToReceiveTo: MintBalance,
amountToRequest: number,
@@ -576,6 +1061,69 @@ const cashuPaymentRequestQueue = async function (
}
const cashuPaymentRequestQueueAwaitable = function (
mintBalanceToReceiveTo: MintBalance,
amountToRequest: number,
unit: MintUnit,
memo: string,
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `cashuPaymentRequestTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_cashuPaymentRequestTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addPrioritizedTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await cashuPaymentRequestTask(
mintBalanceToReceiveTo,
amountToRequest,
unit,
memo
)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) resolveOnce(result)
})
.catch(rejectOnce)
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Cashu payment request timed out'))
}
}, 10000)
})
}
const revertQueue = async function (
transaction: Transaction
@@ -589,6 +1137,61 @@ const revertQueue = async function (
}
const revertQueueAwaitable = function (
transaction: Transaction
): Promise<TransactionTaskResult> {
const now = Date.now()
const taskId = `revertTask-${now}`
return new Promise<TransactionTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: TransactionTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_revertTask_result`
const handler = (result: TransactionTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addPrioritizedTask<TransactionTaskResult>(
taskId,
async () => {
try {
return await revertTask(transaction)
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) resolveOnce(result)
})
.catch(rejectOnce)
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Revert task timed out'))
}
}, 15000)
})
}
const syncStateWithAllMintsQueue = async function (options: {
isPending: boolean
@@ -603,6 +1206,63 @@ const syncStateWithAllMintsQueue = async function (options: {
const syncStateWithAllMintsQueueAwaitable = function (
options: { isPending: boolean }
): Promise<SyncStateTaskResult> {
const { isPending } = options
const now = Date.now()
const taskId = `syncSpendableStateTask-${now}`
return new Promise<SyncStateTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: SyncStateTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_syncSpendableStateTask_result`
const handler = (result: SyncStateTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addPrioritizedTask<SyncStateTaskResult>(
taskId,
async () => {
try {
return await syncStateWithAllMintsTask({ isPending })
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) resolveOnce(result)
})
.catch(rejectOnce)
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Sync all mints state timed out'))
}
}, 30000) // Longer timeout — syncing all mints can take time
})
}
const syncStateWithAllMintsTask = async function (options: {
isPending: boolean
}): Promise<SyncStateTaskResult> {
@@ -714,7 +1374,76 @@ const syncStateWithMintQueue = async function (
)
}
const syncStateWithMintQueueAwaitable = function (
options: {
proofsToSync: Proof[]
mintUrl: string
isPending: boolean
}
): Promise<SyncStateTaskResult> {
const { mintUrl, isPending, proofsToSync } = options
log.trace('[syncStateWithMintQueueAwaitable] start', {
mintUrl,
isPending,
proofsToSyncCount: proofsToSync.length
})
const now = Date.now()
const taskId = `syncStateWithMintTask-${now}`
return new Promise<SyncStateTaskResult>((resolve, reject) => {
let resolved = false
const resolveOnce = (result: SyncStateTaskResult) => {
if (resolved) return
resolved = true
resolve(result)
}
const rejectOnce = (error: any) => {
if (resolved) return
resolved = true
reject(error)
}
const eventName = `ev_syncStateWithMintTask_result`
const handler = (result: SyncStateTaskResult) => {
EventEmitter.off(eventName, handler)
resolveOnce(result)
}
EventEmitter.on(eventName, handler)
SyncQueue.addTask<SyncStateTaskResult>(
taskId,
async () => {
try {
return await syncStateWithMintTask({
proofsToSync,
mintUrl,
isPending
})
} catch (error) {
rejectOnce(error)
throw error
}
}
)
.then((result) => {
if (!resolved) resolveOnce(result)
})
.catch(rejectOnce)
setTimeout(() => {
if (!resolved) {
EventEmitter.off(eventName, handler)
rejectOnce(new AppError(Err.TIMEOUT_ERROR, 'Sync mint state timed out'))
}
}, 20000)
})
}
/**
* Sync wallet proof state with mint reality (SPENT / PENDING / UNSPENT)
@@ -2499,22 +3228,32 @@ const testQueue = async () => {
export const WalletTask: WalletTaskService = {
syncStateWithAllMintsQueue,
syncStateWithAllMintsQueueAwaitable,
syncStateWithMintQueue,
syncStateWithMintQueueAwaitable,
syncStateWithMintTask,
handleInFlightQueue,
handlePendingQueue,
handleClaimQueue,
handleNwcRequestQueue,
receiveEventsFromRelaysQueue,
receiveQueue,
receiveQueue,
receiveQueueAwaitable,
receiveOfflinePrepareQueue,
receiveOfflineCompleteQueue,
receiveOfflinePrepareQueueAwaitable,
receiveOfflineCompleteQueue,
receiveOfflineCompleteQueueAwaitable,
sendQueue,
sendQueueAwaitable,
swapAllQueue,
transferQueue,
transferQueue,
transferQueueAwaitable,
topupQueue,
topupQueueAwaitable,
cashuPaymentRequestQueue,
cashuPaymentRequestQueueAwaitable,
revertQueue,
revertQueueAwaitable,
testQueue,
recoverMintQuote,
recoverMeltQuoteChange,
+4 -4
View File
@@ -1894,15 +1894,15 @@ __metadata:
languageName: node
linkType: hard
"@cashu/cashu-ts@minibits-cash/cashu-ts#c85d4638c007ac8126bc83a7287a6d848fcc8153":
"@cashu/cashu-ts@minibits-cash/cashu-ts#e97b1a4ee9a7ddbb1468302c6996a9d6d3efcb6f":
version: 2.4.0
resolution: "@cashu/cashu-ts@https://github.com/minibits-cash/cashu-ts.git#commit=c85d4638c007ac8126bc83a7287a6d848fcc8153"
resolution: "@cashu/cashu-ts@https://github.com/minibits-cash/cashu-ts.git#commit=e97b1a4ee9a7ddbb1468302c6996a9d6d3efcb6f"
dependencies:
"@cashu/crypto": ^0.3.4
"@noble/curves": ^1.3.0
"@noble/hashes": ^1.3.3
buffer: ^6.0.3
checksum: 9d60865e47509428ee23ed5fec87282ff1fdad1748529010461e9c239d04616b8dde1ed6afbe51bf1c693aeadeaa4e02ede792d3ee2962e99913c1253c061568
checksum: 68d46ceb9c60e9cef305aad86534c08b23de434de1efcd217d00c73bdf84704801c3cd700901903256de5a408caf029cea7886e9a303dd423027f08184bc1cb5
languageName: node
linkType: hard
@@ -13535,7 +13535,7 @@ __metadata:
"@babel/plugin-proposal-export-namespace-from": ^7.18.9
"@babel/preset-env": ^7.25.3
"@babel/runtime": ^7.25.0
"@cashu/cashu-ts": "minibits-cash/cashu-ts#c85d4638c007ac8126bc83a7287a6d848fcc8153"
"@cashu/cashu-ts": "minibits-cash/cashu-ts#e97b1a4ee9a7ddbb1468302c6996a9d6d3efcb6f"
"@fortawesome/fontawesome-svg-core": ^7.1.0
"@fortawesome/free-brands-svg-icons": ^7.1.0
"@fortawesome/free-regular-svg-icons": ^7.1.0