Skip to content

Commit

Permalink
add hierarchy management API
Browse files Browse the repository at this point in the history
  • Loading branch information
Kukks authored and NicolasDorier committed Nov 26, 2023
1 parent a1a2ffb commit 371505d
Show file tree
Hide file tree
Showing 8 changed files with 210 additions and 18 deletions.
49 changes: 48 additions & 1 deletion NBXplorer.Client/ExplorerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,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 @@ -738,7 +785,7 @@ private FormattableString GetBasePath(TrackedSource trackedSource)
DerivationSchemeTrackedSource dsts => $"v1/cryptos/{CryptoCode}/derivations/{dsts.DerivationStrategy}",
AddressTrackedSource asts => $"v1/cryptos/{CryptoCode}/addresses/{asts.Address}",
WalletTrackedSource wts => $"v1/cryptos/{CryptoCode}/wallets/{wts.WalletId}",
_ => throw UnSupported(trackedSource)
_ => $"v1/cryptos/{CryptoCode}/tracked-sources/{trackedSource}",
};
}
}
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; }
}
48 changes: 48 additions & 0 deletions NBXplorer.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4678,6 +4678,54 @@ await Eventually(async () =>
var kpi = await tester.Client.GetKeyInformationsAsync(addressA.ScriptPubKey, CancellationToken.None);
var tss = kpi.Select(information => information.TrackedSource);
Assert.True(tss.Distinct().Count() == tss.Count(), "The result should only distinct tracked source matches. While this endpoint is marked obsolete, the same logic is used to trigger events, which means there will be duplicated events when the script is matched against");

var parentsOfC = await tester.Client.GetParentWallets(walletC);
Assert.Equal(2, parentsOfC.Length);
Assert.Contains(parentsOfC, w => w == walletA);
Assert.Contains(parentsOfC, w => w == walletB);

var parentsOfB = await tester.Client.GetParentWallets(walletB);
Assert.Equal(1, parentsOfB.Length);
Assert.Contains(parentsOfB, w => w == walletA);

var parentsOfA = await tester.Client.GetParentWallets(walletA);
Assert.Empty(parentsOfA);

var childrenOfA= await tester.Client.GetChildWallets(walletA);
Assert.Equal(2, childrenOfA.Length);

Assert.Contains(childrenOfA, w => w == walletB);
Assert.Contains(childrenOfA, w => w == walletC);

var childrenOfB= await tester.Client.GetChildWallets(walletB);
Assert.Equal(1, childrenOfB.Length);
Assert.Contains(childrenOfB, w => w == walletC);

var childrenOfC = await tester.Client.GetChildWallets(walletC);
Assert.Empty(childrenOfC);

await tester.Client.RemoveParentWallet(walletB, walletA);
await tester.Client.RemoveChildWallet(walletB, walletC);

parentsOfB = await tester.Client.GetParentWallets(walletB);
Assert.Empty(parentsOfB);

childrenOfB = await tester.Client.GetChildWallets(walletB);
Assert.Empty(childrenOfB);


await tester.Client.AddParentWallet(walletB, walletA);
await tester.Client.AddChildWallet(walletB, walletC);


childrenOfB= await tester.Client.GetChildWallets(walletB);
Assert.Equal(1, childrenOfB.Length);
Assert.Contains(childrenOfB, w => w == walletC);

parentsOfB = await tester.Client.GetParentWallets(walletB);
Assert.Equal(1, parentsOfB.Length);
Assert.Contains(parentsOfB, w => w == walletA);

}
}
}
33 changes: 32 additions & 1 deletion NBXplorer/Backends/Postgres/PostgresRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,36 @@ internal WalletKey GetWalletKey(TrackedSource source)
_ => throw new NotSupportedException(source.GetType().ToString())
};
}

internal TrackedSource GetTrackedSource(WalletKey walletKey)
{
var metadata = JObject.Parse(walletKey.metadata);
if (metadata.TryGetValue("type", StringComparison.OrdinalIgnoreCase, out JToken typeJToken) &&
typeJToken.Value<string>() is { } type)
{
if ((metadata.TryGetValue("code", StringComparison.OrdinalIgnoreCase, out JToken codeJToken) &&
codeJToken.Value<string>() is { } code) && !code.Equals(Network.CryptoCode,
StringComparison.InvariantCultureIgnoreCase))
{
return null;
}

switch (type)
{
case "NBXv1-Derivation":
var derivation = metadata["derivation"].Value<string>();
return new DerivationSchemeTrackedSource(Network.DerivationStrategyFactory.Parse(derivation));
case "NBXv1-Address":
var address = metadata["address"].Value<string>();
return new AddressTrackedSource(BitcoinAddress.Create(address, Network.NBitcoinNetwork));
case "Wallet":
return new WalletTrackedSource(walletKey.wid);
}
}

return null;
}

public async Task AssociateScriptsToWalletExplicitly(TrackedSource trackedSource,
Dictionary<IDestination, bool> scripts)
{
Expand Down Expand Up @@ -1304,8 +1334,9 @@ public async Task EnsureWalletCreated(DerivationStrategyBase strategy)
await EnsureWalletCreated(GetWalletKey(strategy));
}

