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 ;
2023-03-26 13:42:38 +02:00
using System.Linq.Expressions ;
2019-08-03 00:42:30 +09:00
using System.Threading.Tasks ;
2022-10-11 17:34:29 +09:00
using BTCPayServer.Abstractions.Extensions ;
2022-11-16 04:11:17 +01:00
using BTCPayServer.Client.Models ;
2019-08-03 00:42:30 +09:00
using BTCPayServer.Data ;
2022-12-01 01:54:55 +01:00
using BTCPayServer.Services.Wallets ;
2023-05-20 23:26:16 +09:00
using Dapper ;
2019-08-03 00:42:30 +09:00
using Microsoft.EntityFrameworkCore ;
2022-10-11 17:34:29 +09:00
using NBitcoin ;
2022-11-16 04:11:17 +01:00
using Newtonsoft.Json ;
2022-10-11 17:34:29 +09:00
using Newtonsoft.Json.Linq ;
2023-05-20 23:26:16 +09:00
using Npgsql ;
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 ) ;
2022-11-19 23:39:41 +09:00
public record ObjectTypeId ( string Type , string Id ) ;
2022-11-19 00:04:46 +09:00
public class GetWalletObjectsQuery
{
2022-11-19 23:39:41 +09:00
public GetWalletObjectsQuery ( )
{
}
public GetWalletObjectsQuery ( WalletId ? walletId ) : this ( walletId , null , null )
2022-11-19 00:04:46 +09:00
{
}
public GetWalletObjectsQuery ( WalletObjectId walletObjectId ) : this ( walletObjectId . WalletId , walletObjectId . Type , new [ ] { walletObjectId . Id } )
{
}
2022-11-19 23:39:41 +09:00
public GetWalletObjectsQuery ( WalletId ? walletId , string type ) : this ( walletId , type , null )
2022-11-19 00:04:46 +09:00
{
}
2022-11-19 23:39:41 +09:00
public GetWalletObjectsQuery ( WalletId ? walletId , string? type , string [ ] ? ids )
2022-11-19 00:04:46 +09:00
{
WalletId = walletId ;
Type = type ;
Ids = ids ;
}
2023-01-06 14:18:07 +01:00
public GetWalletObjectsQuery ( WalletId ? walletId , ObjectTypeId [ ] ? typesIds )
2022-11-19 23:39:41 +09:00
{
2022-12-01 01:54:55 +01:00
WalletId = walletId ;
2022-11-19 23:39:41 +09:00
TypesIds = typesIds ;
}
2022-11-19 00:04:46 +09:00
2022-11-19 23:39:41 +09:00
public WalletId ? WalletId { get ; set ; }
// Either the user passes a list of Types/Ids
public ObjectTypeId [ ] ? TypesIds { get ; set ; }
// Or the user passes one type, and a list of Ids
2022-11-19 00:04:46 +09:00
public string? Type { get ; set ; }
public string [ ] ? Ids { get ; set ; }
public bool IncludeNeighbours { get ; set ; } = true ;
public bool UseInefficientPath { get ; set ; }
2022-12-01 01:54:55 +01:00
public static IEnumerable < ObjectTypeId > Get ( ReceivedCoin coin )
{
yield return new ObjectTypeId ( WalletObjectData . Types . Tx , coin . OutPoint . Hash . ToString ( ) ) ;
2022-12-13 09:09:25 +09:00
yield return new ObjectTypeId ( WalletObjectData . Types . Address , coin . Address . ToString ( ) ) ;
2022-12-01 01:54:55 +01:00
yield return new ObjectTypeId ( WalletObjectData . Types . Utxo , coin . OutPoint . ToString ( ) ) ;
}
2022-11-19 00:04:46 +09:00
}
2022-11-19 23:39:41 +09:00
2022-10-11 17:34:29 +09:00
#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-11-19 00:04:46 +09:00
#nullable enable
public async Task < WalletObjectData ? > GetWalletObject ( WalletObjectId walletObjectId , bool includeNeighbours = true )
2019-08-03 00:42:30 +09:00
{
2022-11-19 00:04:46 +09:00
var r = await GetWalletObjects ( new ( walletObjectId ) { IncludeNeighbours = includeNeighbours } ) ;
return r . Select ( o = > o . Value ) . FirstOrDefault ( ) ;
}
public async Task < Dictionary < WalletObjectId , WalletObjectData > > GetWalletObjects ( GetWalletObjectsQuery queryObject )
{
ArgumentNullException . ThrowIfNull ( queryObject ) ;
if ( queryObject . Ids ! = null & & queryObject . Type is null )
throw new ArgumentException ( "If \"Ids\" is not null, \"Type\" is mandatory" ) ;
2022-11-19 23:39:41 +09:00
if ( queryObject . Type is not null & & queryObject . TypesIds is not null )
throw new ArgumentException ( "If \"Type\" is not null, \"TypesIds\" should be null" ) ;
2022-01-14 17:50:29 +09:00
using var ctx = _ContextFactory . CreateContext ( ) ;
2022-10-11 17:34:29 +09:00
2022-11-25 12:06:57 +09:00
// If we are using postgres, the `transactionIds.Contains(w.BId)` result in a long query like `ANY(@txId1, @txId2, @txId3, @txId4)`
2022-10-11 17:34:29 +09:00
// 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.
2022-11-19 00:04:46 +09:00
// On top of this, the entity version is doing 2 left join to satisfy the Include queries, resulting in n*m row returned for each transaction.
// n being the number of children, m the number of parents.
if ( ctx . Database . IsNpgsql ( ) & & ! queryObject . UseInefficientPath )
2022-10-11 17:34:29 +09:00
{
2022-11-19 00:04:46 +09:00
var connection = ctx . Database . GetDbConnection ( ) ;
if ( connection . State ! = System . Data . ConnectionState . Open )
await connection . OpenAsync ( ) ;
2022-11-19 23:39:41 +09:00
string walletIdFilter = queryObject . WalletId is not null ? " AND wos.\"WalletId\"=@walletId" : "" ;
string typeFilter = queryObject . Type is not null ? " AND wos.\"Type\"=@type" : "" ;
2022-11-19 00:04:46 +09:00
var cmd = connection . CreateCommand ( ) ;
var selectWalletObjects =
2022-11-19 23:39:41 +09:00
queryObject . TypesIds is not null ?
$"SELECT wos.* FROM unnest(@ids, @types) t(i,t) JOIN \" WalletObjects \ " wos ON true{walletIdFilter} AND wos.\"Type\"=t AND wos.\"Id\"=i" :
2022-11-19 00:04:46 +09:00
queryObject . Ids is null ?
2022-11-19 23:39:41 +09:00
$"SELECT wos.* FROM \" WalletObjects \ " wos WHERE true{walletIdFilter}{typeFilter} " :
2022-11-19 00:04:46 +09:00
queryObject . Ids . Length = = 1 ?
2022-11-19 23:39:41 +09:00
$"SELECT wos.* FROM \" WalletObjects \ " wos WHERE true{walletIdFilter} AND wos.\"Type\"=@type AND wos.\"Id\"=@id" :
$"SELECT wos.* FROM unnest(@ids) t JOIN \" WalletObjects \ " wos ON true{walletIdFilter} AND wos.\"Type\"=@type AND wos.\"Id\"=t" ;
2022-11-19 00:04:46 +09:00
var includeNeighbourSelect = queryObject . IncludeNeighbours ? ", wos2.\"Data\" AS \"Data2\"" : "" ;
var includeNeighbourJoin = queryObject . IncludeNeighbours ? "LEFT JOIN \"WalletObjects\" wos2 ON wos.\"WalletId\"=wos2.\"WalletId\" AND wol.\"Type2\"=wos2.\"Type\" AND wol.\"Id2\"=wos2.\"Id\"" : "" ;
var query =
2022-11-19 23:39:41 +09:00
$"SELECT wos.\" WalletId \ ", wos.\"Id\", wos.\"Type\", wos.\"Data\", wol.\"LinkData\", wol.\"Type2\", wol.\"Id2\"{includeNeighbourSelect} FROM ({selectWalletObjects}) wos " +
2022-11-19 00:04:46 +09:00
$"LEFT JOIN LATERAL ( " +
2022-11-25 12:06:57 +09:00
"SELECT \"AType\" AS \"Type2\", \"AId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"BType\"=wos.\"Type\" AND \"BId\"=wos.\"Id\" " +
2022-11-19 00:04:46 +09:00
"UNION " +
2022-11-25 12:06:57 +09:00
"SELECT \"BType\" AS \"Type2\", \"BId\" AS \"Id2\", \"Data\" AS \"LinkData\" FROM \"WalletObjectLinks\" WHERE \"WalletId\"=wos.\"WalletId\" AND \"AType\"=wos.\"Type\" AND \"AId\"=wos.\"Id\"" +
2022-11-19 00:04:46 +09:00
$" ) wol ON true " + includeNeighbourJoin ;
cmd . CommandText = query ;
2022-11-19 23:39:41 +09:00
if ( queryObject . WalletId is not null )
{
var walletIdParam = cmd . CreateParameter ( ) ;
walletIdParam . ParameterName = "walletId" ;
walletIdParam . Value = queryObject . WalletId . ToString ( ) ;
walletIdParam . DbType = System . Data . DbType . String ;
cmd . Parameters . Add ( walletIdParam ) ;
}
2022-11-19 00:04:46 +09:00
if ( queryObject . Type ! = null )
{
var typeParam = cmd . CreateParameter ( ) ;
typeParam . ParameterName = "type" ;
typeParam . Value = queryObject . Type ;
typeParam . DbType = System . Data . DbType . String ;
cmd . Parameters . Add ( typeParam ) ;
}
2022-11-19 23:39:41 +09:00
if ( queryObject . TypesIds ! = null )
{
var typesParam = cmd . CreateParameter ( ) ;
typesParam . ParameterName = "types" ;
typesParam . Value = queryObject . TypesIds . Select ( t = > t . Type ) . ToList ( ) ;
typesParam . DbType = System . Data . DbType . Object ;
cmd . Parameters . Add ( typesParam ) ;
var idParam = cmd . CreateParameter ( ) ;
idParam . ParameterName = "ids" ;
idParam . Value = queryObject . TypesIds . Select ( t = > t . Id ) . ToList ( ) ;
idParam . DbType = System . Data . DbType . Object ;
cmd . Parameters . Add ( idParam ) ;
}
2022-11-19 00:04:46 +09:00
if ( queryObject . Ids ! = null )
{
if ( queryObject . Ids . Length = = 1 )
{
var txIdParam = cmd . CreateParameter ( ) ;
txIdParam . ParameterName = "id" ;
txIdParam . Value = queryObject . Ids [ 0 ] ;
txIdParam . DbType = System . Data . DbType . String ;
cmd . Parameters . Add ( txIdParam ) ;
}
else
{
var txIdsParam = cmd . CreateParameter ( ) ;
txIdsParam . ParameterName = "ids" ;
2022-11-19 23:39:41 +09:00
txIdsParam . Value = queryObject . Ids . ToList ( ) ;
2022-11-19 00:04:46 +09:00
txIdsParam . DbType = System . Data . DbType . Object ;
cmd . Parameters . Add ( txIdsParam ) ;
}
}
await using var reader = await cmd . ExecuteReaderAsync ( ) ;
var wosById = new Dictionary < WalletObjectId , WalletObjectData > ( ) ;
while ( await reader . ReadAsync ( ) )
{
WalletObjectData wo = new WalletObjectData ( ) ;
2022-11-19 23:39:41 +09:00
wo . WalletId = ( string ) reader [ "WalletId" ] ;
2022-11-19 00:04:46 +09:00
wo . Type = ( string ) reader [ "Type" ] ;
wo . Id = ( string ) reader [ "Id" ] ;
2022-11-19 23:39:41 +09:00
var id = new WalletObjectId ( WalletId . Parse ( wo . WalletId ) , wo . Type , wo . Id ) ;
2022-11-19 00:04:46 +09:00
wo . Data = reader [ "Data" ] is DBNull ? null : ( string ) reader [ "Data" ] ;
if ( wosById . TryGetValue ( id , out var wo2 ) )
wo = wo2 ;
else
{
wosById . Add ( id , wo ) ;
2022-11-25 12:06:57 +09:00
wo . Bs = new List < WalletObjectLinkData > ( ) ;
2022-11-19 00:04:46 +09:00
}
if ( reader [ "Type2" ] is not DBNull )
{
var l = new WalletObjectLinkData ( )
{
2022-11-25 12:06:57 +09:00
BType = ( string ) reader [ "Type2" ] ,
BId = ( string ) reader [ "Id2" ] ,
2022-11-19 00:04:46 +09:00
Data = reader [ "LinkData" ] is DBNull ? null : ( string ) reader [ "LinkData" ]
} ;
2022-11-25 12:06:57 +09:00
wo . Bs . Add ( l ) ;
l . B = new WalletObjectData ( )
2022-11-19 00:04:46 +09:00
{
2022-11-25 12:06:57 +09:00
Type = l . BType ,
Id = l . BId ,
2022-11-19 00:04:46 +09:00
Data = ( ! queryObject . IncludeNeighbours | | reader [ "Data2" ] is DBNull ) ? null : ( string ) reader [ "Data2" ]
} ;
}
}
return wosById ;
2022-10-11 17:34:29 +09:00
}
else // Unefficient path
{
2022-11-19 23:39:41 +09:00
IQueryable < WalletObjectData > q ;
if ( queryObject . TypesIds is not null )
{
// Note this is problematic if the type contains '##', but I don't see how to do it properly with entity framework
var idTypes = queryObject . TypesIds . Select ( o = > $"{o.Type}##{o.Id}" ) . ToArray ( ) ;
q = ctx . WalletObjects
. Where ( w = > ( queryObject . WalletId = = null | | w . WalletId = = queryObject . WalletId . ToString ( ) ) & & idTypes . Contains ( w . Type + "##" + w . Id ) ) ;
}
else
{
q = ctx . WalletObjects
. Where ( w = > ( queryObject . WalletId = = null | | w . WalletId = = queryObject . WalletId . ToString ( ) ) & & ( queryObject . Type = = null | | w . Type = = queryObject . Type ) & & ( queryObject . Ids = = null | | queryObject . Ids . Contains ( w . Id ) ) ) ;
}
2022-11-19 00:04:46 +09:00
if ( queryObject . IncludeNeighbours )
2022-10-11 17:34:29 +09:00
{
2022-11-25 12:06:57 +09:00
q = q . Include ( o = > o . Bs ) . ThenInclude ( o = > o . B )
. Include ( o = > o . As ) . ThenInclude ( o = > o . A ) ;
2022-11-19 00:04:46 +09:00
}
q = q . AsNoTracking ( ) ;
2022-10-11 17:34:29 +09:00
2022-11-19 00:04:46 +09:00
var wosById = new Dictionary < WalletObjectId , WalletObjectData > ( ) ;
foreach ( var row in await q . ToListAsync ( ) )
2022-10-11 17:34:29 +09:00
{
2022-11-19 23:39:41 +09:00
var id = new WalletObjectId ( WalletId . Parse ( row . WalletId ) , row . Type , row . Id ) ;
2022-11-19 00:04:46 +09:00
wosById . TryAdd ( id , row ) ;
}
return wosById ;
2022-10-11 17:34:29 +09:00
}
2022-11-19 00:04:46 +09:00
}
#nullable restore
2022-12-01 01:54:55 +01:00
public async Task < Dictionary < string , WalletTransactionInfo > > GetWalletTransactionsInfo ( WalletId walletId ,
string [ ] transactionIds = null )
{
var wos = await GetWalletObjects (
new GetWalletObjectsQuery ( walletId , WalletObjectData . Types . Tx , transactionIds ) ) ;
2022-12-13 09:09:25 +09:00
return GetWalletTransactionsInfoCore ( walletId , wos ) ;
2022-12-01 01:54:55 +01:00
}
public async Task < Dictionary < string , WalletTransactionInfo > > GetWalletTransactionsInfo ( WalletId walletId ,
ObjectTypeId [ ] transactionIds = null )
{
var wos = await GetWalletObjects (
new GetWalletObjectsQuery ( walletId , transactionIds ) ) ;
2023-01-06 14:18:07 +01:00
2022-12-13 09:09:25 +09:00
return GetWalletTransactionsInfoCore ( walletId , wos ) ;
2022-12-01 01:54:55 +01:00
}
2023-01-06 14:18:07 +01:00
2022-12-22 14:17:23 +01:00
public WalletTransactionInfo Merge ( params WalletTransactionInfo [ ] infos )
{
WalletTransactionInfo result = null ;
foreach ( WalletTransactionInfo walletTransactionInfo in infos . Where ( info = > info is not null ) )
{
if ( result is null )
{
result ? ? = walletTransactionInfo ;
}
else
{
2023-01-06 14:18:07 +01:00
2022-12-22 14:17:23 +01:00
result = result . Merge ( walletTransactionInfo ) ;
}
}
return result ;
}
2022-12-01 01:54:55 +01:00
2022-12-13 09:09:25 +09:00
private Dictionary < string , WalletTransactionInfo > GetWalletTransactionsInfoCore ( WalletId walletId ,
2022-12-01 01:54:55 +01:00
Dictionary < WalletObjectId , WalletObjectData > wos )
2022-11-19 00:04:46 +09:00
{
2023-01-06 14:18:07 +01:00
2022-11-19 00:04:46 +09:00
var result = new Dictionary < string , WalletTransactionInfo > ( wos . Count ) ;
foreach ( var obj in wos . Values )
2022-10-11 17:34:29 +09:00
{
2022-11-19 00:04:46 +09:00
var data = obj . Data is null ? null : JObject . Parse ( obj . Data ) ;
var info = new WalletTransactionInfo ( walletId )
2022-10-11 17:34:29 +09:00
{
2022-11-19 00:04:46 +09:00
Comment = data ? [ "comment" ] ? . Value < string > ( )
} ;
result . Add ( obj . Id , info ) ;
foreach ( var neighbour in obj . GetNeighbours ( ) )
2022-10-11 17:34:29 +09:00
{
2022-11-19 00:04:46 +09:00
var neighbourData = neighbour . Data is null ? null : JObject . Parse ( neighbour . Data ) ;
if ( neighbour . Type = = WalletObjectData . Types . Label )
{
2022-12-22 14:17:23 +01:00
info . LabelColors . TryAdd ( neighbour . Id , neighbourData ? [ "color" ] ? . Value < string > ( ) ? ? ColorPalette . Default . DeterministicColor ( neighbour . Id ) ) ;
2022-11-19 00:04:46 +09:00
}
else
{
info . Attachments . Add ( new Attachment ( neighbour . Type , neighbour . Id , neighbourData ) ) ;
}
2022-10-11 17:34:29 +09:00
}
}
return result ;
}
2022-11-19 00:04:46 +09:00
2022-10-11 17:34:29 +09:00
#nullable enable
public async Task < ( string Label , string Color ) [ ] > GetWalletLabels ( WalletId walletId )
2023-03-26 13:42:38 +02:00
{
2023-04-10 11:07:03 +09:00
return await GetWalletLabels ( w = >
2023-03-26 13:42:38 +02:00
w . WalletId = = walletId . ToString ( ) & &
w . Type = = WalletObjectData . Types . Label ) ;
}
2023-04-10 11:07:03 +09:00
2023-03-26 13:42:38 +02:00
public async Task < ( string Label , string Color ) [ ] > GetWalletLabels ( WalletObjectId objectId )
{
2023-04-10 11:07:03 +09:00
2023-04-07 08:58:41 +02:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
var obj = await GetWalletObject ( objectId , true ) ;
return obj is null ? Array . Empty < ( string Label , string Color ) > ( ) : obj . GetNeighbours ( ) . Where ( data = > data . Type = = WalletObjectData . Types . Label ) . Select ( FormatToLabel ) . ToArray ( ) ;
2023-03-26 13:42:38 +02:00
}
2023-04-10 11:07:03 +09:00
2023-03-26 13:42:38 +02:00
private async Task < ( string Label , string Color ) [ ] > GetWalletLabels ( Expression < Func < WalletObjectData , bool > > predicate )
2022-10-11 17:34:29 +09:00
{
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-03-26 13:42:38 +02:00
return ( await ctx . WalletObjects . AsNoTracking ( ) . Where ( predicate ) . ToArrayAsync ( ) )
. Select ( FormatToLabel ) . ToArray ( ) ;
}
private ( string Label , string Color ) FormatToLabel ( WalletObjectData o )
{
return o . Data is null
? ( o . Id , ColorPalette . Default . DeterministicColor ( o . Id ) )
: ( o . Id ,
JObject . Parse ( o . Data ) [ "color" ] ? . Value < string > ( ) ? ? ColorPalette . Default . DeterministicColor ( o . Id ) ) ;
2022-10-11 17:34:29 +09:00
}
2022-11-19 00:04:46 +09:00
public async Task < bool > RemoveWalletObjects ( WalletObjectId walletObjectId )
2022-10-11 17:34:29 +09:00
{
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2022-11-19 00:04:46 +09:00
var entity = new WalletObjectData ( )
2022-11-16 04:11:17 +01:00
{
2022-11-19 00:04:46 +09:00
WalletId = walletObjectId . WalletId . ToString ( ) ,
Type = walletObjectId . Type ,
Id = walletObjectId . Id
} ;
ctx . WalletObjects . Add ( entity ) ;
ctx . Entry ( entity ) . State = EntityState . Deleted ;
try
2022-11-16 04:11:17 +01:00
{
2022-11-19 00:04:46 +09:00
await ctx . SaveChangesAsync ( ) ;
return true ;
2022-11-16 04:11:17 +01:00
}
2022-11-19 00:04:46 +09:00
catch ( DbUpdateException ) // doesn't exists
2022-11-16 04:11:17 +01:00
{
2022-11-19 00:04:46 +09:00
return false ;
2022-11-16 04:11:17 +01:00
}
}
2022-11-19 00:04:46 +09:00
public async Task EnsureWalletObjectLink ( WalletObjectId a , WalletObjectId b , JObject ? data = null )
2022-11-16 04:11:17 +01:00
{
2022-11-19 00:04:46 +09:00
SortWalletObjectLinks ( ref a , ref b ) ;
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-05-20 23:26:16 +09:00
await UpdateWalletObjectLink ( a , b , data , ctx , true ) ;
}
private static async Task UpdateWalletObjectLink ( WalletObjectId a , WalletObjectId b , JObject ? data , ApplicationDbContext ctx , bool doNothingIfExists )
{
2022-10-11 17:34:29 +09:00
var l = new WalletObjectLinkData ( )
{
2022-11-19 00:04:46 +09:00
WalletId = a . WalletId . ToString ( ) ,
2022-11-25 12:06:57 +09:00
AType = a . Type ,
AId = a . Id ,
BType = b . Type ,
BId = b . Id ,
2022-11-16 04:11:17 +01:00
Data = data ? . ToString ( Formatting . None )
2022-10-11 17:34:29 +09:00
} ;
2023-05-20 23:26:16 +09:00
if ( ! ctx . Database . IsNpgsql ( ) )
2019-08-03 00:42:30 +09:00
{
2023-05-20 23:26:16 +09:00
var e = ctx . WalletObjectLinks . Add ( l ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateException ) // already exists
{
if ( ! doNothingIfExists )
{
e . State = EntityState . Modified ;
await ctx . SaveChangesAsync ( ) ;
}
}
2022-01-14 17:50:29 +09:00
}
2023-05-20 23:26:16 +09:00
else
2022-01-14 17:50:29 +09:00
{
2023-05-20 23:26:16 +09:00
var connection = ctx . Database . GetDbConnection ( ) ;
var conflict = doNothingIfExists ? "ON CONFLICT DO NOTHING" : "ON CONFLICT ON CONSTRAINT \"PK_WalletObjectLinks\" DO UPDATE SET \"Data\"=EXCLUDED.\"Data\"" ;
try
{
await connection . ExecuteAsync ( "INSERT INTO \"WalletObjectLinks\" VALUES (@WalletId, @AType, @AId, @BType, @BId, @Data::JSONB) " + conflict , l ) ;
}
catch ( PostgresException ex ) when ( ex . SqlState = = PostgresErrorCodes . ForeignKeyViolation )
{
throw new DbUpdateException ( ) ;
}
2019-08-03 00:42:30 +09:00
}
}
2022-11-19 00:04:46 +09:00
class WalletObjectIdComparer : IComparer < WalletObjectId >
2022-11-16 04:11:17 +01:00
{
2022-11-19 00:04:46 +09:00
public static readonly WalletObjectIdComparer Instance = new WalletObjectIdComparer ( ) ;
public int Compare ( WalletObjectId ? x , WalletObjectId ? y )
{
var c = StringComparer . InvariantCulture . Compare ( x ? . Type , y ? . Type ) ;
if ( c = = 0 )
c = StringComparer . InvariantCulture . Compare ( x ? . Id , y ? . Id ) ;
return c ;
}
}
private void SortWalletObjectLinks ( ref WalletObjectId a , ref WalletObjectId b )
{
if ( a . WalletId ! = b . WalletId )
throw new ArgumentException ( "It shouldn't be possible to set a link between different wallets" ) ;
var ab = new [ ] { a , b } ;
Array . Sort ( ab , WalletObjectIdComparer . Instance ) ;
a = ab [ 0 ] ;
b = ab [ 1 ] ;
}
public async Task SetWalletObjectLink ( WalletObjectId a , WalletObjectId b , JObject ? data = null )
{
SortWalletObjectLinks ( ref a , ref b ) ;
2022-11-19 23:39:41 +09:00
2022-11-19 00:04:46 +09:00
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-05-20 23:26:16 +09:00
await UpdateWalletObjectLink ( a , b , data , ctx , false ) ;
2022-11-16 04:11:17 +01: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 )
{
2023-01-06 14:18:07 +01:00
return AddWalletTransactionAttachment ( walletId , txId . ToString ( ) , new [ ] { attachment } , WalletObjectData . Types . Tx ) ;
2022-12-01 01:54:55 +01:00
}
public Task AddWalletTransactionAttachment ( WalletId walletId , uint256 txId ,
IEnumerable < Attachment > attachments )
{
return AddWalletTransactionAttachment ( walletId , txId . ToString ( ) , attachments , WalletObjectData . Types . Tx ) ;
2022-10-11 17:34:29 +09:00
}
2023-01-06 14:18:07 +01:00
2022-12-01 01:54:55 +01:00
public async Task AddWalletTransactionAttachment ( WalletId walletId , string txId , IEnumerable < Attachment > attachments , string type )
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 ) ;
2022-12-01 01:54:55 +01:00
var txObjId = new WalletObjectId ( walletId , type , txId . ToString ( ) ) ;
2022-10-11 17:34:29 +09:00
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
}
2022-11-16 04:11:17 +01:00
2022-11-19 00:04:46 +09:00
public async Task < bool > RemoveWalletObjectLink ( WalletObjectId a , WalletObjectId b )
2022-11-16 04:11:17 +01:00
{
2022-11-19 00:04:46 +09:00
SortWalletObjectLinks ( ref a , ref b ) ;
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
ctx . WalletObjectLinks . Remove ( new WalletObjectLinkData ( )
{
2022-11-19 00:04:46 +09:00
WalletId = a . WalletId . ToString ( ) ,
2022-11-25 12:06:57 +09:00
AId = a . Id ,
AType = a . Type ,
BId = b . Id ,
BType = b . Type
2022-11-16 04:11:17 +01:00
} ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
2022-11-19 00:04:46 +09:00
return true ;
2022-11-16 04:11:17 +01:00
}
catch ( DbUpdateException ) // Already deleted, do nothing
{
2022-11-19 00:04:46 +09:00
return false ;
2022-11-16 04:11:17 +01: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 ) ;
2022-11-16 04:11:17 +01:00
await RemoveWalletObjectLink ( labelObjId , id ) ;
2019-08-03 00:42:30 +09:00
}
}
2023-09-19 02:55:04 +02:00
public async Task < bool > RemoveWalletLabels ( WalletId id , params string [ ] labels )
{
ArgumentNullException . ThrowIfNull ( id ) ;
var count = 0 ;
foreach ( var l in labels . Select ( l = > l . Trim ( ) ) )
{
var labelObjId = new WalletObjectId ( id , WalletObjectData . Types . Label , l ) ;
count + = await RemoveWalletObjects ( labelObjId ) ? 1 : 0 ;
}
return count > 0 ;
}
2022-10-11 17:34:29 +09:00
public async Task SetWalletObject ( WalletObjectId id , JObject ? data )
{
ArgumentNullException . ThrowIfNull ( id ) ;
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-05-20 23:26:16 +09:00
var wo = NewWalletObjectData ( id , data ) ;
if ( ! ctx . Database . IsNpgsql ( ) )
2022-10-11 17:34:29 +09:00
{
2023-05-20 23:26:16 +09:00
ctx . WalletObjects . Add ( wo ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateException ) // already exists
{
ctx . Entry ( wo ) . State = EntityState . Modified ;
await ctx . SaveChangesAsync ( ) ;
}
2022-10-11 17:34:29 +09:00
}
2023-05-20 23:26:16 +09:00
else
2022-10-11 17:34:29 +09:00
{
2023-05-20 23:26:16 +09:00
var connection = ctx . Database . GetDbConnection ( ) ;
await connection . ExecuteAsync ( "INSERT INTO \"WalletObjects\" VALUES (@WalletId, @Type, @Id, @Data::JSONB) ON CONFLICT ON CONSTRAINT \"PK_WalletObjects\" DO UPDATE SET \"Data\"=EXCLUDED.\"Data\"" , wo ) ;
2022-10-11 17:34:29 +09:00
}
}
public async Task EnsureWalletObject ( WalletObjectId id , JObject ? data = null )
{
ArgumentNullException . ThrowIfNull ( id ) ;
2023-05-20 23:26:16 +09:00
var wo = NewWalletObjectData ( id , data ) ;
2022-11-16 04:11:17 +01:00
await using var ctx = _ContextFactory . CreateContext ( ) ;
2023-05-20 23:26:16 +09:00
if ( ! ctx . Database . IsNpgsql ( ) )
2022-10-11 17:34:29 +09:00
{
2023-05-20 23:26:16 +09:00
ctx . WalletObjects . Add ( wo ) ;
try
{
await ctx . SaveChangesAsync ( ) ;
}
catch ( DbUpdateException ) // already exists
{
}
2022-10-11 17:34:29 +09:00
}
2023-05-20 23:26:16 +09:00
else
2022-10-11 17:34:29 +09:00
{
2023-05-20 23:26:16 +09:00
var connection = ctx . Database . GetDbConnection ( ) ;
await connection . ExecuteAsync ( "INSERT INTO \"WalletObjects\" VALUES (@WalletId, @Type, @Id, @Data::JSONB) ON CONFLICT DO NOTHING" , wo ) ;
2022-10-11 17:34:29 +09:00
}
}
#nullable restore
2019-08-03 00:42:30 +09:00
}
}