Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.2 #25

Merged
merged 11 commits into from
Jun 4, 2024
2 changes: 1 addition & 1 deletion Circles.Index.CirclesV1/LogParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Circles.Index.CirclesV1;

public class LogParser(Address v1HubAddress) : ILogParser
{
private static readonly ConcurrentDictionary<Address, object?> CirclesTokenAddresses = new();
public static readonly ConcurrentDictionary<Address, object?> CirclesTokenAddresses = new();

private readonly Hash256 _transferTopic = new(DatabaseSchema.Transfer.Topic);
private readonly Hash256 _signupTopic = new(DatabaseSchema.Signup.Topic);
Expand Down
19 changes: 18 additions & 1 deletion Circles.Index.Common/DatabaseSchema.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using System.Text.Json;
using Nethermind.Core;

namespace Circles.Index.Common;

public record BlockWithEventCounts(Block Block, IDictionary<string, int> EventCounts);

public class DatabaseSchema : IDatabaseSchema
{
public ISchemaPropertyMap SchemaPropertyMap { get; } = new SchemaPropertyMap();
Expand All @@ -13,8 +18,20 @@ public class DatabaseSchema : IDatabaseSchema
new EventSchema("System", "Block", new byte[32], [
new("blockNumber", ValueTypes.Int, false),
new("timestamp", ValueTypes.Int, true),
new("blockHash", ValueTypes.String, false)
new("blockHash", ValueTypes.String, false),
new("eventCounts", ValueTypes.String, false)
])
}
};

public DatabaseSchema()
{
SchemaPropertyMap.Add(("System", "Block"), new Dictionary<string, Func<BlockWithEventCounts, object?>>
{
{ "blockNumber", o => o.Block.Number },
{ "timestamp", o => (long)o.Block.Timestamp },
{ "blockHash", o => o.Block.Hash!.ToString() },
{ "eventCounts", o => JsonSerializer.Serialize(o.EventCounts) }
});
}
}
19 changes: 8 additions & 11 deletions Circles.Index.Query.Tests/Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,18 @@ public IDbDataParameter CreateParameter(string? name, object? value)
}
}

public class TestDbDataParameter : IDbDataParameter
public class TestDbDataParameter(string? name, object? value) : IDbDataParameter
{
public TestDbDataParameter(string? name, object? value)
{
ParameterName = name;
Value = value;
}

public DbType DbType { get; set; }
public ParameterDirection Direction { get; set; }
public bool IsNullable { get; }
public string ParameterName { get; set; }
public string SourceColumn { get; set; }
public bool IsNullable => false;

#pragma warning disable CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
public string ParameterName { get; set; } = name ?? "";
public string SourceColumn { get; set; } = "";
#pragma warning restore CS8767 // Nullability of reference types in type of parameter doesn't match implicitly implemented member (possibly because of nullability attributes).
public DataRowVersion SourceVersion { get; set; }
public object? Value { get; set; }
public object? Value { get; set; } = value;
public byte Precision { get; set; }
public byte Scale { get; set; }
public int Size { get; set; }
Expand Down
4 changes: 2 additions & 2 deletions Circles.Index.Query/Dto/FilterPredicateDtoConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ public override IFilterPredicateDto[] Read(ref Utf8JsonReader reader, Type typeT
_ => throw new NotSupportedException($"Unknown filter predicate type: {type}")
};

predicates[i++] = result;
predicates[i++] = result ?? throw new JsonException("Failed to deserialize filter predicate.");
}