public async Task EnsureWalletCreated(TrackedSource trackedSource, TrackedSource[] parentTrackedSource = null)
public async Task EnsureWalletCreated(TrackedSource trackedSource, params TrackedSource[] parentTrackedSource)
{
parentTrackedSource = parentTrackedSource.Where(source => source is not null).ToArray();
await EnsureWalletCreated(GetWalletKey(trackedSource), parentTrackedSource?.Select(GetWalletKey).ToArray());
}

Expand Down
11 changes: 3 additions & 8 deletions NBXplorer/Controllers/MainController.PSBT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task<IActionResult> CreatePSBT(
{
if (body == null)
throw new ArgumentNullException(nameof(body));
CreatePSBTRequest request = ParseJObject<CreatePSBTRequest>(body, trackedSourceContext.Network);
CreatePSBTRequest request = trackedSourceContext.Network.ParseJObject<CreatePSBTRequest>(body);

var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network);
var txBuilder = request.Seed is int s ? trackedSourceContext.Network.NBitcoinNetwork.CreateTransactionBuilder(s)
Expand Down Expand Up @@ -388,7 +388,7 @@ public async Task<IActionResult> UpdatePSBT(
[FromBody]
JObject body)
{
var update = ParseJObject<UpdatePSBTRequest>(body, network);
var update = network.ParseJObject<UpdatePSBTRequest>(body);
if (update.PSBT == null)
throw new NBXplorerException(new NBXplorerError(400, "missing-parameter", "'psbt' is missing"));
await UpdatePSBTCore(update, network);
Expand Down Expand Up @@ -586,11 +586,6 @@ await Task.WhenAll(update.PSBT.Inputs
}
}

protected T ParseJObject<T>(JObject requestObj, NBXplorerNetwork network)
{
if (requestObj == null)
return default;
return network.Serializer.ToObject<T>(requestObj);
}

}
}
13 changes: 5 additions & 8 deletions NBXplorer/Controllers/MainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,6 @@ private bool HasTxIndex(string cryptoCode)
}

[HttpPost]

