mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-03-13 11:35:51 +01:00
Fixing payment in different crypto
This commit is contained in:
parent
de6f0008a6
commit
cb4468d3b3
11 changed files with 155 additions and 75 deletions
|
@ -38,7 +38,7 @@ services:
|
|||
- postgres
|
||||
|
||||
bitcoin-nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.42
|
||||
image: nicolasdorier/nbxplorer:1.0.0.45
|
||||
ports:
|
||||
- "32838:32838"
|
||||
expose:
|
||||
|
@ -56,7 +56,7 @@ services:
|
|||
- bitcoind
|
||||
|
||||
litecoin-nbxplorer:
|
||||
image: nicolasdorier/nbxplorer:1.0.0.43
|
||||
image: nicolasdorier/nbxplorer:1.0.0.45
|
||||
ports:
|
||||
- "32839:32839"
|
||||
expose:
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
<PackageReference Include="NBitcoin" Version="4.0.0.51" />
|
||||
<PackageReference Include="NBitpayClient" Version="1.0.0.14" />
|
||||
<PackageReference Include="DBreeze" Version="1.87.0" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.26" />
|
||||
<PackageReference Include="NBXplorer.Client" Version="1.0.0.28" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine" Version="1.0.0.1" />
|
||||
<PackageReference Include="NicolasDorier.CommandLine.Configuration" Version="1.0.0.2" />
|
||||
<PackageReference Include="NicolasDorier.StandardConfiguration" Version="1.0.0.13" />
|
||||
|
|
|
@ -56,6 +56,7 @@ namespace BTCPayServer.Controllers
|
|||
Fiat = FormatCurrency((decimal)dto.Price, dto.Currency),
|
||||
NotificationUrl = invoice.NotificationURL,
|
||||
ProductInformation = invoice.ProductInformation,
|
||||
StatusException = invoice.ExceptionStatus
|
||||
};
|
||||
|
||||
foreach (var data in invoice.GetCryptoData())
|
||||
|
@ -74,7 +75,7 @@ namespace BTCPayServer.Controllers
|
|||
}
|
||||
|
||||
var payments = invoice
|
||||
.Payments
|
||||
.GetPayments()
|
||||
.Select(async payment =>
|
||||
{
|
||||
var m = new InvoiceDetailsModel.Payment();
|
||||
|
|
|
@ -34,6 +34,8 @@ namespace BTCPayServer
|
|||
|
||||
public ExplorerClient GetExplorerClient(BTCPayNetwork network)
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
return GetExplorerClient(network.CryptoCode);
|
||||
}
|
||||
|
||||
|
|
|
@ -149,16 +149,19 @@ namespace BTCPayServer.HostedServices
|
|||
invoice.Status = "expired";
|
||||
}
|
||||
|
||||
foreach (NetworkCoins coins in await GetCoinsPerNetwork(context, invoice))
|
||||
var derivationStrategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
foreach (NetworkCoins coins in await GetCoinsPerNetwork(context, invoice, derivationStrategies))
|
||||
{
|
||||
bool dirtyAddress = false;
|
||||
if (coins.State != null)
|
||||
context.ModifiedKnownStates.AddOrReplace(coins.Strategy.Network, coins.State);
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.Payments.Select(p => p.Outpoint));
|
||||
foreach (var coin in coins.Coins.Where(c => !alreadyAccounted.Contains(c.Outpoint)))
|
||||
var alreadyAccounted = new HashSet<OutPoint>(invoice.GetPayments(coins.Strategy.Network).Select(p => p.Outpoint));
|
||||
foreach (var coin in coins.TimestampedCoins.Where(c => !alreadyAccounted.Contains(c.Coin.Outpoint)))
|
||||
{
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
|
||||
var payment = await _InvoiceRepository.AddPayment(invoice.Id, coin.DateTime, coin.Coin, coins.Strategy.Network.CryptoCode).ConfigureAwait(false);
|
||||
#pragma warning disable CS0618
|
||||
invoice.Payments.Add(payment);
|
||||
#pragma warning restore CS0618
|
||||
context.Events.Add(new InvoicePaymentEvent(invoice.Id));
|
||||
dirtyAddress = true;
|
||||
}
|
||||
|
@ -169,7 +172,7 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
if (invoice.Status == "new" || invoice.Status == "expired")
|
||||
{
|
||||
var totalPaid = (await GetPaymentsWithTransaction(network, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
var totalPaid = (await GetPaymentsWithTransaction(derivationStrategies, invoice)).Select(p => p.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalPaid >= accounting.TotalDue)
|
||||
{
|
||||
if (invoice.Status == "new")
|
||||
|
@ -194,7 +197,7 @@ namespace BTCPayServer.HostedServices
|
|||
context.MarkDirty();
|
||||
}
|
||||
|
||||
if (totalPaid < accounting.TotalDue && invoice.Payments.Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
if (totalPaid < accounting.TotalDue && invoice.GetPayments().Count != 0 && invoice.ExceptionStatus != "paidPartial")
|
||||
{
|
||||
invoice.ExceptionStatus = "paidPartial";
|
||||
context.MarkDirty();
|
||||
|
@ -209,7 +212,7 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
if (invoice.Status == "paid")
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
var transactions = await GetPaymentsWithTransaction(derivationStrategies, invoice);
|
||||
if (invoice.SpeedPolicy == SpeedPolicy.HighSpeed)
|
||||
{
|
||||
transactions = transactions.Where(t => t.Confirmations >= 1 || !t.Transaction.RBF);
|
||||
|
@ -247,7 +250,7 @@ namespace BTCPayServer.HostedServices
|
|||
|
||||
if (invoice.Status == "confirmed")
|
||||
{
|
||||
var transactions = await GetPaymentsWithTransaction(network, invoice);
|
||||
var transactions = await GetPaymentsWithTransaction(derivationStrategies, invoice);
|
||||
transactions = transactions.Where(t => t.Confirmations >= 6);
|
||||
var totalConfirmed = transactions.Select(t => t.Payment.GetValue(cryptoDataAll, cryptoData.CryptoCode)).Sum();
|
||||
if (totalConfirmed >= accounting.TotalDue)
|
||||
|
@ -260,9 +263,8 @@ namespace BTCPayServer.HostedServices
|
|||
}
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice)
|
||||
private async Task<IEnumerable<NetworkCoins>> GetCoinsPerNetwork(UpdateInvoiceContext context, InvoiceEntity invoice, DerivationStrategy[] strategies)
|
||||
{
|
||||
var strategies = invoice.GetDerivationStrategies(_NetworkProvider).ToArray();
|
||||
var getCoinsResponsesAsync = strategies
|
||||
.Select(d => _Wallet.GetCoins(d, context.KnownStates.TryGet(d.Network)))
|
||||
.ToArray();
|
||||
|
@ -270,66 +272,108 @@ namespace BTCPayServer.HostedServices
|
|||
var getCoinsResponses = getCoinsResponsesAsync.Select(g => g.Result).ToArray();
|
||||
foreach (var response in getCoinsResponses)
|
||||
{
|
||||
response.Coins = response.Coins.Where(c => invoice.AvailableAddressHashes.Contains(c.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
|
||||
response.TimestampedCoins = response.TimestampedCoins.Where(c => invoice.AvailableAddressHashes.Contains(c.Coin.ScriptPubKey.Hash.ToString() + response.Strategy.Network.CryptoCode)).ToArray();
|
||||
}
|
||||
return getCoinsResponses.Where(s => s.Coins.Length != 0).ToArray();
|
||||
return getCoinsResponses.Where(s => s.TimestampedCoins.Length != 0).ToArray();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(BTCPayNetwork network, InvoiceEntity invoice)
|
||||
private async Task<IEnumerable<AccountedPaymentEntity>> GetPaymentsWithTransaction(DerivationStrategy[] derivations, InvoiceEntity invoice)
|
||||
{
|
||||
var transactions = await _Wallet.GetTransactions(network, invoice.Payments.Select(t => t.Outpoint.Hash).ToArray());
|
||||
|
||||
var spentTxIn = new Dictionary<OutPoint, AccountedPaymentEntity>();
|
||||
var result = invoice.Payments.Select(p => p.Outpoint).ToHashSet();
|
||||
List<AccountedPaymentEntity> payments = new List<AccountedPaymentEntity>();
|
||||
foreach (var payment in invoice.Payments)
|
||||
List<PaymentEntity> updatedPaymentEntities = new List<PaymentEntity>();
|
||||
List<AccountedPaymentEntity> accountedPayments = new List<AccountedPaymentEntity>();
|
||||
foreach (var network in derivations.Select(d => d.Network))
|
||||
{
|
||||
TransactionResult tx;
|
||||
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
|
||||
var transactions = await _Wallet.GetTransactions(network, invoice.GetPayments(network).Select(t => t.Outpoint.Hash).ToArray());
|
||||
var conflicts = GetConflicts(transactions.Select(t => t.Value));
|
||||
foreach (var payment in invoice.GetPayments(network))
|
||||
{
|
||||
result.Remove(payment.Outpoint);
|
||||
continue;
|
||||
}
|
||||
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
|
||||
{
|
||||
Confirmations = tx.Confirmations,
|
||||
Transaction = tx.Transaction,
|
||||
Payment = payment
|
||||
};
|
||||
payments.Add(accountedPayment);
|
||||
foreach (var txin in tx.Transaction.Inputs)
|
||||
{
|
||||
if (!spentTxIn.TryAdd(txin.PrevOut, accountedPayment))
|
||||
{
|
||||
//We get a double spend
|
||||
var existing = spentTxIn[txin.PrevOut];
|
||||
TransactionResult tx;
|
||||
if (!transactions.TryGetValue(payment.Outpoint.Hash, out tx))
|
||||
continue;
|
||||
|
||||
//Take the most recent, the full node is already comparing fees correctly so we have the most likely to be confirmed
|
||||
if (accountedPayment.Confirmations > 1 || existing.Payment.ReceivedTime < accountedPayment.Payment.ReceivedTime)
|
||||
{
|
||||
spentTxIn[txin.PrevOut] = accountedPayment;
|
||||
result.Remove(existing.Payment.Outpoint);
|
||||
}
|
||||
AccountedPaymentEntity accountedPayment = new AccountedPaymentEntity()
|
||||
{
|
||||
Confirmations = tx.Confirmations,
|
||||
Transaction = tx.Transaction,
|
||||
Payment = payment
|
||||
};
|
||||
var txId = accountedPayment.Transaction.GetHash();
|
||||
var txConflict = conflicts.GetConflict(txId);
|
||||
var accounted = txConflict == null || txConflict.IsWinner(txId);
|
||||
if (accounted != payment.Accounted)
|
||||
{
|
||||
updatedPaymentEntities.Add(payment);
|
||||
payment.Accounted = accounted;
|
||||
}
|
||||
|
||||
if (accounted)
|
||||
accountedPayments.Add(accountedPayment);
|
||||
}
|
||||
}
|
||||
|
||||
List<PaymentEntity> updated = new List<PaymentEntity>();
|
||||
var accountedPayments = payments.Where(p =>
|
||||
{
|
||||
var accounted = result.Contains(p.Payment.Outpoint);
|
||||
if (p.Payment.Accounted != accounted)
|
||||
{
|
||||
p.Payment.Accounted = accounted;
|
||||
updated.Add(p.Payment);
|
||||
}
|
||||
return accounted;
|
||||
}).ToArray();
|
||||
|
||||
await _InvoiceRepository.UpdatePayments(payments);
|
||||
await _InvoiceRepository.UpdatePayments(updatedPaymentEntities);
|
||||
return accountedPayments;
|
||||
}
|
||||
|
||||
|
||||
class TransactionConflict
|
||||
{
|
||||
public Dictionary<uint256, TransactionResult> Transactions { get; set; } = new Dictionary<uint256, TransactionResult>();
|
||||
|
||||
|
||||
uint256 _Winner;
|
||||
public bool IsWinner(uint256 txId)
|
||||
{
|
||||
if (_Winner == null)
|
||||
{
|
||||
var confirmed = Transactions.FirstOrDefault(t => t.Value.Confirmations >= 1);
|
||||
if (!confirmed.Equals(default(KeyValuePair<uint256, TransactionResult>)))
|
||||
{
|
||||
_Winner = confirmed.Key;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Take the most recent (bitcoin node would not forward a conflict without a successfull RBF)
|
||||
_Winner = Transactions
|
||||
.OrderByDescending(t => t.Value.Timestamp)
|
||||
.First()
|
||||
.Key;
|
||||
}
|
||||
}
|
||||
return _Winner == txId;
|
||||
}
|
||||
}
|
||||
class TransactionConflicts : List<TransactionConflict>
|
||||
{
|
||||
public TransactionConflicts(IEnumerable<TransactionConflict> collection) : base(collection)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public TransactionConflict GetConflict(uint256 txId)
|
||||
{
|
||||
return this.FirstOrDefault(c => c.Transactions.ContainsKey(txId));
|
||||
}
|
||||
}
|
||||
private TransactionConflicts GetConflicts(IEnumerable<TransactionResult> transactions)
|
||||
{
|
||||
Dictionary<OutPoint, TransactionConflict> conflictsByOutpoint = new Dictionary<OutPoint, TransactionConflict>();
|
||||
foreach (var tx in transactions)
|
||||
{
|
||||
var hash = tx.Transaction.GetHash();
|
||||
foreach (var input in tx.Transaction.Inputs)
|
||||
{
|
||||
TransactionConflict conflict = new TransactionConflict();
|
||||
if (!conflictsByOutpoint.TryAdd(input.PrevOut, conflict))
|
||||
{
|
||||
conflict = conflictsByOutpoint[input.PrevOut];
|
||||
}
|
||||
if (!conflict.Transactions.ContainsKey(hash))
|
||||
conflict.Transactions.Add(hash, tx);
|
||||
}
|
||||
}
|
||||
return new TransactionConflicts(conflictsByOutpoint.Where(c => c.Value.Transactions.Count > 1).Select(c => c.Value));
|
||||
}
|
||||
|
||||
TimeSpan _PollInterval;
|
||||
public TimeSpan PollInterval
|
||||
{
|
||||
|
|
|
@ -70,7 +70,7 @@ namespace BTCPayServer.Models.InvoicingModels
|
|||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public string StatusException { get; set; }
|
||||
public DateTimeOffset CreatedDate
|
||||
{
|
||||
get; set;
|
||||
|
|
|
@ -180,7 +180,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
var network = networks.GetNetwork(strat.Name);
|
||||
if (network != null)
|
||||
{
|
||||
if (network == networks.BTC && btcReturned)
|
||||
if (network == networks.BTC)
|
||||
btcReturned = true;
|
||||
yield return BTCPayServer.DerivationStrategy.Parse(strat.Value.Value<string>(), network);
|
||||
}
|
||||
|
@ -220,10 +220,27 @@ namespace BTCPayServer.Services.Invoices
|
|||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
[Obsolete("Use GetPayments instead")]
|
||||
public List<PaymentEntity> Payments
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
#pragma warning disable CS0618
|
||||
public List<PaymentEntity> GetPayments()
|
||||
{
|
||||
return Payments.ToList();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(string cryptoCode)
|
||||
{
|
||||
return Payments.Where(p=>p.CryptoCode == cryptoCode).ToList();
|
||||
}
|
||||
public List<PaymentEntity> GetPayments(BTCPayNetwork network)
|
||||
{
|
||||
return GetPayments(network.CryptoCode);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
public bool Refundable
|
||||
{
|
||||
get;
|
||||
|
@ -490,9 +507,9 @@ namespace BTCPayServer.Services.Invoices
|
|||
bool paidEnough = totalDue <= paid;
|
||||
int txCount = 0;
|
||||
var payments =
|
||||
ParentEntity.Payments
|
||||
ParentEntity.GetPayments()
|
||||
.Where(p => p.Accounted)
|
||||
.OrderByDescending(p => p.ReceivedTime)
|
||||
.OrderBy(p => p.ReceivedTime)
|
||||
.Select(_ =>
|
||||
{
|
||||
var txFee = _.GetValue(cryptoData, CryptoCode, cryptoData[_.GetCryptoCode()].TxFee);
|
||||
|
|
|
@ -107,7 +107,9 @@ namespace BTCPayServer.Services.Invoices
|
|||
List<string> textSearch = new List<string>();
|
||||
invoice = Clone(invoice);
|
||||
invoice.Id = Encoders.Base58.EncodeData(RandomUtils.GetBytes(16));
|
||||
#pragma warning disable CS0618
|
||||
invoice.Payments = new List<PaymentEntity>();
|
||||
#pragma warning restore CS0618
|
||||
invoice.StoreId = storeId;
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
|
@ -309,12 +311,14 @@ namespace BTCPayServer.Services.Invoices
|
|||
private InvoiceEntity ToEntity(InvoiceData invoice)
|
||||
{
|
||||
var entity = ToObject<InvoiceEntity>(invoice.Blob);
|
||||
#pragma warning disable CS0618
|
||||
entity.Payments = invoice.Payments.Select(p =>
|
||||
{
|
||||
var paymentEntity = ToObject<PaymentEntity>(p.Blob);
|
||||
paymentEntity.Accounted = p.Accounted;
|
||||
return paymentEntity;
|
||||
}).ToList();
|
||||
#pragma warning restore CS0618
|
||||
entity.ExceptionStatus = invoice.ExceptionStatus;
|
||||
entity.Status = invoice.Status;
|
||||
entity.RefundMail = invoice.CustomerEmail;
|
||||
|
@ -419,7 +423,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
AddToTextSearch(invoiceId, addresses.Select(a => a.ToString()).ToArray());
|
||||
}
|
||||
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, Coin receivedCoin, string cryptoCode)
|
||||
public async Task<PaymentEntity> AddPayment(string invoiceId, DateTimeOffset date, Coin receivedCoin, string cryptoCode)
|
||||
{
|
||||
using (var context = _ContextFactory.CreateContext())
|
||||
{
|
||||
|
@ -430,7 +434,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
Output = receivedCoin.TxOut,
|
||||
CryptoCode = cryptoCode,
|
||||
#pragma warning restore CS0618
|
||||
ReceivedTime = DateTime.UtcNow
|
||||
ReceivedTime = date.UtcDateTime
|
||||
};
|
||||
|
||||
PaymentData data = new PaymentData
|
||||
|
@ -448,7 +452,7 @@ namespace BTCPayServer.Services.Invoices
|
|||
}
|
||||
}
|
||||
|
||||
public async Task UpdatePayments(List<AccountedPaymentEntity> payments)
|
||||
public async Task UpdatePayments(List<PaymentEntity> payments)
|
||||
{
|
||||
if (payments.Count == 0)
|
||||
return;
|
||||
|
@ -457,8 +461,8 @@ namespace BTCPayServer.Services.Invoices
|
|||
foreach (var payment in payments)
|
||||
{
|
||||
var data = new PaymentData();
|
||||
data.Id = payment.Payment.Outpoint.ToString();
|
||||
data.Accounted = payment.Payment.Accounted;
|
||||
data.Id = payment.Outpoint.ToString();
|
||||
data.Accounted = payment.Accounted;
|
||||
context.Attach(data);
|
||||
context.Entry(data).Property(o => o.Accounted).IsModified = true;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,12 @@ namespace BTCPayServer.Services.Wallets
|
|||
}
|
||||
public class NetworkCoins
|
||||
{
|
||||
public Coin[] Coins { get; set; }
|
||||
public class TimestampedCoin
|
||||
{
|
||||
public DateTimeOffset DateTime { get; set; }
|
||||
public Coin Coin { get; set; }
|
||||
}
|
||||
public TimestampedCoin[] TimestampedCoins { get; set; }
|
||||
public KnownState State { get; set; }
|
||||
public DerivationStrategy Strategy { get; set; }
|
||||
}
|
||||
|
@ -50,6 +55,10 @@ namespace BTCPayServer.Services.Wallets
|
|||
|
||||
public Task<TransactionResult> GetTransactionAsync(BTCPayNetwork network, uint256 txId, CancellationToken cancellation = default(CancellationToken))
|
||||
{
|
||||
if (network == null)
|
||||
throw new ArgumentNullException(nameof(network));
|
||||
if (txId == null)
|
||||
throw new ArgumentNullException(nameof(txId));
|
||||
var client = _Client.GetExplorerClient(network);
|
||||
return client.GetTransactionAsync(txId, cancellation);
|
||||
}
|
||||
|
@ -58,12 +67,11 @@ namespace BTCPayServer.Services.Wallets
|
|||
{
|
||||
var client = _Client.GetExplorerClient(strategy.Network);
|
||||
if (client == null)
|
||||
return new NetworkCoins() { Coins = new Coin[0], State = null, Strategy = strategy };
|
||||
return new NetworkCoins() { TimestampedCoins = new NetworkCoins.TimestampedCoin[0], State = null, Strategy = strategy };
|
||||
var changes = await client.SyncAsync(strategy.DerivationStrategyBase, state?.ConfirmedHash, state?.UnconfirmedHash, true, cancellation).ConfigureAwait(false);
|
||||
var utxos = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => c.AsCoin()).ToArray();
|
||||
return new NetworkCoins()
|
||||
{
|
||||
Coins = utxos,
|
||||
TimestampedCoins = changes.Confirmed.UTXOs.Concat(changes.Unconfirmed.UTXOs).Select(c => new NetworkCoins.TimestampedCoin() { Coin = c.AsCoin(), DateTime = c.Timestamp }).ToArray(),
|
||||
State = new KnownState() { ConfirmedHash = changes.Confirmed.Hash, UnconfirmedHash = changes.Unconfirmed.Hash },
|
||||
Strategy = strategy,
|
||||
};
|
||||
|
|
|
@ -613,7 +613,7 @@
|
|||
<div style="text-align:center">
|
||||
@foreach(var crypto in Model.AvailableCryptos)
|
||||
{
|
||||
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.cryptoCode='@crypto.CryptoCode'; return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.CryptoCode" src="@crypto.CryptoImage" /></a>
|
||||
<a style="text-decoration:none;" href="@crypto.Link" onclick="srvModel.cryptoCode='@crypto.CryptoCode'; fetchStatus(); return false;"><img style="height:32px; margin-right:5px; margin-left:5px;" alt="@crypto.CryptoCode" src="@crypto.CryptoImage" /></a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@
|
|||
<th>Status</th>
|
||||
<td>@Model.Status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status Exception</th>
|
||||
<td>@ModelStatusException</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Refund email</th>
|
||||
<td>@Model.RefundEmail</td>
|
||||
|
|
Loading…
Add table
Reference in a new issue