2020-03-29 17:28:22 +02:00
|
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
2020-05-09 12:29:05 +02:00
|
|
|
|
using System.Configuration;
|
2020-05-17 15:21:35 +02:00
|
|
|
|
using System.Globalization;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
using System.Linq;
|
|
|
|
|
using System.Net.Http;
|
|
|
|
|
using System.Text;
|
|
|
|
|
using System.Threading;
|
|
|
|
|
using System.Threading.Tasks;
|
2020-04-09 10:38:55 +02:00
|
|
|
|
using Google.Apis.Http;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
using NBitcoin;
|
|
|
|
|
using Newtonsoft.Json;
|
|
|
|
|
using Newtonsoft.Json.Linq;
|
2020-04-09 10:38:55 +02:00
|
|
|
|
using IHttpClientFactory = System.Net.Http.IHttpClientFactory;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
|
|
|
|
|
namespace BTCPayServer.Services
|
|
|
|
|
{
|
2020-04-08 08:20:19 +02:00
|
|
|
|
|
|
|
|
|
public static class PSBTExtensions
|
|
|
|
|
{
|
2020-04-08 15:14:16 +02:00
|
|
|
|
public static ScriptPubKeyType? GetInputsScriptPubKeyType(this PSBT psbt)
|
2020-04-08 08:20:19 +02:00
|
|
|
|
{
|
2020-04-08 15:14:16 +02:00
|
|
|
|
if (!psbt.IsAllFinalized() || psbt.Inputs.Any(i => i.WitnessUtxo == null))
|
|
|
|
|
throw new InvalidOperationException("The psbt should be finalized with witness information");
|
|
|
|
|
var coinsPerTypes = psbt.Inputs.Select(i =>
|
|
|
|
|
{
|
2020-04-09 12:44:16 +02:00
|
|
|
|
return ((PSBTCoin)i, i.GetInputScriptPubKeyType());
|
2020-04-08 15:14:16 +02:00
|
|
|
|
}).GroupBy(o => o.Item2, o => o.Item1).ToArray();
|
|
|
|
|
if (coinsPerTypes.Length != 1)
|
|
|
|
|
return default;
|
|
|
|
|
return coinsPerTypes[0].Key;
|
2020-04-08 08:20:19 +02:00
|
|
|
|
}
|
2020-04-09 12:44:16 +02:00
|
|
|
|
|
|
|
|
|
public static ScriptPubKeyType? GetInputScriptPubKeyType(this PSBTInput i)
|
|
|
|
|
{
|
|
|
|
|
if (i.WitnessUtxo.ScriptPubKey.IsScriptType(ScriptType.P2WPKH))
|
|
|
|
|
return ScriptPubKeyType.Segwit;
|
|
|
|
|
if (i.WitnessUtxo.ScriptPubKey.IsScriptType(ScriptType.P2SH) &&
|
2020-05-09 12:29:05 +02:00
|
|
|
|
PayToWitPubKeyHashTemplate.Instance.ExtractWitScriptParameters(i.FinalScriptWitness) is { })
|
2020-04-09 12:44:16 +02:00
|
|
|
|
return ScriptPubKeyType.SegwitP2SH;
|
2020-04-17 11:55:24 +02:00
|
|
|
|
return null;
|
2020-04-09 12:44:16 +02:00
|
|
|
|
}
|
2020-04-08 08:20:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-16 22:07:24 +02:00
|
|
|
|
public class PayjoinClientParameters
|
|
|
|
|
{
|
2020-05-17 15:21:35 +02:00
|
|
|
|
public Money MaxAdditionalFeeContribution { get; set; }
|
|
|
|
|
public FeeRate MinFeeRate { get; set; }
|
|
|
|
|
public int? AdditionalFeeOutputIndex { get; set; }
|
2020-05-16 22:07:24 +02:00
|
|
|
|
public int Version { get; set; } = 1;
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-29 17:28:22 +02:00
|
|
|
|
public class PayjoinClient
|
|
|
|
|
{
|
2020-04-09 10:38:55 +02:00
|
|
|
|
public const string PayjoinOnionNamedClient = "payjoin.onion";
|
|
|
|
|
public const string PayjoinClearnetNamedClient = "payjoin.clearnet";
|
2020-04-08 08:20:19 +02:00
|
|
|
|
public static readonly ScriptPubKeyType[] SupportedFormats = {
|
|
|
|
|
ScriptPubKeyType.Segwit,
|
|
|
|
|
ScriptPubKeyType.SegwitP2SH
|
|
|
|
|
};
|
|
|
|
|
|
2020-04-13 11:52:22 +02:00
|
|
|
|
public const string BIP21EndpointKey = "pj";
|
2020-04-08 08:20:19 +02:00
|
|
|
|
|
2020-03-29 17:28:22 +02:00
|
|
|
|
private readonly ExplorerClientProvider _explorerClientProvider;
|
2020-04-09 10:38:55 +02:00
|
|
|
|
private IHttpClientFactory _httpClientFactory;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
|
2020-04-09 10:38:55 +02:00
|
|
|
|
public PayjoinClient(ExplorerClientProvider explorerClientProvider, IHttpClientFactory httpClientFactory)
|
2020-03-29 17:28:22 +02:00
|
|
|
|
{
|
2020-05-09 12:29:05 +02:00
|
|
|
|
if (httpClientFactory == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(httpClientFactory));
|
2020-03-29 17:28:22 +02:00
|
|
|
|
_explorerClientProvider =
|
|
|
|
|
explorerClientProvider ?? throw new ArgumentNullException(nameof(explorerClientProvider));
|
2020-05-09 12:29:05 +02:00
|
|
|
|
_httpClientFactory = httpClientFactory;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-05-16 22:07:24 +02:00
|
|
|
|
public Money MaxFeeBumpContribution { get; set; }
|
2020-05-17 15:21:35 +02:00
|
|
|
|
public FeeRate MinimumFeeRate { get; set; }
|
2020-05-16 22:07:24 +02:00
|
|
|
|
|
2020-03-29 17:28:22 +02:00
|
|
|
|
public async Task<PSBT> RequestPayjoin(Uri endpoint, DerivationSchemeSettings derivationSchemeSettings,
|
|
|
|
|
PSBT originalTx, CancellationToken cancellationToken)
|
|
|
|
|
{
|
2020-05-09 12:29:05 +02:00
|
|
|
|
if (endpoint == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(endpoint));
|
|
|
|
|
if (derivationSchemeSettings == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(derivationSchemeSettings));
|
|
|
|
|
if (originalTx == null)
|
|
|
|
|
throw new ArgumentNullException(nameof(originalTx));
|
2020-04-08 11:24:04 +02:00
|
|
|
|
if (originalTx.IsAllFinalized())
|
|
|
|
|
throw new InvalidOperationException("The original PSBT should not be finalized.");
|
2020-05-16 22:07:24 +02:00
|
|
|
|
var clientParameters = new PayjoinClientParameters();
|
2020-04-08 08:20:19 +02:00
|
|
|
|
var type = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType();
|
|
|
|
|
if (!SupportedFormats.Contains(type))
|
|
|
|
|
{
|
|
|
|
|
throw new PayjoinSenderException($"The wallet does not support payjoin");
|
|
|
|
|
}
|
2020-03-29 17:28:22 +02:00
|
|
|
|
var signingAccount = derivationSchemeSettings.GetSigningAccountKeySettings();
|
2020-05-16 22:07:24 +02:00
|
|
|
|
var changeOutput = originalTx.Outputs.CoinsFor(derivationSchemeSettings.AccountDerivation, signingAccount.AccountKey, signingAccount.GetRootedKeyPath())
|
|
|
|
|
.FirstOrDefault();
|
|
|
|
|
if (changeOutput is PSBTOutput o)
|
2020-05-17 15:21:35 +02:00
|
|
|
|
clientParameters.AdditionalFeeOutputIndex = (int)o.Index;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
var sentBefore = -originalTx.GetBalance(derivationSchemeSettings.AccountDerivation,
|
|
|
|
|
signingAccount.AccountKey,
|
|
|
|
|
signingAccount.GetRootedKeyPath());
|
2020-04-06 16:21:02 +02:00
|
|
|
|
var oldGlobalTx = originalTx.GetGlobalTransaction();
|
|
|
|
|
if (!originalTx.TryGetEstimatedFeeRate(out var originalFeeRate) || !originalTx.TryGetVirtualSize(out var oldVirtualSize))
|
2020-03-29 17:28:22 +02:00
|
|
|
|
throw new ArgumentException("originalTx should have utxo information", nameof(originalTx));
|
2020-04-06 16:21:02 +02:00
|
|
|
|
var originalFee = originalTx.GetFee();
|
2020-05-17 15:21:35 +02:00
|
|
|
|
clientParameters.MaxAdditionalFeeContribution = MaxFeeBumpContribution is null ? originalFee : MaxFeeBumpContribution;
|
|
|
|
|
if (MinimumFeeRate is FeeRate v)
|
|
|
|
|
clientParameters.MinFeeRate = v;
|
2020-03-29 17:28:22 +02:00
|
|
|
|
var cloned = originalTx.Clone();
|
2020-05-16 22:07:24 +02:00
|
|
|
|
cloned.Finalize();
|
2020-03-29 17:28:22 +02:00
|
|
|
|
|
|
|
|
|
// We make sure we don't send unnecessary information to the receiver
|
|
|
|
|
foreach (var finalized in cloned.Inputs.Where(i => i.IsFinalized()))
|
|
|
|
|
{
|
|
|
|
|
finalized.ClearForFinalize();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var output in cloned.Outputs)
|
|
|
|
|
{
|
|
|
|
|
output.HDKeyPaths.Clear();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cloned.GlobalXPubs.Clear();
|
2020-05-16 22:07:24 +02:00
|
|
|
|
|
|
|
|
|
endpoint = ApplyOptionalParameters(endpoint, clientParameters);
|
2020-04-09 10:38:55 +02:00
|
|
|
|
using HttpClient client = CreateHttpClient(endpoint);
|
2020-04-08 15:40:41 +02:00
|
|
|
|
var bpuresponse = await client.PostAsync(endpoint,
|
2020-03-29 17:28:22 +02:00
|
|
|
|
new StringContent(cloned.ToHex(), Encoding.UTF8, "text/plain"), cancellationToken);
|
|
|
|
|
if (!bpuresponse.IsSuccessStatusCode)
|
|
|
|
|
{
|
|
|
|
|
var errorStr = await bpuresponse.Content.ReadAsStringAsync();
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var error = JObject.Parse(errorStr);
|
2020-05-09 12:29:05 +02:00
|
|
|
|
throw new PayjoinReceiverException(error["errorCode"].Value<string>(),
|
2020-03-29 17:28:22 +02:00
|
|
|
|
error["message"].Value<string>());
|
|
|
|
|
}
|
|
|
|
|
catch (JsonReaderException)
|
|
|
|
|
{
|
|
|
|
|
// will throw
|
|
|
|
|
bpuresponse.EnsureSuccessStatusCode();
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hex = await bpuresponse.Content.ReadAsStringAsync();
|
|
|
|
|
var newPSBT = PSBT.Parse(hex, originalTx.Network);
|
|
|
|
|
|
|
|
|
|
// Checking that the PSBT of the receiver is clean
|
|
|
|
|
if (newPSBT.GlobalXPubs.Any())
|
|
|
|
|
{
|
|
|
|
|
throw new PayjoinSenderException("GlobalXPubs should not be included in the receiver's PSBT");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (newPSBT.Outputs.Any(o => o.HDKeyPaths.Count != 0) || newPSBT.Inputs.Any(o => o.HDKeyPaths.Count != 0))
|
|
|
|
|
{
|
|
|
|
|
throw new PayjoinSenderException("Keypath information should not be included in the receiver's PSBT");
|
|
|
|
|
}
|
|
|
|
|
////////////
|
|
|
|
|
|
|
|
|
|
newPSBT = await _explorerClientProvider.UpdatePSBT(derivationSchemeSettings, newPSBT);
|
|
|
|
|
if (newPSBT.CheckSanity() is IList<PSBTError> errors2 && errors2.Count != 0)
|
|
|
|
|
{
|
|
|
|
|
throw new PayjoinSenderException($"The PSBT of the receiver is insane ({errors2[0]})");
|
|
|
|
|
}
|
|
|
|
|
// We make sure we don't sign things what should not be signed
|
|
|
|
|
foreach (var finalized in newPSBT.Inputs.Where(i => i.IsFinalized()))
|
|
|
|
|
{
|
|
|
|
|
finalized.ClearForFinalize();
|
|
|
|
|
}
|
|
|
|
|
// Make sure only the only our output have any information
|
|
|
|
|
foreach (var output in newPSBT.Outputs)
|
|
|
|
|
{
|
|
|
|
|
output.HDKeyPaths.Clear();
|
2020-05-09 12:29:05 +02:00
|
|
|
|
foreach (var originalOutput in originalTx.Outputs)
|
2020-03-29 17:28:22 +02:00
|
|
|
|
{
|
|
|
|
|
if (output.ScriptPubKey == originalOutput.ScriptPubKey)
|
|
|
|
|
output.UpdateFrom(originalOutput);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Making sure that our inputs are finalized, and that some of our inputs have not been added
|
2020-04-06 16:21:02 +02:00
|
|
|
|
var newGlobalTx = newPSBT.GetGlobalTransaction();
|
2020-03-29 17:28:22 +02:00
|
|
|
|
int ourInputCount = 0;
|
2020-04-06 16:21:02 +02:00
|
|
|
|
if (newGlobalTx.Version != oldGlobalTx.Version)
|
|
|
|
|
throw new PayjoinSenderException("The version field of the transaction has been modified");
|
|
|
|
|
if (newGlobalTx.LockTime != oldGlobalTx.LockTime)
|
|
|
|
|
throw new PayjoinSenderException("The LockTime field of the transaction has been modified");
|
2020-03-29 17:28:22 +02:00
|
|
|
|
foreach (var input in newPSBT.Inputs.CoinsFor(derivationSchemeSettings.AccountDerivation,
|
|
|
|
|
signingAccount.AccountKey, signingAccount.GetRootedKeyPath()))
|
|
|
|
|
{
|
2020-04-08 10:42:50 +02:00
|
|
|
|
if (oldGlobalTx.Inputs.FindIndexedInput(input.PrevOut) is IndexedTxIn ourInput)
|
2020-03-29 17:28:22 +02:00
|
|
|
|
{
|
|
|
|
|
ourInputCount++;
|
|
|
|
|
if (input.IsFinalized())
|
|
|
|
|
throw new PayjoinSenderException("A PSBT input from us should not be finalized");
|
2020-04-08 10:51:22 +02:00
|
|
|
|
if (newGlobalTx.Inputs[input.Index].Sequence != ourInput.TxIn.Sequence)
|
2020-04-06 16:21:02 +02:00
|
|
|
|
throw new PayjoinSenderException("The sequence of one of our input has been modified");
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
throw new PayjoinSenderException(
|
|
|
|
|
"The payjoin receiver added some of our own inputs in the proposal");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach (var input in newPSBT.Inputs)
|
|
|
|
|
{
|
2020-04-06 16:21:02 +02:00
|
|
|
|
if (originalTx.Inputs.FindIndexedInput(input.PrevOut) is null)
|
|
|
|
|
{
|
|
|
|
|
if (!input.IsFinalized())
|
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver included a non finalized input");
|
2020-04-09 12:44:16 +02:00
|
|
|
|
// Making sure that the receiver's inputs are finalized and match format
|
|
|
|
|
var payjoinInputType = input.GetInputScriptPubKeyType();
|
|
|
|
|
if (payjoinInputType is null || payjoinInputType.Value != type)
|
|
|
|
|
{
|
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver included an input that is not the same segwit input type");
|
|
|
|
|
}
|
2020-04-06 16:21:02 +02:00
|
|
|
|
}
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ourInputCount < originalTx.Inputs.Count)
|
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver removed some of our inputs");
|
|
|
|
|
|
2020-05-17 15:21:35 +02:00
|
|
|
|
if (!newPSBT.TryGetEstimatedFeeRate(out var newFeeRate) || !newPSBT.TryGetVirtualSize(out var newVirtualSize))
|
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver did not included UTXO information to calculate fee correctly");
|
|
|
|
|
|
|
|
|
|
if (clientParameters.MinFeeRate is FeeRate minFeeRate)
|
|
|
|
|
{
|
|
|
|
|
if (newFeeRate < minFeeRate)
|
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver created a payjoin with a too low fee rate");
|
|
|
|
|
}
|
|
|
|
|
|
2020-03-29 17:28:22 +02:00
|
|
|
|
var sentAfter = -newPSBT.GetBalance(derivationSchemeSettings.AccountDerivation,
|
|
|
|
|
signingAccount.AccountKey,
|
|
|
|
|
signingAccount.GetRootedKeyPath());
|
|
|
|
|
if (sentAfter > sentBefore)
|
|
|
|
|
{
|
2020-04-06 16:21:02 +02:00
|
|
|
|
var overPaying = sentAfter - sentBefore;
|
|
|
|
|
var additionalFee = newPSBT.GetFee() - originalFee;
|
|
|
|
|
if (overPaying > additionalFee)
|
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver is sending more money to himself");
|
2020-05-17 15:21:35 +02:00
|
|
|
|
if (overPaying > clientParameters.MaxAdditionalFeeContribution)
|
2020-05-16 22:07:24 +02:00
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver is making us pay too much fee");
|
2020-04-06 16:21:02 +02:00
|
|
|
|
|
2020-03-29 17:28:22 +02:00
|
|
|
|
// Let's check the difference is only for the fee and that feerate
|
|
|
|
|
// did not changed that much
|
2020-04-06 16:21:02 +02:00
|
|
|
|
var expectedFee = originalFeeRate.GetFee(newVirtualSize);
|
2020-03-29 17:28:22 +02:00
|
|
|
|
// Signing precisely is hard science, give some breathing room for error.
|
2020-04-06 16:21:02 +02:00
|
|
|
|
expectedFee += originalFeeRate.GetFee(newPSBT.Inputs.Count * 2);
|
2020-04-07 08:10:19 +02:00
|
|
|
|
if (overPaying > (expectedFee - originalFee))
|
2020-04-06 16:21:02 +02:00
|
|
|
|
throw new PayjoinSenderException("The payjoin receiver increased the fee rate we are paying too much");
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return newPSBT;
|
|
|
|
|
}
|
2020-04-09 10:38:55 +02:00
|
|
|
|
|
2020-05-16 22:07:24 +02:00
|
|
|
|
private static Uri ApplyOptionalParameters(Uri endpoint, PayjoinClientParameters clientParameters)
|
|
|
|
|
{
|
|
|
|
|
var requestUri = endpoint.AbsoluteUri;
|
|
|
|
|
if (requestUri.IndexOf('?', StringComparison.OrdinalIgnoreCase) is int i && i != -1)
|
|
|
|
|
requestUri = requestUri.Substring(0, i);
|
|
|
|
|
List<string> parameters = new List<string>(3);
|
|
|
|
|
parameters.Add($"v={clientParameters.Version}");
|
2020-05-17 15:21:35 +02:00
|
|
|
|
if (clientParameters.AdditionalFeeOutputIndex is int additionalFeeOutputIndex)
|
|
|
|
|
parameters.Add($"additionalfeeoutputindex={additionalFeeOutputIndex.ToString(CultureInfo.InvariantCulture)}");
|
|
|
|
|
if (clientParameters.MaxAdditionalFeeContribution is Money maxAdditionalFeeContribution)
|
|
|
|
|
parameters.Add($"maxadditionalfeecontribution={maxAdditionalFeeContribution.Satoshi.ToString(CultureInfo.InvariantCulture)}");
|
|
|
|
|
if (clientParameters.MinFeeRate is FeeRate minFeeRate)
|
|
|
|
|
parameters.Add($"minfeerate={minFeeRate.SatoshiPerByte.ToString(CultureInfo.InvariantCulture)}");
|
2020-05-16 22:07:24 +02:00
|
|
|
|
endpoint = new Uri($"{requestUri}?{string.Join('&', parameters)}");
|
|
|
|
|
return endpoint;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-09 10:38:55 +02:00
|
|
|
|
private HttpClient CreateHttpClient(Uri uri)
|
|
|
|
|
{
|
|
|
|
|
if (uri.IsOnion())
|
|
|
|
|
return _httpClientFactory.CreateClient(PayjoinOnionNamedClient);
|
|
|
|
|
else
|
|
|
|
|
return _httpClientFactory.CreateClient(PayjoinClearnetNamedClient);
|
|
|
|
|
}
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class PayjoinException : Exception
|
|
|
|
|
{
|
|
|
|
|
public PayjoinException(string message) : base(message)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-09 12:29:05 +02:00
|
|
|
|
public enum PayjoinReceiverWellknownErrors
|
|
|
|
|
{
|
|
|
|
|
LeakingData,
|
|
|
|
|
PSBTNotFinalized,
|
|
|
|
|
Unavailable,
|
|
|
|
|
OutOfUTXOS,
|
|
|
|
|
NotEnoughMoney,
|
|
|
|
|
InsanePSBT,
|
2020-05-09 12:59:21 +02:00
|
|
|
|
VersionUnsupported,
|
2020-05-12 14:42:51 +02:00
|
|
|
|
NeedUTXOInformation,
|
|
|
|
|
InvalidTransaction
|
2020-05-09 12:29:05 +02:00
|
|
|
|
}
|
2020-03-29 17:28:22 +02:00
|
|
|
|
public class PayjoinReceiverException : PayjoinException
|
|
|
|
|
{
|
2020-05-09 12:29:05 +02:00
|
|
|
|
public PayjoinReceiverException(string errorCode, string receiverDebugMessage) : base(FormatMessage(errorCode, receiverDebugMessage))
|
2020-03-29 17:28:22 +02:00
|
|
|
|
{
|
|
|
|
|
ErrorCode = errorCode;
|
2020-05-09 12:29:05 +02:00
|
|
|
|
ReceiverDebugMessage = receiverDebugMessage;
|
|
|
|
|
WellknownError = errorCode switch
|
|
|
|
|
{
|
|
|
|
|
"leaking-data" => PayjoinReceiverWellknownErrors.LeakingData,
|
|
|
|
|
"psbt-not-finalized" => PayjoinReceiverWellknownErrors.PSBTNotFinalized,
|
|
|
|
|
"unavailable" => PayjoinReceiverWellknownErrors.Unavailable,
|
|
|
|
|
"out-of-utxos" => PayjoinReceiverWellknownErrors.OutOfUTXOS,
|
|
|
|
|
"not-enough-money" => PayjoinReceiverWellknownErrors.NotEnoughMoney,
|
|
|
|
|
"insane-psbt" => PayjoinReceiverWellknownErrors.InsanePSBT,
|
|
|
|
|
"version-unsupported" => PayjoinReceiverWellknownErrors.VersionUnsupported,
|
2020-05-09 12:59:21 +02:00
|
|
|
|
"need-utxo-information" => PayjoinReceiverWellknownErrors.NeedUTXOInformation,
|
2020-05-12 14:42:51 +02:00
|
|
|
|
"invalid-transaction" => PayjoinReceiverWellknownErrors.InvalidTransaction,
|
2020-05-09 12:29:05 +02:00
|
|
|
|
_ => null
|
|
|
|
|
};
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
public string ErrorCode { get; }
|
|
|
|
|
public string ErrorMessage { get; }
|
2020-05-09 12:29:05 +02:00
|
|
|
|
public string ReceiverDebugMessage { get; }
|
2020-03-29 17:28:22 +02:00
|
|
|
|
|
2020-05-09 12:29:05 +02:00
|
|
|
|
public PayjoinReceiverWellknownErrors? WellknownError
|
2020-03-29 17:28:22 +02:00
|
|
|
|
{
|
2020-05-09 12:29:05 +02:00
|
|
|
|
get;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string FormatMessage(string errorCode, string receiverDebugMessage)
|
|
|
|
|
{
|
|
|
|
|
return $"{errorCode}: {GetMessage(errorCode)}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string GetMessage(string errorCode)
|
|
|
|
|
{
|
|
|
|
|
return errorCode switch
|
|
|
|
|
{
|
|
|
|
|
"leaking-data" => "Key path information or GlobalXPubs should not be included in the original PSBT.",
|
|
|
|
|
"psbt-not-finalized" => "The original PSBT must be finalized.",
|
|
|
|
|
"unavailable" => "The payjoin endpoint is not available for now.",
|
|
|
|
|
"out-of-utxos" => "The receiver does not have any UTXO to contribute in a payjoin proposal.",
|
|
|
|
|
"not-enough-money" => "The receiver added some inputs but could not bump the fee of the payjoin proposal.",
|
|
|
|
|
"insane-psbt" => "Some consistency check on the PSBT failed.",
|
|
|
|
|
"version-unsupported" => "This version of payjoin is not supported.",
|
2020-05-09 12:59:21 +02:00
|
|
|
|
"need-utxo-information" => "The witness UTXO or non witness UTXO is missing",
|
2020-05-12 14:42:51 +02:00
|
|
|
|
"invalid-transaction" => "The original transaction is invalid for payjoin",
|
2020-05-09 12:29:05 +02:00
|
|
|
|
_ => "Unknown error"
|
|
|
|
|
};
|
2020-03-29 17:28:22 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public class PayjoinSenderException : PayjoinException
|
|
|
|
|
{
|
|
|
|
|
public PayjoinSenderException(string message) : base(message)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|