Skip to content

Commit

Permalink
More flexible wallets API
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasDorier committed Dec 5, 2023
1 parent 8ec608f commit 9519eef
Show file tree
Hide file tree
Showing 18 changed files with 1,192 additions and 201 deletions.
150 changes: 111 additions & 39 deletions NBXplorer.Client/ExplorerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
using NBitcoin.RPC;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Diagnostics;

namespace NBXplorer
{
Expand Down Expand Up @@ -172,7 +173,8 @@ public PruneResponse Prune(DerivationStrategyBase extKey, PruneRequest pruneRequ
return PruneAsync(extKey, pruneRequest, cancellation).GetAwaiter().GetResult();
}

internal class RawStr {
internal class RawStr
{
private string str;
public RawStr(string str)
{
Expand Down Expand Up @@ -269,7 +271,7 @@ public void Track(DerivationStrategyBase strategy, CancellationToken cancellatio
}
public Task TrackAsync(DerivationStrategyBase strategy, CancellationToken cancellation = default)
{
return TrackAsync(TrackedSource.Create(strategy), cancellation);
return TrackAsync(TrackedSource.Create(strategy), cancellation: cancellation);
}

public void Track(DerivationStrategyBase strategy, TrackWalletRequest trackDerivationRequest, CancellationToken cancellation = default)
Expand All @@ -285,14 +287,13 @@ public async Task TrackAsync(DerivationStrategyBase strategy, TrackWalletRequest

public void Track(TrackedSource trackedSource, CancellationToken cancellation = default)
{
TrackAsync(trackedSource, cancellation).GetAwaiter().GetResult();
TrackAsync(trackedSource, cancellation: cancellation).GetAwaiter().GetResult();
}
public Task TrackAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
public Task TrackAsync(TrackedSource trackedSource, TrackWalletRequest trackDerivationRequest = null, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));

return SendAsync<string>(HttpMethod.Post, null, GetBasePath(trackedSource), cancellation);
return SendAsync<string>(HttpMethod.Post, trackDerivationRequest, GetBasePath(trackedSource), cancellation);
}

private Exception UnSupported(TrackedSource trackedSource)
Expand All @@ -311,7 +312,7 @@ public GetBalanceResponse GetBalance(DerivationStrategyBase userDerivationScheme
}
public Task<GetBalanceResponse> GetBalanceAsync(DerivationStrategyBase userDerivationScheme, CancellationToken cancellation = default)
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{userDerivationScheme}/balance", cancellation);
return GetBalanceAsync(TrackedSource.Create(userDerivationScheme), cancellation);
}


Expand All @@ -321,9 +322,12 @@ public GetBalanceResponse GetBalance(BitcoinAddress address, CancellationToken c
}
public Task<GetBalanceResponse> GetBalanceAsync(BitcoinAddress address, CancellationToken cancellation = default)
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{address}/balance", cancellation);
return GetBalanceAsync(TrackedSource.Create(address), cancellation);
}
public Task<GetBalanceResponse> GetBalanceAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/balance", cancellation);
}

public Task CancelReservationAsync(DerivationStrategyBase strategy, KeyPath[] keyPaths, CancellationToken cancellation = default)
{
return SendAsync<string>(HttpMethod.Post, keyPaths, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/addresses/cancelreservation", cancellation);
Expand Down Expand Up @@ -363,18 +367,7 @@ public Task<GetTransactionsResponse> GetTransactionsAsync(DerivationStrategyBase
}
public Task<GetTransactionsResponse> GetTransactionsAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
{
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions", cancellation);
}
else if (trackedSource is AddressTrackedSource asts)
{
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions", cancellation);
}
else
throw UnSupported(trackedSource);
return SendAsync<GetTransactionsResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions", cancellation);
}


Expand All @@ -399,16 +392,25 @@ public Task<TransactionInformation> GetTransactionAsync(TrackedSource trackedSou
throw new ArgumentNullException(nameof(txId));
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
if (trackedSource is DerivationSchemeTrackedSource dsts)
{
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}/transactions/{txId}", cancellation);
}
else if (trackedSource is AddressTrackedSource asts)
{
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}/transactions/{txId}", cancellation);
}
else
throw UnSupported(trackedSource);
return SendAsync<TransactionInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/transactions/{txId}", cancellation);
}

public async Task AssociateScriptsAsync(TrackedSource trackedSource, AssociateScriptRequest[] scripts, CancellationToken cancellation = default)
{
if (scripts == null)
throw new ArgumentNullException(nameof(scripts));
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
await SendAsync(HttpMethod.Post, scripts, $"{GetBasePath(trackedSource)}/associate", cancellation);
}

