Table of Contents

Class PendingArkTransactionRecoveryService

Namespace
NArk.Core.Services
Assembly
NArk.Core.dll

Reconciles Arkade transactions that the server has registered as pending — i.e. the SDK called SubmitTx(string, string[], CancellationToken) (server locked the inputs as in-flight) but the matching FinalizeTx(string, string[], CancellationToken) never followed because the process crashed, the user closed the app, or the network dropped.

public class PendingArkTransactionRecoveryService
Inheritance
PendingArkTransactionRecoveryService
Inherited Members

Remarks

The Arkade server enforces "you must finalize the exact pending tx; you cannot spend those inputs another way", so without this recovery the user's coins are indefinitely stuck. The server exposes a recovery endpoint (GetPendingTxAsync(string, string, CancellationToken)) gated by a BIP-322 proof of ownership — this service authenticates with proofs derived from each wallet's known VTXOs, retrieves any pending transactions the server is holding, signs the checkpoint PSBTs with the wallet's signer, and finalizes them.

What gets signed: a pending transaction arrives entirely from the server, so it is authorized against locally reconstructed expectations before anything is signed. Each checkpoint must pay the spent input's full value into the checkpoint contract this wallet would itself have built, and the accompanying final Arkade transaction must spend exactly those checkpoint outputs while still carrying this wallet's own signature over it. A pending transaction failing either check is outside what the wallet ever authorized: it is rejected with UnauthorizedPendingArkTransactionException and never signed.

Runs once on host startup across every wallet known to IWalletStorage; can also be invoked on-demand for a specific wallet via FinalizePendingArkTransactionsAsync(string, CancellationToken). Per-transaction failures are logged and skipped so a single bad pending tx never blocks the wallet from booting — the next start-up retries any unfinalized leftovers.

Timing note: the Arkade server marks input VTXOs as pending-spent via an async event projection that runs after SubmitTx returns. Calling recovery in the same process as the original SubmitTx may race that projection (the server returns an empty pending list until the projection catches up). The hands-off path (host startup) never races this — by the time the host restarts, the projection has long since run. If you call FinalizePendingArkTransactionsAsync(string, CancellationToken) in the same process that just crashed mid-Submit, retry briefly until it returns a non-empty list (the projection is typically caught up within a second).

Constructors

PendingArkTransactionRecoveryService(IClientTransport, IWalletStorage, IWalletProvider, IVtxoStorage, ICoinService, ILogger<PendingArkTransactionRecoveryService>?)

Reconciles Arkade transactions that the server has registered as pending — i.e. the SDK called SubmitTx(string, string[], CancellationToken) (server locked the inputs as in-flight) but the matching FinalizeTx(string, string[], CancellationToken) never followed because the process crashed, the user closed the app, or the network dropped.

public PendingArkTransactionRecoveryService(IClientTransport clientTransport, IWalletStorage walletStorage, IWalletProvider walletProvider, IVtxoStorage vtxoStorage, ICoinService coinService, ILogger<PendingArkTransactionRecoveryService>? logger = null)

Parameters

clientTransport IClientTransport
walletStorage IWalletStorage
walletProvider IWalletProvider
vtxoStorage IVtxoStorage
coinService ICoinService
logger ILogger<PendingArkTransactionRecoveryService>

Remarks

The Arkade server enforces "you must finalize the exact pending tx; you cannot spend those inputs another way", so without this recovery the user's coins are indefinitely stuck. The server exposes a recovery endpoint (GetPendingTxAsync(string, string, CancellationToken)) gated by a BIP-322 proof of ownership — this service authenticates with proofs derived from each wallet's known VTXOs, retrieves any pending transactions the server is holding, signs the checkpoint PSBTs with the wallet's signer, and finalizes them.

What gets signed: a pending transaction arrives entirely from the server, so it is authorized against locally reconstructed expectations before anything is signed. Each checkpoint must pay the spent input's full value into the checkpoint contract this wallet would itself have built, and the accompanying final Arkade transaction must spend exactly those checkpoint outputs while still carrying this wallet's own signature over it. A pending transaction failing either check is outside what the wallet ever authorized: it is rejected with UnauthorizedPendingArkTransactionException and never signed.

Runs once on host startup across every wallet known to IWalletStorage; can also be invoked on-demand for a specific wallet via FinalizePendingArkTransactionsAsync(string, CancellationToken). Per-transaction failures are logged and skipped so a single bad pending tx never blocks the wallet from booting — the next start-up retries any unfinalized leftovers.

Timing note: the Arkade server marks input VTXOs as pending-spent via an async event projection that runs after SubmitTx returns. Calling recovery in the same process as the original SubmitTx may race that projection (the server returns an empty pending list until the projection catches up). The hands-off path (host startup) never races this — by the time the host restarts, the projection has long since run. If you call FinalizePendingArkTransactionsAsync(string, CancellationToken) in the same process that just crashed mid-Submit, retry briefly until it returns a non-empty list (the projection is typically caught up within a second).

Methods

CreateProofAsync(ArkCoin, IArkadeWalletSigner, Network, CancellationToken)

Creates the BIP-322-style proof + message that authenticates the GetPendingTx call. Virtual so unit tests can substitute a canned value without forcing a real signer.

protected virtual Task<(string Proof, string Message)> CreateProofAsync(ArkCoin anchor, IArkadeWalletSigner signer, Network network, CancellationToken cancellationToken)

Parameters

anchor ArkCoin
signer IArkadeWalletSigner
network Network
cancellationToken CancellationToken

Returns

Task<(string Proof, string Message)>

FinalizePendingArkTransactionsAsync(string, CancellationToken)

On-demand pending-tx recovery for a single wallet. Call this from app boot if you want deterministic timing; the BackgroundService startup hook covers the hands-off case.

public Task<IReadOnlyList<string>> FinalizePendingArkTransactionsAsync(string walletId, CancellationToken cancellationToken = default)

Parameters

walletId string
cancellationToken CancellationToken

Returns

Task<IReadOnlyList<string>>

The arkTxIds that were successfully finalized during this call.

Remarks

Pending transactions that fail local authorization (see the type-level "what gets signed" note) are rejected without being signed, reported on RecoveryFailed with an UnauthorizedPendingArkTransactionException, and left out of the result.

RecoverAllWalletsAsync(CancellationToken)

Invoked by ArkHostedLifecycle on host startup. Sweeps every wallet known to IWalletStorage for stranded pending Arkade transactions and finalizes them. Failures are scoped per wallet so one bad wallet never blocks the rest.

public Task RecoverAllWalletsAsync(CancellationToken cancellationToken)

Parameters

cancellationToken CancellationToken

Returns

Task

SignCheckpointAsync(ArkCoin, PSBT, IArkadeWalletSigner, CancellationToken)

Signs a checkpoint PSBT input in-place with the wallet's signer. Virtual so unit tests can stub signing without staging a real key + tap-leaf path.

protected virtual Task SignCheckpointAsync(ArkCoin coin, PSBT checkpoint, IArkadeWalletSigner signer, CancellationToken cancellationToken)

Parameters

coin ArkCoin
checkpoint PSBT
signer IArkadeWalletSigner
cancellationToken CancellationToken

Returns

Task

Events

RecoveryFailed

Raised when finalizing a single pending Arkade transaction fails. The recovery loop continues with the next pending tx regardless — subscribers can use this to surface a wallet-UI banner, ship telemetry, or schedule a retry. Subscribers must not throw; handler exceptions are caught and logged but never propagate.

public event EventHandler<PendingTxRecoveryFailureEventArgs>? RecoveryFailed

Event Type

EventHandler<PendingTxRecoveryFailureEventArgs>