Compare commits

...

3 Commits

Author SHA1 Message Date
nicolas.dorier
1d23d0523c
Bump NBitcoin (support taproot for signet) 2021-08-01 00:20:18 +09:00
nicolas.dorier
70c18d119d
bump NBitcoin 2021-07-31 21:01:12 +09:00
nicolas.dorier
4193bfe000
Document and properly test immature transaction handling 2021-07-31 20:34:26 +09:00
8 changed files with 91 additions and 20 deletions

View File

@ -7,9 +7,26 @@ namespace NBXplorer.Models
{
public class GetBalanceResponse
{
/// <summary>
/// How the confirmed balance would be updated once all the unconfirmed transactions were confirmed.
/// </summary>
public IMoney Unconfirmed { get; set; }
/// <summary>
/// The balance of all funds in confirmed transactions.
/// </summary>
public IMoney Confirmed { get; set; }
public IMoney Immature { get; set; }
/// <summary>
/// The total of funds owned (ie, `confirmed + unconfirmed`)
/// </summary>
public IMoney Total { get; set; }
/// <summary>
/// The total unspendable funds (ie, coinbase reward which need 100 confirmations before being spendable)
/// </summary>
public IMoney Immature { get; set; }
/// <summary>
/// The total spendable balance. (ie, `total - immature`)
/// </summary>
public IMoney Available { get; set; }
}
}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Company>Digital Garage</Company>
<Version>4.0.0</Version>
<Version>4.0.2</Version>
<Copyright>Copyright © Digital Garage 2017</Copyright>
<Description>Client API for the minimalist HD Wallet Tracker NBXplorer</Description>
<PackageIconUrl>https://aois.blob.core.windows.net/public/Bitcoin.png</PackageIconUrl>
@ -24,8 +24,8 @@
<Optimize>true</Optimize>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="NBitcoin" Version="6.0.3" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.2" />
<PackageReference Include="NBitcoin" Version="6.0.6" />
<PackageReference Include="NBitcoin.Altcoins" Version="3.0.3" />
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.18" />
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
</ItemGroup>

View File

@ -36,12 +36,11 @@ namespace NBXplorer.Tests
}
}
}
public static void WaitForBlocks(this LongPollingNotificationSession session, params uint256[] txIds)
public static void WaitForBlocks(this LongPollingNotificationSession session, params uint256[] blockIds)
{
if (txIds == null || txIds.Length == 0)
if (blockIds == null || blockIds.Length == 0)
return;
HashSet<uint256> txidsSet = new HashSet<uint256>(txIds);
HashSet<uint256> txidsSet = new HashSet<uint256>(blockIds);
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)))
{
while (true)

View File

@ -2663,6 +2663,45 @@ namespace NBXplorer.Tests
tester.Notifications.WaitForBlocks(blockId);
var savedTx = tester.Client.GetTransaction(txId);
Assert.Equal(blockId[0], savedTx.BlockId);
// Ensure the current state is correct
var balance = tester.Client.GetBalance(pubkey);
Assert.Equal(Money.Coins(2.5m), balance.Confirmed);
Assert.Equal(Money.Coins(0.0m), balance.Unconfirmed);
Assert.Equal(Money.Coins(2.5m), balance.Total);
Assert.Equal(Money.Coins(0.0m), balance.Immature);
Assert.Equal(Money.Coins(2.5m), balance.Available);
Logs.Tester.LogInformation("Let's mine, and check that the balance is not updated until maturity occurs");
var blkid = tester.RPC.GenerateToAddress(1, tester.AddressOf(key, "0/0"))[0];
var blk = tester.RPC.GetBlock(blkid);
var minedTxId = blk.Transactions[0].GetHash();
tester.Notifications.WaitForTransaction(pubkey, minedTxId);
balance = tester.Client.GetBalance(pubkey);
Assert.Equal(Money.Coins(52.5m), balance.Confirmed);
Assert.Equal(Money.Coins(0.0m), balance.Unconfirmed);
Assert.Equal(Money.Coins(52.5m), balance.Total);
Assert.Equal(Money.Coins(50.0m), balance.Immature);
Assert.Equal(Money.Coins(2.5m), balance.Available);
var utxos = tester.Client.GetUTXOs(pubkey);
Assert.DoesNotContain(utxos.Confirmed.UTXOs, u => u.Outpoint.Hash == minedTxId);
Assert.DoesNotContain(utxos.Unconfirmed.UTXOs, u => u.Outpoint.Hash == minedTxId);
var transactions = tester.Client.GetTransactions(pubkey);
Assert.Contains(transactions.ConfirmedTransactions.Transactions, u => u.TransactionId == minedTxId);
Assert.Contains(transactions.ImmatureTransactions.Transactions, u => u.TransactionId == minedTxId);
// Let's generate enough block and see if the transaction is finally mature
var blockIds = tester.RPC.Generate(tester.Network.Consensus.CoinbaseMaturity);
tester.Notifications.WaitForBlocks(blockIds);
balance = tester.Client.GetBalance(pubkey);
Assert.Equal(Money.Coins(52.5m), balance.Confirmed);
Assert.Equal(Money.Coins(0.0m), balance.Unconfirmed);
Assert.Equal(Money.Coins(52.5m), balance.Total);
Assert.Equal(Money.Coins(0.0m), balance.Immature);
Assert.Equal(Money.Coins(52.5m), balance.Available);
transactions = tester.Client.GetTransactions(pubkey);
utxos = tester.Client.GetUTXOs(pubkey);
Assert.Empty(transactions.ImmatureTransactions.Transactions);
Assert.Contains(utxos.Confirmed.UTXOs, u => u.Outpoint.Hash == minedTxId);
Assert.Contains(transactions.ConfirmedTransactions.Transactions, u => u.TransactionId == minedTxId);
}
}
[Fact]

