Skip to content

Commit

Permalink
updates
Browse files Browse the repository at this point in the history
  • Loading branch information
Kukks committed Nov 30, 2023
1 parent e6f1deb commit 1987eb4
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 51 deletions.
4 changes: 3 additions & 1 deletion NBXplorer.Client/Models/ImportUTXORequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ namespace NBXplorer.Models;

public class ImportUTXORequest
{
public Coin Coin { get; set; }
public OutPoint Utxo { get; set; }

public MerkleBlock Proof { get; set; }

}

public class AssociateScriptRequest
Expand Down
14 changes: 7 additions & 7 deletions NBXplorer.Tests/UnitTest1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4647,7 +4647,7 @@ await tester.Client.ImportUTXOs(wallet1TS, new[]
{
new ImportUTXORequest()
{
Coin = utxos.AsCoin(),
Utxo = utxos.OutPoint,
Proof = null
}
});
Expand Down Expand Up @@ -4763,7 +4763,7 @@ public async Task CanImportUTXOs(Backend backend)
{
new()
{
Coin = new Coin(utxo),
Utxo = utxo.ToCoin().Outpoint,
Proof = null
}
});
Expand All @@ -4784,7 +4784,7 @@ await Eventually(async () =>
{
new()
{
Coin = new Coin(utxo2),
Utxo = utxo2.ToCoin().Outpoint,
Proof = null
}
});
Expand Down Expand Up @@ -4821,15 +4821,15 @@ await Eventually(async () =>
{
new()
{
Coin = fakeUtxo,
Utxo = fakeUtxo.Outpoint
},
new()
{
Coin = new Coin(tospendutxo)
Utxo = new Coin(tospendutxo).Outpoint
},
new()
{
Coin = new Coin(validScriptUtxo)
Utxo = new Coin(validScriptUtxo).Outpoint
}
});

Expand Down Expand Up @@ -4859,7 +4859,7 @@ await Eventually(async () =>
{
new()
{
Coin = new Coin(yoUtxo),
Utxo = new Coin(yoUtxo).Outpoint,
Proof = mb
}
});
Expand Down
37 changes: 23 additions & 14 deletions NBXplorer/Backends/Postgres/PostgresRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,7 @@ FROM unnest(@records) AS r (script),
SELECT code, script, wallet_id, addr, descriptor_metadata->>'derivation' derivation,
keypath, descriptors_scripts_metadata->>'redeem' redeem,
descriptors_scripts_metadata->>'blindedAddress' blinded_addr,
descriptors_scripts_metadata->>'blindingKey' blindingKey,
descriptor_metadata->>'descriptor' descriptor
FROM nbxv1_keypath_info ki
WHERE ki.code=@code AND ki.script=r.script
Expand All @@ -551,23 +552,31 @@ JOIN wallets w USING(wallet_id)",
string walletId = r.wallet_id;

var trackedSource = derivationStrategy is not null
?
new DerivationSchemeTrackedSource(derivationStrategy)
: walletType == "Wallet" ? walletId is null ? (TrackedSource)null : new WalletTrackedSource(walletId) : new AddressTrackedSource(addr);

result.Add(script, new KeyPathInformation()
{
Address = addr,
DerivationStrategy = r.derivation is not null ? derivationStrategy : null,
KeyPath = keypath,
ScriptPubKey = script,
TrackedSource = trackedSource,
Feature = keypath is null ? DerivationFeature.Deposit : KeyPathTemplates.GetDerivationFeature(keypath),
Redeem = redeem is null ? null : Script.FromHex(redeem),
});
? new DerivationSchemeTrackedSource(derivationStrategy)
: walletType == "Wallet"
? walletId is null ? (TrackedSource) null : new WalletTrackedSource(walletId)
: new AddressTrackedSource(addr);
var keyPathInformation = Network.IsElement && r.blindingKey is not null
? new LiquidKeyPathInformation()
{
BlindingKey = Key.Parse(r.blindingKey, Network.NBitcoinNetwork)
}
: new KeyPathInformation();
keyPathInformation.Address = addr;
keyPathInformation.DerivationStrategy = r.derivation is not null ? derivationStrategy : null;
keyPathInformation.KeyPath = keypath;
keyPathInformation.ScriptPubKey = script;
keyPathInformation.TrackedSource = trackedSource;
keyPathInformation.Feature = keypath is null ? DerivationFeature.Deposit : KeyPathTemplates.GetDerivationFeature(keypath);
keyPathInformation.Redeem = redeem is null ? null : Script.FromHex(redeem);
result.Add(script, keyPathInformation);
}
return result;
}
public class LiquidKeyPathInformation : KeyPathInformation
{
public Key BlindingKey { get; set; }
}

