Skip to content

Commit eaeeea2

Browse files
Remove dependencies to keytemplates (#502)
1 parent 5170fd9 commit eaeeea2

12 files changed

+178
-111
lines changed

NBXplorer.Client/Models/KeyPathTemplate.cs

-26
Original file line numberDiff line numberDiff line change
@@ -71,25 +71,6 @@ public static bool TryParse(string path, out KeyPathTemplate keyPathTemplate)
7171
return true;
7272
}
7373

74-
public bool TryMatchTemplate(KeyPath keyPath, out uint index)
75-
{
76-
index = 0;
77-
if (keyPath.Length != 1 + PreIndexes.Length + PostIndexes.Length)
78-
return false;
79-
for (int i = 0; i < PreIndexes.Length; i++)
80-
{
81-
if (PreIndexes[i] != keyPath[i])
82-
return false;
83-
}
84-
for (int i = 0; i < PostIndexes.Length; i++)
85-
{
86-
if (PostIndexes[i] != keyPath[i + 1 + PreIndexes.Length])
87-
return false;
88-
}
89-
index = keyPath[PreIndexes.Length];
90-
return true;
91-
}
92-
9374
private static bool TryParseCore(string i, out uint index)
9475
{
9576
if (i.Length == 0)
@@ -147,12 +128,5 @@ public override string ToString()
147128
builder.Append($"/{PostIndexes}");
148129
return builder.ToString();
149130
}
150-
151-
public uint GetIndex(KeyPath keypath)
152-
{
153-
if (TryMatchTemplate(keypath, out var index))
154-
return index;
155-
throw new ArgumentException("Impossible to get the index of this keypath", nameof(keypath));
156-
}
157131
}
158132
}

NBXplorer.Client/Models/KeyPathTemplates.cs

-46
Original file line numberDiff line numberDiff line change
@@ -55,52 +55,6 @@ public KeyPathTemplate GetKeyPathTemplate(DerivationFeature derivationFeature)
5555
}
5656
}
5757

58-
public KeyPathTemplate GetKeyPathTemplate(KeyPath keyPath)
59-
{
60-
if (keyPath == null)
61-
throw new ArgumentNullException(nameof(keyPath));
62-
63-
if (depositKeyPathTemplate.TryMatchTemplate(keyPath, out _))
64-
{
65-
return depositKeyPathTemplate;
66-
}
67-
else if (changeKeyPathTemplate.TryMatchTemplate(keyPath, out _))
68-
{
69-
return changeKeyPathTemplate;
70-
}
71-
else if (directKeyPathTemplate.TryMatchTemplate(keyPath, out _))
72-
{
73-
return directKeyPathTemplate;
74-
}
75-
else if (customKeyPathTemplate != null && customKeyPathTemplate.TryMatchTemplate(keyPath, out _))
76-
{
77-
return customKeyPathTemplate;
78-
}
79-
else
80-
throw new ArgumentException(paramName: nameof(keyPath), message: "No template match this keypath");
81-
}
82-
83-
public DerivationFeature GetDerivationFeature(KeyPath keyPath)
84-
{
85-
if (depositKeyPathTemplate.TryMatchTemplate(keyPath, out _))
86-
{
87-
return DerivationFeature.Deposit;
88-
}
89-
else if (changeKeyPathTemplate.TryMatchTemplate(keyPath, out _))
90-
{
91-
return DerivationFeature.Change;
92-
}
93-
else if (directKeyPathTemplate.TryMatchTemplate(keyPath, out _))
94-
{
95-
return DerivationFeature.Direct;
96-
}
97-
else if (customKeyPathTemplate != null && customKeyPathTemplate.TryMatchTemplate(keyPath, out _))
98-
{
99-
return DerivationFeature.Custom;
100-
}
101-
else
102-
throw new ArgumentException(paramName: nameof(keyPath), message: "No template match this keypath");
103-
}
10458
public IEnumerable<DerivationFeature> GetSupportedDerivationFeatures()
10559
{
10660
return derivationFeatures;

NBXplorer.Client/Models/NewTransactionEvent.cs

+2
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ public class MatchedOutput
6666
public KeyPath KeyPath { get; set; }
6767
public Script ScriptPubKey { get; set; }
6868
public int Index { get; set; }
69+
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
70+
public DerivationFeature? Feature { get; set; }
6971
public IMoney Value { get; set; }
7072
public BitcoinAddress Address { get; set; }
7173
}

