@@ -306,21 +306,26 @@ module.exports = function createPaymentsProcessor(ctx, state, deps) {
306306 return txKey ? { status : "ok" , txKey } : { status : "missing" , message : "wallet returned no tx_key for " + txHash } ;
307307 }
308308
309- async function loadWalletTransfers ( ) {
310- const heightReply = await callWallet ( "get_height" , { } , true ) ;
311- const walletHeight = heightReply && heightReply . result ? normalizeInteger ( heightReply . result . height ) : null ;
312- if ( heightReply instanceof Error || typeof heightReply === "string" || ! heightReply || typeof heightReply !== "object" || walletHeight === null ) {
313- return { status : "wallet_unavailable" , message : "wallet height lookup failed: " + describeWalletReply ( heightReply ) } ;
314- }
315-
316- const reply = await callWallet ( "get_transfers" , {
309+ async function loadWalletTransfers ( options ) {
310+ const params = {
317311 out : true ,
318312 pending : true ,
319- pool : true ,
320- filter_by_height : true ,
321- min_height : Math . max ( 0 , walletHeight - RECENT_TRANSFER_LOOKBACK_BLOCKS ) ,
322- max_height : walletHeight
323- } , true , { connectionClose : false } ) ;
313+ pool : true
314+ } ;
315+ if ( ! options || options . filterByHeight !== false ) {
316+ const heightReply = await callWallet ( "get_height" , { } , true ) ;
317+ const walletHeight = heightReply && heightReply . result ? normalizeInteger ( heightReply . result . height ) : null ;
318+ if ( heightReply instanceof Error || typeof heightReply === "string" || ! heightReply || typeof heightReply !== "object" || walletHeight === null ) {
319+ return { status : "wallet_unavailable" , message : "wallet height lookup failed: " + describeWalletReply ( heightReply ) } ;
320+ }
321+ Object . assign ( params , {
322+ filter_by_height : true ,
323+ min_height : Math . max ( 0 , walletHeight - RECENT_TRANSFER_LOOKBACK_BLOCKS ) ,
324+ max_height : walletHeight
325+ } ) ;
326+ }
327+
328+ const reply = await callWallet ( "get_transfers" , params , true , { connectionClose : false } ) ;
324329 if ( reply instanceof Error || typeof reply === "string" || ! reply || typeof reply !== "object" || ! reply . result ) {
325330 return { status : "wallet_unavailable" , message : describeWalletReply ( reply ) } ;
326331 }
@@ -343,6 +348,9 @@ module.exports = function createPaymentsProcessor(ctx, state, deps) {
343348 if ( reply && reply . error ) {
344349 const message = describeWalletReply ( reply ) ;
345350 if ( / n o t f o u n d / i. test ( message ) ) return { status : "tx_not_found" } ;
351+ if ( normalizeInteger ( reply . error . code ) === 0 && reply . error . message === "" ) {
352+ return { status : "tx_lookup_unavailable" , message } ;
353+ }
346354 return { status : "wallet_unavailable" , message } ;
347355 }
348356 if ( ! reply || typeof reply !== "object" || ! reply . result ) {
@@ -357,6 +365,21 @@ module.exports = function createPaymentsProcessor(ctx, state, deps) {
357365 return transfers . length ? { status : "ok" , transfers } : { status : "tx_not_found" } ;
358366 }
359367
368+ async function loadSubmittedTransfersByTxHash ( txHash ) {
369+ const byTxHash = await loadWalletTransferByTxHash ( txHash ) ;
370+ if ( byTxHash . status !== "tx_lookup_unavailable" ) return byTxHash ;
371+
372+ const fallback = await loadWalletTransfers ( { filterByHeight : false } ) ;
373+ if ( fallback . status === "ok" ) {
374+ const transfers = fallback . transfers . filter ( function hasTxHash ( transfer ) {
375+ return normalizeHash ( transfer && transfer . txid ) === txHash ;
376+ } ) ;
377+ return transfers . length ? { status : "ok" , transfers } : { status : "tx_not_found" } ;
378+ }
379+
380+ return fallback ;
381+ }
382+
360383 async function loadBatchById ( batchId ) {
361384 const rows = await mysqlPool . query ( "SELECT * FROM payment_batches WHERE id = ? LIMIT 1" , [ batchId ] ) ;
362385 return Array . isArray ( rows ) && rows . length ? rows [ 0 ] : null ;
@@ -436,16 +459,16 @@ module.exports = function createPaymentsProcessor(ctx, state, deps) {
436459 function isReconcilableSubmittedTransfer ( transfer ) {
437460 if ( ! transfer || typeof transfer !== "object" ) return false ;
438461 const transferType = typeof transfer . type === "string" ? transfer . type . toLowerCase ( ) : "" ;
439- // Submitted batches may only finalize on durable outgoing entries.
440- // Reject failed, incoming, and still-locked history rows .
441- if ( transferType !== "out" ) return false ;
442- if ( transfer . locked === true ) return false ;
462+ // Submitted batches may finalize on exact outgoing wallet visibility,
463+ // including the normal pre-confirmation states immediately after send .
464+ if ( transferType !== "out" && transferType !== "pending" && transferType !== "pool" ) return false ;
465+ if ( transfer . double_spend_seen === true ) return false ;
443466 return true ;
444467 }
445468
446469 async function findSubmittedTransferVisibility ( batch , items ) {
447470 const txHash = normalizeHash ( batch . tx_hash ) ;
448- const loadedTransfers = await loadWalletTransferByTxHash ( txHash ) ;
471+ const loadedTransfers = await loadSubmittedTransfersByTxHash ( txHash ) ;
449472 if ( loadedTransfers . status !== "ok" ) return loadedTransfers ;
450473 // Once we already know the tx hash, visibility requires both the same
451474 // hash and the same destination set. That keeps a reused hash-shaped
0 commit comments