diff --git a/.idea/.idea.Circles/.idea/.gitignore b/.idea/.idea.Circles/.idea/.gitignore new file mode 100644 index 0000000..32f6f1d --- /dev/null +++ b/.idea/.idea.Circles/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/projectSettingsUpdater.xml +/.idea.Circles.iml +/modules.xml +/contentModel.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Circles.Index/Data/DbIdentifierAttribute.cs b/Circles.Index/Data/DbIdentifierAttribute.cs index ab378d2..8afe6ed 100644 --- a/Circles.Index/Data/DbIdentifierAttribute.cs +++ b/Circles.Index/Data/DbIdentifierAttribute.cs @@ -11,10 +11,17 @@ public static string GetIdentifier(this Enum enumValue) throw new InvalidOperationException($"Enum value {enumValue} (type: {type}) does not have a name."); } var field = type.GetField(name); + if (field == null) + { + throw new InvalidOperationException($"Enum {type} does not have a field named {name}."); + } var attribute = Attribute.GetCustomAttribute(field, typeof(DbIdentifierAttribute)) as DbIdentifierAttribute; var dbName = attribute?.Identifier ?? throw new InvalidOperationException( $"Enum value {enumValue} does not have a DbIdentifierAttribute."); + + // Escape reserved keywords + // TODO: Replace with a more robust solution if (dbName.ToLower() == "limit") { return "\"limit\""; diff --git a/Circles.Index/Data/Model/CirclesHubTransferDto.cs b/Circles.Index/Data/Model/CirclesHubTransferDto.cs deleted file mode 100644 index b722c70..0000000 --- a/Circles.Index/Data/Model/CirclesHubTransferDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Circles.Index.Data.Model; - -public record CirclesHubTransferDto( - string Timestamp, - string BlockNumber, - string TransactionHash, - string FromAddress, - string ToAddress, - string Amount, - string Cursor -); diff --git a/Circles.Index/Data/Model/CirclesHubTransferQuery.cs b/Circles.Index/Data/Model/CirclesHubTransferQuery.cs deleted file mode 100644 index 8054139..0000000 --- a/Circles.Index/Data/Model/CirclesHubTransferQuery.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Circles.Index.Data.Model; - -public enum QueryMode -{ - And, - Or -} - -public class CirclesHubTransferQuery -{ - public QueryMode Mode { get; set; } = QueryMode.And; - public Range BlockNumberRange { get; set; } = new(); - public string? TransactionHash { get; set; } - public string? FromAddress { get; set; } - public string? ToAddress { get; set; } - public string? Cursor { get; set; } - public int? Limit { get; set; } - public SortOrder SortOrder { get; set; } = SortOrder.Ascending; -} diff --git a/Circles.Index/Data/Model/CirclesSignupDto.cs b/Circles.Index/Data/Model/CirclesSignupDto.cs deleted file mode 100644 index 3f44c50..0000000 --- a/Circles.Index/Data/Model/CirclesSignupDto.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Circles.Index.Data.Model; - -public record CirclesSignupDto( - string Timestamp, - string BlockNumber, - string TransactionHash, - string CirclesAddress, - string? TokenAddress, - string Cursor -); diff --git a/Circles.Index/Data/Model/CirclesSignupQuery.cs b/Circles.Index/Data/Model/CirclesSignupQuery.cs deleted file mode 100644 index a62d701..0000000 --- a/Circles.Index/Data/Model/CirclesSignupQuery.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Circles.Index.Data.Model; - -public class CirclesSignupQuery -{ - public Range BlockNumberRange { get; set; } = new(); - public string? TransactionHash { get; set; } - public string? UserAddress { get; set; } - public string? TokenAddress { get; set; } - public string? Cursor { get; set; } - public int? Limit { get; set; } - public SortOrder SortOrder { get; set; } = SortOrder.Ascending; -} diff --git a/Circles.Index/Data/Model/CirclesTransferDto.cs b/Circles.Index/Data/Model/CirclesTransferDto.cs deleted file mode 100644 index 751a7d8..0000000 --- a/Circles.Index/Data/Model/CirclesTransferDto.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Circles.Index.Data.Model; - -public record CirclesTransferDto( - string Timestamp, - string BlockNumber, - string TransactionHash, - string FromAddress, - string ToAddress, - string? Amount, - string TokenAddress, - string Cursor -); diff --git a/Circles.Index/Data/Model/CirclesTransferQuery.cs b/Circles.Index/Data/Model/CirclesTransferQuery.cs deleted file mode 100644 index 794ab84..0000000 --- a/Circles.Index/Data/Model/CirclesTransferQuery.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Circles.Index.Data.Model; - -public class CirclesTransferQuery -{ - public QueryMode Mode { get; set; } = QueryMode.And; - public Range BlockNumberRange { get; set; } = new(); - public string? TransactionHash { get; set; } - public string? TokenAddress { get; set; } - public string? FromAddress { get; set; } - public string? ToAddress { get; set; } - public string? Cursor { get; set; } - public int? Limit { get; set; } - public SortOrder SortOrder { get; set; } = SortOrder.Ascending; -} diff --git a/Circles.Index/Data/Model/CirclesTrustDto.cs b/Circles.Index/Data/Model/CirclesTrustDto.cs deleted file mode 100644 index b15af33..0000000 --- a/Circles.Index/Data/Model/CirclesTrustDto.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Circles.Index.Data.Model; - -public record CirclesTrustDto( - string Timestamp, - string BlockNumber, - string TransactionHash, - string UserAddress, - string CanSendToAddress, - int Limit, - string Cursor -); diff --git a/Circles.Index/Data/Model/CirclesTrustQuery.cs b/Circles.Index/Data/Model/CirclesTrustQuery.cs deleted file mode 100644 index 7e95767..0000000 --- a/Circles.Index/Data/Model/CirclesTrustQuery.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Circles.Index.Data.Model; - -public class CirclesTrustQuery -{ - public Range BlockNumberRange { get; set; }= new(); - public string? TransactionHash { get; set; } - public string? UserAddress { get; set; } - public string? CanSendToAddress { get; set; } - public string? Cursor { get; set; } - public int? Limit { get; set; } - public SortOrder SortOrder { get; set; } = SortOrder.Ascending; - public QueryMode Mode { get; set; } = QueryMode.And; -} diff --git a/Circles.Index/Data/Model/SortOrder.cs b/Circles.Index/Data/Model/SortOrder.cs deleted file mode 100644 index a85d7a2..0000000 --- a/Circles.Index/Data/Model/SortOrder.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Circles.Index.Data.Model; - -public enum SortOrder -{ - Ascending, - Descending -} diff --git a/Circles.Index/Data/Postgresql/CursorUtils.cs b/Circles.Index/Data/Postgresql/CursorUtils.cs deleted file mode 100644 index 256c764..0000000 --- a/Circles.Index/Data/Postgresql/CursorUtils.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Circles.Index.Data.Model; -using Npgsql; -using NpgsqlTypes; - -namespace Circles.Index.Data.Postgresql; - -public static class CursorUtils -{ - public static (string CursorConditionSql, NpgsqlParameter[] cursorParameters) GenerateCursorConditionAndParameters( - string? cursor, SortOrder sortOrder) - { - if (string.IsNullOrEmpty(cursor)) - { - return ("1 = 1", Array.Empty()); - } - - if (TryParseCursor(cursor, out long cursorBlockNumber, out long cursorTransactionIndex, - out long cursorLogIndex)) - { - NpgsqlParameter[] cursorParameters = - { - new("@CursorBlockNumber", NpgsqlDbType.Bigint) { Value = cursorBlockNumber }, - new("@CursorTransactionIndex", NpgsqlDbType.Bigint) { Value = cursorTransactionIndex }, - new("@CursorLogIndex", NpgsqlDbType.Bigint) { Value = cursorLogIndex } - }; - - string cursorConditionSql = sortOrder == SortOrder.Ascending - ? "(block_number > @CursorBlockNumber OR (block_number = @CursorBlockNumber AND (transaction_index > @CursorTransactionIndex OR (transaction_index = @CursorTransactionIndex AND log_index > @CursorLogIndex))))" - : "(block_number < @CursorBlockNumber OR (block_number = @CursorBlockNumber AND (transaction_index < @CursorTransactionIndex OR (transaction_index = @CursorTransactionIndex AND log_index < @CursorLogIndex))))"; - - return (cursorConditionSql, cursorParameters); - } - - throw new ArgumentException("Invalid cursor format", nameof(cursor)); - } - - private static bool TryParseCursor(string cursor, out long blockNumber, out long transactionIndex, - out long logIndex) - { - blockNumber = 0; - transactionIndex = 0; - logIndex = 0; - - var parts = cursor.Split('-'); - if (parts.Length != 3) - { - return false; - } - - return long.TryParse(parts[0], out blockNumber) && - long.TryParse(parts[1], out transactionIndex) && - long.TryParse(parts[2], out logIndex); - } -} \ No newline at end of file diff --git a/Circles.Index/Data/Query/Equals.cs b/Circles.Index/Data/Query/Equals.cs index b560b36..f24f3f2 100644 --- a/Circles.Index/Data/Query/Equals.cs +++ b/Circles.Index/Data/Query/Equals.cs @@ -9,10 +9,10 @@ public class Equals : IQuery public readonly Tables Table; public readonly Columns Column; private readonly string _parameterName; - public readonly object Value; + public readonly object? Value; private readonly DbProviderFactory _provider; - internal Equals(DbProviderFactory provider, Tables table, Columns column, object value) + internal Equals(DbProviderFactory provider, Tables table, Columns column, object? value) { _provider = provider; Table = table; @@ -34,7 +34,7 @@ public IEnumerable GetParameters() parameter.ParameterName = _parameterName; var targetType = Schema.TableSchemas[Table].Columns.First(o => o.Column == Column).Type; - parameter.Value = Query.Convert(Value, targetType); + parameter.Value = Query.Convert(Value, targetType) ?? DBNull.Value; yield return parameter; } } \ No newline at end of file diff --git a/Circles.Index/Data/Query/Query.cs b/Circles.Index/Data/Query/Query.cs index 4253324..0f0ab5e 100644 --- a/Circles.Index/Data/Query/Query.cs +++ b/Circles.Index/Data/Query/Query.cs @@ -71,8 +71,12 @@ public static IEnumerable Execute(DbConnection connection, IQuery quer } } - public static object Convert(object input, ValueTypes target) + public static object? Convert(object? input, ValueTypes target) { + if (input == null) + { + return null; + } switch (target) { case ValueTypes.String: @@ -94,10 +98,16 @@ public static object Convert(object input, ValueTypes target) case ValueTypes.Address when input is string i: return i.ToLowerInvariant(); case ValueTypes.Address: - return input.ToString().ToLowerInvariant(); + return input.ToString()?.ToLowerInvariant(); + case ValueTypes.Boolean when input is bool b: + return b; + case ValueTypes.Boolean when input is string s: + return bool.Parse(s); + case ValueTypes.Boolean when input is int i: + return i != 0; default: throw new ArgumentOutOfRangeException(nameof(target), target, - $"Input was {input} of type {input.GetType()}"); + $"Cannot convert input {input} (type: {input.GetType()?.Name ?? ""}) to target type {target}."); } } } \ No newline at end of file diff --git a/Circles.Index/Data/Query/Select.cs b/Circles.Index/Data/Query/Select.cs index 6513818..a3acda0 100644 --- a/Circles.Index/Data/Query/Select.cs +++ b/Circles.Index/Data/Query/Select.cs @@ -10,6 +10,7 @@ public class Select : IQuery public readonly IEnumerable Columns; private readonly List _fields; public readonly List Conditions; + public readonly List<(Columns Column, SortOrder Order)> OrderBy = new(); public Select(Tables table, IEnumerable columns) { @@ -35,6 +36,11 @@ public string ToSql() sql.Append(" WHERE "); sql.Append(string.Join(" AND ", Conditions.Select(c => c.ToSql()))); } + if (OrderBy.Any()) + { + sql.Append(" ORDER BY "); + sql.Append(string.Join(", ", OrderBy.Select(o => $"{o.Column.GetIdentifier()} {o.Order}"))); + } return sql.ToString(); } @@ -50,9 +56,6 @@ public IEnumerable GetParameters() } } - public IQuery And(IQuery other) => Query.And(this, other); - public IQuery Or(IQuery other) => Query.Or(this, other); - public override string ToString() { StringBuilder sb = new(); diff --git a/Circles.Index/Data/Model/Range.cs b/Circles.Index/Data/Range.cs similarity index 71% rename from Circles.Index/Data/Model/Range.cs rename to Circles.Index/Data/Range.cs index 30f4203..5e9a6b4 100644 --- a/Circles.Index/Data/Model/Range.cs +++ b/Circles.Index/Data/Range.cs @@ -1,4 +1,4 @@ -namespace Circles.Index.Data.Model; +namespace Circles.Index.Data; public class Range { diff --git a/Circles.Index/Data/SortOrder.cs b/Circles.Index/Data/SortOrder.cs new file mode 100644 index 0000000..d193ad2 --- /dev/null +++ b/Circles.Index/Data/SortOrder.cs @@ -0,0 +1,7 @@ +namespace Circles.Index.Data; + +public enum SortOrder +{ + Asc, + Desc +} \ No newline at end of file diff --git a/Circles.Index/Indexer/BlockIndexer.cs b/Circles.Index/Indexer/BlockIndexer.cs index c540681..9762724 100644 --- a/Circles.Index/Indexer/BlockIndexer.cs +++ b/Circles.Index/Indexer/BlockIndexer.cs @@ -3,7 +3,6 @@ using Nethermind.Blockchain; using Nethermind.Blockchain.Receipts; using Nethermind.Core; -using Circles.Index.Data.Model; using Nethermind.Core.Crypto; namespace Circles.Index.Indexer; diff --git a/Circles.Index/Indexer/StateMachine.cs b/Circles.Index/Indexer/StateMachine.cs index 61a5f79..21fd924 100644 --- a/Circles.Index/Indexer/StateMachine.cs +++ b/Circles.Index/Indexer/StateMachine.cs @@ -1,5 +1,4 @@ using Circles.Index.Data; -using Circles.Index.Data.Model; using Circles.Index.Data.Postgresql; using Nethermind.Blockchain; using Nethermind.Blockchain.Receipts; diff --git a/Circles.Index/Rpc/CirclesRpcModule.cs b/Circles.Index/Rpc/CirclesRpcModule.cs index 23e19f5..ec3cb01 100644 --- a/Circles.Index/Rpc/CirclesRpcModule.cs +++ b/Circles.Index/Rpc/CirclesRpcModule.cs @@ -132,14 +132,17 @@ public ResultWrapper> circles_query(CirclesQuery query) using NpgsqlConnection connection = new(_indexConnectionString); connection.Open(); + if (query.Table == null) + { + throw new InvalidOperationException("Table is null"); + } + Tables parsedTableName = Enum.Parse(query.Table); - + var select = Query.Select(parsedTableName, - query.Columns?.Select(c => Enum.Parse(c)) + query.Columns?.Select(Enum.Parse) ?? throw new InvalidOperationException("Columns are null")); - Console.WriteLine(select.ToString()); - if (query.Conditions.Any()) { foreach (var condition in query.Conditions) @@ -148,9 +151,25 @@ public ResultWrapper> circles_query(CirclesQuery query) } } - Console.WriteLine(select.ToString()); - + if (query.OrderBy.Any()) + { + foreach (var orderBy in query.OrderBy) + { + if (orderBy.Column == null || orderBy.SortOrder == null) + { + throw new InvalidOperationException("OrderBy: Column or SortOrder is null"); + } + + Columns parsedColumnName = Enum.Parse(orderBy.Column); + select.OrderBy.Add(( + parsedColumnName, + orderBy.SortOrder.ToLowerInvariant() == "asc" + ? SortOrder.Asc + : SortOrder.Desc)); + } + } + Console.WriteLine(select.ToString()); var result = Query.Execute(connection, select).ToList(); return ResultWrapper>.Success(result); diff --git a/Circles.Index/Rpc/ICirclesRpcModule.cs b/Circles.Index/Rpc/ICirclesRpcModule.cs index e782e16..4a99c27 100644 --- a/Circles.Index/Rpc/ICirclesRpcModule.cs +++ b/Circles.Index/Rpc/ICirclesRpcModule.cs @@ -25,9 +25,17 @@ public CirclesTokenBalance(Address token, string balance) public class CirclesQuery { - public string Table { get; set; } + public string? Table { get; set; } public string[]? Columns { get; set; } public List Conditions { get; set; } = new(); + + public List OrderBy { get; set; } = new(); +} + +public class OrderBy +{ + public string? Column { get; set; } + public string? SortOrder { get; set; } } public class Expression