public async Task ImportUTXOs(TrackedSource trackedSource, ImportUTXORequest request, CancellationToken cancellation = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
await SendAsync(HttpMethod.Post, request, $"{GetBasePath(trackedSource)}/import-utxos", cancellation);
}

public Task RescanAsync(RescanRequest rescanRequest, CancellationToken cancellation = default)
Expand Down Expand Up @@ -447,16 +449,17 @@ public KeyPathInformation GetKeyInformation(DerivationStrategyBase strategy, Scr

public async Task<KeyPathInformation> GetKeyInformationAsync(DerivationStrategyBase strategy, Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{strategy}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
return await GetKeyInformationAsync(new DerivationSchemeTrackedSource(strategy), script, cancellation).ConfigureAwait(false);
}
public async Task<KeyPathInformation> GetKeyInformationAsync(TrackedSource trackedSource, Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
}

[Obsolete("Use GetKeyInformationAsync(DerivationStrategyBase strategy, Script script) instead")]
public async Task<KeyPathInformation[]> GetKeyInformationsAsync(Script script, CancellationToken cancellation = default)
{
return await SendAsync<KeyPathInformation[]>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/scripts/{script.ToHex()}", cancellation).ConfigureAwait(false);
}

[Obsolete("Use GetKeyInformation(DerivationStrategyBase strategy, Script script) instead")]
public KeyPathInformation[] GetKeyInformations(Script script, CancellationToken cancellation = default)
{
return GetKeyInformationsAsync(script, cancellation).GetAwaiter().GetResult();
Expand Down Expand Up @@ -563,6 +566,53 @@ public GenerateWalletResponse GenerateWallet(GenerateWalletRequest request = nul
return GenerateWalletAsync(request, cancellationToken).GetAwaiter().GetResult();
}

public async Task<TrackedSource[]> GetChildWallets(TrackedSource trackedSource,
CancellationToken cancellation = default)
{
return await GetAsync<TrackedSource[]>($"{GetBasePath(trackedSource)}/children", cancellation);
}
public async Task<TrackedSource[]> GetParentWallets(TrackedSource trackedSource,
CancellationToken cancellation = default)
{
return await GetAsync<TrackedSource[]>($"{GetBasePath(trackedSource)}/parents", cancellation);
}
public async Task AddChildWallet(TrackedSource trackedSource, TrackedSource childWallet, CancellationToken cancellation = default)
{
var request = new TrackedSourceRequest()
{
TrackedSource = childWallet
};
await SendAsync(HttpMethod.Post, request, $"{GetBasePath(trackedSource)}/children", cancellation);
}

public async Task AddParentWallet(TrackedSource trackedSource, TrackedSource parentWallet,
CancellationToken cancellation = default)
{
var request = new TrackedSourceRequest()
{
TrackedSource = parentWallet
};
await SendAsync(HttpMethod.Post, request, $"{GetBasePath(trackedSource)}/parents", cancellation);
}
public async Task RemoveChildWallet(TrackedSource trackedSource, TrackedSource childWallet, CancellationToken cancellation = default)
{
var request = new TrackedSourceRequest()
{
TrackedSource = childWallet
};
await SendAsync(HttpMethod.Delete, request, $"{GetBasePath(trackedSource)}/children", cancellation);
}

public async Task RemoveParentWallet(TrackedSource trackedSource, TrackedSource parentWallet,
CancellationToken cancellation = default)
{
var request = new TrackedSourceRequest()
{
TrackedSource = parentWallet
};
await SendAsync(HttpMethod.Delete, request, $"{GetBasePath(trackedSource)}/parents", cancellation);
}

private static readonly HttpClient SharedClient = new HttpClient();
internal HttpClient Client = SharedClient;

Expand Down Expand Up @@ -611,8 +661,8 @@ static FormattableString EncodeUrlParameters(FormattableString url)
return FormattableStringFactory.Create(
url.Format,
url.GetArguments()
.Select(a =>
a is RawStr ? a :
.Select(a =>
a is RawStr ? a :
a is FormattableString o ? EncodeUrlParameters(o) :
Uri.EscapeDataString(a?.ToString() ?? ""))
.ToArray());
Expand Down Expand Up @@ -653,11 +703,30 @@ internal async Task<T> SendAsync<T>(HttpMethod method, object body, FormattableS
if (Auth.RefreshCache())
{
message = CreateMessage(method, body, relativePath);
result = await Client.SendAsync(message).ConfigureAwait(false);
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
}
}
return await ParseResponse<T>(result).ConfigureAwait(false);
}
internal async Task SendAsync(HttpMethod method, object body, FormattableString relativePath, CancellationToken cancellation)
{
var message = CreateMessage(method, body, relativePath);
var result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);

if (result.StatusCode == HttpStatusCode.GatewayTimeout || result.StatusCode == HttpStatusCode.RequestTimeout)
{
throw new HttpRequestException($"HTTP error {(int)result.StatusCode}", new TimeoutException());
}
if ((int)result.StatusCode == 401)
{
if (Auth.RefreshCache())
{
message = CreateMessage(method, body, relativePath);
result = await Client.SendAsync(message, cancellation).ConfigureAwait(false);
}
}
await ParseResponse(result).ConfigureAwait(false);
}

internal HttpRequestMessage CreateMessage(HttpMethod method, object body, FormattableString relativePath)
{
Expand Down Expand Up @@ -717,11 +786,14 @@ private async Task ParseResponse(HttpResponseMessage response)

private FormattableString GetBasePath(TrackedSource trackedSource)
{
if (trackedSource is null)
throw new ArgumentNullException(nameof(trackedSource));
return trackedSource switch
{
DerivationSchemeTrackedSource dsts => $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}",
AddressTrackedSource asts => $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}",
_ => throw UnSupported(trackedSource)
WalletTrackedSource wts => $"v1/cryptos/{CryptoCode}/wallets/{wts.WalletId}",
_ => $"v1/cryptos/{CryptoCode}/tracked-sources/{trackedSource}"
};
}
}
Expand Down
2 changes: 2 additions & 0 deletions NBXplorer.Client/Models/GenerateWalletRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ public class GenerateWalletRequest
public bool ImportKeysToRPC { get; set; }
public bool SavePrivateKeys { get; set; }
public Dictionary<string, string> AdditionalOptions { get; set; }