[Route($"{CommonRoutes.DerivationEndpoint}")]
[Route($"{CommonRoutes.AddressEndpoint}")]
[Route($"{CommonRoutes.WalletEndpoint}")]
Expand All @@ -512,7 +511,7 @@ public async Task<IActionResult> TrackWallet(
TrackedSourceContext trackedSourceContext,
[FromBody] JObject rawRequest = null)
{
var request = ParseJObject<TrackWalletRequest>(rawRequest ?? new JObject(), trackedSourceContext.Network);
var request = trackedSourceContext.Network.ParseJObject<TrackWalletRequest>(rawRequest ?? new JObject());

var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network);
if (repo is PostgresRepository postgresRepository &&
Expand All @@ -524,7 +523,7 @@ public async Task<IActionResult> TrackWallet(
throw new NBXplorerException(new NBXplorerError(400, "parent-wallet-same-as-tracked-source",
"Parent wallets cannot be the same as the tracked source"));
}
await postgresRepository.EnsureWalletCreated(trackedSourceContext.TrackedSource, request?.ParentWallet is null? null: new []{request?.ParentWallet });
await postgresRepository.EnsureWalletCreated(trackedSourceContext.TrackedSource, request?.ParentWallet);
}
if (repo is not PostgresRepository && request.ParentWallet is not null)
throw new NBXplorerException(new NBXplorerError(400, "parent-wallet-not-supported",
Expand Down Expand Up @@ -586,9 +585,7 @@ public async Task<IActionResult> GetTransactions(
bool includeTransaction = true)
{
TransactionInformation fetchedTransactionInfo = null;

var repo = RepositoryProvider.GetRepository(trackedSourceContext.Network);

var repo = trackedSourceContext.Repository;
var response = new GetTransactionsResponse();
int currentHeight = (await repo.GetTip()).Height;
response.Height = currentHeight;
Expand Down Expand Up @@ -676,7 +673,7 @@ public async Task<IActionResult> Rescan(TrackedSourceContext trackedSourceContex
{
if (body == null)
throw new ArgumentNullException(nameof(body));
var rescanRequest = ParseJObject<RescanRequest>(body, trackedSourceContext.Network);
var rescanRequest = trackedSourceContext.Network.ParseJObject<RescanRequest>(body);
if (rescanRequest == null)
throw new ArgumentNullException(nameof(rescanRequest));
if (rescanRequest?.Transactions == null)
Expand Down Expand Up @@ -1036,7 +1033,7 @@ public async Task<BroadcastResult> Broadcast(
[TrackedSourceContext.TrackedSourceContextRequirement(false, false)]
public async Task<IActionResult> GenerateWallet(TrackedSourceContext trackedSourceContext, [FromBody] JObject rawRequest = null)
{
var request = ParseJObject<GenerateWalletRequest>(rawRequest, trackedSourceContext.Network) ?? new GenerateWalletRequest();
var request = trackedSourceContext.Network.ParseJObject<GenerateWalletRequest>(rawRequest ) ?? new GenerateWalletRequest();

if (request.ImportKeysToRPC && trackedSourceContext.RpcClient is null)
{
Expand Down
60 changes: 60 additions & 0 deletions NBXplorer/Controllers/PostgresMainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using NBXplorer.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Serialization;

namespace NBXplorer.Controllers
{
Expand Down Expand Up @@ -256,5 +257,64 @@ public async Task<IActionResult> GetUTXOs( TrackedSourceContext trackedSourceCon
}
return Json(changes, trackedSourceContext.Network.JsonSerializerSettings);
}


[HttpGet("children")]
public async Task<IActionResult> GetWalletChildren( TrackedSourceContext trackedSourceContext)
{
var repo = (PostgresRepository)trackedSourceContext.Repository;
await using var conn = await ConnectionFactory.CreateConnection();
var children = await conn.QueryAsync($"SELECT w.wallet_id, w.metadata FROM wallets_wallets ww JOIN wallets w ON ww.wallet_id = w.wallet_id WHERE ww.parent_id=@walletId", new { walletId = repo.GetWalletKey(trackedSourceContext.TrackedSource).wid });

return Json(children.Select(c => repo.GetTrackedSource(new PostgresRepository.WalletKey(c.wallet_id, c.metadata)) ).ToArray(), trackedSourceContext.Network.JsonSerializerSettings);
}
[HttpGet("parents")]
public async Task<IActionResult> GetWalletParents( TrackedSourceContext trackedSourceContext)
{
var repo = (PostgresRepository)trackedSourceContext.Repository;
await using var conn = await ConnectionFactory.CreateConnection();
var children = await conn.QueryAsync($"SELECT w.wallet_id, w.metadata FROM wallets_wallets ww JOIN wallets w ON ww.parent_id = w.wallet_id WHERE ww.wallet_id=@walletId", new { walletId = repo.GetWalletKey(trackedSourceContext.TrackedSource).wid });

return Json(children.Select(c => repo.GetTrackedSource(new PostgresRepository.WalletKey(c.wallet_id, c.metadata)) ).ToArray(), trackedSourceContext.Network.JsonSerializerSettings);
}
[HttpPost("children")]
public async Task<IActionResult> AddWalletChild( TrackedSourceContext trackedSourceContext, [FromBody] JObject request)
{
var trackedSource = trackedSourceContext.Network.ParseJObject<TrackedSourceRequest>(request).TrackedSource;
var repo = (PostgresRepository)trackedSourceContext.Repository;
await repo.EnsureWalletCreated(trackedSource, trackedSourceContext.TrackedSource);
return Ok();
}
[HttpPost("parents")]
public async Task<IActionResult> AddWalletParent( TrackedSourceContext trackedSourceContext, [FromBody] JObject request)
{
var trackedSource = trackedSourceContext.Network.ParseJObject<TrackedSourceRequest>(request).TrackedSource;
var repo = (PostgresRepository)trackedSourceContext.Repository;
await repo.EnsureWalletCreated(trackedSourceContext.TrackedSource, trackedSource);
return Ok();
}
[HttpDelete("children")]
public async Task<IActionResult> RemoveWalletChild( TrackedSourceContext trackedSourceContext, [FromBody] JObject request)
{
var repo = (PostgresRepository)trackedSourceContext.Repository;

var trackedSource = repo.GetWalletKey(trackedSourceContext.Network
.ParseJObject<TrackedSourceRequest>(request).TrackedSource);
var conn = await ConnectionFactory.CreateConnection();
await conn.ExecuteAsync($"DELETE FROM wallets_wallets WHERE wallet_id=@walletId AND parent_id=@parentId", new { walletId = trackedSource.wid, parentId = repo.GetWalletKey(trackedSourceContext.TrackedSource).wid });
return Ok();
}
[HttpDelete("parents")]
public async Task<IActionResult> RemoveWalletParent( TrackedSourceContext trackedSourceContext, [FromBody] JObject request)
{
var repo = (PostgresRepository)trackedSourceContext.Repository;

var trackedSource = repo.GetWalletKey(trackedSourceContext.Network
.ParseJObject<TrackedSourceRequest>(request).TrackedSource);
var conn = await ConnectionFactory.CreateConnection();
await conn.ExecuteAsync($"DELETE FROM wallets_wallets WHERE wallet_id=@walletId AND parent_id=@parentId", new { walletId = repo.GetWalletKey(trackedSourceContext.TrackedSource).wid, parentId = trackedSource.wid });
return Ok();
}
}

}
8 changes: 8 additions & 0 deletions NBXplorer/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,19 @@
using Npgsql;
using NBitcoin.Altcoins;
using System.Threading;
using Newtonsoft.Json.Linq;

namespace NBXplorer
{
public static class Extensions
{
public static T ParseJObject<T>(this NBXplorerNetwork network, JObject requestObj)
{
if (requestObj == null)
return default;
return network.Serializer.ToObject<T>(requestObj);
}

public static async Task<NpgsqlConnection> ReliableOpenConnectionAsync(this NpgsqlDataSource ds, CancellationToken cancellationToken = default)
{
int maxRetries = 10;
Expand Down

0 comments on commit 371505d

Please sign in to comment.