Skip to content

Commit 246de73

Browse files
Improve API classes
1 parent db00a47 commit 246de73

21 files changed

+462
-175
lines changed

WatchOnlyBitcoinWallet/MainWindow.axaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@
8181
<TextBlock Text="{Binding BitcoinBalanceLC, StringFormat=\{0:N0\}}"
8282
Grid.Column="1" Grid.Row="3"/>
8383

84-
<TextBlock Text="{Binding Errors}"
84+
<TextBlock Text="{Binding Error}"
8585
IsVisible="{Binding IsErrorMsgVisible}"
8686
TextWrapping="Wrap"
8787
Background="#FFECB1B1"

WatchOnlyBitcoinWallet/Models/BitcoinAddress.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ public decimal ForkBalance
6464
set => SetField(ref _forkBalance, value);
6565
}
6666

67-
public List<Transaction> TransactionList { get; set; } = new();
67+
public List<TxModel> TransactionList { get; set; } = new();
6868
}
6969
}

WatchOnlyBitcoinWallet/Models/Transaction.cs renamed to WatchOnlyBitcoinWallet/Models/TxModel.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,25 @@
88

99
namespace WatchOnlyBitcoinWallet.Models
1010
{
11-
public class Transaction : InpcBase
11+
public class TxModel : InpcBase
1212
{
13+
public TxModel()
14+
{
15+
}
16+
17+
public TxModel(string txId, int blkHeight, decimal amount, DateTime confTime)
18+
{
19+
TxId = txId;
20+
BlockHeight = blkHeight;
21+
Amount = amount;
22+
ConfirmedTime = confTime;
23+
}
24+
25+
1326
/// <summary>
1427
/// Transaction ID
1528
/// </summary>
16-
public string TxId { get; set; }
29+
public string TxId { get; set; } = string.Empty;
1730

1831
/// <summary>
1932
/// Block height that contains the transaction.

WatchOnlyBitcoinWallet/Services/Api.cs

Lines changed: 0 additions & 67 deletions
This file was deleted.
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// WatchOnlyBitcoinWallet
2+
// Copyright (c) 2016 Coding Enthusiast
3+
// Distributed under the MIT software license, see the accompanying
4+
// file LICENCE or http://www.opensource.org/licenses/mit-license.php.
5+
6+
using Newtonsoft.Json.Linq;
7+
using System;
8+
using System.Net.Http;
9+
using System.Threading.Tasks;
10+
11+
namespace WatchOnlyBitcoinWallet.Services
12+
{
13+
public enum PriceServiceNames
14+
{
15+
MempoolSpace,
16+
Bitfinex,
17+
Coindesk,
18+
}
19+
public enum BalanceServiceNames
20+
{
21+
MempoolSpace,
22+
BlockCypher,
23+
Blockonomics,
24+
}
25+
26+
public abstract class ApiBase
27+
{
28+
protected static async Task<Response<JObject>> SendApiRequestAsync(string url)
29+
{
30+
Response<JObject> resp = new();
31+
try
32+
{
33+
using HttpClient client = new();
34+
string result = await client.GetStringAsync(url);
35+
resp.Result = JObject.Parse(result);
36+
resp.IsSuccess = true;
37+
}
38+
catch (Exception ex)
39+
{
40+
resp.Error = ex.Message;
41+
}
42+
return resp;
43+
}
44+
45+
protected static string BuildError(string paramName, string apiName)
46+
{
47+
return $"JSON in API response doesn't include \"{paramName}\" parameter " +
48+
$"({apiName} API may have changed, please report this as a bug).";
49+
}
50+
51+
protected static bool TryExtract(JToken token, string fieldName, out int result, out string error)
52+
{
53+
JToken? temp = token[fieldName];
54+
if (temp is null)
55+
{
56+
error = BuildError(fieldName, "BlockCypher");
57+
result = 0;
58+
return false;
59+
}
60+
61+
try
62+
{
63+
result = (int)temp;
64+
error = string.Empty;
65+
return true;
66+
}
67+
catch (Exception ex)
68+
{
69+
result = 0;
70+
error = $"Cannot convert to int {ex.Message}";
71+
return false;
72+
}
73+
}
74+
75+
protected static bool TryExtract(JToken token, string fieldName, out ulong result, out string error)
76+
{
77+
JToken? temp = token[fieldName];
78+
if (temp is null)
79+
{
80+
error = BuildError(fieldName, "BlockCypher");
81+
result = 0;
82+
return false;
83+
}
84+
85+
try
86+
{
87+
result = (ulong)temp;
88+
error = string.Empty;
89+
return true;
90+
}
91+
catch (Exception ex)
92+
{
93+
result = 0;
94+
error = $"Cannot convert to ulong {ex.Message}";
95+
return false;
96+
}
97+
}
98+
99+
protected static bool TryExtract(JToken token, string fieldName, out DateTime result, out string error)
100+
{
101+
JToken? temp = token[fieldName];
102+
if (temp is null)
103+
{
104+
error = BuildError(fieldName, "BlockCypher");
105+
result = DateTime.Now;
106+
return false;
107+
}
108+
109+
try
110+
{
111+
result = (DateTime)temp;
112+
error = string.Empty;
113+
return true;
114+
}
115+
catch (Exception ex)
116+
{
117+
result = DateTime.Now;
118+
error = $"Cannot convert to ulong {ex.Message}";
119+
return false;
120+
}
121+
}
122+
123+
protected static bool TryExtract(JToken token, string fieldName, out string result, out string error)
124+
{
125+
JToken? temp = token[fieldName];
126+
if (temp is null)
127+
{
128+
error = BuildError(fieldName, "BlockCypher");
129+
result = string.Empty;
130+
return false;
131+
}
132+
133+
result = temp.ToString();
134+
error = string.Empty;
135+
return true;
136+
}
137+
}
138+
}

WatchOnlyBitcoinWallet/Services/BalanceServices/BlockCypher.cs

Lines changed: 70 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,59 +3,106 @@
33
// Distributed under the MIT software license, see the accompanying
44
// file LICENCE or http://www.opensource.org/licenses/mit-license.php.
55

6+
using Autarkysoft.Bitcoin;
67
using Newtonsoft.Json.Linq;
78
using System;
89
using System.Collections.Generic;
10+
using System.Diagnostics;
911
using System.Threading.Tasks;
1012
using WatchOnlyBitcoinWallet.Models;
1113

1214
namespace WatchOnlyBitcoinWallet.Services.BalanceServices
1315
{
14-
public class BlockCypher : BalanceApi
16+
public class BlockCypher : ApiBase, IBalanceApi
1517
{
16-
public override async Task<Response> UpdateBalancesAsync(List<BitcoinAddress> addrList)
18+
public async Task<Response> UpdateBalancesAsync(List<BitcoinAddress> addrList)
1719
{
18-
Response resp = new Response();
19-
foreach (var addr in addrList)
20+
Response resp = new();
21+
foreach (BitcoinAddress addr in addrList)
2022
{
21-
string url = "https://api.blockcypher.com/v1/btc/main/addrs/" + addr.Address + "/balance";
23+
string url = $"https://api.blockcypher.com/v1/btc/main/addrs/{addr.Address}/balance";
2224
Response<JObject> apiResp = await SendApiRequestAsync(url);
23-
if (apiResp.Errors.Any())
25+
if (!apiResp.IsSuccess)
2426
{
25-
resp.Errors.AddRange(apiResp.Errors);
27+
resp.Error = apiResp.Error;
2628
break;
2729
}
28-
decimal bal = (Int64)apiResp.Result["final_balance"] * Satoshi;
30+
Debug.Assert(apiResp.Result is not null);
31+
32+
ulong? t = (ulong?)apiResp.Result["final_balance"];
33+
if (t is null)
34+
{
35+
resp.Error = BuildError("final_balance", "BlockCypher");
36+
return resp;
37+
}
38+
39+
decimal bal = t.Value * Constants.Satoshi;
2940
addr.Difference = bal - addr.Balance;
3041
addr.Balance = bal;
3142
}
43+
44+
resp.IsSuccess = true;
3245
return resp;
3346
}
3447

3548

36-
public override async Task<Response> UpdateTransactionListAsync(List<BitcoinAddress> addrList)
49+
public async Task<Response> UpdateTransactionListAsync(List<BitcoinAddress> addrList)
3750
{
38-
Response resp = new Response();
51+
Response resp = new();
3952
foreach (var addr in addrList)
4053
{
4154
string url = "https://api.blockcypher.com/v1/btc/main/addrs/" + addr.Address + "?limit=2000";
4255
Response<JObject> apiResp = await SendApiRequestAsync(url);
43-
if (apiResp.Errors.Any())
56+
if (!apiResp.IsSuccess)
4457
{
45-
resp.Errors.AddRange(apiResp.Errors);
58+
resp.Error = apiResp.Error;
4659
break;
4760
}
48-
List<Transaction> temp = new List<Transaction>();
49-
foreach (var item in apiResp.Result["txrefs"])
61+
Debug.Assert(apiResp.Result is not null);
62+
63+
if (!TryExtract(apiResp.Result, "final_n_tx", out int cap, out string error))
64+
{
65+
resp.Error = error;
66+
return resp;
67+
}
68+
69+
if (cap == 0)
70+
{
71+
resp.IsSuccess = true;
72+
return resp;
73+
}
74+
75+
List<TxModel> temp = new(cap);
76+
JToken? array = apiResp.Result["txrefs"];
77+
if (array is null)
78+
{
79+
resp.Error = BuildError("txrefs", "BlockCypher");
80+
return resp;
81+
}
82+
83+
foreach (JToken? item in array)
5084
{
51-
Transaction tx = new Transaction();
52-
tx.TxId = item["tx_hash"].ToString();
53-
tx.BlockHeight = (int)item["block_height"];
54-
tx.Amount = ((Int64)item["tx_input_n"] == -1) ? (Int64)item["value"] : -(Int64)item["value"];
55-
tx.ConfirmedTime = (DateTime)item["confirmed"];
56-
57-
Transaction tempTx = temp.Find(x => x.TxId == tx.TxId);
58-
if (tempTx == null)
85+
if (item is null)
86+
{
87+
resp.Error = BuildError("txrefs", "BlockCypher");
88+
return resp;
89+
}
90+
91+
if (!TryExtract(item, "tx_hash", out string txId, out error) ||
92+
!TryExtract(item, "block_height", out int height, out error) ||
93+
!TryExtract(item, "tx_input_n", out int isOutput, out error) ||
94+
!TryExtract(item, "value", out ulong value, out error) ||
95+
!TryExtract(item, "confirmed", out DateTime time, out error))
96+
{
97+
resp.Error = error;
98+
return resp;
99+
}
100+
101+
decimal amount = (isOutput == -1) ? (Constants.Satoshi * value) : (-Constants.Satoshi * value);
102+
TxModel tx = new(txId, height, amount, time);
103+
104+
TxModel? tempTx = temp.Find(x => x.TxId == tx.TxId);
105+
if (tempTx is null)
59106
{
60107
temp.Add(tx);
61108
}
@@ -64,7 +111,7 @@ public override async Task<Response> UpdateTransactionListAsync(List<BitcoinAddr
64111
tempTx.Amount += tx.Amount;
65112
}
66113
}
67-
temp.ForEach(x => x.Amount *= Satoshi);
114+
68115
addr.TransactionList = temp;
69116
}
70117
return resp;

0 commit comments

Comments
 (0)