public TrackedSource ParentWallet { get; set; }
}
}
20 changes: 20 additions & 0 deletions NBXplorer.Client/Models/ImportUTXORequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using NBitcoin;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace NBXplorer.Models;

public class ImportUTXORequest
{
[JsonProperty("UTXOs")]
public OutPoint[] Utxos { get; set; }

public MerkleBlock[] Proofs { get; set; }
}

public class AssociateScriptRequest
{
public IDestination Destination { get; set; }
public bool Used { get; set; }
public JObject Metadata { get; set; }
}
1 change: 1 addition & 0 deletions NBXplorer.Client/Models/TrackWalletRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class TrackWalletRequest
{
public TrackDerivationOption[] DerivationOptions { get; set; }
public bool Wait { get; set; } = false;
public TrackedSource ParentWallet { get; set; }
}

public class TrackDerivationOption
Expand Down
40 changes: 40 additions & 0 deletions NBXplorer.Client/Models/TrackedSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ public static bool TryParse(string str, out TrackedSource trackedSource, NBXplor
return false;
trackedSource = addressTrackedSource;
}
else if (strSpan.StartsWith("WALLET:".AsSpan(), StringComparison.Ordinal))
{
if (!WalletTrackedSource.TryParse(strSpan, out var walletTrackedSource))
return false;
trackedSource = walletTrackedSource;
}
else
{
return false;
Expand Down Expand Up @@ -97,6 +103,40 @@ public static TrackedSource Parse(string str, NBXplorerNetwork network)
}
}

public class WalletTrackedSource : TrackedSource
{
public string WalletId { get; }

public WalletTrackedSource(string walletId)
{
WalletId = walletId;
}

public static bool TryParse(ReadOnlySpan<char> strSpan, out WalletTrackedSource walletTrackedSource)
{
if (strSpan == null)
throw new ArgumentNullException(nameof(strSpan));
walletTrackedSource = null;
if (!strSpan.StartsWith("WALLET:".AsSpan(), StringComparison.Ordinal))
return false;
try
{
walletTrackedSource = new WalletTrackedSource(strSpan.Slice("WALLET:".Length).ToString());
return true;
}
catch { return false; }
}

public override string ToString()
{
return "WALLET:" + WalletId;
}
public override string ToPrettyString()
{
return WalletId;
}
}

public class AddressTrackedSource : TrackedSource, IDestination
{
// Note that we should in theory access BitcoinAddress. But parsing BitcoinAddress is very expensive, so we keep storing plain strings
Expand Down
6 changes: 6 additions & 0 deletions NBXplorer.Client/Models/TrackedSourceRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace NBXplorer.Models;

public class TrackedSourceRequest
{
public TrackedSource TrackedSource { get; set; }
}
Loading

0 comments on commit 9519eef

Please sign in to comment.