2020-06-28 21:44:35 -05:00
using System ;
2019-08-03 00:42:30 +09:00
using System.Collections.Generic ;
using System.Linq ;
2022-10-11 17:34:29 +09:00
using System.Text ;
2019-08-03 00:42:30 +09:00
using System.Threading.Tasks ;
2022-10-11 17:34:29 +09:00
using BTCPayServer.Abstractions.Extensions ;
2019-08-03 00:42:30 +09:00
using BTCPayServer.Data ;
2022-10-11 17:34:29 +09:00
using BTCPayServer.HostedServices ;
using BTCPayServer.Services.Labels ;
2019-08-03 00:42:30 +09:00
using Microsoft.EntityFrameworkCore ;
2022-10-11 17:34:29 +09:00
using NBitcoin ;
using NBitcoin.Crypto ;
using Newtonsoft.Json.Linq ;
2019-08-03 00:42:30 +09:00
namespace BTCPayServer.Services
{
2022-10-11 17:34:29 +09:00
#nullable enable
public record WalletObjectId ( WalletId WalletId , string Type , string Id ) ;
#nullable restore
2019-08-03 00:42:30 +09:00
public class WalletRepository
{
2020-06-28 22:07:48 -05:00
private readonly ApplicationDbContextFactory _ContextFactory ;
2019-08-03 00:42:30 +09:00
public WalletRepository ( ApplicationDbContextFactory contextFactory )
{
_ContextFactory = contextFactory ? ? throw new ArgumentNullException ( nameof ( contextFactory ) ) ;
}
2022-10-11 17:34:29 +09:00
public async Task < Dictionary < string , WalletTransactionInfo > > GetWalletTransactionsInfo ( WalletId walletId , string [ ] transactionIds = null )
2019-08-03 00:42:30 +09:00
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( walletId ) ;
2022-01-14 17:50:29 +09:00
using var ctx = _ContextFactory . CreateContext ( ) ;
2022-10-11 17:34:29 +09:00
IQueryable < WalletObjectLinkData > wols ;
IQueryable < WalletObjectData > wos ;
// If we are using postgres, the `transactionIds.Contains(w.ChildId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
// Such request isn't well optimized by postgres, and create different requests clogging up
// pg_stat_statements output, making it impossible to analyze the performance impact of this query.
if ( ctx . Database . IsNpgsql ( ) & & transactionIds is not null )
{
wos = ctx . WalletObjects
. FromSqlInterpolated ( $"SELECT wos.* FROM unnest({transactionIds}) t JOIN \" WalletObjects \ " wos ON wos.\"WalletId\"={walletId.ToString()} AND wos.\"Type\"={WalletObjectData.Types.Tx} AND wos.\"Id\"=t" )
. AsNoTracking ( ) ;
wols = ctx . WalletObjectLinks
. FromSqlInterpolated ( $"SELECT wol.* FROM unnest({transactionIds}) t JOIN \" WalletObjectLinks \ " wol ON wol.\"WalletId\"={walletId.ToString()} AND wol.\"ChildType\"={WalletObjectData.Types.Tx} AND wol.\"ChildId\"=t" )
. AsNoTracking ( ) ;
}
else // Unefficient path
{
wos = ctx . WalletObjects
. AsNoTracking ( )
. Where ( w = > w . WalletId = = walletId . ToString ( ) & & w . Type = = WalletObjectData . Types . Tx & & ( transactionIds = = null | | transactionIds . Contains ( w . Id ) ) ) ;
wols = ctx . WalletObjectLinks
. AsNoTracking ( )
. Where ( w = > w . WalletId = = walletId . ToString ( ) & & w . ChildType = = WalletObjectData . Types . Tx & & ( transactionIds = = null | | transactionIds . Contains ( w . ChildId ) ) ) ;
}
var links = await wols
. Select ( tx = >
new
{
TxId = tx . ChildId ,
AssociatedDataId = tx . ParentId ,
AssociatedDataType = tx . ParentType ,
AssociatedData = tx . Parent . Data
} )
. ToArrayAsync ( ) ;
var objs = await wos
. Select ( tx = >
new
{
TxId = tx . Id ,
Data = tx . Data
} )
. ToArrayAsync ( ) ;
var result = new Dictionary < string , WalletTransactionInfo > ( objs . Length ) ;
foreach ( var obj in objs )
{
var data = obj . Data is null ? null : JObject . Parse ( obj . Data ) ;
result . Add ( obj . TxId , new WalletTransactionInfo ( walletId )
{
Comment = data ? [ "comment" ] ? . Value < string > ( )
} ) ;
}
foreach ( var row in links )
{
JObject data = row . AssociatedData is null ? null : JObject . Parse ( row . AssociatedData ) ;
var info = result [ row . TxId ] ;
if ( row . AssociatedDataType = = WalletObjectData . Types . Label )
{
info . LabelColors . TryAdd ( row . AssociatedDataId , data [ "color" ] ? . Value < string > ( ) ? ? "#000" ) ;
}
else
{
info . Attachments . Add ( new Attachment ( row . AssociatedDataType , row . AssociatedDataId , row . AssociatedData is null ? null : JObject . Parse ( row . AssociatedData ) ) ) ;
}
}
return result ;
}
#nullable enable
public async Task < ( string Label , string Color ) [ ] > GetWalletLabels ( WalletId walletId )
{
using var ctx = _ContextFactory . CreateContext ( ) ;
return ( await ctx . WalletObjects
. AsNoTracking ( )
. Where ( w = > w . WalletId = = walletId . ToString ( ) & & w . Type = = WalletObjectData . Types . Label )
. Select ( o = > new { o . Id , o . Data } )
. ToArrayAsync ( ) )
. Select ( o = > ( o . Id , JObject . Parse ( o . Data ) [ "color" ] ! . Value < string > ( ) ! ) )
. ToArray ( ) ;
}
public async Task EnsureWalletObjectLink ( WalletObjectId parent , WalletObjectId child )
{
using var ctx = _ContextFactory . CreateContext ( ) ;
var l = new WalletObjectLinkData ( )
{
WalletId = parent . WalletId . ToString ( ) ,
ChildType = child . Type ,
ChildId = child . Id ,
ParentType = parent . Type ,
ParentId = parent . Id
} ;
ctx . WalletObjectLinks . Add ( l ) ;
2022-01-14 17:50:29 +09:00
try
2019-08-03 00:42:30 +09:00
{
2022-01-14 17:50:29 +09:00
await ctx . SaveChangesAsync ( ) ;
}
2022-10-11 17:34:29 +09:00
catch ( DbUpdateException ) // already exists
2022-01-14 17:50:29 +09:00
{
2019-08-03 00:42:30 +09:00
}
}
2022-10-11 17:34:29 +09:00
public static int MaxCommentSize = 200 ;
public async Task SetWalletObjectComment ( WalletObjectId id , string comment )
2019-08-03 00:42:30 +09:00
{
2022-10-11 17:34:29 +09:00
ArgumentNullException . ThrowIfNull ( id ) ;
ArgumentNullException . ThrowIfNull ( comment ) ;
if ( ! string . IsNullOrEmpty ( comment ) )
await ModifyWalletObjectData ( id , ( o ) = > o [ "comment" ] = comment . Trim ( ) . Truncate ( MaxCommentSize ) ) ;
else
await ModifyWalletObjectData ( id , ( o ) = > o . Remove ( "comment" ) ) ;
2019-08-03 00:42:30 +09:00
}
2022-10-11 17:34:29 +09:00
static WalletObjectData NewWalletObjectData ( WalletObjectId id , JObject ? data = null )
2019-08-03 00:42:30 +09:00
{
2022-10-11 17:34:29 +09:00
return new WalletObjectData ( )
{
WalletId = id . WalletId . ToString ( ) ,
Type = id . Type ,
Id = id . Id ,
Data = data ? . ToString ( )
} ;
}
public async Task ModifyWalletObjectData ( WalletObjectId id , Action < JObject > modify )
{
ArgumentNullException . ThrowIfNull ( id ) ;
ArgumentNullException . ThrowIfNull ( modify ) ;
2022-01-14 17:50:29 +09:00
using var ctx = _ContextFactory . CreateContext ( ) ;
2022-10-11 17:34:29 +09:00
var obj = await ctx . WalletObjects . FindAsync ( id . WalletId . ToString ( ) , id . Type , id . Id ) ;
if ( obj is null )
{
obj = NewWalletObjectData ( id ) ;
ctx . WalletObjects . Add ( obj ) ;
}
var currentData = obj . Data is null ? new JObject ( ) : JObject . Parse ( obj . Data ) ;
modify ( currentData ) ;
obj . Data = currentData . ToString ( ) ;
if ( obj . Data = = "{}" )
obj . Data = null ;
await ctx . SaveChangesAsync ( ) ;
}
const int MaxLabelSize = 50 ;
public async Task AddWalletObjectLabels ( WalletObjectId id , params string [ ] labels )
{
ArgumentNullException . ThrowIfNull ( id ) ;
await EnsureWalletObject ( id ) ;
foreach ( var l in labels . Select ( l = > l . Trim ( ) . Truncate ( MaxLabelSize ) ) )
{
var labelObjId = new WalletObjectId ( id . WalletId , WalletObjectData . Types . Label , l ) ;
await EnsureWalletObject ( labelObjId , new JObject ( )
{
["color"] = ColorPalette . Default . DeterministicColor ( l )
} ) ;
await EnsureWalletObjectLink ( labelObjId , id ) ;
}
2019-08-03 00:42:30 +09:00
}
2022-10-11 17:34:29 +09:00
public Task AddWalletTransactionAttachment ( WalletId walletId , uint256 txId , Attachment attachment )
{
return AddWalletTransactionAttachment ( walletId , txId , new [ ] { attachment } ) ;
}
public async Task AddWalletTransactionAttachment ( WalletId walletId , uint256 txId , IEnumerable < Attachment > attachments )
2019-08-03 00:42:30 +09:00
{
2021-12-28 17:39:54 +09:00
ArgumentNullException . ThrowIfNull ( walletId ) ;
2022-10-11 17:34:29 +09:00
ArgumentNullException . ThrowIfNull ( txId ) ;
var txObjId = new WalletObjectId ( walletId , WalletObjectData . Types . Tx , txId . ToString ( ) ) ;
await EnsureWalletObject ( txObjId ) ;
foreach ( var attachment in attachments )
2022-01-14 17:50:29 +09:00
{
2022-10-11 17:34:29 +09:00
var labelObjId = new WalletObjectId ( walletId , WalletObjectData . Types . Label , attachment . Type ) ;
await EnsureWalletObject ( labelObjId , new JObject ( )
{
["color"] = ColorPalette . Default . DeterministicColor ( attachment . Type )
} ) ;
await EnsureWalletObjectLink ( labelObjId , txObjId ) ;
if ( attachment . Data is not null | | attachment . Id . Length ! = 0 )
{
var data = new WalletObjectId ( walletId , attachment . Type , attachment . Id ) ;
await EnsureWalletObject ( data , attachment . Data ) ;
await EnsureWalletObjectLink ( data , txObjId ) ;
}
2022-01-14 17:50:29 +09:00
}
2022-10-11 17:34:29 +09:00
}
public async Task RemoveWalletObjectLabels ( WalletObjectId id , params string [ ] labels )
{
ArgumentNullException . ThrowIfNull ( id ) ;
foreach ( var l in labels . Select ( l = > l . Trim ( ) ) )
2019-08-03 00:42:30 +09:00
{
2022-10-11 17:34:29 +09:00
var labelObjId = new WalletObjectId ( id . WalletId , WalletObjectData . Types . Label , l ) ;
using var ctx = _ContextFactory . CreateContext ( ) ;
ctx . WalletObjectLinks . Remove ( new WalletObjectLinkData ( )
{
WalletId = id . WalletId . ToString ( ) ,
ChildId = id . Id ,
ChildType = id . Type ,
ParentId = labelObjId . Id ,
ParentType = labelObjId . Type
} ) ;
2019-08-03 00:42:30 +09:00
try
{
await ctx . SaveChangesAsync ( ) ;
}
2022-10-11 17:34:29 +09:00
catch ( DbUpdateException ) // Already deleted, do nothing
2019-08-03 00:42:30 +09:00
{
}
}
}
2022-10-11 17:34:29 +09:00
public async Task SetWalletObject ( WalletObjectId id , JObject ? data )
{
ArgumentNullException . ThrowIfNull ( id ) ;
using var ctx = _ContextFactory . CreateContext ( ) ;
var o = NewWalletObjectData ( id , data ) ;
ctx . WalletObjects . Add ( o ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateException ) // already exists
{
ctx . Entry ( o ) . State = EntityState . Modified ;
await ctx . SaveChangesAsync ( ) ;
}
}
public async Task EnsureWalletObject ( WalletObjectId id , JObject ? data = null )
{
ArgumentNullException . ThrowIfNull ( id ) ;
using var ctx = _ContextFactory . CreateContext ( ) ;
ctx . WalletObjects . Add ( NewWalletObjectData ( id , data ) ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateException ) // already exists
{
}
}
#nullable restore
2019-08-03 00:42:30 +09:00
}
}