Skip to content

Commit

Permalink
Merge pull request #25 from CirclesUBI/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
jaensen authored Jun 4, 2024
2 parents 15ceef0 + 465ff67 commit 3885877
Show file tree
Hide file tree
Showing 20 changed files with 971 additions and 79 deletions.
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

0 comments on commit 3885877

Please sign in to comment.