Class 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 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
clientTransportIClientTransportwalletStorageIWalletStoragewalletProviderIWalletProvidervtxoStorageIVtxoStoragecoinServiceICoinServiceloggerILogger<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
anchorArkCoinsignerIArkadeWalletSignernetworkNetworkcancellationTokenCancellationToken
Returns
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
walletIdstringcancellationTokenCancellationToken
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
cancellationTokenCancellationToken
Returns
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
coinArkCoincheckpointPSBTsignerIArkadeWalletSignercancellationTokenCancellationToken
Returns
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