return predicates;
Expand All @@ -77,7 +77,7 @@ public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonS
switch (reader.TokenType)
{
case JsonTokenType.String:
return reader.GetString();
return reader.GetString() ?? throw new JsonException("Unexpected null string value.");
case JsonTokenType.Number:
if (reader.TryGetInt32(out int intValue))
{
Expand Down
7 changes: 3 additions & 4 deletions Circles.Index.Query/Select.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ public record Select(
IEnumerable<IFilterPredicate> Filter,
IEnumerable<OrderBy> Order,
int? Limit = null,
bool Distinct = false) : ISql
bool Distinct = false,
int MaxLimit = 1000) : ISql
{
private const int MaxLimit = 1000;

public ParameterizedSql ToSql(IDatabaseUtils database)
{
if (!database.Schema.Tables.TryGetValue((Namespace, Table), out var tableSchema))
Expand Down Expand Up @@ -74,7 +73,7 @@ public ParameterizedSql ToSql(IDatabaseUtils database)
sql += orderBySql;
}

if (Limit is > 0 and <= MaxLimit)
if (Limit > 0 && Limit <= MaxLimit)
{
sql += $" LIMIT {Limit.Value}";
}
Expand Down
1 change: 1 addition & 0 deletions Circles.Index.Rpc/Circles.Index.Rpc.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.2.0" />
<PackageReference Include="Nethermind.ReferenceAssemblies" Version="1.25.4" />
</ItemGroup>

Expand Down
28 changes: 17 additions & 11 deletions Circles.Index.Rpc/CirclesRpcModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,10 @@ public Task<ResultWrapper<CirclesTrustRelations>> circles_getTrustRelations(Addr

foreach (var resultRow in result.Rows)
{
var user = new Address(resultRow[0].ToString() ?? throw new Exception("A user in the result set is null"));
var canSendTo = new Address(resultRow[1].ToString() ??
var user = new Address(resultRow[0]?.ToString() ?? throw new Exception("A user in the result set is null"));
var canSendTo = new Address(resultRow[1]?.ToString() ??
throw new Exception("A canSendTo in the result set is null"));
var limit = int.Parse(resultRow[2].ToString() ?? throw new Exception("A limit in the result set is null"));
var limit = int.Parse(resultRow[2]?.ToString() ?? throw new Exception("A limit in the result set is null"));

if (user == address)
{
Expand Down Expand Up @@ -112,7 +112,7 @@ public Task<ResultWrapper<string>> circlesV2_getTotalBalance(Address address, bo

private string TotalBalanceV2(IEthRpcModule rpcModule, Address address, bool asTimeCircles)
{
IEnumerable<UInt256> tokenIds = V2TokenIdsForAccount(_pluginLogger, address);
IEnumerable<UInt256> tokenIds = V2TokenIdsForAccount(address);

// Call the erc1155's balanceOf function for each token using _ethRpcModule.eth_call().
// Solidity function signature: balanceOf(address _account, uint256 _id) public view returns (uint256)
Expand Down Expand Up @@ -156,7 +156,7 @@ public async Task<ResultWrapper<CirclesTokenBalanceV2[]>> circlesV2_getTokenBala
await rentedEthRpcModule.Rent();

var balances =
V2CirclesTokenBalances(_pluginLogger, rentedEthRpcModule.RpcModule!, address,
V2CirclesTokenBalances(rentedEthRpcModule.RpcModule!, address,
_indexerContext.Settings.CirclesV2HubAddress, asTimeCircles);

return ResultWrapper<CirclesTokenBalanceV2[]>.Success(balances.ToArray());
Expand All @@ -183,6 +183,12 @@ public ResultWrapper<DatabaseQueryResult> circles_query(SelectDto query)
return ResultWrapper<DatabaseQueryResult>.Success(result);
}

public ResultWrapper<CirclesEvent[]> circles_events(Address address, long fromBlock, long? toBlock = null)
{
var queryEvents = new QueryEvents(_indexerContext);
return ResultWrapper<CirclesEvent[]>.Success(queryEvents.CirclesEvents(address, fromBlock, toBlock));
}

#region private methods

private IEnumerable<Address> TokenAddressesForAccount(Address circlesAccount)
Expand All @@ -203,7 +209,7 @@ private IEnumerable<Address> TokenAddressesForAccount(Address circlesAccount)
return _indexerContext.Database
.Select(sql)
.Rows
.Select(o => new Address(o[0].ToString()
.Select(o => new Address(o[0]?.ToString()
?? throw new Exception("A token address in the result set is null"))
);
}
Expand All @@ -230,8 +236,8 @@ private IDictionary<string, string> GetTokenOwners(string[] tokenAddresses)
var tokenOwners = new Dictionary<string, string>();
foreach (var row in result.Rows)
{
var avatar = row[0].ToString() ?? throw new Exception("An avatar in the result set is null");
var tokenId = row[1].ToString() ?? throw new Exception("A tokenId in the result set is null");
var avatar = row[0]?.ToString() ?? throw new Exception("An avatar in the result set is null");
var tokenId = row[1]?.ToString() ?? throw new Exception("A tokenId in the result set is null");
tokenOwners[tokenId] = avatar;
}

Expand Down Expand Up @@ -302,10 +308,10 @@ private static string FormatTimeCircles(UInt256 tokenBalance)
: ether.ToString(CultureInfo.InvariantCulture);
}

private List<CirclesTokenBalanceV2> V2CirclesTokenBalances(ILogger logger, IEthRpcModule rpcModule, Address address,
private List<CirclesTokenBalanceV2> V2CirclesTokenBalances(IEthRpcModule rpcModule, Address address,
Address hubAddress, bool asTimeCircles)
{
IEnumerable<UInt256> tokenIds = V2TokenIdsForAccount(logger, address);
IEnumerable<UInt256> tokenIds = V2TokenIdsForAccount(address);

// Call the erc1155's balanceOf function for each token using _ethRpcModule.eth_call().
// Solidity function signature: balanceOf(address _account, uint256 _id) public view returns (uint256)
Expand Down Expand Up @@ -350,7 +356,7 @@ private List<CirclesTokenBalanceV2> V2CirclesTokenBalances(ILogger logger, IEthR
return balances;
}

private IEnumerable<UInt256> V2TokenIdsForAccount(ILogger logger, Address address)
private IEnumerable<UInt256> V2TokenIdsForAccount(Address address)
{
var select = new Select(
"V_CrcV2"
Expand Down
130 changes: 130 additions & 0 deletions Circles.Index.Rpc/CirclesSubscription.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
using Circles.Index.Common;
using Circles.Index.Query;
using Nethermind.Core;
using Nethermind.JsonRpc;
using Nethermind.JsonRpc.Modules.Subscribe;

namespace Circles.Index.Rpc;

public class NotifyEventArgs(CirclesEvent[] events) : EventArgs
{
public CirclesEvent[] Events { get; } = events;
}

public class CirclesSubscription : Subscription
{
public override string Type => "circles";

private readonly CirclesSubscriptionParams _param;

public static long SubscriberCount => _subscriberCount;
private static long _subscriberCount;

public CirclesSubscription(IJsonRpcDuplexClient jsonRpcDuplexClient, Context context,
CirclesSubscriptionParams param) : base(
jsonRpcDuplexClient)
{
Notification += OnNotification;
_param = param;

if (param.Address == Address.Zero)
{
throw new Exception("The zero address cannot be subscribed to.");
}

if (param.Address != null)
{
var select = new Select("V_Crc", "Avatars", ["avatar"], [
new FilterPredicate("avatar", FilterType.Equals, param.Address?.ToString(true, false))
], [], 1);

var parameterizedSql = select.ToSql(context.Database);
var avatarInfo = context.Database.Select(parameterizedSql);
if (!avatarInfo.Rows.Any())
{
throw new Exception($"The address {param.Address} is not a circles avatar.");
}
}

Interlocked.Increment(ref _subscriberCount);
}

public static event EventHandler<NotifyEventArgs>? Notification;

public static void Notify(Context context, Range<long> importedRange)
{
if (_subscriberCount == 0)
{
return;
}

var queryEvents = new QueryEvents(context);
var events = queryEvents.CirclesEvents(null, importedRange.Min, importedRange.Max);

if (events.Length == 0)
{
return;
}

Notification?.Invoke(null, new NotifyEventArgs(events));
}

private void OnNotification(object? sender, NotifyEventArgs e)
{
ScheduleAction(async () =>
{
CirclesEvent[] events;

if (_param.Address != null)
{
var filterAddress = _param.Address!.ToString(true, false);
events = FilterForAffectedAddress(e, filterAddress);
}
else
{
events = e.Events;
}

JsonRpcResult result = CreateSubscriptionMessage(events);
await JsonRpcDuplexClient.SendJsonRpcResult(result);
});
}

private CirclesEvent[] FilterForAffectedAddress(NotifyEventArgs e, string filterAddress)
{
var filteredForAddress = new List<CirclesEvent>();
var addressesInEvent = new HashSet<string>();

foreach (var circlesEvent in e.Events)
{
addressesInEvent.Clear();

foreach (var circlesEventValue in circlesEvent.Values)
{
if (!QueryEvents.AddressColumns.Contains(circlesEventValue.Key))
{
continue;
}

if (circlesEventValue.Value is string address)
{
addressesInEvent.Add(address);
}
}

if (addressesInEvent.Contains(filterAddress))
{
filteredForAddress.Add(circlesEvent);
}
}

return filteredForAddress.ToArray();
}

public override void Dispose()
{
base.Dispose();

Interlocked.Decrement(ref _subscriberCount);
}
}
32 changes: 32 additions & 0 deletions Circles.Index.Rpc/CirclesSubscriptionParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Text.Json;
using Nethermind.Core;
using Nethermind.JsonRpc;

namespace Circles.Index.Rpc;

public class CirclesSubscriptionParams : IJsonRpcParam
{
public Address? Address { get; private set; }

public void ReadJson(JsonElement element, JsonSerializerOptions options)
{
JsonDocument? doc = null;
try
{
if (element.ValueKind == JsonValueKind.String)
{
doc = JsonDocument.Parse(element.GetString() ?? "{}");
element = doc.RootElement;
}

if (element.TryGetProperty("address", out JsonElement addressElement))
{
Address = Address.TryParse(addressElement.GetString(), out Address? address) ? address : null;
}
}
finally
{
doc?.Dispose();
}
}
}
Loading
Loading