diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala index 8d4fbc341a..0105032aa0 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletService.scala @@ -403,7 +403,7 @@ class ErgoWalletServiceImpl(override val ergoSettings: ErgoSettings) extends Erg val confirmed = state.registry.walletUnspentBoxes(state.maxInputsToUse * BoxSelector.ScanDepthFactor) if (considerUnconfirmed) { // We filter out spent boxes in the same way as wallet does when assembling a transaction - (confirmed ++ state.offChainRegistry.offChainBoxes).filter(state.walletFilter) + (confirmed ++ state.offChainRegistry.walletOffChainBoxes).filter(state.walletFilter) } else { confirmed } @@ -411,7 +411,7 @@ class ErgoWalletServiceImpl(override val ergoSettings: ErgoSettings) extends Erg val confirmed = state.registry.walletConfirmedBoxes() if (considerUnconfirmed) { // Just adding boxes created off-chain - confirmed ++ state.offChainRegistry.offChainBoxes + confirmed ++ state.offChainRegistry.walletOffChainBoxes } else { confirmed } diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletState.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletState.scala index 03fe2cc98d..06e5806a78 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletState.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/ErgoWalletState.scala @@ -121,7 +121,7 @@ case class ErgoWalletState( */ def getBoxesToSpend: Seq[TrackedBox] = { require(walletVars.publicKeyAddresses.nonEmpty, "No public keys in the prover to extract change address from") - (registry.walletUnspentBoxes(maxInputsToUse * BoxSelector.ScanDepthFactor) ++ offChainRegistry.offChainBoxes).distinct + (registry.walletUnspentBoxes(maxInputsToUse * BoxSelector.ScanDepthFactor) ++ offChainRegistry.walletOffChainBoxes).distinct } } diff --git a/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/OffChainRegistry.scala b/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/OffChainRegistry.scala index 50f10904b9..33ae620d00 100644 --- a/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/OffChainRegistry.scala +++ b/src/main/scala/org/ergoplatform/nodeView/wallet/persistence/OffChainRegistry.scala @@ -38,6 +38,13 @@ case class OffChainRegistry(height: Int, WalletDigest(height, balance, tokensBalance.toSeq) } + /** + * Off-chain boxes belonging to the wallet itself (i.e. tracked by the payments scan), + * excluding boxes tracked by external application scans only, which must not be + * spent by wallet transactions (see #1905). + */ + def walletOffChainBoxes: Seq[TrackedBox] = offChainBoxes.filter(_.scans.contains(PaymentsScanId)) + /** * Update on receiving new off-chain transaction. */ diff --git a/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala b/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala index cc261eebc5..f6834a099a 100644 --- a/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala +++ b/src/test/scala/org/ergoplatform/nodeView/wallet/ErgoWalletServiceSpec.scala @@ -433,4 +433,36 @@ class ErgoWalletServiceSpec } } + + property("wallet-related APIs should not use boxes belonging to external scans only") { + withVersionedStore(2) { versionedStore => + withStore { store => + val customScanId = ScanId @@ 42.shortValue() + val emptyTx = ErgoLikeTransaction(IndexedSeq(), IndexedSeq()) + val walletBox = + TrackedBox(emptyTx, creationOutIndex = 0, None, testBox(1000000000L, TrueTree, 0), Set(PaymentsScanId)) + val customScanBox = + TrackedBox(emptyTx, creationOutIndex = 1, None, testBox(2000000000L, TrueTree, 0, boxIndex = 1), Set(customScanId)) + val sharedBox = + TrackedBox(emptyTx, creationOutIndex = 2, None, testBox(3000000000L, TrueTree, 0, boxIndex = 2), Set(PaymentsScanId, customScanId)) + + val offChainRegistry = + OffChainRegistry.empty.copy(offChainBoxes = Seq(walletBox, customScanBox, sharedBox)) + val walletState = initialState(store, versionedStore).copy(offChainRegistry = offChainRegistry) + val walletService = new ErgoWalletServiceImpl(settings) + + // boxes the wallet can spend must not include boxes tracked by external scans only + walletState.getBoxesToSpend.map(_.boxId) should contain theSameElementsAs Seq(walletBox.boxId, sharedBox.boxId) + + // /wallet/boxes/unspent with considerUnconfirmed must not return them either + val unspent = walletService.getWalletBoxes(walletState, unspentOnly = true, considerUnconfirmed = true) + unspent.map(_.trackedBox.boxId) should contain theSameElementsAs Seq(walletBox.boxId, sharedBox.boxId) + + // while scan-related API still sees the box of the external scan + val scanBoxes = walletService.getScanUnspentBoxes(walletState, customScanId, considerUnconfirmed = true, 0, Int.MaxValue) + scanBoxes.map(_.trackedBox.boxId) should contain theSameElementsAs Seq(customScanBox.boxId, sharedBox.boxId) + } + } + } + }