NBXplorer.Tests/UnitTest1.cs

+21
Original file line numberDiff line numberDiff line change
@@ -2659,6 +2659,27 @@ public async Task CanReserveAddress()
26592659
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Deposit);
26602660
Assert.Equal(new KeyPath("0/100"), addr2.KeyPath);
26612661
Assert.Equal(100, addr2.Index);
2662+
2663+
// Cancel the even addresses
2664+
var toCancel = Enumerable.Range(0, 100).Where(i => i % 2 == 0).Select(i => new KeyPath("0/" + i)).ToArray();
2665+
await tester.Client.CancelReservationAsync(bob, toCancel);
2666+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Deposit);
2667+
Assert.Equal(new KeyPath("0/2"), addr2.KeyPath);
2668+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Deposit, skip: 1);
2669+
Assert.Equal(new KeyPath("0/4"), addr2.KeyPath);
2670+
2671+
// Cancelling direct derivation doesn't cancel other derivation features
2672+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Direct, reserve: true);
2673+
Assert.Equal(new KeyPath("0"), addr2.KeyPath);
2674+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Direct, reserve: true);
2675+
Assert.Equal(new KeyPath("1"), addr2.KeyPath);
2676+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Direct, reserve: true);
2677+
Assert.Equal(new KeyPath("2"), addr2.KeyPath);
2678+
await tester.Client.CancelReservationAsync(bob, [new KeyPath("2")]);
2679+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Direct, reserve: true);
2680+
Assert.Equal(new KeyPath("2"), addr2.KeyPath);
2681+
addr2 = await tester.Client.GetUnusedAsync(bob, DerivationFeature.Deposit);
2682+
Assert.Equal(new KeyPath("0/2"), addr2.KeyPath);
26622683
}
26632684
}
26642685

NBXplorer/AddressPoolService.cs

+4-5
Original file line numberDiff line numberDiff line change
@@ -68,16 +68,14 @@ private async Task Listen(CancellationToken cancellationToken)
6868
}
6969
}
7070

71-
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider, KeyPathTemplates keyPathTemplates)
71+
public AddressPoolService(NBXplorerNetworkProvider networks, RepositoryProvider repositoryProvider)
7272
{
7373
this.networks = networks;
7474
this.repositoryProvider = repositoryProvider;
75-
this.keyPathTemplates = keyPathTemplates;
7675
}
7776
Dictionary<NBXplorerNetwork, AddressPool> _AddressPoolByNetwork;
7877
private readonly NBXplorerNetworkProvider networks;
7978
private readonly RepositoryProvider repositoryProvider;
80-
private readonly KeyPathTemplates keyPathTemplates;
8179

