2019-05-12 04:13:04 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
2020-01-06 13:57:32 +01:00
using System.Net.Http ;
using System.Text ;
2019-05-12 04:13:04 +02:00
using System.Threading ;
using System.Threading.Tasks ;
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 ;
using Microsoft.AspNetCore.Mvc ;
using NBitcoin ;
2020-01-21 09:33:12 +01:00
using NBXplorer ;
2019-05-12 04:13:04 +02:00
using NBXplorer.Models ;
namespace BTCPayServer.Controllers
{
public partial class WalletsController
{
[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 )
{
psbtRequest . IncludeOnlyOutpoints = sendModel . SelectedInputs ? . Select ( OutPoint . Parse ) ? . ToList ( ) ? ? new List < OutPoint > ( ) ;
}
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 ;
}
2019-05-12 04:13:04 +02:00
if ( network . SupportRBF )
{
psbtRequest . RBF = ! sendModel . DisableRBF ;
}
2019-05-21 10:10:07 +02:00
2019-05-12 04:13:04 +02:00
psbtRequest . FeePreference = new FeePreference ( ) ;
psbtRequest . FeePreference . ExplicitFeeRate = new FeeRate ( Money . Satoshis ( sendModel . FeeSatoshiPerByte ) , 1 ) ;
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
}
2019-05-21 10:10:07 +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 ;
}
[HttpGet]
[Route("{walletId}/psbt")]
2019-05-19 16:27:18 +02:00
public async Task < IActionResult > WalletPSBT ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2019-05-15 08:00:09 +02:00
WalletId walletId , WalletPSBTViewModel vm )
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 ;
2020-01-21 09:33:12 +01:00
vm . NBXSeedAvailable = await CanUseHotWallet ( ) & & ! string . IsNullOrEmpty ( await ExplorerClientProvider . GetExplorerClient ( network )
. GetMetadataAsync < string > ( GetDerivationSchemeSettings ( walletId ) . AccountDerivation ,
WellknownMetadataKeys . Mnemonic ) ) ;
2019-05-19 16:27:18 +02:00
if ( await vm . GetPSBT ( network . NBitcoinNetwork ) is PSBT psbt )
2019-05-15 08:00:09 +02:00
{
2019-05-19 16:27:18 +02:00
vm . Decoded = psbt . ToString ( ) ;
vm . PSBT = psbt . ToBase64 ( ) ;
2019-05-15 08:00:09 +02:00
}
2020-02-13 14:06:00 +01:00
return View ( nameof ( WalletPSBT ) , vm ? ? new WalletPSBTViewModel ( ) { CryptoCode = walletId . CryptoCode } ) ;
2019-05-12 04:13:04 +02:00
}
[HttpPost]
[Route("{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 ,
2019-05-12 06:13:52 +02:00
WalletPSBTViewModel vm , string command = null )
2019-05-12 04:13:04 +02:00
{
2019-11-08 12:21:33 +01:00
if ( command = = null )
return await WalletPSBT ( walletId , vm ) ;
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 ;
2019-05-19 16:27:18 +02:00
var psbt = await vm . GetPSBT ( network . NBitcoinNetwork ) ;
2019-05-12 06:13:52 +02:00
if ( psbt = = null )
{
ModelState . AddModelError ( nameof ( vm . PSBT ) , "Invalid PSBT" ) ;
return View ( vm ) ;
}
2019-05-14 18:03:48 +02:00
switch ( command )
2019-05-12 04:13:04 +02:00
{
2019-11-08 12:21:33 +01:00
case "decode" :
2019-05-14 18:03:48 +02:00
vm . Decoded = psbt . ToString ( ) ;
2019-05-19 16:27:18 +02:00
ModelState . Remove ( nameof ( vm . PSBT ) ) ;
ModelState . Remove ( nameof ( vm . FileName ) ) ;
ModelState . Remove ( nameof ( vm . UploadedPSBTFile ) ) ;
vm . PSBT = psbt . ToBase64 ( ) ;
vm . FileName = vm . UploadedPSBTFile ? . FileName ;
2019-05-14 18:03:48 +02:00
return View ( vm ) ;
2019-11-11 06:22:04 +01:00
case "vault" :
return ViewVault ( walletId , psbt ) ;
2019-05-14 18:03:48 +02:00
case "ledger" :
2020-01-21 13:04:35 +01:00
return ViewWalletSendLedger ( walletId , psbt ) ;
2019-05-30 16:16:05 +02:00
case "update" :
2019-10-12 13:35:30 +02:00
var derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
2019-05-30 16:16:05 +02:00
psbt = await UpdatePSBT ( derivationSchemeSettings , psbt , network ) ;
if ( psbt = = null )
{
2019-05-30 17:00:20 +02:00
ModelState . AddModelError ( nameof ( vm . PSBT ) , "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!" ;
2020-02-13 14:06:00 +01:00
return RedirectToWalletPSBT ( psbt , vm . FileName ) ;
2019-05-14 18:03:48 +02:00
case "seed" :
2019-05-15 08:00:09 +02:00
return SignWithSeed ( walletId , psbt . ToBase64 ( ) ) ;
2020-01-21 09:33:12 +01:00
case "nbx-seed" :
2020-01-23 14:02:37 +01:00
if ( await CanUseHotWallet ( ) )
2020-01-21 09:33:12 +01:00
{
2020-01-23 14:02:37 +01:00
var derivationScheme = GetDerivationSchemeSettings ( walletId ) ;
var extKey = await ExplorerClientProvider . GetExplorerClient ( network )
. GetMetadataAsync < string > ( derivationScheme . AccountDerivation ,
WellknownMetadataKeys . MasterHDKey ) ;
2020-01-06 13:57:32 +01:00
return await SignWithSeed ( walletId ,
2020-01-23 14:02:37 +01:00
new SignWithSeedViewModel ( ) { SeedOrKey = extKey , PSBT = psbt . ToBase64 ( ) } ) ;
}
return View ( vm ) ;
2019-05-14 18:03:48 +02:00
case "broadcast" :
2019-05-12 06:13:52 +02:00
{
2020-02-13 14:06:00 +01:00
return RedirectToWalletPSBTReady ( psbt . ToBase64 ( ) ) ;
2019-05-12 06:13:52 +02:00
}
2019-05-14 18:03:48 +02:00
case "combine" :
ModelState . Remove ( nameof ( vm . PSBT ) ) ;
return View ( nameof ( WalletPSBTCombine ) , new WalletPSBTCombineViewModel ( ) { OtherPSBT = psbt . ToBase64 ( ) } ) ;
case "save-psbt" :
return FilePSBT ( psbt , vm . FileName ) ;
default :
return View ( vm ) ;
2019-05-12 04:13:04 +02:00
}
}
2019-05-30 16:16:05 +02:00
private async Task < PSBT > UpdatePSBT ( DerivationSchemeSettings derivationSchemeSettings , PSBT psbt , BTCPayNetwork network )
{
var result = await ExplorerClientProvider . GetExplorerClient ( network ) . UpdatePSBTAsync ( new UpdatePSBTRequest ( )
{
PSBT = psbt ,
DerivationScheme = derivationSchemeSettings . AccountDerivation ,
} ) ;
if ( result = = null )
return null ;
derivationSchemeSettings . RebaseKeyPaths ( result . PSBT ) ;
return result . PSBT ;
}
2020-03-14 12:21:12 +01:00
2020-01-06 13:57:32 +01:00
private async Task < PSBT > TryGetBPProposedTX ( PSBT psbt , DerivationSchemeSettings derivationSchemeSettings , BTCPayNetwork btcPayNetwork )
{
if ( TempData . TryGetValue ( "bpu" , out var bpu ) & & ! string . IsNullOrEmpty ( bpu ? . ToString ( ) ) & & Uri . TryCreate ( bpu . ToString ( ) , UriKind . Absolute , out var endpoint ) )
{
TempData . Remove ( "bpu" ) ;
2020-03-17 07:53:20 +01:00
var httpClient = _httpClientFactory . CreateClient ( "payjoin" ) ;
2020-03-14 12:21:12 +01:00
2020-01-06 13:57:32 +01:00
var cloned = psbt . Clone ( ) ;
if ( ! cloned . IsAllFinalized ( ) & & ! cloned . TryFinalize ( out var errors ) )
{
return null ;
}
var bpuresponse = await httpClient . PostAsync ( bpu . ToString ( ) , new StringContent ( cloned . ToHex ( ) , Encoding . UTF8 , "text/plain" ) ) ;
if ( bpuresponse . IsSuccessStatusCode )
{
var hex = await bpuresponse . Content . ReadAsStringAsync ( ) ;
if ( PSBT . TryParse ( hex , btcPayNetwork . NBitcoinNetwork , out var newPSBT ) )
{
//check that all the inputs we provided are still there and that there is at least one new(signed) input.
bool valid = false ;
2020-03-26 12:41:32 +01:00
var existingInputs = psbt . Inputs . Select ( input = > input . PrevOut ) . ToHashSet ( ) ;
2020-01-06 13:57:32 +01:00
foreach ( var input in newPSBT . Inputs )
{
var existingInput = existingInputs . SingleOrDefault ( point = > point = = input . PrevOut ) ;
if ( existingInput ! = null )
{
existingInputs . Remove ( existingInput ) ;
continue ;
}
if ( ! input . TryFinalizeInput ( out _ ) )
{
valid = false ;
break ;
}
// a new signed input was provided
valid = true ;
}
if ( ! valid | | existingInputs . Any ( ) )
{
return null ;
}
newPSBT = await UpdatePSBT ( derivationSchemeSettings , newPSBT , btcPayNetwork ) ;
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Info ,
AllowDismiss = false ,
2020-03-13 16:52:50 +01:00
Html = "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. The amount being sent may appear higher but is in fact the same"
2020-01-06 13:57:32 +01:00
} ) ;
return newPSBT ;
}
}
}
return null ;
}
2019-05-12 04:13:04 +02:00
[HttpGet]
[Route("{walletId}/psbt/ready")]
2019-05-15 08:00:09 +02:00
public async Task < IActionResult > WalletPSBTReady (
2019-05-12 04:13:04 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
2019-05-15 08:00:09 +02:00
WalletId walletId , string psbt = null ,
string signingKey = null ,
string signingKeyPath = null )
{
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-15 08:00:09 +02:00
var vm = new WalletPSBTReadyViewModel ( ) { PSBT = psbt } ;
vm . SigningKey = signingKey ;
vm . SigningKeyPath = signingKeyPath ;
2019-10-12 13:35:30 +02:00
var derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
2019-05-27 14:50:08 +02:00
if ( derivationSchemeSettings = = null )
return NotFound ( ) ;
2019-07-12 05:57:56 +02:00
try
{
await FetchTransactionDetails ( derivationSchemeSettings , vm , network ) ;
}
catch { return BadRequest ( ) ; }
2019-05-15 08:00:09 +02:00
return View ( nameof ( WalletPSBTReady ) , vm ) ;
}
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
{
2019-05-15 08:00:09 +02:00
var psbtObject = PSBT . Parse ( vm . PSBT , network . NBitcoinNetwork ) ;
2019-11-11 06:22:04 +01:00
if ( ! psbtObject . IsAllFinalized ( ) )
psbtObject = await UpdatePSBT ( derivationSchemeSettings , psbtObject , network ) ? ? 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 ) )
{
2019-05-19 16:27:18 +02:00
vm . Destinations . Add ( new WalletPSBTReadyViewModel . DestinationViewModel ( )
{
Positive = false ,
Balance = ValueToString ( - fee , network ) ,
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
}
[HttpPost]
[Route("{walletId}/psbt/ready")]
public async Task < IActionResult > WalletPSBTReady (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId , WalletPSBTReadyViewModel vm , string command = null )
{
2019-11-08 12:21:33 +01:00
if ( command = = null )
return await WalletPSBTReady ( walletId , vm . PSBT , vm . SigningKey , vm . SigningKeyPath ) ;
2019-05-12 04:13:04 +02:00
PSBT psbt = null ;
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-12 04:13:04 +02:00
try
{
psbt = PSBT . Parse ( vm . PSBT , network . NBitcoinNetwork ) ;
2019-10-12 13:35:30 +02:00
var derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
2019-05-27 14:50:08 +02:00
if ( derivationSchemeSettings = = null )
return NotFound ( ) ;
await FetchTransactionDetails ( derivationSchemeSettings , vm , network ) ;
2019-05-12 04:13:04 +02:00
}
catch
{
2019-05-19 16:27:18 +02:00
vm . GlobalError = "Invalid PSBT" ;
2020-02-13 14:06:00 +01:00
return View ( nameof ( WalletPSBTReady ) , vm ) ;
2019-05-12 04:13:04 +02:00
}
if ( command = = "broadcast" )
{
if ( ! psbt . IsAllFinalized ( ) & & ! psbt . TryFinalize ( out var errors ) )
{
2019-05-19 16:27:18 +02:00
vm . SetErrors ( errors ) ;
2020-02-13 14:06:00 +01:00
return View ( nameof ( WalletPSBTReady ) , vm ) ;
2019-05-12 04:13:04 +02:00
}
var transaction = psbt . ExtractTransaction ( ) ;
try
{
var broadcastResult = await ExplorerClientProvider . GetExplorerClient ( network ) . BroadcastAsync ( transaction ) ;
if ( ! broadcastResult . Success )
{
2019-05-19 16:27:18 +02:00
vm . GlobalError = $"RPC Error while broadcasting: {broadcastResult.RPCCode} {broadcastResult.RPCCodeMessage} {broadcastResult.RPCMessage}" ;
2020-02-13 14:06:00 +01:00
return View ( nameof ( WalletPSBTReady ) , vm ) ;
2019-05-12 04:13:04 +02:00
}
}
catch ( Exception ex )
{
2019-05-19 16:27:18 +02:00
vm . GlobalError = "Error while broadcasting: " + ex . Message ;
2020-02-13 14:06:00 +01:00
return View ( nameof ( WalletPSBTReady ) , vm ) ;
2019-05-12 04:13:04 +02:00
}
2019-10-12 13:35:30 +02:00
return RedirectToWalletTransaction ( walletId , transaction ) ;
2019-05-12 04:13:04 +02:00
}
else if ( command = = "analyze-psbt" )
{
2020-02-13 14:06:00 +01:00
return RedirectToWalletPSBT ( psbt ) ;
2019-05-12 04:13:04 +02:00
}
else
{
2019-05-19 16:27:18 +02:00
vm . GlobalError = "Unknown command" ;
2020-02-13 14:06:00 +01:00
return View ( nameof ( WalletPSBTReady ) , 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
[HttpPost]
[Route("{walletId}/psbt/combine")]
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-02-13 14:06:00 +01:00
return RedirectToWalletPSBT ( sourcePSBT ) ;
2019-05-12 06:13:52 +02:00
}
2019-05-12 04:13:04 +02:00
}
}