View File

@ -204,9 +204,8 @@ namespace NBXplorer
{
if (tx.Height is int)
{
if(tx.IsMature)
ConfirmedTransactions.Add(tx);
else
ConfirmedTransactions.Add(tx);
if (!tx.IsMature)
ImmatureTransactions.Add(tx);
}
else

View File

@ -858,9 +858,10 @@ namespace NBXplorer.Controllers
{
Confirmed = CalculateBalance(network, transactions.ConfirmedTransactions),
Unconfirmed = CalculateBalance(network, transactions.UnconfirmedTransactions),
Immature = CalculateBalance(network,transactions.ImmatureTransactions),
};
balance.Total = balance.Confirmed.Add(balance.Unconfirmed).Add(balance.Immature);
Immature = CalculateBalance(network, transactions.ImmatureTransactions)
};
balance.Total = balance.Confirmed.Add(balance.Unconfirmed);
balance.Available = balance.Total.Sub(balance.Immature);
return Json(balance, jsonResult.SerializerSettings);
}

View File

@ -3,7 +3,7 @@
<OutputType>Exe</OutputType>
<TargetFramework Condition="'$(TargetFrameworkOverride)' == ''">netcoreapp3.1</TargetFramework>
<TargetFramework Condition="'$(TargetFrameworkOverride)' != ''">$(TargetFrameworkOverride)</TargetFramework>
<Version>2.1.53</Version>
<Version>2.1.55</Version>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\NBXplorer.xml</DocumentationFile>
<NoWarn>1701;1702;1705;1591;CS1591</NoWarn>
<LangVersion>8.0</LangVersion>

View File

@ -19,8 +19,8 @@ NBXplorer does not index the whole blockchain, rather, it listens transactions a
* [Get connection status to the chain](#status)
* [Get a new unused address](#unused)
* [Get scriptPubKey information of a Derivation Scheme](#scriptPubKey)
* [Get Unspent Transaction Outputs (UTXOs)](#utxos)
* [Get Unspent Transaction Outputs of a specific address](#address-utxos)
* [Get available Unspent Transaction Outputs (UTXOs)](#utxos)
* [Get available Unspent Transaction Outputs of a specific address](#address-utxos)
* [Notifications via websocket](#websocket)
* [Broadcast a transaction](#broadcast)
* [Rescan a transaction](#rescan)
@ -233,15 +233,20 @@ Returns:
"replacedBy": "7ec0bcbd3b7685b6bbdb4287a250b64bfcb799dbbbcffa78c00e6cc11185e5f1"
}
]
}
},
"immatureTransactions": {
"transactions": []
}
}
```
* `replaceable`: `true` if the transaction can be replaced (the transaction has RBF activated, is in the unconfirmed list and is not an intermediate transaction in a chain of unconfirmed transaction)
* `replacing`: Only set in the unconfirmed list, and is pointing to a transaction id in the replaced list.
* `replacedBy`: Only set in the replaced list, and is pointing to a transaction id in the unconfirmed list.
* `immatureTransactions`: Coinbase transactions with less than 100 confirmations.
Note for liquid, `balanceChange` is an array of [AssetMoney](#liquid).
Note that the list of confirmed transaction also include immature transactions.
## <a name="address-transactions"></a>Query transactions associated to a specific address
@ -351,11 +356,21 @@ Returns:
{
"unconfirmed": 110000000,
"confirmed": 100000000,
"available": 210000000,
"immature": 0,
"total": 210000000
}
```
Note for liquid, the values are array of [AssetMoney](#liquid).
* `unconfirmed`: How the confirmed balance would be updated once all the unconfirmed transactions were confirmed.
* `confirmed`: The balance of all funds in confirmed transactions.
* `total`: The total of funds owned (ie, `confirmed + unconfirmed`)
* `immature`: The total unspendable funds (ie, coinbase reward which need 100 confirmations before being spendable)
* `available`: The total spendable balance. (ie, `total - immature`)
Immature funds is the sum of UTXO's belonging to a coinbase transaction with less than 100 confirmations.
## <a name="gettransaction"></a>Get a transaction
HTTP GET v1/cryptos/{cryptoCode}/transactions/{txId}
@ -470,7 +485,7 @@ Returns:
}
```
## <a name="utxos"></a>Get Unspent Transaction Outputs (UTXOs)
## <a name="utxos"></a>Get available Unspent Transaction Outputs (UTXOs)
HTTP GET v1/cryptos/{cryptoCode}/derivations/{derivationScheme}/utxos
@ -526,8 +541,9 @@ Result:
```
This call does not returns conflicted unconfirmed UTXOs.
Note that confirmed utxo, do not include immature UTXOs. (ie. UTXOs belonging to a coinbase transaction with less than 100 confirmations)
## <a name="address-utxos"></a>Get Unspent Transaction Outputs of a specific address
## <a name="address-utxos"></a>Get available Unspent Transaction Outputs of a specific address
Assuming you use Track on this specific address: