Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions NBXplorer.Client/ExplorerClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,17 @@ public RawStr(string str)
public override string ToString() => str;
};
internal static RawStr Raw(string str) => new RawStr(str);
public async Task ScanUTXOSetAsync(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)

public async Task ScanUTXOSetAsync(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null,
int? fromIndex = null, CancellationToken cancellation = default)
{
if (extKey == null)
throw new ArgumentNullException(nameof(extKey));
await ScanUTXOSetAsync(new DerivationSchemeTrackedSource(extKey), batchSize, gapLimit, fromIndex,
cancellation);
}
public async Task ScanUTXOSetAsync(TrackedSource trackedSource, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
{
if (trackedSource == null)
throw new ArgumentNullException(nameof(trackedSource));
List<string> args = new List<string>();
if (batchSize != null)
args.Add($"batchsize={batchSize.Value}");
Expand All @@ -196,16 +203,24 @@ public async Task ScanUTXOSetAsync(DerivationStrategyBase extKey, int? batchSize
var argsString = string.Join("&", args.ToArray());
if (argsString != string.Empty)
argsString = $"?{argsString}";
await SendAsync<bool>(HttpMethod.Post, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan{Raw(argsString)}", cancellation).ConfigureAwait(false);


await SendAsync<bool>(HttpMethod.Post, null, $"{GetBasePath(trackedSource)}/utxos/scan{Raw(argsString)}", cancellation).ConfigureAwait(false);
}
public void ScanUTXOSet(DerivationStrategyBase extKey, int? batchSize = null, int? gapLimit = null, int? fromIndex = null, CancellationToken cancellation = default)
{
ScanUTXOSetAsync(extKey, batchSize, gapLimit, fromIndex, cancellation).GetAwaiter().GetResult();
}

public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(DerivationStrategyBase extKey, CancellationToken cancellation = default)
public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(DerivationStrategyBase extKey,
CancellationToken cancellation = default)
{
return await GetScanUTXOSetInformationAsync(new DerivationSchemeTrackedSource(extKey), cancellation);
}

public async Task<ScanUTXOInformation> GetScanUTXOSetInformationAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, $"v1/cryptos/{CryptoCode}/derivations/{extKey}/utxos/scan", cancellation).ConfigureAwait(false);
return await SendAsync<ScanUTXOInformation>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/utxos/scan", cancellation).ConfigureAwait(false);
}

public ScanUTXOInformation GetScanUTXOSetInformation(DerivationStrategyBase extKey, CancellationToken cancellation = default)
Expand Down Expand Up @@ -338,6 +353,12 @@ public Task<GetBalanceResponse> GetBalanceAsync(TrackedSource trackedSource, Can
{
return SendAsync<GetBalanceResponse>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/balance", cancellation);
}

public async Task<BitcoinAddress[]> GetAddresses(TrackedSource trackedSource, CancellationToken cancellation = default)
{
var addresses = await SendAsync<string[]>(HttpMethod.Get, null, $"{GetBasePath(trackedSource)}/addresses", cancellation);
return addresses.Select(s => BitcoinAddress.Create(s, Network.NBitcoinNetwork)).ToArray();
}
public async Task<bool> IsTrackedAsync(TrackedSource trackedSource, CancellationToken cancellation = default)
{

Expand Down
55 changes: 55 additions & 0 deletions NBXplorer.Tests/UnitTest1.Groups.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,61 @@ public async Task CanAliceAndBobShareWallet()
balance = await tester.Client.GetBalanceAsync(gts);
Assert.Equal(Money.Coins(1.0m + 1.2m), balance.Unconfirmed);
}

[Fact]
public async Task CanGetGroupAddresses()
{
using var tester = ServerTester.Create();
var g = await tester.Client.CreateGroupAsync();
var addresses = Enumerable.Range(0, 10).Select(_ => new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, tester.Network).ToString()).ToArray();
await tester.Client.AddGroupAddressAsync("BTC", g.GroupId, addresses);

var groupAddresses = await tester.Client.GetAddresses(new GroupTrackedSource(g.GroupId));
Assert.Equal(addresses.Length, groupAddresses.Length);
foreach (var a in addresses)
{
Assert.Contains(BitcoinAddress.Create(a, tester.Network), groupAddresses);
}
}


[Fact]
public async Task CanScanUTXOSetForGroups()
{
using var tester = ServerTester.Create();
var g = await tester.Client.CreateGroupAsync(Cancel);
var newAddress = new Key().GetAddress(ScriptPubKeyType.TaprootBIP86, tester.Network);
await tester.Client.AddGroupAddressAsync("BTC", g.GroupId, [newAddress.ToString()], Cancel);

var txid = await tester.SendToAddressAsync(newAddress, Money.Coins(1.0m));
tester.RPC.Generate(1);
var block = await tester.RPC.GetBlockAsync(await tester.RPC.GetBestBlockHashAsync(Cancel), Cancel);
var match = block.Transactions.Single(t => t.GetHash() == txid);
var coin = match.Outputs.AsCoins().Single(c => c.ScriptPubKey == newAddress.ScriptPubKey);

var gts = new GroupTrackedSource(g.GroupId);
await tester.Client.ScanUTXOSetAsync(gts, cancellation: Cancel);

ScanUTXOInformation progress = null;
while (true)
{
progress = await tester.Client.GetScanUTXOSetInformationAsync(gts, Cancel);
Assert.NotNull(progress);
if (progress.Status is ScanUTXOStatus.Complete or ScanUTXOStatus.Error)
break;
await Task.Delay(100, Cancel);
}
Assert.Equal(ScanUTXOStatus.Complete, progress.Status);
Assert.Equal(1, progress.Progress.Found);

var utxos = await tester.Client.GetUTXOsAsync(gts, Cancel);
var unspent = utxos.GetUnspentCoins();
Assert.Single(unspent);
Assert.Equal(coin.Outpoint, unspent[0].Outpoint);

var balance = await tester.Client.GetBalanceAsync(gts, Cancel);
Assert.Equal(Money.Coins(1.0m), balance.Confirmed);
}

private async Task<NBXplorerException> AssertNBXplorerException(int httpCode, Task<GroupInformation> task)
{
Expand Down
10 changes: 10 additions & 0 deletions NBXplorer/Backend/Repository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1320,5 +1320,15 @@ public void RemoveFromCache(IEnumerable<uint256> txIds)

internal static readonly string WalletInsertQuery = "INSERT INTO wallets (wallet_id, metadata) VALUES (@wid, @metadata::JSONB) ON CONFLICT DO NOTHING;";
internal static readonly string WalletCheckQuery = "SELECT COUNT(*) FROM wallets WHERE wallet_id=@wid";

public async Task<string[]> GetAddresses(TrackedSource trackedSource, NBXplorerNetwork network)
{
var walletKey = GetWalletKey(trackedSource, network);
await using var conn = await ConnectionFactory.CreateConnection();
return (await conn.QueryAsync<string>("SELECT s.addr FROM wallets_scripts JOIN scripts s USING (code, script) WHERE code=@code AND wallet_id=@wid", new
{
code = network.CryptoCode, walletKey.wid
})).ToArray();
}
}
}
47 changes: 46 additions & 1 deletion NBXplorer/Controllers/CommonRoutesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,61 @@ namespace NBXplorer.Controllers
[Authorize]
public class CommonRoutesController : Controller
{
public ScanUTXOSetService ScanUTXOSetService { get; }
public GroupsController GroupsController{ get; }
public AddressPoolService AddressPoolService{ get; }
public DbConnectionFactory ConnectionFactory { get; }
public CommonRoutesController(DbConnectionFactory connectionFactory, AddressPoolService addressPoolService, GroupsController groupsController)
public CommonRoutesController(
DbConnectionFactory connectionFactory,
AddressPoolService addressPoolService,
GroupsController groupsController,
ScanUTXOSetServiceAccessor scanUTXOSetService)
{
ScanUTXOSetService = scanUTXOSetService.Instance;
GroupsController = groupsController;
AddressPoolService = addressPoolService;
ConnectionFactory = connectionFactory;
}

[HttpPost("utxos/scan")]
[TrackedSourceContext.TrackedSourceContextRequirement(requireRPC: true)]
public IActionResult ScanUTXOSet(TrackedSourceContext trackedSourceContext, int? batchSize = null, int? gapLimit = null, int? from = null)
{
var network = trackedSourceContext.Network;
var rpc = trackedSourceContext.RpcClient;
if (!rpc.Capabilities.SupportScanUTXOSet)
throw new NBXplorerError(405, "scanutxoset-not-suported", "ScanUTXOSet is not supported for this currency").AsException();

ScanUTXOSetOptions options = new ScanUTXOSetOptions();
if (batchSize != null)
options.BatchSize = batchSize.Value;
if (gapLimit != null)
options.GapLimit = gapLimit.Value;
if (from != null)
options.From = from.Value;
if (!ScanUTXOSetService.EnqueueScan(network, trackedSourceContext.TrackedSource, options))
throw new NBXplorerError(409, "scanutxoset-in-progress", "ScanUTXOSet has already been called for this derivationScheme").AsException();
return Ok();
}

[HttpGet($"addresses")]
public async Task<IActionResult> GetAddresses(TrackedSourceContext trackedSourceContext)
{
var addresses = await trackedSourceContext.Repository.GetAddresses(trackedSourceContext.TrackedSource,
trackedSourceContext.Network);
return Ok(addresses);
}

[HttpGet($"utxos/scan")]
[TrackedSourceContext.TrackedSourceContextRequirement()]
public IActionResult GetScanUTXOSetInformation(TrackedSourceContext trackedSourceContext)
{
var network = trackedSourceContext.Network;
var info = ScanUTXOSetService.GetInformation(network, trackedSourceContext.TrackedSource);
if (info == null)
throw new NBXplorerError(404, "scanutxoset-info-not-found", "ScanUTXOSet has not been called with this derivationScheme of the result has expired").AsException();
return Json(info, network.Serializer.Settings);
}

[HttpGet("")]
public async Task<IActionResult> IsTracked(TrackedSourceContext trackedSourceContext)
Expand Down
38 changes: 0 additions & 38 deletions NBXplorer/Controllers/DerivationSchemesController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,18 @@ namespace NBXplorer.Controllers
[Route($"v1/{CommonRoutes.BaseDerivationEndpoint}")]
public class DerivationSchemesController : Controller
{
public ScanUTXOSetService ScanUTXOSetService { get; }
public MainController MainController { get; }
public RepositoryProvider RepositoryProvider { get; }
public KeyPathTemplates KeyPathTemplates { get; }
public Indexers Indexers { get; }
public AddressPoolService AddressPoolService { get; }
public DerivationSchemesController(
MainController mainController,
ScanUTXOSetServiceAccessor scanUTXOSetService,
RepositoryProvider repositoryProvider,
KeyPathTemplates keyPathTemplates,
Indexers indexers,
AddressPoolService addressPoolService)
{
ScanUTXOSetService = scanUTXOSetService.Instance;
MainController = mainController;
RepositoryProvider = repositoryProvider;
KeyPathTemplates = keyPathTemplates;
Expand Down Expand Up @@ -104,41 +101,6 @@ public async Task<IActionResult> Wipe(TrackedSourceContext trackedSourceContext)
return Ok();
}

[HttpPost("utxos/scan")]
[HttpPost($"~/v1/{CommonRoutes.DerivationEndpoint}/utxos/scan")]
[TrackedSourceContext.TrackedSourceContextRequirement(requireRPC: true, allowedTrackedSourceTypes: typeof(DerivationSchemeTrackedSource))]
public IActionResult ScanUTXOSet(TrackedSourceContext trackedSourceContext, int? batchSize = null, int? gapLimit = null, int? from = null)
{
var network = trackedSourceContext.Network;
var rpc = trackedSourceContext.RpcClient;
var derivationScheme = ((DerivationSchemeTrackedSource)trackedSourceContext.TrackedSource).DerivationStrategy;
if (!rpc.Capabilities.SupportScanUTXOSet)
throw new NBXplorerError(405, "scanutxoset-not-suported", "ScanUTXOSet is not supported for this currency").AsException();

ScanUTXOSetOptions options = new ScanUTXOSetOptions();
if (batchSize != null)
options.BatchSize = batchSize.Value;
if (gapLimit != null)
options.GapLimit = gapLimit.Value;
if (from != null)
options.From = from.Value;
if (!ScanUTXOSetService.EnqueueScan(network, derivationScheme, options))
throw new NBXplorerError(409, "scanutxoset-in-progress", "ScanUTXOSet has already been called for this derivationScheme").AsException();
return Ok();
}

[HttpGet($"~/v1/{CommonRoutes.DerivationEndpoint}/utxos/scan")]
[TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: typeof(DerivationSchemeTrackedSource))]
public IActionResult GetScanUTXOSetInformation(TrackedSourceContext trackedSourceContext)
{
var network = trackedSourceContext.Network;
var derivationScheme = ((DerivationSchemeTrackedSource)trackedSourceContext.TrackedSource).DerivationStrategy;
var info = ScanUTXOSetService.GetInformation(network, derivationScheme);
if (info == null)
throw new NBXplorerError(404, "scanutxoset-info-not-found", "ScanUTXOSet has not been called with this derivationScheme of the result has expired").AsException();
return Json(info, network.Serializer.Settings);
}

[HttpPost($"~/v1/{CommonRoutes.DerivationEndpoint}/prune")]
[TrackedSourceContext.TrackedSourceContextRequirement(allowedTrackedSourceTypes: [typeof(DerivationSchemeTrackedSource)])]
public async Task<PruneResponse> Prune(TrackedSourceContext trackedSourceContext, [FromBody] PruneRequest request)
Expand Down
Loading