2020-06-29 04:44:35 +02:00
using System ;
2019-05-12 04:13:04 +02:00
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2022-02-21 15:46:43 +01:00
using BTCPayServer.Abstractions.Constants ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2021-12-31 08:59:02 +01:00
using BTCPayServer.BIP78.Sender ;
2020-04-29 09:09:16 +02:00
using BTCPayServer.HostedServices ;
2019-05-12 04:13:04 +02:00
using BTCPayServer.ModelBinders ;
2020-01-06 13:57:32 +01:00
using BTCPayServer.Models ;
2019-05-12 04:13:04 +02:00
using BTCPayServer.Models.WalletViewModels ;
2021-03-01 14:44:53 +01:00
using BTCPayServer.Payments.PayJoin.Sender ;
2020-04-06 06:19:51 +02:00
using BTCPayServer.Services ;
2019-05-12 04:13:04 +02:00
using Microsoft.AspNetCore.Mvc ;
using NBitcoin ;
2020-06-17 14:43:56 +02:00
using NBitcoin.Payment ;
2020-01-21 09:33:12 +01:00
using NBXplorer ;
2019-05-12 04:13:04 +02:00
using NBXplorer.Models ;
namespace BTCPayServer.Controllers
{
2022-01-07 04:32:00 +01:00
public partial class UIWalletsController
2019-05-12 04:13:04 +02:00
{
[NonAction]
public async Task < CreatePSBTResponse > CreatePSBT ( BTCPayNetwork network , DerivationSchemeSettings derivationSettings , WalletSendModel sendModel , CancellationToken cancellationToken )
{
var nbx = ExplorerClientProvider . GetExplorerClient ( network ) ;
CreatePSBTRequest psbtRequest = new CreatePSBTRequest ( ) ;
2020-03-19 09:44:47 +01:00
if ( sendModel . InputSelection )
{
2020-06-28 10:55:27 +02:00
psbtRequest . IncludeOnlyOutpoints = sendModel . SelectedInputs ? . Select ( OutPoint . Parse ) ? . ToList ( ) ? ? new List < OutPoint > ( ) ;
2020-03-19 09:44:47 +01:00
}
2019-05-21 10:10:07 +02:00
foreach ( var transactionOutput in sendModel . Outputs )
{
var psbtDestination = new CreatePSBTDestination ( ) ;
psbtRequest . Destinations . Add ( psbtDestination ) ;
psbtDestination . Destination = BitcoinAddress . Create ( transactionOutput . DestinationAddress , network . NBitcoinNetwork ) ;
psbtDestination . Amount = Money . Coins ( transactionOutput . Amount . Value ) ;
psbtDestination . SubstractFees = transactionOutput . SubtractFeesFromOutput ;
}
2020-06-28 10:55:27 +02:00
2019-05-12 04:13:04 +02:00
if ( network . SupportRBF )
{
2020-05-24 14:11:33 +02:00
if ( sendModel . AllowFeeBump is WalletSendModel . ThreeStateBool . Yes )
psbtRequest . RBF = true ;
if ( sendModel . AllowFeeBump is WalletSendModel . ThreeStateBool . No )
psbtRequest . RBF = false ;
2019-05-12 04:13:04 +02:00
}
2020-06-12 13:58:55 +02:00
psbtRequest . AlwaysIncludeNonWitnessUTXO = sendModel . AlwaysIncludeNonWitnessUTXO ;
2020-06-28 10:55:27 +02:00
2019-05-12 04:13:04 +02:00
psbtRequest . FeePreference = new FeePreference ( ) ;
2020-05-05 12:06:59 +02:00
if ( sendModel . FeeSatoshiPerByte is decimal v & &
v > decimal . Zero )
{
2020-09-03 10:27:48 +02:00
psbtRequest . FeePreference . ExplicitFeeRate = new FeeRate ( v ) ;
2020-05-05 12:06:59 +02:00
}
2019-05-12 04:13:04 +02:00
if ( sendModel . NoChange )
{
2019-05-21 10:10:07 +02:00
psbtRequest . ExplicitChangeAddress = psbtRequest . Destinations . First ( ) . Destination ;
2019-05-12 04:13:04 +02:00
}
2020-06-28 10:55:27 +02:00
2019-05-12 04:13:04 +02:00
var psbt = ( await nbx . CreatePSBTAsync ( derivationSettings . AccountDerivation , psbtRequest , cancellationToken ) ) ;
if ( psbt = = null )
throw new NotSupportedException ( "You need to update your version of NBXplorer" ) ;
2019-07-25 12:38:29 +02:00
// Not supported by coldcard, remove when they do support it
psbt . PSBT . GlobalXPubs . Clear ( ) ;
2019-05-12 04:13:04 +02:00
return psbt ;
}
2022-02-10 04:24:28 +01:00
[HttpPost("{walletId}/cpfp")]
public async Task < IActionResult > WalletCPFP ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
WalletId walletId , string [ ] outpoints , string [ ] transactionHashes , string returnUrl )
{
outpoints ? ? = Array . Empty < string > ( ) ;
transactionHashes ? ? = Array . Empty < string > ( ) ;
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
var explorer = ExplorerClientProvider . GetExplorerClient ( network ) ;
var fr = _feeRateProvider . CreateFeeProvider ( network ) ;
var targetFeeRate = await fr . GetFeeRateAsync ( 1 ) ;
// Since we don't know the actual fee rate paid by a tx from NBX
// we just assume that it is 20 blocks
var assumedFeeRate = await fr . GetFeeRateAsync ( 20 ) ;
var settings = ( this . GetCurrentStore ( ) . GetDerivationSchemeSettings ( NetworkProvider , network . CryptoCode ) ) ;
var derivationScheme = settings . AccountDerivation ;
if ( derivationScheme is null )
return NotFound ( ) ;
var utxos = await explorer . GetUTXOsAsync ( derivationScheme ) ;
var outpointsHashet = outpoints . ToHashSet ( ) ;
var transactionHashesSet = transactionHashes . ToHashSet ( ) ;
var bumpableUTXOs = utxos . GetUnspentUTXOs ( ) . Where ( u = > u . Confirmations = = 0 & &
( outpointsHashet . Contains ( u . Outpoint . ToString ( ) ) | |
transactionHashesSet . Contains ( u . Outpoint . Hash . ToString ( ) ) ) ) . ToArray ( ) ;
if ( bumpableUTXOs . Length = = 0 )
{
TempData [ WellKnownTempData . ErrorMessage ] = "There isn't any UTXO available to bump fee" ;
2022-05-13 03:26:20 +02:00
return LocalRedirect ( returnUrl ) ;
2022-02-10 04:24:28 +01:00
}
Money bumpFee = Money . Zero ;
foreach ( var txid in bumpableUTXOs . Select ( u = > u . TransactionHash ) . ToHashSet ( ) )
{
var tx = await explorer . GetTransactionAsync ( txid ) ;
var vsize = tx . Transaction . GetVirtualSize ( ) ;
var assumedFeePaid = assumedFeeRate . GetFee ( vsize ) ;
var expectedFeePaid = targetFeeRate . GetFee ( vsize ) ;
bumpFee + = Money . Max ( Money . Zero , expectedFeePaid - assumedFeePaid ) ;
}
var returnAddress = ( await explorer . GetUnusedAsync ( derivationScheme , NBXplorer . DerivationStrategy . DerivationFeature . Deposit ) ) . Address ;
TransactionBuilder builder = explorer . Network . NBitcoinNetwork . CreateTransactionBuilder ( ) ;
builder . AddCoins ( bumpableUTXOs . Select ( utxo = > utxo . AsCoin ( derivationScheme ) ) ) ;
// The fee of the bumped transaction should pay for both, the fee
// of the bump transaction and those that are being bumped
builder . SendEstimatedFees ( targetFeeRate ) ;
builder . SendFees ( bumpFee ) ;
builder . SendAll ( returnAddress ) ;
2022-04-11 10:53:10 +02:00
try {
var psbt = builder . BuildPSBT ( false ) ;
psbt = ( await explorer . UpdatePSBTAsync ( new UpdatePSBTRequest ( )
{
PSBT = psbt ,
DerivationScheme = derivationScheme
} ) ) . PSBT ;
return View ( "PostRedirect" , new PostRedirectViewModel
{
AspController = "UIWallets" ,
AspAction = nameof ( UIWalletsController . WalletSign ) ,
RouteParameters = {
{ "walletId" , walletId . ToString ( ) } ,
{ "returnUrl" , returnUrl }
} ,
FormParameters =
{
{ "walletId" , walletId . ToString ( ) } ,
{ "psbt" , psbt . ToHex ( ) }
}
} ) ;
} catch ( Exception ex ) {
TempData [ WellKnownTempData . ErrorMessage ] = ex . Message ;
2022-05-13 03:26:20 +02:00
return LocalRedirect ( returnUrl ) ;
2022-04-11 10:53:10 +02:00
}
2022-02-10 04:24:28 +01:00
}
[HttpPost("{walletId}/sign")]
public async Task < IActionResult > WalletSign ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
WalletId walletId , WalletPSBTViewModel vm , string returnUrl = null , string command = null )
{
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
if ( returnUrl is null )
returnUrl = Url . Action ( nameof ( WalletTransactions ) , new { walletId } ) ;
2022-02-17 09:58:56 +01:00
var psbt = await vm . GetPSBT ( network . NBitcoinNetwork ) ;
if ( psbt is null | | vm . InvalidPSBT )
{
ModelState . AddModelError ( nameof ( vm . PSBT ) , "Invalid PSBT" ) ;
return View ( "WalletSigningOptions" , new WalletSigningOptionsModel ( vm . SigningContext , returnUrl ) ) ;
}
2022-02-10 04:24:28 +01:00
switch ( command )
{
case "vault" :
return ViewVault ( walletId , vm . SigningContext ) ;
case "seed" :
return SignWithSeed ( walletId , vm . SigningContext ) ;
2022-02-17 09:58:56 +01:00
case "decode" :
return await WalletPSBT ( walletId , vm , "decode" ) ;
2022-02-10 04:24:28 +01:00
default :
break ;
}
if ( await CanUseHotWallet ( ) )
{
var derivationScheme = GetDerivationSchemeSettings ( walletId ) ;
if ( derivationScheme . IsHotWallet )
{
var extKey = await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode )
. GetMetadataAsync < string > ( derivationScheme . AccountDerivation ,
WellknownMetadataKeys . MasterHDKey ) ;
if ( extKey ! = null )
{
2022-02-17 09:58:56 +01:00
return await SignWithSeed ( walletId ,
2022-02-10 04:24:28 +01:00
new SignWithSeedViewModel { SeedOrKey = extKey , SigningContext = vm . SigningContext } ) ;
}
}
}
return View ( "WalletSigningOptions" , new WalletSigningOptionsModel ( vm . SigningContext , returnUrl ) ) ;
}
2021-06-14 07:06:56 +02:00
[HttpGet("{walletId}/psbt")]
2019-05-19 16:27:18 +02:00
public async Task < IActionResult > WalletPSBT ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2022-02-17 09:58:56 +01:00
WalletId walletId )
2019-05-12 04:13:04 +02:00
{
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2022-02-17 09:58:56 +01:00
var vm = new WalletPSBTViewModel ( ) ;
2019-12-03 10:57:07 +01:00
vm . CryptoCode = network . CryptoCode ;
2021-01-16 11:48:05 +01:00
var derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
if ( derivationSchemeSettings = = null )
return NotFound ( ) ;
2021-08-03 12:43:16 +02:00
vm . NBXSeedAvailable = await CanUseHotWallet ( ) & & derivationSchemeSettings . IsHotWallet ;
2021-07-29 17:13:46 +02:00
return View ( vm ) ;
2019-05-12 04:13:04 +02:00
}
2021-06-14 07:06:56 +02:00
[HttpPost("{walletId}/psbt")]
2019-05-12 06:13:52 +02:00
public async Task < IActionResult > WalletPSBT (
2019-05-12 04:13:04 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId ,
2022-02-17 09:58:56 +01:00
WalletPSBTViewModel vm , string command )
2019-05-12 04:13:04 +02:00
{
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-12-03 10:57:07 +01:00
vm . CryptoCode = network . CryptoCode ;
2021-01-16 11:48:05 +01:00
var derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
if ( derivationSchemeSettings = = null )
return NotFound ( ) ;
2021-08-03 12:43:16 +02:00
vm . NBXSeedAvailable = await CanUseHotWallet ( ) & & derivationSchemeSettings . IsHotWallet ;
2019-05-19 16:27:18 +02:00
var psbt = await vm . GetPSBT ( network . NBitcoinNetwork ) ;
2022-02-17 09:58:56 +01:00
if ( vm . InvalidPSBT )
2019-05-12 06:13:52 +02:00
{
ModelState . AddModelError ( nameof ( vm . PSBT ) , "Invalid PSBT" ) ;
return View ( vm ) ;
}
2022-02-17 09:58:56 +01:00
if ( psbt is null )
{
return View ( "WalletPSBT" , vm ) ;
}
2019-05-14 18:03:48 +02:00
switch ( command )
2019-05-12 04:13:04 +02:00
{
2022-02-10 04:24:28 +01:00
case "sign" :
2022-02-17 09:58:56 +01:00
return await WalletSign ( walletId , vm ) ;
2019-11-08 12:21:33 +01:00
case "decode" :
2019-05-19 16:27:18 +02:00
ModelState . Remove ( nameof ( vm . PSBT ) ) ;
ModelState . Remove ( nameof ( vm . FileName ) ) ;
ModelState . Remove ( nameof ( vm . UploadedPSBTFile ) ) ;
2021-07-27 17:01:00 +02:00
await FetchTransactionDetails ( derivationSchemeSettings , vm , network ) ;
return View ( "WalletPSBTDecoded" , vm ) ;
2021-12-31 08:59:02 +01:00
2021-08-03 12:43:16 +02:00
case "save-psbt" :
return FilePSBT ( psbt , vm . FileName ) ;
2020-05-23 21:31:21 +02:00
2019-05-30 16:16:05 +02:00
case "update" :
2020-03-29 17:28:22 +02:00
psbt = await ExplorerClientProvider . UpdatePSBT ( derivationSchemeSettings , psbt ) ;
2019-05-30 16:16:05 +02:00
if ( psbt = = null )
{
2021-08-03 12:43:16 +02:00
TempData [ WellKnownTempData . ErrorMessage ] = "You need to update your version of NBXplorer" ;
2019-05-30 16:16:05 +02:00
return View ( vm ) ;
}
2019-10-31 04:29:59 +01:00
TempData [ WellKnownTempData . SuccessMessage ] = "PSBT updated!" ;
2021-06-14 07:06:56 +02:00
return RedirectToWalletPSBT ( new WalletPSBTViewModel
2020-05-24 21:55:28 +02:00
{
PSBT = psbt . ToBase64 ( ) ,
2020-05-25 00:05:01 +02:00
FileName = vm . FileName
2020-05-24 21:55:28 +02:00
} ) ;
2021-12-31 08:59:02 +01:00
2021-08-03 12:43:16 +02:00
case "combine" :
ModelState . Remove ( nameof ( vm . PSBT ) ) ;
return View ( nameof ( WalletPSBTCombine ) , new WalletPSBTCombineViewModel { OtherPSBT = psbt . ToBase64 ( ) } ) ;
2020-01-23 14:02:37 +01:00
2019-05-14 18:03:48 +02:00
case "broadcast" :
2020-05-24 21:55:28 +02:00
{
2021-06-14 07:06:56 +02:00
return RedirectToWalletPSBTReady ( new WalletPSBTReadyViewModel
2020-06-28 10:55:27 +02:00
{
SigningContext = new SigningContextModel ( psbt )
} ) ;
}
2021-12-31 08:59:02 +01:00
2019-05-14 18:03:48 +02:00
default :
2022-02-17 09:58:56 +01:00
return View ( "WalletPSBTDecoded" , vm ) ;
2019-05-12 04:13:04 +02:00
}
}
2020-06-17 14:43:56 +02:00
private async Task < PSBT > GetPayjoinProposedTX ( BitcoinUrlBuilder bip21 , PSBT psbt , DerivationSchemeSettings derivationSchemeSettings , BTCPayNetwork btcPayNetwork , CancellationToken cancellationToken )
2020-01-06 13:57:32 +01:00
{
2020-04-06 06:19:51 +02:00
var cloned = psbt . Clone ( ) ;
cloned = cloned . Finalize ( ) ;
2020-04-28 17:23:51 +02:00
await _broadcaster . Schedule ( DateTimeOffset . UtcNow + TimeSpan . FromMinutes ( 2.0 ) , cloned . ExtractTransaction ( ) , btcPayNetwork ) ;
2021-10-20 10:06:27 +02:00
using var cts = CancellationTokenSource . CreateLinkedTokenSource ( cancellationToken ) ;
cts . CancelAfter ( TimeSpan . FromSeconds ( 30 ) ) ;
return await _payjoinClient . RequestPayjoin ( bip21 , new PayjoinWallet ( derivationSchemeSettings ) , psbt , cts . Token ) ;
2020-01-06 13:57:32 +01:00
}
2020-06-28 10:55:27 +02:00
2019-05-30 16:16:05 +02:00
private async Task FetchTransactionDetails ( DerivationSchemeSettings derivationSchemeSettings , WalletPSBTReadyViewModel vm , BTCPayNetwork network )
2019-05-12 04:13:04 +02:00
{
2020-05-24 23:27:01 +02:00
var psbtObject = PSBT . Parse ( vm . SigningContext . PSBT , network . NBitcoinNetwork ) ;
2019-11-11 06:22:04 +01:00
if ( ! psbtObject . IsAllFinalized ( ) )
2020-03-29 17:28:22 +02:00
psbtObject = await ExplorerClientProvider . UpdatePSBT ( derivationSchemeSettings , psbtObject ) ? ? psbtObject ;
2019-05-15 08:00:09 +02:00
IHDKey signingKey = null ;
RootedKeyPath signingKeyPath = null ;
try
{
signingKey = new BitcoinExtPubKey ( vm . SigningKey , network . NBitcoinNetwork ) ;
}
catch { }
try
{
signingKey = signingKey ? ? new BitcoinExtKey ( vm . SigningKey , network . NBitcoinNetwork ) ;
}
catch { }
try
{
signingKeyPath = RootedKeyPath . Parse ( vm . SigningKeyPath ) ;
}
catch { }
if ( signingKey = = null | | signingKeyPath = = null )
{
2019-05-19 16:27:18 +02:00
var signingKeySettings = derivationSchemeSettings . GetSigningAccountKeySettings ( ) ;
2019-05-15 08:00:09 +02:00
if ( signingKey = = null )
{
signingKey = signingKeySettings . AccountKey ;
vm . SigningKey = signingKey . ToString ( ) ;
}
if ( vm . SigningKeyPath = = null )
{
signingKeyPath = signingKeySettings . GetRootedKeyPath ( ) ;
vm . SigningKeyPath = signingKeyPath ? . ToString ( ) ;
}
}
2019-05-19 16:27:18 +02:00
if ( psbtObject . IsAllFinalized ( ) )
{
vm . CanCalculateBalance = false ;
}
else
{
var balanceChange = psbtObject . GetBalance ( derivationSchemeSettings . AccountDerivation , signingKey , signingKeyPath ) ;
vm . BalanceChange = ValueToString ( balanceChange , network ) ;
vm . CanCalculateBalance = true ;
vm . Positive = balanceChange > = Money . Zero ;
}
2020-02-13 14:06:00 +01:00
vm . Inputs = new List < WalletPSBTReadyViewModel . InputViewModel > ( ) ;
2019-05-30 17:23:23 +02:00
foreach ( var input in psbtObject . Inputs )
{
var inputVm = new WalletPSBTReadyViewModel . InputViewModel ( ) ;
vm . Inputs . Add ( inputVm ) ;
var mine = input . HDKeysFor ( derivationSchemeSettings . AccountDerivation , signingKey , signingKeyPath ) . Any ( ) ;
var balanceChange2 = input . GetTxOut ( ) ? . Value ? ? Money . Zero ;
if ( mine )
balanceChange2 = - balanceChange2 ;
inputVm . BalanceChange = ValueToString ( balanceChange2 , network ) ;
inputVm . Positive = balanceChange2 > = Money . Zero ;
inputVm . Index = ( int ) input . Index ;
}
2020-02-13 14:06:00 +01:00
vm . Destinations = new List < WalletPSBTReadyViewModel . DestinationViewModel > ( ) ;
2019-05-15 08:00:09 +02:00
foreach ( var output in psbtObject . Outputs )
{
var dest = new WalletPSBTReadyViewModel . DestinationViewModel ( ) ;
vm . Destinations . Add ( dest ) ;
2019-05-19 16:27:18 +02:00
var mine = output . HDKeysFor ( derivationSchemeSettings . AccountDerivation , signingKey , signingKeyPath ) . Any ( ) ;
2019-05-15 08:00:09 +02:00
var balanceChange2 = output . Value ;
if ( ! mine )
balanceChange2 = - balanceChange2 ;
dest . Balance = ValueToString ( balanceChange2 , network ) ;
dest . Positive = balanceChange2 > = Money . Zero ;
dest . Destination = output . ScriptPubKey . GetDestinationAddress ( network . NBitcoinNetwork ) ? . ToString ( ) ? ? output . ScriptPubKey . ToString ( ) ;
}
if ( psbtObject . TryGetFee ( out var fee ) )
{
2021-09-01 17:31:42 +02:00
vm . Destinations . Add ( new WalletPSBTReadyViewModel . DestinationViewModel
2019-05-19 16:27:18 +02:00
{
Positive = false ,
2020-06-28 10:55:27 +02:00
Balance = ValueToString ( - fee , network ) ,
2019-05-19 16:27:18 +02:00
Destination = "Mining fees"
} ) ;
2019-05-15 08:00:09 +02:00
}
2019-05-16 05:56:06 +02:00
if ( psbtObject . TryGetEstimatedFeeRate ( out var feeRate ) )
{
vm . FeeRate = feeRate . ToString ( ) ;
}
2019-05-19 16:27:18 +02:00
2019-05-30 17:23:23 +02:00
var sanityErrors = psbtObject . CheckSanity ( ) ;
if ( sanityErrors . Count ! = 0 )
{
vm . SetErrors ( sanityErrors ) ;
}
else if ( ! psbtObject . IsAllFinalized ( ) & & ! psbtObject . TryFinalize ( out var errors ) )
2019-05-19 16:27:18 +02:00
{
vm . SetErrors ( errors ) ;
}
2019-05-12 04:13:04 +02:00
}
2021-06-14 07:06:56 +02:00
[HttpPost("{walletId}/psbt/ready")]
2019-05-12 04:13:04 +02:00
public async Task < IActionResult > WalletPSBTReady (
[ModelBinder(typeof(WalletIdModelBinder))]
2022-02-17 09:58:56 +01:00
WalletId walletId , WalletPSBTViewModel vm , string command , CancellationToken cancellationToken = default )
2019-05-12 04:13:04 +02:00
{
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2022-02-17 09:58:56 +01:00
PSBT psbt = await vm . GetPSBT ( network . NBitcoinNetwork ) ;
if ( vm . InvalidPSBT | | psbt is null )
2019-05-12 04:13:04 +02:00
{
2022-02-17 09:58:56 +01:00
if ( vm . InvalidPSBT )
2022-05-27 09:34:05 +02:00
vm . Errors . Add ( "Invalid PSBT" ) ;
2021-07-30 18:30:13 +02:00
return View ( nameof ( WalletPSBT ) , vm ) ;
2019-05-12 04:13:04 +02:00
}
2022-02-17 09:58:56 +01:00
DerivationSchemeSettings derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
if ( derivationSchemeSettings = = null )
return NotFound ( ) ;
await FetchTransactionDetails ( derivationSchemeSettings , vm , network ) ;
2020-06-28 10:55:27 +02:00
2020-03-30 08:31:30 +02:00
switch ( command )
2019-05-12 04:13:04 +02:00
{
2020-03-30 08:31:30 +02:00
case "payjoin" :
2021-09-01 17:31:42 +02:00
string error ;
2020-04-06 06:19:51 +02:00
try
2020-03-30 08:31:30 +02:00
{
2020-06-17 14:43:56 +02:00
var proposedPayjoin = await GetPayjoinProposedTX ( new BitcoinUrlBuilder ( vm . SigningContext . PayJoinBIP21 , network . NBitcoinNetwork ) , psbt ,
2020-04-06 06:19:51 +02:00
derivationSchemeSettings , network , cancellationToken ) ;
2020-03-30 08:31:30 +02:00
try
{
2021-09-01 17:31:42 +02:00
proposedPayjoin . Settings . SigningOptions = new SigningOptions
2021-07-29 13:29:34 +02:00
{
EnforceLowR = ! ( vm . SigningContext ? . EnforceLowR is false )
} ;
2020-03-30 08:31:30 +02:00
var extKey = ExtKey . Parse ( vm . SigningKey , network . NBitcoinNetwork ) ;
2020-03-29 17:28:22 +02:00
proposedPayjoin = proposedPayjoin . SignAll ( derivationSchemeSettings . AccountDerivation ,
extKey ,
2021-07-29 13:29:34 +02:00
RootedKeyPath . Parse ( vm . SigningKeyPath ) ) ;
2020-05-24 23:27:01 +02:00
vm . SigningContext . PSBT = proposedPayjoin . ToBase64 ( ) ;
vm . SigningContext . OriginalPSBT = psbt . ToBase64 ( ) ;
2020-04-06 06:19:51 +02:00
proposedPayjoin . Finalize ( ) ;
2020-04-29 09:09:16 +02:00
var hash = proposedPayjoin . ExtractTransaction ( ) . GetHash ( ) ;
2020-06-01 06:10:21 +02:00
_EventAggregator . Publish ( new UpdateTransactionLabel ( walletId , hash , UpdateTransactionLabel . PayjoinLabelTemplate ( ) ) ) ;
2021-09-01 17:31:42 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2020-04-06 06:19:51 +02:00
{
Severity = StatusMessageModel . StatusSeverity . Success ,
AllowDismiss = false ,
Html = $"The payjoin transaction has been successfully broadcasted ({proposedPayjoin.ExtractTransaction().GetHash()})"
} ) ;
2020-03-30 08:31:30 +02:00
return await WalletPSBTReady ( walletId , vm , "broadcast" ) ;
}
2020-03-29 17:28:22 +02:00
catch ( Exception )
2020-03-26 15:42:54 +01:00
{
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
2020-04-06 06:19:51 +02:00
Severity = StatusMessageModel . StatusSeverity . Warning ,
2020-03-26 15:42:54 +01:00
AllowDismiss = false ,
2020-03-30 08:31:30 +02:00
Html =
2021-09-01 17:31:42 +02:00
"This transaction has been coordinated between the receiver and you to create a <a href='https://en.bitcoin.it/wiki/PayJoin' target='_blank'>payjoin transaction</a> by adding inputs from the receiver.<br/>" +
"The amount being sent may appear higher but is in fact almost same.<br/><br/>" +
"If you cancel or refuse to sign this transaction, the payment will proceed without payjoin"
2020-03-26 15:42:54 +01:00
} ) ;
2020-05-24 23:27:01 +02:00
vm . SigningContext . PSBT = proposedPayjoin . ToBase64 ( ) ;
vm . SigningContext . OriginalPSBT = psbt . ToBase64 ( ) ;
return ViewVault ( walletId , vm . SigningContext ) ;
2020-03-26 15:42:54 +01:00
}
2020-03-30 08:31:30 +02:00
}
2020-04-06 06:19:51 +02:00
catch ( PayjoinReceiverException ex )
{
error = $"The payjoin receiver could not complete the payjoin: {ex.Message}" ;
}
catch ( PayjoinSenderException ex )
{
error = $"We rejected the receiver's payjoin proposal: {ex.Message}" ;
}
catch ( Exception ex )
{
error = $"Unexpected payjoin error: {ex.Message}" ;
}
2020-06-28 10:55:27 +02:00
2020-04-06 06:19:51 +02:00
//we possibly exposed the tx to the receiver, so we need to broadcast straight away
psbt . Finalize ( ) ;
2021-09-01 17:31:42 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2020-04-06 06:19:51 +02:00
{
Severity = StatusMessageModel . StatusSeverity . Warning ,
AllowDismiss = false ,
Html = $"The payjoin transaction could not be created.<br/>" +
$"The original transaction was broadcasted instead. ({psbt.ExtractTransaction().GetHash()})<br/><br/>" +
$"{error}"
} ) ;
return await WalletPSBTReady ( walletId , vm , "broadcast" ) ;
2020-03-30 08:31:30 +02:00
case "broadcast" when ! psbt . IsAllFinalized ( ) & & ! psbt . TryFinalize ( out var errors ) :
vm . SetErrors ( errors ) ;
2021-07-30 18:30:13 +02:00
return View ( nameof ( WalletPSBT ) , vm ) ;
2020-03-30 08:31:30 +02:00
case "broadcast" :
{
2020-06-28 10:55:27 +02:00
var transaction = psbt . ExtractTransaction ( ) ;
try
2020-03-30 08:31:30 +02:00
{
2020-06-28 10:55:27 +02:00
var broadcastResult = await ExplorerClientProvider . GetExplorerClient ( network ) . BroadcastAsync ( transaction ) ;
if ( ! broadcastResult . Success )
2020-03-30 08:31:30 +02:00
{
2020-06-28 10:55:27 +02:00
if ( ! string . IsNullOrEmpty ( vm . SigningContext . OriginalPSBT ) )
2020-03-30 08:31:30 +02:00
{
2021-09-01 17:31:42 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2020-06-28 10:55:27 +02:00
{
Severity = StatusMessageModel . StatusSeverity . Warning ,
AllowDismiss = false ,
Html = $"The payjoin transaction could not be broadcasted.<br/>({broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}).<br/>The transaction has been reverted back to its original format and has been broadcast."
} ) ;
vm . SigningContext . PSBT = vm . SigningContext . OriginalPSBT ;
vm . SigningContext . OriginalPSBT = null ;
return await WalletPSBTReady ( walletId , vm , "broadcast" ) ;
}
2022-05-27 09:34:05 +02:00
vm . Errors . Add ( $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" ) ;
2021-07-30 18:30:13 +02:00
return View ( nameof ( WalletPSBT ) , vm ) ;
2020-03-30 08:31:30 +02:00
}
2022-02-10 04:24:28 +01:00
else
{
var wallet = _walletProvider . GetWallet ( network ) ;
var derivationSettings = GetDerivationSchemeSettings ( walletId ) ;
wallet . InvalidateCache ( derivationSettings . AccountDerivation ) ;
}
2020-03-30 08:31:30 +02:00
}
2020-06-28 10:55:27 +02:00
catch ( Exception ex )
{
2022-05-27 09:34:05 +02:00
vm . Errors . Add ( "Error while broadcasting: " + ex . Message ) ;
2021-07-30 18:30:13 +02:00
return View ( nameof ( WalletPSBT ) , vm ) ;
2020-06-28 10:55:27 +02:00
}
2020-04-06 06:19:51 +02:00
2020-06-28 10:55:27 +02:00
if ( ! TempData . HasStatusMessage ( ) )
{
TempData [ WellKnownTempData . SuccessMessage ] = $"Transaction broadcasted successfully ({transaction.GetHash()})" ;
}
2022-02-10 04:24:28 +01:00
var returnUrl = this . HttpContext . Request . Query [ "returnUrl" ] . FirstOrDefault ( ) ;
if ( returnUrl is not null )
{
2022-05-13 03:26:20 +02:00
return LocalRedirect ( returnUrl ) ;
2022-02-10 04:24:28 +01:00
}
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId = walletId . ToString ( ) } ) ;
2020-04-06 06:19:51 +02:00
}
2020-03-30 08:31:30 +02:00
case "analyze-psbt" :
2020-05-24 21:55:28 +02:00
return RedirectToWalletPSBT ( new WalletPSBTViewModel ( )
{
2020-05-25 00:05:01 +02:00
PSBT = psbt . ToBase64 ( )
2020-05-24 21:55:28 +02:00
} ) ;
2022-02-17 09:58:56 +01:00
case "decode" :
await FetchTransactionDetails ( derivationSchemeSettings , vm , network ) ;
return View ( "WalletPSBTDecoded" , vm ) ;
2020-03-30 08:31:30 +02:00
default :
2022-05-27 09:34:05 +02:00
vm . Errors . Add ( "Unknown command" ) ;
2021-07-30 18:30:13 +02:00
return View ( nameof ( WalletPSBT ) , vm ) ;
2019-05-12 04:13:04 +02:00
}
}
private IActionResult FilePSBT ( PSBT psbt , string fileName )
{
return File ( psbt . ToBytes ( ) , "application/octet-stream" , fileName ) ;
}
2019-05-12 06:13:52 +02:00
2021-06-14 07:06:56 +02:00
[HttpPost("{walletId}/psbt/combine")]
2019-05-12 06:13:52 +02:00
public async Task < IActionResult > WalletPSBTCombine ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
WalletId walletId , WalletPSBTCombineViewModel vm )
{
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-12 06:13:52 +02:00
var psbt = await vm . GetPSBT ( network . NBitcoinNetwork ) ;
if ( psbt = = null )
{
ModelState . AddModelError ( nameof ( vm . PSBT ) , "Invalid PSBT" ) ;
return View ( vm ) ;
}
var sourcePSBT = vm . GetSourcePSBT ( network . NBitcoinNetwork ) ;
if ( sourcePSBT = = null )
{
ModelState . AddModelError ( nameof ( vm . OtherPSBT ) , "Invalid PSBT" ) ;
return View ( vm ) ;
}
sourcePSBT = sourcePSBT . Combine ( psbt ) ;
2019-10-31 04:29:59 +01:00
TempData [ WellKnownTempData . SuccessMessage ] = "PSBT Successfully combined!" ;
2020-05-24 21:55:28 +02:00
return RedirectToWalletPSBT ( new WalletPSBTViewModel ( )
{
PSBT = sourcePSBT . ToBase64 ( )
} ) ;
2019-05-12 06:13:52 +02:00
}
2019-05-12 04:13:04 +02:00
}
}