8280
public async Task StartAsync(CancellationToken cancellationToken)
8381
{
@@ -111,9 +109,10 @@ internal Task GenerateAddresses(NBXplorerNetwork network, TrackedTransaction[] m
111109
var derivationStrategy = (m.TrackedSource as Models.DerivationSchemeTrackedSource)?.DerivationStrategy;
112110
if (derivationStrategy == null)
113111
continue;
114-
foreach (var feature in m.InOuts.Select(kv => keyPathTemplates.GetDerivationFeature(kv.KeyPath)).Distinct())
112+
foreach (var feature in m.InOuts.Select(kv => kv.Feature).Distinct())
115113
{
116-
refill.Add(GenerateAddresses(network, derivationStrategy, feature));
114+
if (feature is not null)
115+
refill.Add(GenerateAddresses(network, derivationStrategy, feature.Value));
117116
}
118117
}
119118
return Task.WhenAll(refill.ToArray());

NBXplorer/Backend/DbConnectionFactory.cs

+2-5
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,10 @@ public class DbConnectionFactory : IAsyncDisposable
1313
{
1414
public DbConnectionFactory(ILogger<DbConnectionFactory> logger,
1515
IConfiguration configuration,
16-
ExplorerConfiguration conf,
17-
KeyPathTemplates keyPathTemplates)
16+
ExplorerConfiguration conf)
1817
{
1918
Logger = logger;
2019
ExplorerConfiguration = conf;
21-
KeyPathTemplates = keyPathTemplates;
2220
ConnectionString = configuration.GetRequired("POSTGRES");
2321
_DS = CreateDataSourceBuilder(null).Build();
2422
}
@@ -43,11 +41,10 @@ public NpgsqlDataSourceBuilder CreateDataSourceBuilder(Action<NpgsqlConnectionSt
4341
public string ConnectionString { get; }
4442
public ILogger<DbConnectionFactory> Logger { get; }
4543
public ExplorerConfiguration ExplorerConfiguration { get; }
46-
public KeyPathTemplates KeyPathTemplates { get; }
4744

4845
public async Task<DbConnectionHelper> CreateConnectionHelper(NBXplorerNetwork network)
4946
{
50-
return new DbConnectionHelper(network, await CreateConnection(), KeyPathTemplates)
47+
return new DbConnectionHelper(network, await CreateConnection())
5148
{
5249
MinPoolSize = ExplorerConfiguration.MinGapSize,
5350
MaxPoolSize = ExplorerConfiguration.MaxGapSize

NBXplorer/Backend/DbConnectionHelper.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,16 @@ namespace NBXplorer.Backend
1616
public class DbConnectionHelper : IDisposable, IAsyncDisposable
1717
{
1818
public DbConnectionHelper(NBXplorerNetwork network,
19-
DbConnection connection,
20-
KeyPathTemplates keyPathTemplates)
19+
DbConnection connection)
2120
{
2221
derivationStrategyFactory = new DerivationStrategyFactory(network.NBitcoinNetwork);
2322
Network = network;
2423
Connection = connection;
25-
KeyPathTemplates = keyPathTemplates;
2624
}
2725
DerivationStrategyFactory derivationStrategyFactory;
2826

2927
public NBXplorerNetwork Network { get; }
3028
public DbConnection Connection { get; }
31-
public KeyPathTemplates KeyPathTemplates { get; }
3229
public int MinPoolSize { get; set; }
3330
public int MaxPoolSize { get; set; }
3431

NBXplorer/Backend/Repository.cs

+23-21
Original file line numberDiff line numberDiff line change
@@ -52,27 +52,27 @@ public Repository(DbConnectionFactory connectionFactory, NBXplorerNetwork networ
5252

5353
public async Task CancelReservation(DerivationStrategyBase strategy, KeyPath[] keyPaths)
5454
{
55+
var w = GetWalletKey(strategy, Network);
5556
await using var conn = await GetConnection();
56-
var parameters = keyPaths
57-
.Select(o =>
58-
{
59-
var template = KeyPathTemplates.GetKeyPathTemplate(o);
60-
var descriptor = GetDescriptorKey(strategy, KeyPathTemplates.GetDerivationFeature(o));
61-
return new
62-
{
63-
descriptor.code,
64-
descriptor.descriptor,
65-
idx = (int)template.GetIndex(o)
66-
};
67-
})
68-
.ToList();
6957
// We can only set to used='t' descriptors whose scripts haven't been used on chain
70-
await conn.Connection.ExecuteAsync(
71-
"UPDATE descriptors_scripts ds SET used='f' " +
72-
"FROM scripts s " +
73-
"WHERE " +
74-
"ds.code=@code AND ds.descriptor=@descriptor AND ds.idx=@idx AND " +
75-
"s.code=ds.code AND s.script=ds.script AND s.used IS FALSE", parameters);
58+
await conn.Connection.ExecuteAsync(@"
59+
UPDATE descriptors_scripts
60+
SET used = 'f'
61+
WHERE (code, descriptor, idx)
62+
IN (
63+
SELECT ds.code, ds.descriptor, ds.idx FROM wallets w
64+
JOIN wallets_descriptors USING (wallet_id)
65+
JOIN descriptors d USING (code, descriptor)
66+
JOIN descriptors_scripts ds USING(code, descriptor)
67+
JOIN scripts s USING(code, script)
68+
JOIN unnest(@keypaths) AS k(keypath) ON nbxv1_get_keypath_index(d.metadata, k.keypath) = ds.idx
69+
WHERE code=@code AND w.wallet_id=@wid AND s.used IS FALSE)",
70+
new
71+
{
72+
code = Network.CryptoCode,
73+
wid = w.wid,
74+
keypaths = keyPaths.Select(k => k.ToString()).ToArray()
75+
}) ;
7676
}
7777

7878
public record DescriptorKey(string code, string descriptor);
@@ -242,7 +242,7 @@ await connection.ExecuteAsync(
242242

243243
private async Task ImportDescriptorToRPCIfNeeded(DbConnection connection, WalletKey walletKey, long fromIndex, long toGenerate, KeyPathTemplate keyTemplate)
244244
{
245-
var helper = new DbConnectionHelper(Network, connection, KeyPathTemplates);
245+
var helper = new DbConnectionHelper(Network, connection);
246246
var importAddressToRPC = ImportRPCMode.Parse(await helper.GetMetadata<string>(walletKey.wid, WellknownMetadataKeys.ImportAddressToRPC));
247247
if (importAddressToRPC == ImportRPCMode.Descriptors || importAddressToRPC == ImportRPCMode.DescriptorsReadOnly)
248248
{
@@ -754,7 +754,7 @@ async Task<TrackedTransaction[]> GetTransactions(DbConnectionHelper connection,
754754
var para = new DynamicParameters();
755755
var sql = query.GetSql(para, Network);
756756
var utxos = await
757-
connection.Connection.QueryAsync<(string wallet_id, string tx_id, long idx, string blk_id, long? blk_height, int? blk_idx, bool is_out, string spent_tx_id, long spent_idx, string script, string addr, long value, string asset_id, bool immature, string keypath, DateTime seen_at)>(sql, para);
757+
connection.Connection.QueryAsync<(string wallet_id, string tx_id, long idx, string blk_id, long? blk_height, int? blk_idx, bool is_out, string spent_tx_id, long spent_idx, string script, string addr, long value, string asset_id, bool immature, string keypath, DateTime seen_at, string feature)>(sql, para);
758758
utxos.TryGetNonEnumeratedCount(out int c);
759759
var trackedById = new Dictionary<(TrackedSource, string), TrackedTransaction>(c);
760760
foreach (var utxo in utxos)
@@ -772,6 +772,7 @@ async Task<TrackedTransaction[]> GetTransactions(DbConnectionHelper connection,
772772
ScriptPubKey = Script.FromHex(utxo.script),
773773
KeyPath = utxo.keypath is string kp ? KeyPath.Parse(kp) : null,
774774
Index = (int)utxo.idx,
775+
Feature = utxo.feature is string f ? Enum.Parse<DerivationFeature>(f) : null,
775776
Address = BitcoinAddress.Create(utxo.addr, this.Network.NBitcoinNetwork)
776777
};
777778
tracked.MatchedOutputs.Add(matchedOutput);
@@ -783,6 +784,7 @@ async Task<TrackedTransaction[]> GetTransactions(DbConnectionHelper connection,
783784
{
784785
InputIndex = (int)utxo.idx,
785786
Index = (int)utxo.spent_idx,
787+
Feature = utxo.feature is string f ? Enum.Parse<DerivationFeature>(f) : null,
786788
TransactionId = uint256.Parse(utxo.spent_tx_id),
787789
Address = utxo.addr is null ? null : BitcoinAddress.Create(utxo.addr, Network.NBitcoinNetwork),
788790
KeyPath = utxo.keypath is string kp ? KeyPath.Parse(kp) : null,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
CREATE OR REPLACE VIEW nbxv1_tracked_txs AS
2+
SELECT ws.wallet_id,
3+
io.code,
4+
io.tx_id,
5+
io.idx,
6+
io.is_out,
7+
io.spent_tx_id,
8+
io.spent_idx,
9+
io.script,
10+
io.value,
11+
io.asset_id,
12+
io.immature,
13+
io.blk_id,
14+
io.blk_idx,
15+
io.blk_height,
16+
io.mempool,
17+
io.replaced_by,
18+
io.seen_at,
19+
nbxv1_get_keypath(d.metadata, ds.idx) AS keypath,
20+
d.metadata->>'feature' feature
21+
FROM ((wallets_scripts ws
22+
JOIN ins_outs io ON (((io.code = ws.code) AND (io.script = ws.script))))
23+
LEFT JOIN ((wallets_descriptors wd
24+
JOIN descriptors_scripts ds ON (((ds.code = wd.code) AND (ds.descriptor = wd.descriptor))))
25+
JOIN descriptors d ON (((d.code = ds.code) AND (d.descriptor = ds.descriptor)))) ON (((wd.wallet_id = ws.wallet_id) AND (wd.code = ws.code) AND (ds.script = ws.script))))
26+
WHERE ((io.blk_id IS NOT NULL) OR (io.mempool IS TRUE));
27+
28+
-- Convert a template '0/1/*/2/3' and a keypath `0/1/123/2/3` to 123
29+
CREATE OR REPLACE FUNCTION nbxv1_get_keypath_index(metadata JSONB, keypath TEXT) RETURNS BIGINT language SQL IMMUTABLE AS $$
30+
SELECT
31+
CASE WHEN keypath LIKE (prefix || '%') AND
32+
keypath LIKE ('%' || suffix) AND
33+
idx ~ '^\d+$'
34+
THEN CAST(idx AS BIGINT) END
35+
FROM (SELECT SUBSTRING(
36+
keypath
37+
FROM LENGTH(prefix) + 1
38+
FOR LENGTH(keypath) - LENGTH(prefix) - LENGTH(suffix)
39+
) idx, prefix, suffix
40+
FROM (
41+
SELECT
42+
split_part(metadata->>'keyPathTemplate', '*', 1) AS prefix,
43+
split_part(metadata->>'keyPathTemplate', '*', 2) AS suffix
44+
) parts) q;
45+
$$;
46+
47+
CREATE OR REPLACE FUNCTION nbxv1_get_keypath(metadata jsonb, idx bigint) RETURNS text
48+
LANGUAGE sql IMMUTABLE
49+
AS $$
50+
SELECT REPLACE(metadata->>'keyPathTemplate', '*', idx::TEXT)
51+
$$;

NBXplorer/DBScripts/FullSchema.sql

+24-4
Original file line numberDiff line numberDiff line change
@@ -469,11 +469,29 @@ $$;
469469
CREATE FUNCTION nbxv1_get_keypath(metadata jsonb, idx bigint) RETURNS text
470470
LANGUAGE sql IMMUTABLE
471471
AS $$
472-
SELECT CASE WHEN metadata->>'type' = 'NBXv1-Derivation'
473-
THEN REPLACE(metadata->>'keyPathTemplate', '*', idx::TEXT)
474-
ELSE NULL END
472+
SELECT REPLACE(metadata->>'keyPathTemplate', '*', idx::TEXT)
475473
$$;
476474

475+
CREATE FUNCTION nbxv1_get_keypath_index(metadata jsonb, keypath text) RETURNS bigint
476+
LANGUAGE sql IMMUTABLE
477+
AS $_$
478+
SELECT
479+
CASE WHEN keypath LIKE (prefix || '%') AND
480+
keypath LIKE ('%' || suffix) AND
481+
idx ~ '^\d+$'
482+
THEN CAST(idx AS BIGINT) END
483+
FROM (SELECT SUBSTRING(
484+
keypath
485+
FROM LENGTH(prefix) + 1
486+
FOR LENGTH(keypath) - LENGTH(prefix) - LENGTH(suffix)
487+
) idx, prefix, suffix
488+
FROM (
489+
SELECT
490+
split_part(metadata->>'keyPathTemplate', '*', 1) AS prefix,
491+
split_part(metadata->>'keyPathTemplate', '*', 2) AS suffix
492+
) parts) q;
493+
$_$;
494+
477495
CREATE FUNCTION nbxv1_get_wallet_id(in_code text, in_scheme_or_address text) RETURNS text
478496
LANGUAGE sql IMMUTABLE
479497
AS $$
@@ -1000,7 +1018,8 @@ CREATE VIEW nbxv1_tracked_txs AS
10001018
io.mempool,
10011019
io.replaced_by,
10021020
io.seen_at,
1003-
nbxv1_get_keypath(d.metadata, ds.idx) AS keypath
1021+
nbxv1_get_keypath(d.metadata, ds.idx) AS keypath,
1022+
(d.metadata ->> 'feature'::text) AS feature
10041023
FROM ((wallets_scripts ws
10051024
JOIN ins_outs io ON (((io.code = ws.code) AND (io.script = ws.script))))
10061025
LEFT JOIN ((wallets_descriptors wd
@@ -1371,6 +1390,7 @@ INSERT INTO nbxv1_migrations VALUES ('020.ReplacingShouldBeIdempotent');
13711390
INSERT INTO nbxv1_migrations VALUES ('021.KeyPathInfoReturnsWalletId');
13721391
INSERT INTO nbxv1_migrations VALUES ('022.WalletsWalletsParentIdIndex');
13731392
INSERT INTO nbxv1_migrations VALUES ('023.KeyPathInfoReturnsIndex');
1393+
INSERT INTO nbxv1_migrations VALUES ('024.TrackedTxsReturnsFeature');
13741394

13751395
ALTER TABLE ONLY nbxv1_migrations
13761396
ADD CONSTRAINT nbxv1_migrations_pkey PRIMARY KEY (script_name);

0 commit comments

Comments
 (0)