private BitcoinAddress GetAddress(dynamic r)
{
Expand Down
24 changes: 13 additions & 11 deletions NBXplorer/Controllers/PostgresMainController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,40 +98,40 @@ public async Task<IActionResult> ImportUTXOs( TrackedSourceContext trackedSource

var repo = (PostgresRepository)trackedSourceContext.Repository;
var jsonSerializer = JsonSerializer.Create(trackedSourceContext.Network.JsonSerializerSettings);
var coins = rawRequest.ToObject<ImportUTXORequest[]>(jsonSerializer)?.Where(c => c.Coin != null).ToArray();
var coins = rawRequest.ToObject<ImportUTXORequest[]>(jsonSerializer)?.Where(c => c.Utxo != null).ToArray();
if (coins?.Any() is not true)
throw new ArgumentNullException(nameof(coins));

var rpc = trackedSourceContext.RpcClient;

var clientBatch = rpc.PrepareBatch();
var coinToTxOut = new ConcurrentDictionary<Coin, GetTxOutResponse>();
var coinToBlock = new ConcurrentDictionary<Coin, BlockHeader>();
var coinToTxOut = new ConcurrentDictionary<OutPoint, GetTxOutResponse>();
var coinToBlock = new ConcurrentDictionary<OutPoint, BlockHeader>();
await Task.WhenAll(coins.SelectMany(o =>
{
return new[]
{
Task.Run(async () =>
{
var txOutResponse =
await clientBatch.GetTxOutAsync(o.Coin.Outpoint.Hash, (int) o.Coin.Outpoint.N);
await clientBatch.GetTxOutAsync(o.Utxo.Hash, (int) o.Utxo.N);
if (txOutResponse is not null)
coinToTxOut.TryAdd(o.Coin, txOutResponse);
coinToTxOut.TryAdd(o.Utxo, txOutResponse);
}),
Task.Run(async () =>
{
if (o.Proof is not null && o.Proof.PartialMerkleTree.Hashes.Contains(o.Coin.Outpoint.Hash))
if (o.Proof is not null && o.Proof.PartialMerkleTree.Hashes.Contains(o.Utxo.Hash))
{
var txoutproofResult =
await clientBatch.SendCommandAsync("verifytxoutproof", Encoders.Hex.EncodeData(o.Proof.ToBytes()));
var txHash = o.Coin.Outpoint.Hash.ToString();
var txHash = o.Utxo.Hash.ToString();
if (txoutproofResult.Error is not null && txoutproofResult.Result is JArray prooftxs &&
prooftxs.Any(token =>
token.Value<string>()
?.Equals(txHash, StringComparison.InvariantCultureIgnoreCase) is true))
{
coinToBlock.TryAdd(o.Coin, o.Proof.Header);
coinToBlock.TryAdd(o.Utxo, o.Proof.Header);
}
}
})
Expand All @@ -142,7 +142,7 @@ await Task.WhenAll(coins.SelectMany(o =>

var scripts = coinToTxOut
.Select(pair => (
pair.Key.ScriptPubKey.GetDestinationAddress(trackedSourceContext.Network.NBitcoinNetwork), pair))
pair.Value.TxOut.ScriptPubKey.GetDestinationAddress(), pair))
.Where(pair => pair.Item1 is not null).Select(tuple => new AssociateScriptRequest()
{
Destination = tuple.Item1,
Expand All @@ -154,9 +154,11 @@ await Task.WhenAll(coins.SelectMany(o =>
await repo.SaveMatches(coinToTxOut.Select(pair =>
{
coinToBlock.TryGetValue(pair.Key, out var blockHeader);
var coin = new Coin(pair.Key, pair.Value.TxOut);
var ttx = repo.CreateTrackedTransaction(trackedSourceContext.TrackedSource,
new TrackedTransactionKey(pair.Key.Outpoint.Hash, blockHeader?.GetHash(), true){},
new[] {pair.Key}, null);
new TrackedTransactionKey(pair.Key.Hash, blockHeader?.GetHash(), true){},
new[] {coin}, null);
ttx.Inserted = now;
ttx.Immature = pair.Value.IsCoinBase && pair.Value.Confirmations <= 100;
ttx.FirstSeen = blockHeader?.BlockTime?? NBitcoin.Utils.UnixTimeToDateTime(0);;
Expand Down
56 changes: 38 additions & 18 deletions NBXplorer/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,27 +97,47 @@ public static T As<T>(this IActionResult actionResult)
return default;
}

public static async Task<ElementsTransaction> UnblindTransaction(this RPCClient rpc, TrackedTransaction tx, IEnumerable<KeyPathInformation> keyInfos)
public static async Task<ElementsTransaction> UnblindTransaction(this RPCClient rpc, TrackedTransaction tx,
IEnumerable<KeyPathInformation> keyInfos)
{
if (tx.TrackedSource is DerivationSchemeTrackedSource ts &&
!ts.DerivationStrategy.Unblinded() &&
tx.Transaction is ElementsTransaction elementsTransaction)
{
var keys = keyInfos
.Select(kv => (KeyPath: kv.KeyPath,
Address: kv.Address as BitcoinBlindedAddress,
BlindingKey: NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey(ts.DerivationStrategy, kv.KeyPath, kv.ScriptPubKey, rpc.Network)))
.Where(o => o.Address != null)
.Select(o => new UnblindTransactionBlindingAddressKey()
{
Address = o.Address,
BlindingKey = o.BlindingKey
}).ToList();
if (keys.Count != 0)
var elementsTransaction = tx.Transaction as ElementsTransaction;
if (elementsTransaction == null)
return null;

var keys = keyInfos
.Select(kv => (KeyPath: kv.KeyPath,
Address: kv.Address as BitcoinBlindedAddress,
BlindingKey: GetBlindingKey(tx.TrackedSource, kv, rpc.Network)))
.Where(o => o.Address != null && o.BlindingKey != null)
.Select(o => new UnblindTransactionBlindingAddressKey()
{
return await rpc.UnblindTransaction(keys, elementsTransaction, rpc.Network);
}
Address = o.Address,
BlindingKey = o.BlindingKey
}).ToList();
if (keys.Count != 0)
{
return await rpc.UnblindTransaction(keys, elementsTransaction, rpc.Network);
}

return null;
}

private static Key GetBlindingKey(TrackedSource trackedSource, KeyPathInformation keyPathInformation, Network network)
{
if(keyPathInformation.Address is not BitcoinBlindedAddress)
{
return null;
}
if (keyPathInformation is PostgresRepository.LiquidKeyPathInformation {BlindingKey: not null} liquidKeyPathInformation)
{
return liquidKeyPathInformation.BlindingKey;
}
if(trackedSource is DerivationSchemeTrackedSource derivationSchemeTrackedSource && !derivationSchemeTrackedSource.DerivationStrategy.Unblinded())
{
return NBXplorerNetworkProvider.LiquidNBXplorerNetwork.GenerateBlindingKey(
derivationSchemeTrackedSource.DerivationStrategy, keyPathInformation.KeyPath, keyPathInformation.ScriptPubKey, network);
}

return null;
}
public static async Task<DateTimeOffset?> GetBlockTimeAsync(this RPCClient client, uint256 blockId, bool throwIfNotFound = true)
Expand Down
58 changes: 58 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1301,10 +1301,68 @@ Response:

NOTE: Batch commands are also supported by sending the JSON-RPC requests in an array. The result is also returned in an array.

## <a name="associate-scripts"></a>Associate scripts to wallet

Note: This API is only available for Postgres.

`HTTP POST v1/cryptos/{cryptoCode}/derivations/{derivationScheme}/associate`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/addresses/{address}/associate`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/addresses/{address}/associate`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/wallets/{wallet}/associate`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/tracked-sources/{trackedSource}/associate`

Request:


* `destinatiom`: Mandatory, the address to add to the wallet
* `used`: Optional, indicate that the address was used already (default: `false`)
* `metadata`: Optional, a json object that will be attached to the script as part of the descriptor (default: null) (note: for liquid, you will want to use the properties `blinded_address` and `blindingKey` to handle confidential addresses)

```json
[
{
"destinatiom": "bc1q...",
"used": true,
"metadata": { ...}
}
]
```


## <a name="import-utxos"></a>Import UTXOs to wallet

Note: This API is only available for Postgres.

In the case where you start tracking a wallet that already has UTXOs, you can import them to NBXplorer so that it can be aware of them. NBXplorer will validate against the bitcoin node's utxoset that the UTXOs are valid.

`HTTP POST v1/cryptos/{cryptoCode}/derivations/{derivationScheme}/import-utxos`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/addresses/{address}/import-utxos`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/addresses/{address}/import-utxos`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/wallets/{wallet}/import-utxos`<br/>
`HTTP POST v1/cryptos/{cryptoCode}/tracked-sources/{trackedSource}/import-utxos`

Request:


* `utxo`: Mandatory, the utxo to import, in the format `txid-vout`, where `txid` is the transaction id and `vout` is the output index
* `proof`: Optional, a merkle proof that the utxo being imported was included in a block (as provided by Bitcoin Core's gettxoutproof)

```json
[
{
"utxo": "txid-vout",
"proof": {}
}
]
```

No response body


## <a name="hierarchy"></a>Hierarchy APIs

Note: These APIs are only available for Postgres.

### View wallet parents

`HTTP GET v1/cryptos/{cryptoCode}/derivations/{derivationScheme}/parents`<br/>
Expand Down

0 comments on commit 1987eb4

Please sign in to comment.