Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17564957cb | ||
|
|
64ecf531bd | ||
|
|
ec0f7a6790 | ||
|
|
930c44f037 | ||
|
|
dc33f0655c | ||
|
|
8a4d9fecea | ||
|
|
cb259a9086 | ||
|
|
9d8ec6f794 | ||
|
|
acc9526cd5 | ||
|
|
43a160a9cc | ||
|
|
4de2626a33 | ||
|
|
255e08a301 | ||
|
|
71409b06ae | ||
|
|
f25bca727d | ||
|
|
fb73312e80 | ||
|
|
9a626b7260 | ||
|
|
ede80410ea | ||
|
|
00c720137a | ||
|
|
890cff93d5 | ||
|
|
678b719cef | ||
|
|
1b7c11b849 | ||
|
|
c6a7489f11 | ||
|
|
f7fcef56c7 | ||
|
|
29fa554146 | ||
|
|
2f10b30283 | ||
|
|
391a2653d9 | ||
|
|
55be07d711 | ||
|
|
75757e37fb | ||
|
|
2be31a4d3b | ||
|
|
4b74a9d5fe | ||
|
|
4f2539c59f | ||
|
|
7df1f3da7b | ||
|
|
fb57998aa8 |
@@ -1,4 +1,4 @@
|
||||
name: Docker - Reusable
|
||||
name: Reusable - Docker
|
||||
|
||||
|
||||
on:
|
||||
@@ -66,26 +66,26 @@ jobs:
|
||||
PLATFORM: ${{ format('{0}/{1}', 'linux', matrix.target.arch) }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Docker - Build and Push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@v4
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ${{ env.DOCKER_FILE }}
|
||||
@@ -101,9 +101,9 @@ jobs:
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Docker - Upload digest
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: digests
|
||||
name: digests-${{ matrix.target.arch }}
|
||||
path: /tmp/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
@@ -139,17 +139,18 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Docker - Download digests
|
||||
uses: actions/download-artifact@v3
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: digests
|
||||
pattern: digests-*
|
||||
merge-multiple: true
|
||||
path: /tmp/digests
|
||||
|
||||
- name: Docker - Set up Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Docker - Meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.DOCKER_REPO }}
|
||||
flavor: |
|
||||
@@ -161,7 +162,7 @@ jobs:
|
||||
type=sha,enable=${{ env.TAG_SHA }}
|
||||
|
||||
- name: Docker - Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
name: Docker - Rewarder Bot
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
paths:
|
||||
- 'Tools/TestNetRewarder/**'
|
||||
- '!Tools/TestNetRewarder/docker/docker-compose.yaml'
|
||||
- 'Framework/**'
|
||||
- 'ProjectPlugins/**'
|
||||
- .github/workflows/docker-rewarder.yml
|
||||
- .github/workflows/docker-reusable.yml
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
name: Build and Push
|
||||
uses: ./.github/workflows/docker-reusable.yml
|
||||
with:
|
||||
docker_file: Tools/TestNetRewarder/docker/Dockerfile
|
||||
docker_repo: codexstorage/codex-rewarderbot
|
||||
secrets: inherit
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Report - HoldMyBeerTest
|
||||
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 */2 * *'
|
||||
# schedule:
|
||||
# - cron: '30 1 */2 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Report - PeersTest
|
||||
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 */2 * *'
|
||||
# schedule:
|
||||
# - cron: '30 1 */2 * *'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ${{ inputs.workflow_source }}
|
||||
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
[[ -n "${{ inputs.tests_cleanup }}" ]] && echo "TESTS_CLEANUP=${{ inputs.tests_cleanup }}" >>"$GITHUB_ENV" || echo "TESTS_CLEANUP=${{ env.TESTS_CLEANUP }}" >>"$GITHUB_ENV"
|
||||
|
||||
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
|
||||
uses: azure/setup-kubectl@v3
|
||||
uses: azure/setup-kubectl@releases/v4.0.0
|
||||
with:
|
||||
version: ${{ env.KUBE_VERSION }}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Variables
|
||||
run: |
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
[[ -n "${{ inputs.command }}" ]] && COMMAND="${{ inputs.command }}" || COMMAND="${{ env.COMMAND }}"
|
||||
|
||||
- name: Kubectl - Install ${{ env.KUBE_VERSION }}
|
||||
uses: azure/setup-kubectl@v3
|
||||
uses: azure/setup-kubectl@releases/v4.0.0
|
||||
with:
|
||||
version: ${{ env.KUBE_VERSION }}
|
||||
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
obj
|
||||
bin
|
||||
.vscode
|
||||
.idea
|
||||
Tools/CommitBlamer/CodexTestLogs
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Utils;
|
||||
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class CheckConfig
|
||||
{
|
||||
public CheckType Type { get; set; }
|
||||
public ulong MinNumberOfHosts { get; set; }
|
||||
public ByteSize MinSlotSize { get; set; } = 0.Bytes();
|
||||
public TimeSpan MinDuration { get; set; } = TimeSpan.Zero;
|
||||
}
|
||||
|
||||
public enum CheckType
|
||||
{
|
||||
Uninitialized,
|
||||
FilledSlot,
|
||||
FinishedSlot,
|
||||
PostedContract,
|
||||
StartedContract,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\Utils\Utils.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class GiveRewardsCommand
|
||||
{
|
||||
public RewardUsersCommand[] Rewards { get; set; } = Array.Empty<RewardUsersCommand>();
|
||||
}
|
||||
|
||||
public class RewardUsersCommand
|
||||
{
|
||||
public ulong RewardId { get; set; }
|
||||
public string[] UserAddresses { get; set; } = Array.Empty<string>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class RewardConfig
|
||||
{
|
||||
public const string UsernameTag = "<USER>";
|
||||
|
||||
public RewardConfig(ulong roleId, string message, CheckConfig checkConfig)
|
||||
{
|
||||
RoleId = roleId;
|
||||
Message = message;
|
||||
CheckConfig = checkConfig;
|
||||
}
|
||||
|
||||
public ulong RoleId { get; }
|
||||
public string Message { get; }
|
||||
public CheckConfig CheckConfig { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Utils;
|
||||
|
||||
namespace DiscordRewards
|
||||
{
|
||||
public class RewardRepo
|
||||
{
|
||||
private static string Tag => RewardConfig.UsernameTag;
|
||||
|
||||
public RewardConfig[] Rewards { get; } = new RewardConfig[]
|
||||
{
|
||||
// Filled any slot
|
||||
new RewardConfig(1187039439558541498, $"{Tag} successfully filled their first slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FilledSlot
|
||||
}),
|
||||
|
||||
// Finished any slot
|
||||
new RewardConfig(1202286165630390339, $"{Tag} successfully finished their first slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot
|
||||
}),
|
||||
|
||||
// Finished a sizable slot
|
||||
new RewardConfig(1202286218738405418, $"{Tag} finished their first 1GB-24h slot!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinSlotSize = 1.GB(),
|
||||
MinDuration = TimeSpan.FromHours(24.0),
|
||||
}),
|
||||
|
||||
// Posted any contract
|
||||
new RewardConfig(1202286258370383913, $"{Tag} posted their first contract!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.PostedContract
|
||||
}),
|
||||
|
||||
// Started any contract
|
||||
new RewardConfig(1202286330873126992, $"A contract created by {Tag} reached Started state for the first time!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.StartedContract
|
||||
}),
|
||||
|
||||
// Started a sizable contract
|
||||
new RewardConfig(1202286381670608909, $"A large contract created by {Tag} reached Started state for the first time!", new CheckConfig
|
||||
{
|
||||
Type = CheckType.FinishedSlot,
|
||||
MinNumberOfHosts = 4,
|
||||
MinSlotSize = 1.GB(),
|
||||
MinDuration = TimeSpan.FromHours(24.0),
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using CodexContractsPlugin;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
|
||||
namespace GethConnector
|
||||
{
|
||||
public class GethConnector
|
||||
{
|
||||
public IGethNode GethNode { get; }
|
||||
public ICodexContracts CodexContracts { get; }
|
||||
|
||||
public static GethConnector? Initialize(ILog log)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(GethInput.LoadError))
|
||||
{
|
||||
var msg = "Geth input incorrect: " + GethInput.LoadError;
|
||||
log.Error(msg);
|
||||
return null;
|
||||
}
|
||||
|
||||
var contractsDeployment = new CodexContractsDeployment(
|
||||
marketplaceAddress: GethInput.MarketplaceAddress,
|
||||
abi: GethInput.ABI,
|
||||
tokenAddress: GethInput.TokenAddress
|
||||
);
|
||||
|
||||
var gethNode = new CustomGethNode(log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
|
||||
var contracts = new CodexContractsAccess(log, gethNode, contractsDeployment);
|
||||
|
||||
return new GethConnector(gethNode, contracts);
|
||||
}
|
||||
|
||||
private GethConnector(IGethNode gethNode, ICodexContracts codexContracts)
|
||||
{
|
||||
GethNode = gethNode;
|
||||
CodexContracts = codexContracts;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\CodexContractsPlugin\CodexContractsPlugin.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\GethPlugin\GethPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,52 @@
|
||||
namespace GethConnector
|
||||
{
|
||||
public static class GethInput
|
||||
{
|
||||
private const string GethHostVar = "GETH_HOST";
|
||||
private const string GethPortVar = "GETH_HTTP_PORT";
|
||||
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
|
||||
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
|
||||
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
|
||||
private const string AbiVar = "CODEXCONTRACTS_ABI";
|
||||
|
||||
static GethInput()
|
||||
{
|
||||
var error = new List<string>();
|
||||
var gethHost = GetEnvVar(error, GethHostVar);
|
||||
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
|
||||
var privateKey = GetEnvVar(error, GethPrivKeyVar);
|
||||
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
|
||||
var tokenAddress = GetEnvVar(error, TokenAddressVar);
|
||||
var abi = GetEnvVar(error, AbiVar);
|
||||
|
||||
if (error.Any())
|
||||
{
|
||||
LoadError = string.Join(", ", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
GethHost = gethHost!;
|
||||
GethPort = gethPort;
|
||||
PrivateKey = privateKey!;
|
||||
MarketplaceAddress = marketplaceAddress!;
|
||||
TokenAddress = tokenAddress!;
|
||||
ABI = abi!;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GethHost { get; } = string.Empty;
|
||||
public static int GethPort { get; }
|
||||
public static string PrivateKey { get; } = string.Empty;
|
||||
public static string MarketplaceAddress { get; } = string.Empty;
|
||||
public static string TokenAddress { get; } = string.Empty;
|
||||
public static string ABI { get; } = string.Empty;
|
||||
public static string LoadError { get; } = string.Empty;
|
||||
|
||||
private static string? GetEnvVar(List<string> error, string name)
|
||||
{
|
||||
var result = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public partial class BlockTimeFinder
|
||||
{
|
||||
public class BlockTimeEntry
|
||||
{
|
||||
public BlockTimeEntry(ulong blockNumber, DateTime utc)
|
||||
{
|
||||
BlockNumber = blockNumber;
|
||||
Utc = utc;
|
||||
}
|
||||
|
||||
public ulong BlockNumber { get; }
|
||||
public DateTime Utc { get; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{BlockNumber}] @ {Utc.ToString("o")}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
using Logging;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
{
|
||||
public partial class BlockTimeFinder
|
||||
{
|
||||
private const ulong FetchRange = 6;
|
||||
private const int MaxEntries = 1024;
|
||||
private static readonly Dictionary<ulong, BlockTimeEntry> entries = new Dictionary<ulong, BlockTimeEntry>();
|
||||
private readonly Web3 web3;
|
||||
private readonly ILog log;
|
||||
|
||||
public BlockTimeFinder(Web3 web3, ILog log)
|
||||
{
|
||||
this.web3 = web3;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public ulong GetHighestBlockNumberBefore(DateTime moment)
|
||||
{
|
||||
log.Log("Looking for highest block before " + moment.ToString("o"));
|
||||
AssertMomentIsInPast(moment);
|
||||
Initialize();
|
||||
|
||||
return GetHighestBlockBefore(moment);
|
||||
}
|
||||
|
||||
public ulong GetLowestBlockNumberAfter(DateTime moment)
|
||||
{
|
||||
log.Log("Looking for lowest block after " + moment.ToString("o"));
|
||||
AssertMomentIsInPast(moment);
|
||||
Initialize();
|
||||
|
||||
return GetLowestBlockAfter(moment);
|
||||
}
|
||||
|
||||
private ulong GetHighestBlockBefore(DateTime moment)
|
||||
{
|
||||
var closestBefore = FindClosestBeforeEntry(moment);
|
||||
var closestAfter = FindClosestAfterEntry(moment);
|
||||
|
||||
if (closestBefore != null &&
|
||||
closestAfter != null &&
|
||||
closestBefore.Utc < moment &&
|
||||
closestAfter.Utc > moment &&
|
||||
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
|
||||
{
|
||||
log.Log("Found highest-Before: " + closestBefore);
|
||||
return closestBefore.BlockNumber;
|
||||
}
|
||||
|
||||
FetchBlocksAround(moment);
|
||||
return GetHighestBlockBefore(moment);
|
||||
}
|
||||
|
||||
private ulong GetLowestBlockAfter(DateTime moment)
|
||||
{
|
||||
var closestBefore = FindClosestBeforeEntry(moment);
|
||||
var closestAfter = FindClosestAfterEntry(moment);
|
||||
|
||||
if (closestBefore != null &&
|
||||
closestAfter != null &&
|
||||
closestBefore.Utc < moment &&
|
||||
closestAfter.Utc > moment &&
|
||||
closestBefore.BlockNumber + 1 == closestAfter.BlockNumber)
|
||||
{
|
||||
log.Log("Found lowest-after: " + closestAfter);
|
||||
return closestAfter.BlockNumber;
|
||||
}
|
||||
|
||||
FetchBlocksAround(moment);
|
||||
return GetLowestBlockAfter(moment);
|
||||
}
|
||||
|
||||
private void FetchBlocksAround(DateTime moment)
|
||||
{
|
||||
var timePerBlock = EstimateTimePerBlock();
|
||||
log.Debug("Fetching blocks around " + moment.ToString("o") + " timePerBlock: " + timePerBlock.TotalSeconds);
|
||||
|
||||
EnsureRecentBlockIfNecessary(moment, timePerBlock);
|
||||
|
||||
var max = entries.Keys.Max();
|
||||
var blockDifference = CalculateBlockDifference(moment, timePerBlock, max);
|
||||
|
||||
FetchUp(max, blockDifference);
|
||||
FetchDown(max, blockDifference);
|
||||
}
|
||||
|
||||
private void FetchDown(ulong max, ulong blockDifference)
|
||||
{
|
||||
var target = max - blockDifference - 1;
|
||||
var fetchDown = FetchRange;
|
||||
while (fetchDown > 0)
|
||||
{
|
||||
if (!entries.ContainsKey(target))
|
||||
{
|
||||
var newBlock = AddBlockNumber(target);
|
||||
if (newBlock == null) return;
|
||||
fetchDown--;
|
||||
}
|
||||
target--;
|
||||
if (target <= 0) return;
|
||||
}
|
||||
}
|
||||
|
||||
private void FetchUp(ulong max, ulong blockDifference)
|
||||
{
|
||||
var target = max - blockDifference;
|
||||
var fetchUp = FetchRange;
|
||||
while (fetchUp > 0)
|
||||
{
|
||||
if (!entries.ContainsKey(target))
|
||||
{
|
||||
var newBlock = AddBlockNumber(target);
|
||||
if (newBlock == null) return;
|
||||
fetchUp--;
|
||||
}
|
||||
target++;
|
||||
if (target >= max) return;
|
||||
}
|
||||
}
|
||||
|
||||
private ulong CalculateBlockDifference(DateTime moment, TimeSpan timePerBlock, ulong max)
|
||||
{
|
||||
var latest = entries[max];
|
||||
var timeDifference = latest.Utc - moment;
|
||||
double secondsDifference = Math.Abs(timeDifference.TotalSeconds);
|
||||
double secondsPerBlock = timePerBlock.TotalSeconds;
|
||||
|
||||
double numberOfBlocksDifference = secondsDifference / secondsPerBlock;
|
||||
var blockDifference = Convert.ToUInt64(numberOfBlocksDifference);
|
||||
if (blockDifference < 1) blockDifference = 1;
|
||||
return blockDifference;
|
||||
}
|
||||
|
||||
private void EnsureRecentBlockIfNecessary(DateTime moment, TimeSpan timePerBlock)
|
||||
{
|
||||
var max = entries.Keys.Max();
|
||||
var latest = entries[max];
|
||||
var maxRetry = 10;
|
||||
while (moment > latest.Utc)
|
||||
{
|
||||
var newBlock = AddCurrentBlock();
|
||||
if (newBlock == null || newBlock.BlockNumber == latest.BlockNumber)
|
||||
{
|
||||
maxRetry--;
|
||||
if (maxRetry == 0) throw new Exception("Unable to fetch recent block after 10x tries.");
|
||||
Thread.Sleep(timePerBlock);
|
||||
}
|
||||
max = entries.Keys.Max();
|
||||
latest = entries[max];
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddBlockNumber(decimal blockNumber)
|
||||
{
|
||||
return AddBlockNumber(Convert.ToUInt64(blockNumber));
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddBlockNumber(ulong blockNumber)
|
||||
{
|
||||
if (entries.ContainsKey(blockNumber))
|
||||
{
|
||||
return entries[blockNumber];
|
||||
}
|
||||
|
||||
if (entries.Count > MaxEntries)
|
||||
{
|
||||
log.Debug("Entries cleared!");
|
||||
entries.Clear();
|
||||
Initialize();
|
||||
}
|
||||
|
||||
var time = GetTimestampFromBlock(blockNumber);
|
||||
if (time == null)
|
||||
{
|
||||
log.Log("Failed to get block for number: " + blockNumber);
|
||||
return null;
|
||||
}
|
||||
var entry = new BlockTimeEntry(blockNumber, time.Value);
|
||||
log.Debug("Found block " + entry.BlockNumber + " at " + entry.Utc.ToString("o"));
|
||||
entries.Add(blockNumber, entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
private TimeSpan EstimateTimePerBlock()
|
||||
{
|
||||
var min = entries.Keys.Min();
|
||||
var max = entries.Keys.Max();
|
||||
var clippedMin = Math.Max(max - 100, min);
|
||||
var minTime = entries[min].Utc;
|
||||
var clippedMinBlock = AddBlockNumber(clippedMin);
|
||||
if (clippedMinBlock != null) minTime = clippedMinBlock.Utc;
|
||||
|
||||
var maxTime = entries[max].Utc;
|
||||
var elapsedTime = maxTime - minTime;
|
||||
|
||||
double elapsedSeconds = elapsedTime.TotalSeconds;
|
||||
double numberOfBlocks = max - min;
|
||||
double secondsPerBlock = elapsedSeconds / numberOfBlocks;
|
||||
|
||||
var result = TimeSpan.FromSeconds(secondsPerBlock);
|
||||
if (result.TotalSeconds < 1.0) result = TimeSpan.FromSeconds(1.0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
if (!entries.Any())
|
||||
{
|
||||
AddCurrentBlock();
|
||||
AddBlockNumber(entries.Single().Key - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertMomentIsInPast(DateTime moment)
|
||||
{
|
||||
if (moment > DateTime.UtcNow) throw new Exception("Moment must be UTC and must be in the past.");
|
||||
}
|
||||
|
||||
private BlockTimeEntry? AddCurrentBlock()
|
||||
{
|
||||
var number = Time.Wait(web3.Eth.Blocks.GetBlockNumber.SendRequestAsync());
|
||||
var blockNumber = number.ToDecimal();
|
||||
return AddBlockNumber(blockNumber);
|
||||
}
|
||||
|
||||
private DateTime? GetTimestampFromBlock(ulong blockNumber)
|
||||
{
|
||||
try
|
||||
{
|
||||
var block = Time.Wait(web3.Eth.Blocks.GetBlockWithTransactionsByNumber.SendRequestAsync(new BlockParameter(blockNumber)));
|
||||
if (block == null) return null;
|
||||
return DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(block.Timestamp.ToDecimal())).UtcDateTime;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
int i = 0;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTimeEntry? FindClosestBeforeEntry(DateTime moment)
|
||||
{
|
||||
BlockTimeEntry? result = null;
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
if (entry.Utc < moment) result = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.Utc > result.Utc && entry.Utc < moment) result = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private BlockTimeEntry? FindClosestAfterEntry(DateTime moment)
|
||||
{
|
||||
BlockTimeEntry? result = null;
|
||||
foreach (var entry in entries.Values)
|
||||
{
|
||||
if (result == null)
|
||||
{
|
||||
if (entry.Utc > moment) result = entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry.Utc < result.Utc && entry.Utc > moment) result = entry;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using Nethereum.Web3;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Utils;
|
||||
|
||||
namespace NethereumWorkflow
|
||||
@@ -54,6 +56,12 @@ namespace NethereumWorkflow
|
||||
return receipt.TransactionHash;
|
||||
}
|
||||
|
||||
public Transaction GetTransaction(string transactionHash)
|
||||
{
|
||||
log.Debug();
|
||||
return Time.Wait(web3.Eth.Transactions.GetTransactionByHash.SendRequestAsync(transactionHash));
|
||||
}
|
||||
|
||||
public decimal? GetSyncedBlockNumber()
|
||||
{
|
||||
log.Debug();
|
||||
@@ -77,5 +85,24 @@ namespace NethereumWorkflow
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
var blockTimeFinder = new BlockTimeFinder(web3, log);
|
||||
|
||||
var fromBlock = blockTimeFinder.GetLowestBlockNumberAfter(timeRange.From);
|
||||
var toBlock = blockTimeFinder.GetHighestBlockNumberBefore(timeRange.To);
|
||||
|
||||
return GetEvents<TEvent>(address, fromBlock, toBlock);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
|
||||
{
|
||||
var eventHandler = web3.Eth.GetEvent<TEvent>(address);
|
||||
var from = new BlockParameter(fromBlockNumber);
|
||||
var to = new BlockParameter(toBlockNumber);
|
||||
var blockFilter = Time.Wait(eventHandler.CreateFilterBlockRangeAsync(from, to));
|
||||
return Time.Wait(eventHandler.GetAllChangesAsync(blockFilter));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ContinuousTests
|
||||
namespace Utils
|
||||
{
|
||||
public class TaskFactory
|
||||
{
|
||||
@@ -13,6 +13,11 @@
|
||||
return task.Result;
|
||||
}
|
||||
|
||||
public static void Wait(Task task)
|
||||
{
|
||||
task.Wait();
|
||||
}
|
||||
|
||||
public static string FormatDuration(TimeSpan d)
|
||||
{
|
||||
var result = "";
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Utils
|
||||
{
|
||||
public class TimeRange
|
||||
{
|
||||
public TimeRange(DateTime from, DateTime to)
|
||||
{
|
||||
if (from < to)
|
||||
{
|
||||
From = from;
|
||||
To = to;
|
||||
}
|
||||
else
|
||||
{
|
||||
From = to;
|
||||
To = from;
|
||||
}
|
||||
Duration = To - From;
|
||||
}
|
||||
|
||||
public DateTime From { get; }
|
||||
public DateTime To { get; }
|
||||
public TimeSpan Duration { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
using GethPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.ABI;
|
||||
using Nethereum.Hex.HexTypes;
|
||||
using Nethereum.Util;
|
||||
using NethereumWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace CodexContractsPlugin
|
||||
{
|
||||
@@ -12,6 +18,23 @@ namespace CodexContractsPlugin
|
||||
string MintTestTokens(EthAddress ethAddress, TestToken testTokens);
|
||||
TestToken GetTestTokenBalance(IHasEthAddress owner);
|
||||
TestToken GetTestTokenBalance(EthAddress ethAddress);
|
||||
|
||||
Request[] GetStorageRequests(TimeRange timeRange);
|
||||
EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex);
|
||||
RequestState GetRequestState(Request request);
|
||||
RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange);
|
||||
RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange);
|
||||
SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange);
|
||||
SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange);
|
||||
}
|
||||
|
||||
public enum RequestState
|
||||
{
|
||||
New,
|
||||
Started,
|
||||
Cancelled,
|
||||
Finished,
|
||||
Failed
|
||||
}
|
||||
|
||||
public class CodexContractsAccess : ICodexContracts
|
||||
@@ -30,8 +53,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
public bool IsDeployed()
|
||||
{
|
||||
var interaction = new ContractInteractions(log, gethNode);
|
||||
return !string.IsNullOrEmpty(interaction.GetTokenName(Deployment.TokenAddress));
|
||||
return !string.IsNullOrEmpty(StartInteraction().GetTokenName(Deployment.TokenAddress));
|
||||
}
|
||||
|
||||
public string MintTestTokens(IHasEthAddress owner, TestToken testTokens)
|
||||
@@ -41,8 +63,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
public string MintTestTokens(EthAddress ethAddress, TestToken testTokens)
|
||||
{
|
||||
var interaction = new ContractInteractions(log, gethNode);
|
||||
return interaction.MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
|
||||
return StartInteraction().MintTestTokens(ethAddress, testTokens.Amount, Deployment.TokenAddress);
|
||||
}
|
||||
|
||||
public TestToken GetTestTokenBalance(IHasEthAddress owner)
|
||||
@@ -52,9 +73,108 @@ namespace CodexContractsPlugin
|
||||
|
||||
public TestToken GetTestTokenBalance(EthAddress ethAddress)
|
||||
{
|
||||
var interaction = new ContractInteractions(log, gethNode);
|
||||
var balance = interaction.GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
var balance = StartInteraction().GetBalance(Deployment.TokenAddress, ethAddress.Address);
|
||||
return balance.TestTokens();
|
||||
}
|
||||
|
||||
public Request[] GetStorageRequests(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<StorageRequestedEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
var i = StartInteraction();
|
||||
return events
|
||||
.Select(e =>
|
||||
{
|
||||
var requestEvent = i.GetRequest(Deployment.MarketplaceAddress, e.Event.RequestId);
|
||||
var result = requestEvent.ReturnValue1;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
result.RequestId = e.Event.RequestId;
|
||||
return result;
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public RequestFulfilledEventDTO[] GetRequestFulfilledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestFulfilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public RequestCancelledEventDTO[] GetRequestCancelledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<RequestCancelledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFilledEventDTO[] GetSlotFilledEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFilledEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
result.Host = GetEthAddressFromTransaction(e.Log.TransactionHash);
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public SlotFreedEventDTO[] GetSlotFreedEvents(TimeRange timeRange)
|
||||
{
|
||||
var events = gethNode.GetEvents<SlotFreedEventDTO>(Deployment.MarketplaceAddress, timeRange);
|
||||
return events.Select(e =>
|
||||
{
|
||||
var result = e.Event;
|
||||
result.BlockNumber = e.Log.BlockNumber.ToUlong();
|
||||
return result;
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
public EthAddress? GetSlotHost(Request storageRequest, decimal slotIndex)
|
||||
{
|
||||
var encoder = new ABIEncode();
|
||||
var encoded = encoder.GetABIEncoded(
|
||||
new ABIValue("bytes32", storageRequest.RequestId),
|
||||
new ABIValue("uint256", slotIndex.ToBig())
|
||||
);
|
||||
|
||||
var hashed = Sha3Keccack.Current.CalculateHash(encoded);
|
||||
|
||||
var func = new GetHostFunction
|
||||
{
|
||||
SlotId = hashed
|
||||
};
|
||||
var address = gethNode.Call<GetHostFunction, string>(Deployment.MarketplaceAddress, func);
|
||||
if (string.IsNullOrEmpty(address)) return null;
|
||||
return new EthAddress(address);
|
||||
}
|
||||
|
||||
public RequestState GetRequestState(Request request)
|
||||
{
|
||||
var func = new RequestStateFunction
|
||||
{
|
||||
RequestId = request.RequestId
|
||||
};
|
||||
return gethNode.Call<RequestStateFunction, RequestState>(Deployment.MarketplaceAddress, func);
|
||||
}
|
||||
|
||||
private EthAddress GetEthAddressFromTransaction(string transactionHash)
|
||||
{
|
||||
var transaction = gethNode.GetTransaction(transactionHash);
|
||||
return new EthAddress(transaction.From);
|
||||
}
|
||||
|
||||
private ContractInteractions StartInteraction()
|
||||
{
|
||||
return new ContractInteractions(log, gethNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace CodexContractsPlugin
|
||||
|
||||
private void WaitUntil(Func<bool> predicate)
|
||||
{
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(3), TimeSpan.FromSeconds(2));
|
||||
Time.WaitUntil(predicate, TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
private StartupConfig CreateStartupConfig(IGethNode gethNode)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using GethPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.Hex.HexConvertors.Extensions;
|
||||
using NethereumWorkflow;
|
||||
using System.Numerics;
|
||||
|
||||
@@ -59,6 +61,17 @@ namespace CodexContractsPlugin
|
||||
return gethNode.Call<GetTokenBalanceFunction, BigInteger>(tokenAddress, function).ToDecimal();
|
||||
}
|
||||
|
||||
public GetRequestOutputDTO GetRequest(string marketplaceAddress, byte[] requestId)
|
||||
{
|
||||
|
||||
log.Debug($"({marketplaceAddress}) {requestId.ToHex(true)}");
|
||||
var func = new GetRequestFunction
|
||||
{
|
||||
RequestId = requestId
|
||||
};
|
||||
return gethNode.Call<GetRequestFunction, GetRequestOutputDTO>(marketplaceAddress, func);
|
||||
}
|
||||
|
||||
public bool IsSynced(string marketplaceAddress, string marketplaceAbi)
|
||||
{
|
||||
log.Debug();
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
using GethPlugin;
|
||||
|
||||
namespace CodexContractsPlugin.Marketplace
|
||||
{
|
||||
public partial class Request : RequestBase
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
public byte[] RequestId { get; set; }
|
||||
|
||||
public EthAddress ClientAddress { get { return new EthAddress(Client); } }
|
||||
}
|
||||
|
||||
public partial class RequestFulfilledEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestCancelledEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
public EthAddress Host { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFreedEventDTO
|
||||
{
|
||||
public ulong BlockNumber { get; set; }
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
@@ -0,0 +1,517 @@
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using System.Numerics;
|
||||
|
||||
// Generated code, do not modify.
|
||||
|
||||
#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
namespace CodexContractsPlugin.Marketplace
|
||||
{
|
||||
public partial class ConfigFunction : ConfigFunctionBase { }
|
||||
|
||||
[Function("config", typeof(ConfigOutputDTO))]
|
||||
public class ConfigFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class FillSlotFunction : FillSlotFunctionBase { }
|
||||
|
||||
[Function("fillSlot")]
|
||||
public class FillSlotFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
[Parameter("bytes", "proof", 3)]
|
||||
public virtual byte[] Proof { get; set; }
|
||||
}
|
||||
|
||||
public partial class FreeSlotFunction : FreeSlotFunctionBase { }
|
||||
|
||||
[Function("freeSlot")]
|
||||
public class FreeSlotFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetActiveSlotFunction : GetActiveSlotFunctionBase { }
|
||||
|
||||
[Function("getActiveSlot", typeof(GetActiveSlotOutputDTO))]
|
||||
public class GetActiveSlotFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetChallengeFunction : GetChallengeFunctionBase { }
|
||||
|
||||
[Function("getChallenge", "bytes32")]
|
||||
public class GetChallengeFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetHostFunction : GetHostFunctionBase { }
|
||||
|
||||
[Function("getHost", "address")]
|
||||
public class GetHostFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetPointerFunction : GetPointerFunctionBase { }
|
||||
|
||||
[Function("getPointer", "uint8")]
|
||||
public class GetPointerFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetRequestFunction : GetRequestFunctionBase { }
|
||||
|
||||
[Function("getRequest", typeof(GetRequestOutputDTO))]
|
||||
public class GetRequestFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class IsProofRequiredFunction : IsProofRequiredFunctionBase { }
|
||||
|
||||
[Function("isProofRequired", "bool")]
|
||||
public class IsProofRequiredFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class MarkProofAsMissingFunction : MarkProofAsMissingFunctionBase { }
|
||||
|
||||
[Function("markProofAsMissing")]
|
||||
public class MarkProofAsMissingFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
[Parameter("uint256", "period", 2)]
|
||||
public virtual BigInteger Period { get; set; }
|
||||
}
|
||||
|
||||
public partial class MissingProofsFunction : MissingProofsFunctionBase { }
|
||||
|
||||
[Function("missingProofs", "uint256")]
|
||||
public class MissingProofsFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class MyRequestsFunction : MyRequestsFunctionBase { }
|
||||
|
||||
[Function("myRequests", "bytes32[]")]
|
||||
public class MyRequestsFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class MySlotsFunction : MySlotsFunctionBase { }
|
||||
|
||||
[Function("mySlots", "bytes32[]")]
|
||||
public class MySlotsFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class RequestEndFunction : RequestEndFunctionBase { }
|
||||
|
||||
[Function("requestEnd", "uint256")]
|
||||
public class RequestEndFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestStateFunction : RequestStateFunctionBase { }
|
||||
|
||||
[Function("requestState", "uint8")]
|
||||
public class RequestStateFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestStorageFunction : RequestStorageFunctionBase { }
|
||||
|
||||
[Function("requestStorage")]
|
||||
public class RequestStorageFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("tuple", "request", 1)]
|
||||
public virtual Request Request { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotStateFunction : SlotStateFunctionBase { }
|
||||
|
||||
[Function("slotState", "uint8")]
|
||||
public class SlotStateFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "slotId", 1)]
|
||||
public virtual byte[] SlotId { get; set; }
|
||||
}
|
||||
|
||||
public partial class SubmitProofFunction : SubmitProofFunctionBase { }
|
||||
|
||||
[Function("submitProof")]
|
||||
public class SubmitProofFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
[Parameter("bytes", "proof", 2)]
|
||||
public virtual byte[] Proof { get; set; }
|
||||
}
|
||||
|
||||
public partial class TokenFunction : TokenFunctionBase { }
|
||||
|
||||
[Function("token", "address")]
|
||||
public class TokenFunctionBase : FunctionMessage
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public partial class WillProofBeRequiredFunction : WillProofBeRequiredFunctionBase { }
|
||||
|
||||
[Function("willProofBeRequired", "bool")]
|
||||
public class WillProofBeRequiredFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "id", 1)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
}
|
||||
|
||||
public partial class WithdrawFundsFunction : WithdrawFundsFunctionBase { }
|
||||
|
||||
[Function("withdrawFunds")]
|
||||
public class WithdrawFundsFunctionBase : FunctionMessage
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class ProofSubmittedEventDTO : ProofSubmittedEventDTOBase { }
|
||||
|
||||
[Event("ProofSubmitted")]
|
||||
public class ProofSubmittedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "id", 1, false)]
|
||||
public virtual byte[] Id { get; set; }
|
||||
[Parameter("bytes", "proof", 2, false)]
|
||||
public virtual byte[] Proof { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestCancelledEventDTO : RequestCancelledEventDTOBase { }
|
||||
|
||||
[Event("RequestCancelled")]
|
||||
public class RequestCancelledEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestFailedEventDTO : RequestFailedEventDTOBase { }
|
||||
|
||||
[Event("RequestFailed")]
|
||||
public class RequestFailedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestFulfilledEventDTO : RequestFulfilledEventDTOBase { }
|
||||
|
||||
[Event("RequestFulfilled")]
|
||||
public class RequestFulfilledEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFilledEventDTO : SlotFilledEventDTOBase { }
|
||||
|
||||
[Event("SlotFilled")]
|
||||
public class SlotFilledEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2, false)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
}
|
||||
|
||||
public partial class SlotFreedEventDTO : SlotFreedEventDTOBase { }
|
||||
|
||||
[Event("SlotFreed")]
|
||||
public class SlotFreedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, true)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2, false)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
}
|
||||
|
||||
public partial class StorageRequestedEventDTO : StorageRequestedEventDTOBase { }
|
||||
|
||||
[Event("StorageRequested")]
|
||||
public class StorageRequestedEventDTOBase : IEventDTO
|
||||
{
|
||||
[Parameter("bytes32", "requestId", 1, false)]
|
||||
public virtual byte[] RequestId { get; set; }
|
||||
[Parameter("tuple", "ask", 2, false)]
|
||||
public virtual Ask Ask { get; set; }
|
||||
[Parameter("uint256", "expiry", 3, false)]
|
||||
public virtual BigInteger Expiry { get; set; }
|
||||
}
|
||||
|
||||
public partial class ConfigOutputDTO : ConfigOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class ConfigOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("tuple", "collateral", 1)]
|
||||
public virtual CollateralConfig Collateral { get; set; }
|
||||
[Parameter("tuple", "proofs", 2)]
|
||||
public virtual ProofConfig Proofs { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public partial class GetActiveSlotOutputDTO : GetActiveSlotOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetActiveSlotOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("tuple", "", 1)]
|
||||
public virtual ActiveSlot ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetChallengeOutputDTO : GetChallengeOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetChallengeOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bytes32", "", 1)]
|
||||
public virtual byte[] ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetHostOutputDTO : GetHostOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetHostOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("address", "", 1)]
|
||||
public virtual string ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetPointerOutputDTO : GetPointerOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetPointerOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint8", "", 1)]
|
||||
public virtual byte ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class GetRequestOutputDTO : GetRequestOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class GetRequestOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("tuple", "", 1)]
|
||||
public virtual Request ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class IsProofRequiredOutputDTO : IsProofRequiredOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class IsProofRequiredOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bool", "", 1)]
|
||||
public virtual bool ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class MissingProofsOutputDTO : MissingProofsOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class MissingProofsOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint256", "", 1)]
|
||||
public virtual BigInteger ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class MyRequestsOutputDTO : MyRequestsOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class MyRequestsOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bytes32[]", "", 1)]
|
||||
public virtual List<byte[]> ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class MySlotsOutputDTO : MySlotsOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class MySlotsOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bytes32[]", "", 1)]
|
||||
public virtual List<byte[]> ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestEndOutputDTO : RequestEndOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class RequestEndOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint256", "", 1)]
|
||||
public virtual BigInteger ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class RequestStateOutputDTO : RequestStateOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class RequestStateOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint8", "", 1)]
|
||||
public virtual byte ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class SlotStateOutputDTO : SlotStateOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class SlotStateOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("uint8", "", 1)]
|
||||
public virtual byte ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class TokenOutputDTO : TokenOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class TokenOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("address", "", 1)]
|
||||
public virtual string ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
public partial class WillProofBeRequiredOutputDTO : WillProofBeRequiredOutputDTOBase { }
|
||||
|
||||
[FunctionOutput]
|
||||
public class WillProofBeRequiredOutputDTOBase : IFunctionOutputDTO
|
||||
{
|
||||
[Parameter("bool", "", 1)]
|
||||
public virtual bool ReturnValue1 { get; set; }
|
||||
}
|
||||
|
||||
|
||||
|
||||
public partial class CollateralConfig : CollateralConfigBase { }
|
||||
|
||||
public class CollateralConfigBase
|
||||
{
|
||||
[Parameter("uint8", "repairRewardPercentage", 1)]
|
||||
public virtual byte RepairRewardPercentage { get; set; }
|
||||
[Parameter("uint8", "maxNumberOfSlashes", 2)]
|
||||
public virtual byte MaxNumberOfSlashes { get; set; }
|
||||
[Parameter("uint16", "slashCriterion", 3)]
|
||||
public virtual ushort SlashCriterion { get; set; }
|
||||
[Parameter("uint8", "slashPercentage", 4)]
|
||||
public virtual byte SlashPercentage { get; set; }
|
||||
}
|
||||
|
||||
public partial class ProofConfig : ProofConfigBase { }
|
||||
|
||||
public class ProofConfigBase
|
||||
{
|
||||
[Parameter("uint256", "period", 1)]
|
||||
public virtual BigInteger Period { get; set; }
|
||||
[Parameter("uint256", "timeout", 2)]
|
||||
public virtual BigInteger Timeout { get; set; }
|
||||
[Parameter("uint8", "downtime", 3)]
|
||||
public virtual byte Downtime { get; set; }
|
||||
}
|
||||
|
||||
public partial class MarketplaceConfig : MarketplaceConfigBase { }
|
||||
|
||||
public class MarketplaceConfigBase
|
||||
{
|
||||
[Parameter("tuple", "collateral", 1)]
|
||||
public virtual CollateralConfig Collateral { get; set; }
|
||||
[Parameter("tuple", "proofs", 2)]
|
||||
public virtual ProofConfig Proofs { get; set; }
|
||||
}
|
||||
|
||||
public partial class Ask : AskBase { }
|
||||
|
||||
public class AskBase
|
||||
{
|
||||
[Parameter("uint64", "slots", 1)]
|
||||
public virtual ulong Slots { get; set; }
|
||||
[Parameter("uint256", "slotSize", 2)]
|
||||
public virtual BigInteger SlotSize { get; set; }
|
||||
[Parameter("uint256", "duration", 3)]
|
||||
public virtual BigInteger Duration { get; set; }
|
||||
[Parameter("uint256", "proofProbability", 4)]
|
||||
public virtual BigInteger ProofProbability { get; set; }
|
||||
[Parameter("uint256", "reward", 5)]
|
||||
public virtual BigInteger Reward { get; set; }
|
||||
[Parameter("uint256", "collateral", 6)]
|
||||
public virtual BigInteger Collateral { get; set; }
|
||||
[Parameter("uint64", "maxSlotLoss", 7)]
|
||||
public virtual ulong MaxSlotLoss { get; set; }
|
||||
}
|
||||
|
||||
public partial class Content : ContentBase { }
|
||||
|
||||
public class ContentBase
|
||||
{
|
||||
[Parameter("string", "cid", 1)]
|
||||
public virtual string Cid { get; set; }
|
||||
[Parameter("bytes32", "merkleRoot", 2)]
|
||||
public virtual byte[] MerkleRoot { get; set; }
|
||||
}
|
||||
|
||||
public partial class Request : RequestBase { }
|
||||
|
||||
public class RequestBase
|
||||
{
|
||||
[Parameter("address", "client", 1)]
|
||||
public virtual string Client { get; set; }
|
||||
[Parameter("tuple", "ask", 2)]
|
||||
public virtual Ask Ask { get; set; }
|
||||
[Parameter("tuple", "content", 3)]
|
||||
public virtual Content Content { get; set; }
|
||||
[Parameter("uint256", "expiry", 4)]
|
||||
public virtual BigInteger Expiry { get; set; }
|
||||
[Parameter("bytes32", "nonce", 5)]
|
||||
public virtual byte[] Nonce { get; set; }
|
||||
}
|
||||
|
||||
public partial class ActiveSlot : ActiveSlotBase { }
|
||||
|
||||
public class ActiveSlotBase
|
||||
{
|
||||
[Parameter("tuple", "request", 1)]
|
||||
public virtual Request Request { get; set; }
|
||||
[Parameter("uint256", "slotIndex", 2)]
|
||||
public virtual BigInteger SlotIndex { get; set; }
|
||||
}
|
||||
}
|
||||
#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
|
||||
@@ -0,0 +1 @@
|
||||
This code was generated using the Nethereum code generator, here: http://playground.nethereum.com
|
||||
@@ -26,7 +26,7 @@ namespace CodexPlugin
|
||||
|
||||
protected override void Initialize(StartupConfig startupConfig)
|
||||
{
|
||||
SetResourcesRequest(milliCPUs: 100, memory: 2.GB());
|
||||
SetResourcesRequest(milliCPUs: 100, memory: 100.MB());
|
||||
//SetResourceLimits(milliCPUs: 4000, memory: 12.GB());
|
||||
|
||||
SetSchedulingAffinity(notIn: "false");
|
||||
|
||||
@@ -6,8 +6,12 @@ namespace CodexPlugin
|
||||
{
|
||||
public class CodexDeployment
|
||||
{
|
||||
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment, CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer, RunningContainers? discordBotContainer, DeploymentMetadata metadata)
|
||||
public CodexDeployment(CodexInstance[] codexInstances, GethDeployment gethDeployment,
|
||||
CodexContractsDeployment codexContractsDeployment, RunningContainers? prometheusContainer,
|
||||
RunningContainers? discordBotContainer, DeploymentMetadata metadata,
|
||||
String id)
|
||||
{
|
||||
Id = id;
|
||||
CodexInstances = codexInstances;
|
||||
GethDeployment = gethDeployment;
|
||||
CodexContractsDeployment = codexContractsDeployment;
|
||||
@@ -16,6 +20,7 @@ namespace CodexPlugin
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
public String Id { get; }
|
||||
public CodexInstance[] CodexInstances { get; }
|
||||
public GethDeployment GethDeployment { get; }
|
||||
public CodexContractsDeployment CodexContractsDeployment { get; }
|
||||
@@ -38,7 +43,10 @@ namespace CodexPlugin
|
||||
|
||||
public class DeploymentMetadata
|
||||
{
|
||||
public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace, int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel, int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI, int blockMN)
|
||||
public DeploymentMetadata(string name, DateTime startUtc, DateTime finishedUtc, string kubeNamespace,
|
||||
int numberOfCodexNodes, int numberOfValidators, int storageQuotaMB, CodexLogLevel codexLogLevel,
|
||||
int initialTestTokens, int minPrice, int maxCollateral, int maxDuration, int blockTTL, int blockMI,
|
||||
int blockMN)
|
||||
{
|
||||
Name = name;
|
||||
StartUtc = startUtc;
|
||||
@@ -73,4 +81,4 @@ namespace CodexPlugin
|
||||
public int BlockMI { get; }
|
||||
public int BlockMN { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,13 @@ namespace CodexPlugin
|
||||
|
||||
public class CodexLogCustomTopics
|
||||
{
|
||||
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p, CodexLogLevel blockExchange)
|
||||
{
|
||||
DiscV5 = discV5;
|
||||
Libp2p = libp2p;
|
||||
BlockExchange = blockExchange;
|
||||
}
|
||||
|
||||
public CodexLogCustomTopics(CodexLogLevel discV5, CodexLogLevel libp2p)
|
||||
{
|
||||
DiscV5 = discV5;
|
||||
@@ -35,6 +42,7 @@ namespace CodexPlugin
|
||||
|
||||
public CodexLogLevel DiscV5 { get; set; }
|
||||
public CodexLogLevel Libp2p { get; set; }
|
||||
public CodexLogLevel? BlockExchange { get; }
|
||||
}
|
||||
|
||||
public class CodexSetup : CodexStartupConfig, ICodexSetup
|
||||
|
||||
@@ -52,12 +52,31 @@ namespace CodexPlugin
|
||||
"connection",
|
||||
"connmanager",
|
||||
"websock",
|
||||
"ws-session"
|
||||
"ws-session",
|
||||
"dialer",
|
||||
"muxedupgrade",
|
||||
"upgrade",
|
||||
"identify"
|
||||
};
|
||||
var blockExchangeTopics = new[]
|
||||
{
|
||||
"codex",
|
||||
"pendingblocks",
|
||||
"peerctxstore",
|
||||
"discoveryengine",
|
||||
"blockexcengine",
|
||||
"blockexcnetwork",
|
||||
"blockexcnetworkpeer"
|
||||
};
|
||||
|
||||
level = $"{level};" +
|
||||
$"{CustomTopics.DiscV5.ToString()!.ToLowerInvariant()}:{string.Join(",", discV5Topics)};" +
|
||||
$"{CustomTopics.Libp2p.ToString()!.ToLowerInvariant()}:{string.Join(",", libp2pTopics)}";
|
||||
|
||||
if (CustomTopics.BlockExchange != null)
|
||||
{
|
||||
level += $";{CustomTopics.BlockExchange.ToString()!.ToLowerInvariant()}:{string.Join(",", blockExchangeTopics)}";
|
||||
}
|
||||
}
|
||||
return level;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ namespace CodexPlugin
|
||||
|
||||
if (response == "Purchasing not available" ||
|
||||
response == "Expiry required" ||
|
||||
response == "Expiry needs to be in future")
|
||||
response == "Expiry needs to be in future" ||
|
||||
response == "Expiry has to be before the request's end (now + duration)")
|
||||
{
|
||||
throw new InvalidOperationException(response);
|
||||
}
|
||||
|
||||
@@ -9,11 +9,22 @@
|
||||
{
|
||||
public EthAddress(string address)
|
||||
{
|
||||
Address = address;
|
||||
Address = address.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public string Address { get; }
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is EthAddress address &&
|
||||
Address == address.Address;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Address);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Address;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
using Core;
|
||||
using KubernetesWorkflow.Types;
|
||||
using Logging;
|
||||
using Nethereum.ABI.FunctionEncoding.Attributes;
|
||||
using Nethereum.Contracts;
|
||||
using Nethereum.RPC.Eth.DTOs;
|
||||
using NethereumWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace GethPlugin
|
||||
{
|
||||
@@ -17,9 +20,12 @@ namespace GethPlugin
|
||||
string SendEth(EthAddress account, Ether eth);
|
||||
TResult Call<TFunction, TResult>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
|
||||
string SendTransaction<TFunction>(string contractAddress, TFunction function) where TFunction : FunctionMessage, new();
|
||||
Transaction GetTransaction(string transactionHash);
|
||||
decimal? GetSyncedBlockNumber();
|
||||
bool IsContractAvailable(string abi, string contractAddress);
|
||||
GethBootstrapNode GetBootstrapRecord();
|
||||
List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new();
|
||||
List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new();
|
||||
}
|
||||
|
||||
public class DeploymentGethNode : BaseGethNode, IGethNode
|
||||
@@ -123,6 +129,11 @@ namespace GethPlugin
|
||||
return StartInteraction().SendTransaction(contractAddress, function);
|
||||
}
|
||||
|
||||
public Transaction GetTransaction(string transactionHash)
|
||||
{
|
||||
return StartInteraction().GetTransaction(transactionHash);
|
||||
}
|
||||
|
||||
public decimal? GetSyncedBlockNumber()
|
||||
{
|
||||
return StartInteraction().GetSyncedBlockNumber();
|
||||
@@ -133,6 +144,16 @@ namespace GethPlugin
|
||||
return StartInteraction().IsContractAvailable(abi, contractAddress);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, ulong fromBlockNumber, ulong toBlockNumber) where TEvent : IEventDTO, new()
|
||||
{
|
||||
return StartInteraction().GetEvents<TEvent>(address, fromBlockNumber, toBlockNumber);
|
||||
}
|
||||
|
||||
public List<EventLog<TEvent>> GetEvents<TEvent>(string address, TimeRange timeRange) where TEvent : IEventDTO, new()
|
||||
{
|
||||
return StartInteraction().GetEvents<TEvent>(address, timeRange);
|
||||
}
|
||||
|
||||
protected abstract NethereumInteraction StartInteraction();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using DistTestCore.Logs;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
using TaskFactory = Utils.TaskFactory;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
@@ -26,10 +27,11 @@ namespace ContinuousTests
|
||||
var startTime = DateTime.UtcNow;
|
||||
|
||||
var overviewLog = new LogSplitter(
|
||||
new FixtureLog(logConfig, startTime, "Overview"),
|
||||
new FixtureLog(logConfig, startTime, config.CodexDeployment.Id, "Overview"),
|
||||
new ConsoleLog()
|
||||
);
|
||||
var statusLog = new StatusLog(logConfig, startTime, "continuous-tests", "ContinuousTestRun");
|
||||
var statusLog = new StatusLog(logConfig, startTime, "continuous-tests", config.CodexDeployment.Id,
|
||||
"ContinuousTestRun");
|
||||
|
||||
overviewLog.Log("Initializing...");
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ using CodexPlugin;
|
||||
using DistTestCore.Logs;
|
||||
using Core;
|
||||
using KubernetesWorkflow.Types;
|
||||
using TaskFactory = Utils.TaskFactory;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
@@ -24,7 +25,9 @@ namespace ContinuousTests
|
||||
private readonly string testName;
|
||||
private static int failureCount = 0;
|
||||
|
||||
public SingleTestRun(EntryPointFactory entryPointFactory, TaskFactory taskFactory, Configuration config, ILog overviewLog, StatusLog statusLog, TestHandle handle, StartupChecker startupChecker, CancellationToken cancelToken)
|
||||
public SingleTestRun(EntryPointFactory entryPointFactory,
|
||||
TaskFactory taskFactory, Configuration config, ILog overviewLog, StatusLog statusLog, TestHandle handle,
|
||||
StartupChecker startupChecker, CancellationToken cancelToken, string deployId)
|
||||
{
|
||||
this.taskFactory = taskFactory;
|
||||
this.config = config;
|
||||
@@ -33,8 +36,9 @@ namespace ContinuousTests
|
||||
this.handle = handle;
|
||||
this.cancelToken = cancelToken;
|
||||
testName = handle.Test.GetType().Name;
|
||||
fixtureLog = new FixtureLog(new LogConfig(config.LogPath), DateTime.UtcNow, testName);
|
||||
entryPoint = entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath, config.CodexDeployment.Metadata.KubeNamespace, fixtureLog);
|
||||
fixtureLog = new FixtureLog(new LogConfig(config.LogPath), DateTime.UtcNow, deployId, testName);
|
||||
entryPoint = entryPointFactory.CreateEntryPoint(config.KubeConfigFile, config.DataPath,
|
||||
config.CodexDeployment.Metadata.KubeNamespace, fixtureLog);
|
||||
ApplyLogReplacements(fixtureLog, startupChecker);
|
||||
|
||||
nodes = CreateRandomNodes();
|
||||
@@ -79,6 +83,7 @@ namespace ContinuousTests
|
||||
{
|
||||
fixtureLog.Delete();
|
||||
}
|
||||
|
||||
resultHandler(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -113,6 +118,7 @@ namespace ContinuousTests
|
||||
{
|
||||
effectiveStart = config.CodexDeployment.Metadata.StartUtc.Subtract(TimeSpan.FromSeconds(30));
|
||||
}
|
||||
|
||||
var effectiveEnd = DateTime.UtcNow;
|
||||
var elasticSearchLogDownloader = new ElasticSearchLogDownloader(entryPoint.Tools, fixtureLog);
|
||||
|
||||
@@ -121,14 +127,17 @@ namespace ContinuousTests
|
||||
var container = node.Container;
|
||||
var deploymentName = container.RunningContainers.StartResult.Deployment.Name;
|
||||
var namespaceName = container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace;
|
||||
var openingLine = $"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().id}";
|
||||
elasticSearchLogDownloader.Download(fixtureLog.CreateSubfile(), node.Container, effectiveStart, effectiveEnd, openingLine);
|
||||
var openingLine =
|
||||
$"{namespaceName} - {deploymentName} = {node.Container.Name} = {node.GetDebugInfo().id}";
|
||||
elasticSearchLogDownloader.Download(fixtureLog.CreateSubfile(), node.Container, effectiveStart,
|
||||
effectiveEnd, openingLine);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLogReplacements(FixtureLog fixtureLog, StartupChecker startupChecker)
|
||||
{
|
||||
foreach (var replacement in startupChecker.LogReplacements) fixtureLog.AddStringReplace(replacement.From, replacement.To);
|
||||
foreach (var replacement in startupChecker.LogReplacements)
|
||||
fixtureLog.AddStringReplace(replacement.From, replacement.To);
|
||||
}
|
||||
|
||||
private void RunTestMoments()
|
||||
@@ -160,9 +169,11 @@ namespace ContinuousTests
|
||||
{
|
||||
ThrowFailTest();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
fixtureLog.Log("Test run has been cancelled.");
|
||||
}
|
||||
|
||||
@@ -192,6 +203,7 @@ namespace ContinuousTests
|
||||
result.Add("testname", testName);
|
||||
result.Add("message", message);
|
||||
result.Add("involvedpods", string.Join(",", nodes.Select(n => n.GetName())));
|
||||
result.Add("involvedpodnames", string.Join(",", nodes.Select(n => n.GetPodInfo().Name)));
|
||||
|
||||
var error = message.Split(Environment.NewLine).First();
|
||||
if (error.Contains(":")) error = error.Substring(1 + error.LastIndexOf(":"));
|
||||
@@ -221,6 +233,7 @@ namespace ContinuousTests
|
||||
{
|
||||
return UnpackException(a.InnerExceptions.First());
|
||||
}
|
||||
|
||||
if (exception is TargetInvocationException t)
|
||||
{
|
||||
return UnpackException(t.InnerException!);
|
||||
@@ -293,7 +306,8 @@ namespace ContinuousTests
|
||||
{
|
||||
result[i] = containers.PickOneRandom();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,8 @@ namespace ContinuousTests
|
||||
|
||||
public void Check()
|
||||
{
|
||||
var log = new FixtureLog(new LogConfig(config.LogPath), DateTime.UtcNow, "StartupChecks");
|
||||
var log = new FixtureLog(new LogConfig(config.LogPath), DateTime.UtcNow, config.CodexDeployment.Id,
|
||||
"StartupChecks");
|
||||
log.Log("Starting continuous test run...");
|
||||
IncludeDeploymentConfiguration(log);
|
||||
log.Log("Checking configuration...");
|
||||
@@ -46,13 +47,15 @@ namespace ContinuousTests
|
||||
{
|
||||
var podInfo = workflow.GetPodInfo(container);
|
||||
log.Log($"Codex environment variables for '{container.Name}':");
|
||||
log.Log($"Namespace: {container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace} - " +
|
||||
log.Log(
|
||||
$"Namespace: {container.RunningContainers.StartResult.Cluster.Configuration.KubernetesNamespace} - " +
|
||||
$"Pod name: {podInfo.Name} - Deployment name: {instance.Containers.StartResult.Deployment.Name}");
|
||||
var codexVars = container.Recipe.EnvVars;
|
||||
foreach (var vars in codexVars) log.Log(vars.ToString());
|
||||
log.Log("");
|
||||
}
|
||||
}
|
||||
|
||||
log.Log($"Deployment metadata: {JsonConvert.SerializeObject(deployment.Metadata)}");
|
||||
log.Log("");
|
||||
}
|
||||
@@ -64,6 +67,7 @@ namespace ContinuousTests
|
||||
{
|
||||
throw new Exception("Unable to find any tests.");
|
||||
}
|
||||
|
||||
foreach (var test in tests)
|
||||
{
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
@@ -87,7 +91,8 @@ namespace ContinuousTests
|
||||
|
||||
private void CheckCodexNodes(BaseLog log, Configuration config)
|
||||
{
|
||||
var nodes = entryPoint.CreateInterface().WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Containers).ToArray());
|
||||
var nodes = entryPoint.CreateInterface()
|
||||
.WrapCodexContainers(config.CodexDeployment.CodexInstances.Select(i => i.Containers).ToArray());
|
||||
var pass = true;
|
||||
foreach (var n in nodes)
|
||||
{
|
||||
@@ -106,6 +111,7 @@ namespace ContinuousTests
|
||||
pass = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pass)
|
||||
{
|
||||
throw new Exception("Not all codex nodes responded.");
|
||||
@@ -126,6 +132,7 @@ namespace ContinuousTests
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -145,7 +152,8 @@ namespace ContinuousTests
|
||||
propertyName: nameof(ContinuousTest.CustomK8sNamespace));
|
||||
}
|
||||
|
||||
private void DuplicatesCheck(ContinuousTest[] tests, List<string> errors, Func<ContinuousTest, bool> considerCondition, Func<ContinuousTest, object> getValue, string propertyName)
|
||||
private void DuplicatesCheck(ContinuousTest[] tests, List<string> errors,
|
||||
Func<ContinuousTest, bool> considerCondition, Func<ContinuousTest, object> getValue, string propertyName)
|
||||
{
|
||||
foreach (var test in tests)
|
||||
{
|
||||
@@ -155,7 +163,8 @@ namespace ContinuousTests
|
||||
if (duplicates.Any())
|
||||
{
|
||||
duplicates.Add(test);
|
||||
errors.Add($"Tests '{string.Join(",", duplicates.Select(d => d.Name))}' have the same '{propertyName}'. These must be unique.");
|
||||
errors.Add(
|
||||
$"Tests '{string.Join(",", duplicates.Select(d => d.Name))}' have the same '{propertyName}'. These must be unique.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -170,14 +179,16 @@ namespace ContinuousTests
|
||||
{
|
||||
if (test.RequiredNumberOfNodes < 1)
|
||||
{
|
||||
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Test must require > 0 nodes, or -1 to select all nodes.");
|
||||
errors.Add(
|
||||
$"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Test must require > 0 nodes, or -1 to select all nodes.");
|
||||
}
|
||||
else if (test.RequiredNumberOfNodes > config.CodexDeployment.CodexInstances.Length)
|
||||
{
|
||||
errors.Add($"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Deployment only has {config.CodexDeployment.CodexInstances.Length}");
|
||||
errors.Add(
|
||||
$"Test '{test.Name}' requires {test.RequiredNumberOfNodes} nodes. Deployment only has {config.CodexDeployment.CodexInstances.Length}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using DistTestCore.Logs;
|
||||
using Logging;
|
||||
using TaskFactory = Utils.TaskFactory;
|
||||
|
||||
namespace ContinuousTests
|
||||
{
|
||||
@@ -76,7 +77,8 @@ namespace ContinuousTests
|
||||
{
|
||||
var test = (ContinuousTest)Activator.CreateInstance(testType)!;
|
||||
var handle = new TestHandle(test);
|
||||
var run = new SingleTestRun(entryPointFactory, taskFactory, config, overviewLog, statusLog, handle, startupChecker, cancelToken);
|
||||
var run = new SingleTestRun(entryPointFactory, taskFactory, config, overviewLog, statusLog, handle,
|
||||
startupChecker, cancelToken, config.CodexDeployment.Id);
|
||||
|
||||
runFinishedHandle.Reset();
|
||||
run.Run(runFinishedHandle, result =>
|
||||
|
||||
@@ -1,84 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
kubeconfig="/opt/kubeconfig.yaml"
|
||||
replication=5
|
||||
name=profiling-two-client-tests
|
||||
filter=TwoClient
|
||||
cleanup=1
|
||||
replication=$DNR_REP
|
||||
name=$DNR_NAME
|
||||
filter=$DNR_FILTER
|
||||
duration=$DNR_DURATION
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [-m <deploy|run>] [-r <replication>] [-k <kubeconfig>] [-c <cleanup>]" 1>&2
|
||||
exit 1
|
||||
}
|
||||
echo "Deploying..."
|
||||
cd ../../Tools/CodexNetDeployer
|
||||
for i in $( seq 0 $replication)
|
||||
do
|
||||
dotnet run \
|
||||
--deploy-name=codex-continuous-$name-$i \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--kube-namespace=codex-continuous-$name-tests-$i \
|
||||
--deploy-file=codex-deployment-$name-$i.json \
|
||||
--nodes=5 \
|
||||
--validators=3 \
|
||||
--log-level=Trace \
|
||||
--storage-quota=20480 \
|
||||
--storage-sell=1024 \
|
||||
--min-price=1024 \
|
||||
--max-collateral=1024 \
|
||||
--max-duration=3600000 \
|
||||
--block-ttl=99999999 \
|
||||
--block-mi=99999999 \
|
||||
--block-mn=100 \
|
||||
--metrics-endpoints=1 \
|
||||
--metrics-scraper=1 \
|
||||
--check-connect=1 \
|
||||
-y
|
||||
|
||||
while getopts ":r:k:m:c:" o; do
|
||||
case "$o" in
|
||||
r)
|
||||
replication="$OPTARG"
|
||||
;;
|
||||
k)
|
||||
kubeconfig="$OPTARG"
|
||||
;;
|
||||
m)
|
||||
mode="$OPTARG"
|
||||
;;
|
||||
c)
|
||||
cleanup="$OPTARG"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
cp codex-deployment-$name-$i.json ../../Tests/CodexContinuousTests
|
||||
done
|
||||
echo "Starting tests..."
|
||||
cd ../../Tests/CodexContinuousTests
|
||||
for i in $( seq 0 $replication)
|
||||
do
|
||||
screen -d -m dotnet run \
|
||||
--kube-config=/opt/kubeconfig.yaml \
|
||||
--codex-deployment=codex-deployment-$name-$i.json \
|
||||
--log-path=/var/log/codex-continuous-tests/logs-$name-$i \
|
||||
--data-path=data-$name-$i \
|
||||
--keep=1 \
|
||||
--stop=1 \
|
||||
--filter=$filter \
|
||||
--cleanup=1 \
|
||||
--full-container-logs=1 \
|
||||
--target-duration=$duration
|
||||
|
||||
sleep 30
|
||||
done
|
||||
|
||||
echo "Replication: $replication; Kube config: $kubeconfig; Mode: $mode; Cleanup: $cleanup"
|
||||
|
||||
if [ -z "$mode" ] || [ "$mode" == "deploy" ]; then
|
||||
echo "Deploying..."
|
||||
cd ../../Tools/CodexNetDeployer
|
||||
for i in $(seq 0 "$replication")
|
||||
do
|
||||
dotnet run \
|
||||
--deploy-name=codex-continuous-"$name-$i" \
|
||||
--kube-config="$kubeconfig" \
|
||||
--kube-namespace=codex-continuous-tests-"$name-$i" \
|
||||
--deploy-file=codex-deployment-"$name-$i".json \
|
||||
--nodes=2 \
|
||||
--validators=1 \
|
||||
--log-level=Trace \
|
||||
--storage-quota=20480 \
|
||||
--storage-sell=1024 \
|
||||
--min-price=1024 \
|
||||
--max-collateral=1024 \
|
||||
--max-duration=3600000 \
|
||||
--block-ttl=99999999 \
|
||||
--block-mi=99999999 \
|
||||
--block-mn=100 \
|
||||
--metrics=1 \
|
||||
--check-connect=1 \
|
||||
-y
|
||||
|
||||
cp codex-deployment-"$name-$i".json ../../Tests/CodexContinuousTests
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ -z "$mode" ] || [ "$mode" == "run" ]; then
|
||||
echo "Starting tests..."
|
||||
cd ../../Tests/CodexContinuousTests
|
||||
for i in $(seq 0 "$replication")
|
||||
do
|
||||
screen -d -m dotnet run \
|
||||
--kube-config="${kubeconfig}" \
|
||||
--codex-deployment=codex-deployment-"$name-$i".json \
|
||||
--log-path=logs-"$i" \
|
||||
--data-path=data-"$i" \
|
||||
--keep=1 \
|
||||
--stop=1 \
|
||||
--filter="$filter" \
|
||||
--cleanup="$cleanup" \
|
||||
--full-container-logs=1 \
|
||||
--target-duration=172800 # 48 hours
|
||||
done
|
||||
fi
|
||||
echo "Done! Sleeping indefinitely..."
|
||||
while true; do sleep 1d; done
|
||||
|
||||
@@ -23,5 +23,6 @@ namespace CodexTests
|
||||
}
|
||||
|
||||
protected ICodexNode? BootstrapNode { get; private set; }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using CodexPlugin;
|
||||
using DistTestCore;
|
||||
using GethPlugin;
|
||||
using MetricsPlugin;
|
||||
using Nethereum.Hex.HexConvertors.Extensions;
|
||||
using NUnit.Framework;
|
||||
using Utils;
|
||||
|
||||
@@ -59,6 +60,7 @@ namespace CodexTests.BasicTests
|
||||
var contracts = Ci.StartCodexContracts(geth);
|
||||
|
||||
var seller = AddCodex(s => s
|
||||
.WithLogLevel(CodexLogLevel.Trace, new CodexLogCustomTopics(CodexLogLevel.Error, CodexLogLevel.Error, CodexLogLevel.Warn))
|
||||
.WithStorageQuota(11.GB())
|
||||
.EnableMarketplace(geth, contracts, initialEth: 10.Eth(), initialTokens: sellerInitialBalance, isValidator: true)
|
||||
.WithSimulateProofFailures(failEveryNProofs: 3));
|
||||
@@ -88,14 +90,40 @@ namespace CodexTests.BasicTests
|
||||
|
||||
purchaseContract.WaitForStorageContractStarted(fileSize);
|
||||
|
||||
var requests = contracts.GetStorageRequests(GetTestRunTimeRange());
|
||||
Assert.That(requests.Length, Is.EqualTo(1));
|
||||
var request = requests.Single();
|
||||
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Started));
|
||||
Assert.That(request.ClientAddress, Is.EqualTo(buyer.EthAddress));
|
||||
Assert.That(request.Ask.Slots, Is.EqualTo(1));
|
||||
|
||||
AssertBalance(contracts, seller, Is.LessThan(sellerInitialBalance), "Collateral was not placed.");
|
||||
|
||||
var requestFulfilledEvents = contracts.GetRequestFulfilledEvents(GetTestRunTimeRange());
|
||||
Assert.That(requestFulfilledEvents.Length, Is.EqualTo(1));
|
||||
CollectionAssert.AreEqual(request.RequestId, requestFulfilledEvents[0].RequestId);
|
||||
var filledSlotEvents = contracts.GetSlotFilledEvents(GetTestRunTimeRange());
|
||||
Assert.That(filledSlotEvents.Length, Is.EqualTo(1));
|
||||
var filledSlotEvent = filledSlotEvents.Single();
|
||||
Assert.That(filledSlotEvent.SlotIndex.IsZero);
|
||||
Assert.That(filledSlotEvent.RequestId.ToHex(), Is.EqualTo(request.RequestId.ToHex()));
|
||||
Assert.That(filledSlotEvent.Host, Is.EqualTo(seller.EthAddress));
|
||||
|
||||
var slotHost = contracts.GetSlotHost(request, 0);
|
||||
Assert.That(slotHost, Is.EqualTo(seller.EthAddress));
|
||||
|
||||
purchaseContract.WaitForStorageContractFinished();
|
||||
|
||||
AssertBalance(contracts, seller, Is.GreaterThan(sellerInitialBalance), "Seller was not paid for storage.");
|
||||
AssertBalance(contracts, buyer, Is.LessThan(buyerInitialBalance), "Buyer was not charged for storage.");
|
||||
Assert.That(contracts.GetRequestState(request), Is.EqualTo(RequestState.Finished));
|
||||
|
||||
CheckLogForErrors(seller, buyer);
|
||||
var log = Ci.DownloadLog(seller);
|
||||
log.AssertLogContains("Received a request to store a slot!");
|
||||
log.AssertLogContains("Received proof challenge");
|
||||
log.AssertLogContains("Collecting input for proof");
|
||||
|
||||
//CheckLogForErrors(seller, buyer);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -20,16 +20,19 @@ namespace DistTestCore
|
||||
private readonly object lifecycleLock = new object();
|
||||
private readonly EntryPoint globalEntryPoint;
|
||||
private readonly Dictionary<string, TestLifecycle> lifecycles = new Dictionary<string, TestLifecycle>();
|
||||
private readonly string deployId;
|
||||
|
||||
public DistTest()
|
||||
{
|
||||
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||
testAssemblies = assemblies.Where(a => a.FullName!.ToLowerInvariant().Contains("test")).ToArray();
|
||||
|
||||
deployId = NameUtils.MakeDeployId();
|
||||
|
||||
var logConfig = configuration.GetLogConfig();
|
||||
var startTime = DateTime.UtcNow;
|
||||
fixtureLog = new FixtureLog(logConfig, startTime);
|
||||
statusLog = new StatusLog(logConfig, startTime, "dist-tests");
|
||||
fixtureLog = new FixtureLog(logConfig, startTime, deployId);
|
||||
statusLog = new StatusLog(logConfig, startTime, "dist-tests", deployId);
|
||||
|
||||
globalEntryPoint = new EntryPoint(fixtureLog, configuration.GetK8sConfiguration(new DefaultTimeSet(), TestNamespacePrefix), configuration.GetFileManagerFolder());
|
||||
|
||||
@@ -143,6 +146,11 @@ namespace DistTestCore
|
||||
Stopwatch.Measure(Get().Log, name, action);
|
||||
}
|
||||
|
||||
protected TimeRange GetTestRunTimeRange()
|
||||
{
|
||||
return new TimeRange(Get().TestStart, DateTime.UtcNow);
|
||||
}
|
||||
|
||||
protected virtual void Initialize(FixtureLog fixtureLog)
|
||||
{
|
||||
}
|
||||
@@ -176,7 +184,7 @@ namespace DistTestCore
|
||||
lock (lifecycleLock)
|
||||
{
|
||||
var testNamespace = TestNamespacePrefix + Guid.NewGuid().ToString();
|
||||
var lifecycle = new TestLifecycle(fixtureLog.CreateTestLog(), configuration, GetTimeSet(), testNamespace);
|
||||
var lifecycle = new TestLifecycle(fixtureLog.CreateTestLog(), configuration, GetTimeSet(), testNamespace, deployId);
|
||||
lifecycles.Add(testName, lifecycle);
|
||||
LifecycleStart(lifecycle);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,18 @@ namespace DistTestCore.Logs
|
||||
public abstract class BaseTestLog : BaseLog
|
||||
{
|
||||
private bool hasFailed;
|
||||
private readonly string deployId;
|
||||
|
||||
protected BaseTestLog(string deployId)
|
||||
{
|
||||
this.deployId = deployId;
|
||||
}
|
||||
|
||||
public void WriteLogTag()
|
||||
{
|
||||
var runId = NameUtils.GetRunId();
|
||||
var category = NameUtils.GetCategoryName();
|
||||
var name = NameUtils.GetTestMethodName();
|
||||
LogFile.WriteRaw($"{runId} {category} {name}");
|
||||
LogFile.WriteRaw($"{deployId} {category} {name}");
|
||||
}
|
||||
|
||||
public void MarkAsFailed()
|
||||
|
||||
@@ -5,15 +5,17 @@ namespace DistTestCore.Logs
|
||||
public class FixtureLog : BaseTestLog
|
||||
{
|
||||
private readonly string fullName;
|
||||
private readonly string deployId;
|
||||
|
||||
public FixtureLog(LogConfig config, DateTime start, string name = "")
|
||||
public FixtureLog(LogConfig config, DateTime start, string deployId, string name = "") : base(deployId)
|
||||
{
|
||||
this.deployId = deployId;
|
||||
fullName = NameUtils.GetFixtureFullName(config, start, name);
|
||||
}
|
||||
|
||||
public TestLog CreateTestLog(string name = "")
|
||||
{
|
||||
return new TestLog(fullName, name);
|
||||
return new TestLog(fullName, deployId, name);
|
||||
}
|
||||
|
||||
public void DeleteFolder()
|
||||
@@ -26,4 +28,4 @@ namespace DistTestCore.Logs
|
||||
return fullName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,18 @@ namespace DistTestCore.Logs
|
||||
{
|
||||
public class StatusLog
|
||||
{
|
||||
private readonly object fileLock = new object();
|
||||
private readonly object fileLock = new();
|
||||
private readonly string deployId;
|
||||
private readonly string fullName;
|
||||
private readonly string fixtureName;
|
||||
private readonly string testType;
|
||||
|
||||
public StatusLog(LogConfig config, DateTime start, string testType, string name = "")
|
||||
public StatusLog(LogConfig config, DateTime start, string testType, string deployId, string name = "")
|
||||
{
|
||||
fullName = NameUtils.GetFixtureFullName(config, start, name) + "_STATUS.log";
|
||||
fixtureName = NameUtils.GetRawFixtureName();
|
||||
this.testType = testType;
|
||||
this.deployId = deployId;
|
||||
}
|
||||
|
||||
public void ConcludeTest(string resultStatus, TimeSpan testDuration, Dictionary<string, string> data)
|
||||
@@ -26,7 +28,7 @@ namespace DistTestCore.Logs
|
||||
public void ConcludeTest(string resultStatus, string testDuration, Dictionary<string, string> data)
|
||||
{
|
||||
data.Add("timestamp", DateTime.UtcNow.ToString("o"));
|
||||
data.Add("runid", NameUtils.GetRunId());
|
||||
data.Add("deployid", deployId);
|
||||
data.Add("status", resultStatus);
|
||||
data.Add("category", NameUtils.GetCategoryName());
|
||||
data.Add("fixturename", fixtureName);
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
{
|
||||
public class TestLog : BaseTestLog
|
||||
{
|
||||
private readonly string methodName;
|
||||
private readonly string fullName;
|
||||
|
||||
public TestLog(string folder, string name = "")
|
||||
public TestLog(string folder, string deployId, string name = "") : base(deployId)
|
||||
{
|
||||
methodName = NameUtils.GetTestMethodName(name);
|
||||
var methodName = NameUtils.GetTestMethodName(name);
|
||||
fullName = Path.Combine(folder, methodName);
|
||||
|
||||
Log($"*** Begin: {methodName}");
|
||||
|
||||
@@ -5,14 +5,6 @@ namespace DistTestCore
|
||||
{
|
||||
public static class NameUtils
|
||||
{
|
||||
private static readonly string defaultRunId;
|
||||
|
||||
static NameUtils()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
defaultRunId = now.ToString("yyyyMMdd-hhmmss");
|
||||
}
|
||||
|
||||
public static string GetTestMethodName(string name = "")
|
||||
{
|
||||
if (!string.IsNullOrEmpty(name)) return name;
|
||||
@@ -48,9 +40,9 @@ namespace DistTestCore
|
||||
return GetEnvVar("TESTID", "EnvVar-TESTID-NotSet");
|
||||
}
|
||||
|
||||
public static string GetRunId()
|
||||
public static string MakeDeployId()
|
||||
{
|
||||
return GetEnvVar("RUNID", defaultRunId);
|
||||
return DateTime.UtcNow.ToString("yyyyMMdd-hhmmss");
|
||||
}
|
||||
|
||||
private static string GetEnvVar(string name, string defaultValue)
|
||||
|
||||
@@ -11,25 +11,27 @@ namespace DistTestCore
|
||||
public class TestLifecycle : IK8sHooks
|
||||
{
|
||||
private const string TestsType = "dist-tests";
|
||||
private readonly DateTime testStart;
|
||||
private readonly EntryPoint entryPoint;
|
||||
private readonly Dictionary<string, string> metadata;
|
||||
private readonly List<RunningContainers> runningContainers = new List<RunningContainers>();
|
||||
private readonly Dictionary<string, string> metadata;
|
||||
private readonly List<RunningContainers> runningContainers = new();
|
||||
private readonly string deployId;
|
||||
|
||||
public TestLifecycle(TestLog log, Configuration configuration, ITimeSet timeSet, string testNamespace)
|
||||
public TestLifecycle(TestLog log, Configuration configuration, ITimeSet timeSet, string testNamespace, string deployId)
|
||||
{
|
||||
Log = log;
|
||||
Configuration = configuration;
|
||||
TimeSet = timeSet;
|
||||
testStart = DateTime.UtcNow;
|
||||
TestStart = DateTime.UtcNow;
|
||||
|
||||
entryPoint = new EntryPoint(log, configuration.GetK8sConfiguration(timeSet, this, testNamespace), configuration.GetFileManagerFolder(), timeSet);
|
||||
metadata = entryPoint.GetPluginMetadata();
|
||||
CoreInterface = entryPoint.CreateInterface();
|
||||
this.deployId = deployId;
|
||||
|
||||
log.WriteLogTag();
|
||||
}
|
||||
|
||||
public DateTime TestStart { get; }
|
||||
public TestLog Log { get; }
|
||||
public Configuration Configuration { get; }
|
||||
public ITimeSet TimeSet { get; }
|
||||
@@ -60,7 +62,7 @@ namespace DistTestCore
|
||||
|
||||
public TimeSpan GetTestDuration()
|
||||
{
|
||||
return DateTime.UtcNow - testStart;
|
||||
return DateTime.UtcNow - TestStart;
|
||||
}
|
||||
|
||||
public void OnContainersStarted(RunningContainers rc)
|
||||
@@ -76,7 +78,7 @@ namespace DistTestCore
|
||||
public void OnContainerRecipeCreated(ContainerRecipe recipe)
|
||||
{
|
||||
recipe.PodLabels.Add("tests-type", TestsType);
|
||||
recipe.PodLabels.Add("runid", NameUtils.GetRunId());
|
||||
recipe.PodLabels.Add("deployid", deployId);
|
||||
recipe.PodLabels.Add("testid", NameUtils.GetTestId());
|
||||
recipe.PodLabels.Add("category", NameUtils.GetCategoryName());
|
||||
recipe.PodLabels.Add("fixturename", NameUtils.GetRawFixtureName());
|
||||
|
||||
@@ -1,87 +1,18 @@
|
||||
using BiblioTech.Options;
|
||||
using CodexContractsPlugin;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
public static class GethInput
|
||||
{
|
||||
private const string GethHostVar = "GETH_HOST";
|
||||
private const string GethPortVar = "GETH_HTTP_PORT";
|
||||
private const string GethPrivKeyVar = "GETH_PRIVATE_KEY";
|
||||
private const string MarketplaceAddressVar = "CODEXCONTRACTS_MARKETPLACEADDRESS";
|
||||
private const string TokenAddressVar = "CODEXCONTRACTS_TOKENADDRESS";
|
||||
private const string AbiVar = "CODEXCONTRACTS_ABI";
|
||||
|
||||
static GethInput()
|
||||
{
|
||||
var error = new List<string>();
|
||||
var gethHost = GetEnvVar(error, GethHostVar);
|
||||
var gethPort = Convert.ToInt32(GetEnvVar(error, GethPortVar));
|
||||
var privateKey = GetEnvVar(error, GethPrivKeyVar);
|
||||
var marketplaceAddress = GetEnvVar(error, MarketplaceAddressVar);
|
||||
var tokenAddress = GetEnvVar(error, TokenAddressVar);
|
||||
var abi = GetEnvVar(error, AbiVar);
|
||||
|
||||
if (error.Any())
|
||||
{
|
||||
LoadError = string.Join(", ", error);
|
||||
}
|
||||
else
|
||||
{
|
||||
GethHost = gethHost!;
|
||||
GethPort = gethPort;
|
||||
PrivateKey = privateKey!;
|
||||
MarketplaceAddress = marketplaceAddress!;
|
||||
TokenAddress = tokenAddress!;
|
||||
ABI = abi!;
|
||||
}
|
||||
}
|
||||
|
||||
public static string GethHost { get; } = string.Empty;
|
||||
public static int GethPort { get; }
|
||||
public static string PrivateKey { get; } = string.Empty;
|
||||
public static string MarketplaceAddress { get; } = string.Empty;
|
||||
public static string TokenAddress { get; } = string.Empty;
|
||||
public static string ABI { get; } = string.Empty;
|
||||
public static string LoadError { get; } = string.Empty;
|
||||
|
||||
private static string? GetEnvVar(List<string> error, string name)
|
||||
{
|
||||
var result = Environment.GetEnvironmentVariable(name);
|
||||
if (string.IsNullOrEmpty(result)) error.Add($"'{name}' is not set.");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class BaseGethCommand : BaseCommand
|
||||
{
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(GethInput.LoadError))
|
||||
{
|
||||
var msg = "Geth input incorrect: " + GethInput.LoadError;
|
||||
Program.Log.Error(msg);
|
||||
if (IsInAdminChannel(context.Command))
|
||||
{
|
||||
await context.Followup(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
await context.Followup("I'm sorry, there seems to be a configuration error.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
var gethConnector = GethConnector.GethConnector.Initialize(Program.Log);
|
||||
|
||||
var contractsDeployment = new CodexContractsDeployment(
|
||||
marketplaceAddress: GethInput.MarketplaceAddress,
|
||||
abi: GethInput.ABI,
|
||||
tokenAddress: GethInput.TokenAddress
|
||||
);
|
||||
|
||||
var gethNode = new CustomGethNode(Program.Log, GethInput.GethHost, GethInput.GethPort, GethInput.PrivateKey);
|
||||
var contracts = new CodexContractsAccess(Program.Log, gethNode, contractsDeployment);
|
||||
if (gethConnector == null) return;
|
||||
var gethNode = gethConnector.GethNode;
|
||||
var contracts = gethConnector.CodexContracts;
|
||||
|
||||
if (!contracts.IsDeployed())
|
||||
{
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.12.0" />
|
||||
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\DiscordRewards\DiscordRewards.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\GethConnector\GethConnector.csproj" />
|
||||
<ProjectReference Include="..\..\ProjectPlugins\CodexPlugin\CodexPlugin.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Discord.WebSocket;
|
||||
using Discord;
|
||||
using Newtonsoft.Json;
|
||||
using BiblioTech.Rewards;
|
||||
|
||||
namespace BiblioTech
|
||||
{
|
||||
@@ -25,6 +26,9 @@ namespace BiblioTech
|
||||
Program.AdminChecker.SetGuild(guild);
|
||||
Program.Log.Log($"Initializing for guild: '{guild.Name}'");
|
||||
|
||||
var roleController = new RoleController(client);
|
||||
var rewardsApi = new RewardsApi(roleController);
|
||||
|
||||
var adminChannels = guild.TextChannels.Where(Program.AdminChecker.IsAdminChannel).ToArray();
|
||||
if (adminChannels == null || !adminChannels.Any()) throw new Exception("No admin message channel");
|
||||
Program.AdminChecker.SetAdminChannel(adminChannels.First());
|
||||
@@ -58,6 +62,8 @@ namespace BiblioTech
|
||||
var json = JsonConvert.SerializeObject(exception.Errors, Formatting.Indented);
|
||||
Program.Log.Error(json);
|
||||
}
|
||||
|
||||
rewardsApi.Start();
|
||||
}
|
||||
|
||||
private async Task SlashCommandHandler(SocketSlashCommand command)
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
using BiblioTech.Options;
|
||||
|
||||
namespace BiblioTech.Commands
|
||||
{
|
||||
public class NotifyCommand : BaseCommand
|
||||
{
|
||||
private readonly BoolOption boolOption = new BoolOption(name: "enabled", description: "Controls whether the bot will @-mention you.", isRequired: false);
|
||||
|
||||
public override string Name => "notify";
|
||||
public override string StartingMessage => RandomBusyMessage.Get();
|
||||
public override string Description => "Enable or disable notifications from the bot.";
|
||||
public override CommandOption[] Options => new CommandOption[] { boolOption };
|
||||
|
||||
protected override async Task Invoke(CommandContext context)
|
||||
{
|
||||
var user = context.Command.User;
|
||||
var enabled = await boolOption.Parse(context);
|
||||
if (enabled == null) return;
|
||||
|
||||
Program.UserRepo.SetUserNotificationPreference(user, enabled.Value);
|
||||
await context.Followup("Done!");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,12 @@ namespace BiblioTech.Commands
|
||||
{
|
||||
public class UserAssociateCommand : BaseCommand
|
||||
{
|
||||
public UserAssociateCommand(NotifyCommand notifyCommand)
|
||||
{
|
||||
this.notifyCommand = notifyCommand;
|
||||
}
|
||||
|
||||
private readonly NotifyCommand notifyCommand;
|
||||
private readonly EthAddressOption ethOption = new EthAddressOption(isRequired: false);
|
||||
private readonly UserOption optionalUser = new UserOption(
|
||||
description: "If set, associates Ethereum address for another user. (Optional, admin-only)",
|
||||
@@ -30,7 +36,12 @@ namespace BiblioTech.Commands
|
||||
var result = Program.UserRepo.AssociateUserWithAddress(user, data);
|
||||
if (result)
|
||||
{
|
||||
await context.Followup("Done! Thank you for joining the test net!");
|
||||
await context.Followup(new string[]
|
||||
{
|
||||
"Done! Thank you for joining the test net!",
|
||||
"By default, the bot will @-mention you with test-net reward related notifications.",
|
||||
$"You can enable/disable this behavior with the '/{notifyCommand.Name}' command."
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -19,6 +19,10 @@ namespace BiblioTech
|
||||
[Uniform("admin-channel-name", "ac", "ADMINCHANNELNAME", true, "Name of the Discord server channel where admin commands are allowed.")]
|
||||
public string AdminChannelName { get; set; } = "admin-channel";
|
||||
|
||||
[Uniform("rewards-channel-name", "ac", "REWARDSCHANNELNAME", false, "Name of the Discord server channel where participation rewards will be announced.")]
|
||||
public string RewardsChannelName { get; set; } = "";
|
||||
|
||||
|
||||
public string EndpointsPath
|
||||
{
|
||||
get
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Discord;
|
||||
|
||||
namespace BiblioTech.Options
|
||||
{
|
||||
public class BoolOption : CommandOption
|
||||
{
|
||||
public BoolOption(string name, string description, bool isRequired)
|
||||
: base(name, description, type: ApplicationCommandOptionType.Boolean, isRequired)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<bool?> Parse(CommandContext context)
|
||||
{
|
||||
var bData = context.Options.SingleOrDefault(o => o.Name == Name);
|
||||
if (bData == null || !(bData.Value is bool))
|
||||
{
|
||||
await context.Followup("Bool option not received.");
|
||||
return null;
|
||||
}
|
||||
return (bool) bData.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ namespace BiblioTech
|
||||
Config = uniformArgs.Parse();
|
||||
|
||||
Log = new LogSplitter(
|
||||
new FileLog(Path.Combine(Config.LogPath, "discordbot.log")),
|
||||
new FileLog(Path.Combine(Config.LogPath, "discordbot")),
|
||||
new ConsoleLog()
|
||||
);
|
||||
|
||||
@@ -38,13 +38,15 @@ namespace BiblioTech
|
||||
client = new DiscordSocketClient();
|
||||
client.Log += ClientLog;
|
||||
|
||||
var associateCommand = new UserAssociateCommand();
|
||||
var notifyCommand = new NotifyCommand();
|
||||
var associateCommand = new UserAssociateCommand(notifyCommand);
|
||||
var sprCommand = new SprCommand();
|
||||
var handler = new CommandHandler(client,
|
||||
new GetBalanceCommand(associateCommand),
|
||||
new MintCommand(associateCommand),
|
||||
sprCommand,
|
||||
associateCommand,
|
||||
notifyCommand,
|
||||
new AdminCommand(sprCommand)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
using DiscordRewards;
|
||||
using Newtonsoft.Json;
|
||||
using System.Net;
|
||||
using TaskFactory = Utils.TaskFactory;
|
||||
|
||||
namespace BiblioTech.Rewards
|
||||
{
|
||||
public interface IDiscordRoleController
|
||||
{
|
||||
Task GiveRewards(GiveRewardsCommand rewards);
|
||||
}
|
||||
|
||||
public class RewardsApi
|
||||
{
|
||||
private readonly HttpListener listener = new HttpListener();
|
||||
private readonly TaskFactory taskFactory = new TaskFactory();
|
||||
private readonly IDiscordRoleController roleController;
|
||||
private CancellationTokenSource cts = new CancellationTokenSource();
|
||||
|
||||
public RewardsApi(IDiscordRoleController roleController)
|
||||
{
|
||||
this.roleController = roleController;
|
||||
}
|
||||
|
||||
public void Start()
|
||||
{
|
||||
cts = new CancellationTokenSource();
|
||||
listener.Prefixes.Add($"http://*:31080/");
|
||||
listener.Start();
|
||||
taskFactory.Run(ConnectionDispatcher, nameof(ConnectionDispatcher));
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
listener.Stop();
|
||||
cts.Cancel();
|
||||
taskFactory.WaitAll();
|
||||
}
|
||||
|
||||
private void ConnectionDispatcher()
|
||||
{
|
||||
while (!cts.Token.IsCancellationRequested)
|
||||
{
|
||||
var wait = listener.GetContextAsync();
|
||||
wait.Wait(cts.Token);
|
||||
if (wait.IsCompletedSuccessfully)
|
||||
{
|
||||
taskFactory.Run(() =>
|
||||
{
|
||||
var context = wait.Result;
|
||||
try
|
||||
{
|
||||
HandleConnection(context).Wait();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Log.Error("Exception during HTTP handler: " + ex);
|
||||
}
|
||||
// Whatever happens, everything's always OK.
|
||||
context.Response.StatusCode = 200;
|
||||
context.Response.OutputStream.Close();
|
||||
}, nameof(HandleConnection));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleConnection(HttpListenerContext context)
|
||||
{
|
||||
using var reader = new StreamReader(context.Request.InputStream);
|
||||
var content = reader.ReadToEnd();
|
||||
|
||||
if (content == "Ping")
|
||||
{
|
||||
using var writer = new StreamWriter(context.Response.OutputStream);
|
||||
writer.Write("Pong");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.StartsWith("{")) return;
|
||||
var rewards = JsonConvert.DeserializeObject<GiveRewardsCommand>(content);
|
||||
if (rewards != null)
|
||||
{
|
||||
await ProcessRewards(rewards);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessRewards(GiveRewardsCommand rewards)
|
||||
{
|
||||
await roleController.GiveRewards(rewards);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using Discord;
|
||||
using Discord.WebSocket;
|
||||
using DiscordRewards;
|
||||
|
||||
namespace BiblioTech.Rewards
|
||||
{
|
||||
public class RoleController : IDiscordRoleController
|
||||
{
|
||||
private readonly DiscordSocketClient client;
|
||||
private readonly SocketTextChannel? rewardsChannel;
|
||||
private readonly RewardRepo repo = new RewardRepo();
|
||||
|
||||
public RoleController(DiscordSocketClient client)
|
||||
{
|
||||
this.client = client;
|
||||
|
||||
if (!string.IsNullOrEmpty(Program.Config.RewardsChannelName))
|
||||
{
|
||||
rewardsChannel = GetGuild().TextChannels.SingleOrDefault(c => c.Name == Program.Config.RewardsChannelName);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task GiveRewards(GiveRewardsCommand rewards)
|
||||
{
|
||||
var guild = GetGuild();
|
||||
// We load all role and user information first,
|
||||
// so we don't ask the server for the same info multiple times.
|
||||
var context = new RewardContext(
|
||||
await LoadAllUsers(guild),
|
||||
LookUpAllRoles(guild, rewards),
|
||||
rewardsChannel);
|
||||
|
||||
await context.ProcessGiveRewardsCommand(LookUpUsers(rewards));
|
||||
}
|
||||
|
||||
private async Task<Dictionary<ulong, IGuildUser>> LoadAllUsers(SocketGuild guild)
|
||||
{
|
||||
var result = new Dictionary<ulong, IGuildUser>();
|
||||
var users = guild.GetUsersAsync();
|
||||
await foreach (var ulist in users)
|
||||
{
|
||||
foreach (var u in ulist)
|
||||
{
|
||||
result.Add(u.Id, u);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Dictionary<ulong, RoleReward> LookUpAllRoles(SocketGuild guild, GiveRewardsCommand rewards)
|
||||
{
|
||||
var result = new Dictionary<ulong, RoleReward>();
|
||||
foreach (var r in rewards.Rewards)
|
||||
{
|
||||
if (!result.ContainsKey(r.RewardId))
|
||||
{
|
||||
var rewardConfig = repo.Rewards.SingleOrDefault(rr => rr.RoleId == r.RewardId);
|
||||
if (rewardConfig == null)
|
||||
{
|
||||
Program.Log.Log($"No Reward is configured for id '{r.RewardId}'.");
|
||||
}
|
||||
else
|
||||
{
|
||||
var socketRole = guild.GetRole(r.RewardId);
|
||||
if (socketRole == null)
|
||||
{
|
||||
Program.Log.Log($"Guild Role by id '{r.RewardId}' not found.");
|
||||
}
|
||||
else
|
||||
{
|
||||
result.Add(r.RewardId, new RoleReward(socketRole, rewardConfig));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private UserReward[] LookUpUsers(GiveRewardsCommand rewards)
|
||||
{
|
||||
return rewards.Rewards.Select(LookUpUserData).ToArray();
|
||||
}
|
||||
|
||||
private UserReward LookUpUserData(RewardUsersCommand command)
|
||||
{
|
||||
return new UserReward(command,
|
||||
command.UserAddresses
|
||||
.Select(LookUpUserDataForAddress)
|
||||
.Where(d => d != null)
|
||||
.Cast<UserData>()
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
private UserData? LookUpUserDataForAddress(string address)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Program.UserRepo.GetUserDataForAddress(new GethPlugin.EthAddress(address));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Log.Error("Error during UserData lookup: " + ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private SocketGuild GetGuild()
|
||||
{
|
||||
return client.Guilds.Single(g => g.Name == Program.Config.ServerName);
|
||||
}
|
||||
}
|
||||
|
||||
public class RoleReward
|
||||
{
|
||||
public RoleReward(SocketRole socketRole, RewardConfig reward)
|
||||
{
|
||||
SocketRole = socketRole;
|
||||
Reward = reward;
|
||||
}
|
||||
|
||||
public SocketRole SocketRole { get; }
|
||||
public RewardConfig Reward { get; }
|
||||
}
|
||||
|
||||
public class UserReward
|
||||
{
|
||||
public UserReward(RewardUsersCommand rewardCommand, UserData[] users)
|
||||
{
|
||||
RewardCommand = rewardCommand;
|
||||
Users = users;
|
||||
}
|
||||
|
||||
public RewardUsersCommand RewardCommand { get; }
|
||||
public UserData[] Users { get; }
|
||||
}
|
||||
|
||||
public class RewardContext
|
||||
{
|
||||
private readonly Dictionary<ulong, IGuildUser> users;
|
||||
private readonly Dictionary<ulong, RoleReward> roles;
|
||||
private readonly SocketTextChannel? rewardsChannel;
|
||||
|
||||
public RewardContext(Dictionary<ulong, IGuildUser> users, Dictionary<ulong, RoleReward> roles, SocketTextChannel? rewardsChannel)
|
||||
{
|
||||
this.users = users;
|
||||
this.roles = roles;
|
||||
this.rewardsChannel = rewardsChannel;
|
||||
}
|
||||
|
||||
public async Task ProcessGiveRewardsCommand(UserReward[] rewards)
|
||||
{
|
||||
foreach (var rewardCommand in rewards)
|
||||
{
|
||||
if (roles.ContainsKey(rewardCommand.RewardCommand.RewardId))
|
||||
{
|
||||
var role = roles[rewardCommand.RewardCommand.RewardId];
|
||||
await ProcessRewardCommand(role, rewardCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessRewardCommand(RoleReward role, UserReward reward)
|
||||
{
|
||||
foreach (var user in reward.Users)
|
||||
{
|
||||
await GiveReward(role, user);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task GiveReward(RoleReward role, UserData user)
|
||||
{
|
||||
if (!users.ContainsKey(user.DiscordId))
|
||||
{
|
||||
Program.Log.Log($"User by id '{user.DiscordId}' not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var guildUser = users[user.DiscordId];
|
||||
|
||||
var alreadyHas = guildUser.RoleIds.ToArray();
|
||||
if (alreadyHas.Any(r => r == role.Reward.RoleId)) return;
|
||||
|
||||
await GiveRole(guildUser, role.SocketRole);
|
||||
await SendNotification(role, user, guildUser);
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
private async Task GiveRole(IGuildUser user, SocketRole role)
|
||||
{
|
||||
try
|
||||
{
|
||||
Program.Log.Log($"Giving role {role.Name}={role.Id} to user {user.DisplayName}");
|
||||
await user.AddRoleAsync(role);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Log.Error($"Failed to give role '{role.Name}' to user '{user.DisplayName}': {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendNotification(RoleReward reward, UserData userData, IGuildUser user)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (userData.NotificationsEnabled && rewardsChannel != null)
|
||||
{
|
||||
var msg = reward.Reward.Message.Replace(RewardConfig.UsernameTag, $"<@{user.Id}>");
|
||||
await rewardsChannel.SendMessageAsync(msg);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Program.Log.Error($"Failed to notify user '{user.DisplayName}' about role '{reward.SocketRole.Name}': {ex}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,14 @@ namespace BiblioTech
|
||||
}
|
||||
}
|
||||
|
||||
public void SetUserNotificationPreference(IUser user, bool enableNotifications)
|
||||
{
|
||||
lock (repoLock)
|
||||
{
|
||||
SetUserNotification(user, enableNotifications);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddMintEventForUser(IUser user, EthAddress usedAddress, Transaction<Ether>? eth, Transaction<TestToken>? tokens)
|
||||
{
|
||||
lock (repoLock)
|
||||
@@ -96,6 +104,29 @@ namespace BiblioTech
|
||||
return userData.CreateOverview();
|
||||
}
|
||||
|
||||
public UserData? GetUserDataForAddress(EthAddress? address)
|
||||
{
|
||||
if (address == null) return null;
|
||||
|
||||
// If this becomes a performance problem, switch to in-memory cached list.
|
||||
var files = Directory.GetFiles(Program.Config.UserDataPath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
|
||||
if (user.CurrentAddress != null &&
|
||||
user.CurrentAddress.Address == address.Address)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool SetUserAddress(IUser user, EthAddress? address)
|
||||
{
|
||||
if (GetUserDataForAddress(address) != null)
|
||||
@@ -110,6 +141,14 @@ namespace BiblioTech
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetUserNotification(IUser user, bool notifyEnabled)
|
||||
{
|
||||
var userData = GetUserData(user);
|
||||
if (userData == null) return;
|
||||
userData.NotificationsEnabled = notifyEnabled;
|
||||
SaveUserData(userData);
|
||||
}
|
||||
|
||||
private UserData? GetUserData(IUser user)
|
||||
{
|
||||
var filename = GetFilename(user);
|
||||
@@ -132,34 +171,11 @@ namespace BiblioTech
|
||||
|
||||
private UserData CreateAndSaveNewUserData(IUser user)
|
||||
{
|
||||
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>());
|
||||
var newUser = new UserData(user.Id, user.GlobalName, DateTime.UtcNow, null, new List<UserAssociateAddressEvent>(), new List<UserMintEvent>(), true);
|
||||
SaveUserData(newUser);
|
||||
return newUser;
|
||||
}
|
||||
|
||||
private UserData? GetUserDataForAddress(EthAddress? address)
|
||||
{
|
||||
if (address == null) return null;
|
||||
|
||||
// If this becomes a performance problem, switch to in-memory cached list.
|
||||
var files = Directory.GetFiles(Program.Config.UserDataPath);
|
||||
foreach (var file in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
var user = JsonConvert.DeserializeObject<UserData>(File.ReadAllText(file))!;
|
||||
if (user.CurrentAddress != null &&
|
||||
user.CurrentAddress.Address == address.Address)
|
||||
{
|
||||
return user;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SaveUserData(UserData userData)
|
||||
{
|
||||
var filename = GetFilename(userData);
|
||||
@@ -185,7 +201,7 @@ namespace BiblioTech
|
||||
|
||||
public class UserData
|
||||
{
|
||||
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents)
|
||||
public UserData(ulong discordId, string name, DateTime createdUtc, EthAddress? currentAddress, List<UserAssociateAddressEvent> associateEvents, List<UserMintEvent> mintEvents, bool notificationsEnabled)
|
||||
{
|
||||
DiscordId = discordId;
|
||||
Name = name;
|
||||
@@ -193,6 +209,7 @@ namespace BiblioTech
|
||||
CurrentAddress = currentAddress;
|
||||
AssociateEvents = associateEvents;
|
||||
MintEvents = mintEvents;
|
||||
NotificationsEnabled = notificationsEnabled;
|
||||
}
|
||||
|
||||
public ulong DiscordId { get; }
|
||||
@@ -201,6 +218,7 @@ namespace BiblioTech
|
||||
public EthAddress? CurrentAddress { get; set; }
|
||||
public List<UserAssociateAddressEvent> AssociateEvents { get; }
|
||||
public List<UserMintEvent> MintEvents { get; }
|
||||
public bool NotificationsEnabled { get; set; }
|
||||
|
||||
public string[] CreateOverview()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using ArgsUniform;
|
||||
using CodexPlugin;
|
||||
using DistTestCore;
|
||||
|
||||
namespace CodexNetDeployer
|
||||
{
|
||||
@@ -16,6 +17,9 @@ namespace CodexNetDeployer
|
||||
|
||||
[Uniform("kube-namespace", "kn", "KUBENAMESPACE", true, "Kubernetes namespace to be used for deployment.")]
|
||||
public string KubeNamespace { get; set; } = string.Empty;
|
||||
|
||||
[Uniform("deploy-id", "di", "DEPLOYID", false, "ID of the deployment. (default) to current time)")]
|
||||
public string DeployId { get; set; } = NameUtils.MakeDeployId();
|
||||
|
||||
[Uniform("deploy-file", "df", "DEPLOYFILE", false, "Output deployment JSON file that will be written. Defaults to 'codex-deployment.json'.")]
|
||||
public string DeployFile { get; set; } = "codex-deployment.json";
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace CodexNetDeployer
|
||||
Console.CursorLeft = longestKey + 5;
|
||||
Console.WriteLine($"= {entry.Value}");
|
||||
}
|
||||
|
||||
Log("");
|
||||
}
|
||||
|
||||
@@ -84,7 +85,8 @@ namespace CodexNetDeployer
|
||||
|
||||
var discordBotContainer = DeployDiscordBot(ci, gethDeployment, contractsDeployment);
|
||||
|
||||
return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService, discordBotContainer, CreateMetadata(startUtc));
|
||||
return new CodexDeployment(codexInstances, gethDeployment, contractsDeployment, metricsService,
|
||||
discordBotContainer, CreateMetadata(startUtc), config.DeployId);
|
||||
}
|
||||
|
||||
private EntryPoint CreateEntryPoint(ILog log)
|
||||
@@ -98,7 +100,7 @@ namespace CodexNetDeployer
|
||||
kubernetesNamespace: config.KubeNamespace);
|
||||
|
||||
var result = new EntryPoint(log, configuration, string.Empty, new FastHttpTimeSet());
|
||||
configuration.Hooks = new K8sHook(config.TestsTypePodLabel, result.GetPluginMetadata());
|
||||
configuration.Hooks = new K8sHook(config.TestsTypePodLabel, config.DeployId, result.GetPluginMetadata());
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -120,7 +122,8 @@ namespace CodexNetDeployer
|
||||
});
|
||||
}
|
||||
|
||||
private RunningContainers? DeployDiscordBot(CoreInterface ci, GethDeployment gethDeployment, CodexContractsDeployment contractsDeployment)
|
||||
private RunningContainers? DeployDiscordBot(CoreInterface ci, GethDeployment gethDeployment,
|
||||
CodexContractsDeployment contractsDeployment)
|
||||
{
|
||||
if (!config.DeployDiscordBot) return null;
|
||||
Log("Deploying Discord bot...");
|
||||
@@ -201,7 +204,8 @@ namespace CodexNetDeployer
|
||||
foreach (var startResult in startResults)
|
||||
{
|
||||
var watcher = startResult.CodexNode.CrashWatcher;
|
||||
if (watcher == null) throw new Exception("Expected each CodexNode container to be created with a crash-watcher.");
|
||||
if (watcher == null)
|
||||
throw new Exception("Expected each CodexNode container to be created with a crash-watcher.");
|
||||
if (watcher.HasContainerCrashed()) crashes.Add(startResult.CodexNode.Container);
|
||||
}
|
||||
|
||||
@@ -211,7 +215,8 @@ namespace CodexNetDeployer
|
||||
}
|
||||
else
|
||||
{
|
||||
Log($"Check failed. The following containers have crashed: {string.Join(",", crashes.Select(c => c.Name))}");
|
||||
Log(
|
||||
$"Check failed. The following containers have crashed: {string.Join(",", crashes.Select(c => c.Name))}");
|
||||
throw new Exception("Deployment failed: One or more containers crashed.");
|
||||
}
|
||||
}
|
||||
@@ -269,4 +274,4 @@ namespace CodexNetDeployer
|
||||
return TimeSpan.FromSeconds(30);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,13 @@ namespace CodexNetDeployer
|
||||
public class K8sHook : IK8sHooks
|
||||
{
|
||||
private readonly string testsTypeLabel;
|
||||
private readonly string deployId;
|
||||
private readonly Dictionary<string, string> metadata;
|
||||
|
||||
public K8sHook(string testsTypeLabel, Dictionary<string, string> metadata)
|
||||
public K8sHook(string testsTypeLabel, string deployId, Dictionary<string, string> metadata)
|
||||
{
|
||||
this.testsTypeLabel = testsTypeLabel;
|
||||
this.deployId = deployId;
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
@@ -27,7 +29,7 @@ namespace CodexNetDeployer
|
||||
public void OnContainerRecipeCreated(ContainerRecipe recipe)
|
||||
{
|
||||
recipe.PodLabels.Add("tests-type", testsTypeLabel);
|
||||
recipe.PodLabels.Add("runid", NameUtils.GetRunId());
|
||||
recipe.PodLabels.Add("deployid", deployId);
|
||||
recipe.PodLabels.Add("testid", NameUtils.GetTestId());
|
||||
|
||||
foreach (var pair in metadata)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export DEPLOYMENT_NAME="-chronos-v4"
|
||||
export DEPLOYMENT_NAMESPACE="chronos-v4-testnet"
|
||||
export CODEX_STORAGE_QUOTA=60000
|
||||
export CODEX_BLOCK_TTL=9999999
|
||||
export CODEX_BLOCK_MI=9999999
|
||||
export DEPLOYMENT_NODES=5
|
||||
export DEPLOYMENT_VALIDATORS=3
|
||||
|
||||
REL_PATH=$(dirname "${BASH_SOURCE[0]}")
|
||||
|
||||
. "${REL_PATH}/deploy-continuous-testnet.sh"
|
||||
@@ -1,12 +0,0 @@
|
||||
export DEPLOYMENT_NAME="-chronos-v4-control"
|
||||
export DEPLOYMENT_NAMESPACE="chronos-v4-control"
|
||||
export CODEX_STORAGE_QUOTA=60000
|
||||
export CODEX_BLOCK_TTL=9999999
|
||||
export CODEX_BLOCK_MI=9999999
|
||||
export DEPLOYMENT_NODES=5
|
||||
export DEPLOYMENT_VALIDATORS=3
|
||||
export DEPLOYMENT_FILE="codex-chronos-v4-control-deployment.json"
|
||||
|
||||
REL_PATH=$(dirname "${BASH_SOURCE[0]}")
|
||||
|
||||
. "${REL_PATH}/deploy-continuous-testnet.sh"
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.28.0" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Tests\CodexTests\CodexTests.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,74 @@
|
||||
using LibGit2Sharp;
|
||||
using CodexTests.BasicTests;
|
||||
using CodexPlugin;
|
||||
using System.Diagnostics;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("Hello, World!");
|
||||
|
||||
var codexPath = @"D:\Projects\nim-codex";
|
||||
|
||||
using var repo = new Repository(codexPath);
|
||||
var masterBranch = repo.Branches.Single(b => b.FriendlyName == "master");
|
||||
var take = masterBranch.Commits.Take(10).ToArray();
|
||||
foreach (var c in take)
|
||||
{
|
||||
RunWith(c);
|
||||
}
|
||||
|
||||
Console.ReadLine();
|
||||
}
|
||||
|
||||
private static void RunWith(Commit c)
|
||||
{
|
||||
var shortSha = c.Sha.Substring(0, 7);
|
||||
var image = $"codexstorage/nim-codex:sha-{shortSha}-dist-tests";
|
||||
CodexContainerRecipe.DockerImageOverride = image;
|
||||
|
||||
if (!DoesExist(image))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Console.WriteLine("Running test for: " + c.MessageShort);
|
||||
|
||||
var test = new ExampleTests();
|
||||
test.GlobalSetup();
|
||||
test.SetUpDistTest();
|
||||
|
||||
try
|
||||
{
|
||||
test.CodexLogExample();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("test ex: " + ex);
|
||||
}
|
||||
|
||||
test.TearDownDistTest();
|
||||
test.GlobalTearDown();
|
||||
|
||||
Console.WriteLine("Test passed");
|
||||
}
|
||||
catch (Exception a)
|
||||
{
|
||||
Console.WriteLine("ex: " + a);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool DoesExist(string image)
|
||||
{
|
||||
var info = new ProcessStartInfo("docker", "image pull " + image);
|
||||
info.RedirectStandardOutput = false;
|
||||
var process = Process.Start(info);
|
||||
process.WaitForExit();
|
||||
|
||||
return process.ExitCode == 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
using DiscordRewards;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class BotClient
|
||||
{
|
||||
private readonly Configuration configuration;
|
||||
private readonly ILog log;
|
||||
|
||||
public BotClient(Configuration configuration, ILog log)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public async Task<bool> IsOnline()
|
||||
{
|
||||
return await HttpPost("Ping") == "Ping";
|
||||
}
|
||||
|
||||
public async Task SendRewards(GiveRewardsCommand command)
|
||||
{
|
||||
if (command == null || command.Rewards == null || !command.Rewards.Any()) return;
|
||||
await HttpPost(JsonConvert.SerializeObject(command));
|
||||
}
|
||||
|
||||
private async Task<string> HttpPost(string content)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = new HttpClient();
|
||||
var response = await client.PostAsync(GetUrl(), new StringContent(content));
|
||||
return await response.Content.ReadAsStringAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error(ex.ToString());
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetUrl()
|
||||
{
|
||||
return $"{configuration.DiscordHost}:{configuration.DiscordPort}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using CodexContractsPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using Utils;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class ChainState
|
||||
{
|
||||
private readonly HistoricState historicState;
|
||||
|
||||
public ChainState(HistoricState historicState, ICodexContracts contracts, TimeRange timeRange)
|
||||
{
|
||||
NewRequests = contracts.GetStorageRequests(timeRange);
|
||||
historicState.ProcessNewRequests(NewRequests);
|
||||
historicState.UpdateStorageRequests(contracts);
|
||||
|
||||
StartedRequests = historicState.StorageRequests.Where(r => r.RecentlyStarted).ToArray();
|
||||
FinishedRequests = historicState.StorageRequests.Where(r => r.RecentlyFininshed).ToArray();
|
||||
RequestFulfilledEvents = contracts.GetRequestFulfilledEvents(timeRange);
|
||||
RequestCancelledEvents = contracts.GetRequestCancelledEvents(timeRange);
|
||||
SlotFilledEvents = contracts.GetSlotFilledEvents(timeRange);
|
||||
SlotFreedEvents = contracts.GetSlotFreedEvents(timeRange);
|
||||
this.historicState = historicState;
|
||||
}
|
||||
|
||||
public Request[] NewRequests { get; }
|
||||
public StorageRequest[] AllRequests => historicState.StorageRequests;
|
||||
public StorageRequest[] StartedRequests { get; private set; }
|
||||
public StorageRequest[] FinishedRequests { get; private set; }
|
||||
public RequestFulfilledEventDTO[] RequestFulfilledEvents { get; }
|
||||
public RequestCancelledEventDTO[] RequestCancelledEvents { get; }
|
||||
public SlotFilledEventDTO[] SlotFilledEvents { get; }
|
||||
public SlotFreedEventDTO[] SlotFreedEvents { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
using NethereumWorkflow;
|
||||
using Utils;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public interface ICheck
|
||||
{
|
||||
EthAddress[] Check(ChainState state);
|
||||
}
|
||||
|
||||
public class FilledAnySlotCheck : ICheck
|
||||
{
|
||||
public EthAddress[] Check(ChainState state)
|
||||
{
|
||||
return state.SlotFilledEvents.Select(e => e.Host).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class FinishedSlotCheck : ICheck
|
||||
{
|
||||
private readonly ByteSize minSize;
|
||||
private readonly TimeSpan minDuration;
|
||||
|
||||
public FinishedSlotCheck(ByteSize minSize, TimeSpan minDuration)
|
||||
{
|
||||
this.minSize = minSize;
|
||||
this.minDuration = minDuration;
|
||||
}
|
||||
|
||||
public EthAddress[] Check(ChainState state)
|
||||
{
|
||||
return state.FinishedRequests
|
||||
.Where(r =>
|
||||
MeetsSizeRequirement(r) &&
|
||||
MeetsDurationRequirement(r))
|
||||
.SelectMany(r => r.Hosts)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private bool MeetsSizeRequirement(StorageRequest r)
|
||||
{
|
||||
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
|
||||
decimal min = minSize.SizeInBytes;
|
||||
return slotSize >= min;
|
||||
}
|
||||
|
||||
private bool MeetsDurationRequirement(StorageRequest r)
|
||||
{
|
||||
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
|
||||
return duration >= minDuration;
|
||||
}
|
||||
}
|
||||
|
||||
public class PostedContractCheck : ICheck
|
||||
{
|
||||
private readonly ulong minNumberOfHosts;
|
||||
private readonly ByteSize minSlotSize;
|
||||
private readonly TimeSpan minDuration;
|
||||
|
||||
public PostedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
|
||||
{
|
||||
this.minNumberOfHosts = minNumberOfHosts;
|
||||
this.minSlotSize = minSlotSize;
|
||||
this.minDuration = minDuration;
|
||||
}
|
||||
|
||||
public EthAddress[] Check(ChainState state)
|
||||
{
|
||||
return state.NewRequests
|
||||
.Where(r =>
|
||||
MeetsNumSlotsRequirement(r) &&
|
||||
MeetsSizeRequirement(r) &&
|
||||
MeetsDurationRequirement(r))
|
||||
.Select(r => r.ClientAddress)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private bool MeetsNumSlotsRequirement(Request r)
|
||||
{
|
||||
return r.Ask.Slots >= minNumberOfHosts;
|
||||
}
|
||||
|
||||
private bool MeetsSizeRequirement(Request r)
|
||||
{
|
||||
var slotSize = r.Ask.SlotSize.ToDecimal();
|
||||
decimal min = minSlotSize.SizeInBytes;
|
||||
return slotSize >= min;
|
||||
}
|
||||
|
||||
private bool MeetsDurationRequirement(Request r)
|
||||
{
|
||||
var duration = TimeSpan.FromSeconds((double)r.Ask.Duration);
|
||||
return duration >= minDuration;
|
||||
}
|
||||
}
|
||||
|
||||
public class StartedContractCheck : ICheck
|
||||
{
|
||||
private readonly ulong minNumberOfHosts;
|
||||
private readonly ByteSize minSlotSize;
|
||||
private readonly TimeSpan minDuration;
|
||||
|
||||
public StartedContractCheck(ulong minNumberOfHosts, ByteSize minSlotSize, TimeSpan minDuration)
|
||||
{
|
||||
this.minNumberOfHosts = minNumberOfHosts;
|
||||
this.minSlotSize = minSlotSize;
|
||||
this.minDuration = minDuration;
|
||||
}
|
||||
|
||||
public EthAddress[] Check(ChainState state)
|
||||
{
|
||||
return state.StartedRequests
|
||||
.Where(r =>
|
||||
MeetsNumSlotsRequirement(r) &&
|
||||
MeetsSizeRequirement(r) &&
|
||||
MeetsDurationRequirement(r))
|
||||
.Select(r => r.Request.ClientAddress)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private bool MeetsNumSlotsRequirement(StorageRequest r)
|
||||
{
|
||||
return r.Request.Ask.Slots >= minNumberOfHosts;
|
||||
}
|
||||
|
||||
private bool MeetsSizeRequirement(StorageRequest r)
|
||||
{
|
||||
var slotSize = r.Request.Ask.SlotSize.ToDecimal();
|
||||
decimal min = minSlotSize.SizeInBytes;
|
||||
return slotSize >= min;
|
||||
}
|
||||
|
||||
private bool MeetsDurationRequirement(StorageRequest r)
|
||||
{
|
||||
var duration = TimeSpan.FromSeconds((double)r.Request.Ask.Duration);
|
||||
return duration >= minDuration;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using ArgsUniform;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class Configuration
|
||||
{
|
||||
[Uniform("datapath", "dp", "DATAPATH", false, "Root path where all data files will be saved.")]
|
||||
public string DataPath { get; set; } = "datapath";
|
||||
|
||||
[Uniform("discordbot-host", "dh", "DISCORDBOTHOST", true, "http address of the discord bot.")]
|
||||
public string DiscordHost { get; set; } = "host";
|
||||
|
||||
[Uniform("discordbot-port", "dp", "DISCORDBOTPORT", true, "port number of the discord bot reward API. (31080 by default)")]
|
||||
public int DiscordPort { get; set; } = 31080;
|
||||
|
||||
[Uniform("interval-minutes", "im", "INTERVALMINUTES", false, "time in minutes between reward updates. (default 15)")]
|
||||
public int Interval { get; set; } = 15;
|
||||
|
||||
[Uniform("check-history", "ch", "CHECKHISTORY", true, "Unix epoc timestamp of a moment in history on which processing begins. Required for hosting rewards. Should be 'launch of the testnet'.")]
|
||||
public int CheckHistoryTimestamp { get; set; } = 0;
|
||||
|
||||
public string LogPath
|
||||
{
|
||||
get
|
||||
{
|
||||
return Path.Combine(DataPath, "logs");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using CodexContractsPlugin;
|
||||
using CodexContractsPlugin.Marketplace;
|
||||
using GethPlugin;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class HistoricState
|
||||
{
|
||||
private readonly List<StorageRequest> storageRequests = new List<StorageRequest>();
|
||||
|
||||
public StorageRequest[] StorageRequests { get { return storageRequests.ToArray(); } }
|
||||
|
||||
public void ProcessNewRequests(Request[] requests)
|
||||
{
|
||||
storageRequests.AddRange(requests.Select(r => new StorageRequest(r)));
|
||||
}
|
||||
|
||||
public void UpdateStorageRequests(ICodexContracts contracts)
|
||||
{
|
||||
foreach (var r in storageRequests) r.Update(contracts);
|
||||
}
|
||||
}
|
||||
|
||||
public class StorageRequest
|
||||
{
|
||||
public StorageRequest(Request request)
|
||||
{
|
||||
Request = request;
|
||||
Hosts = Array.Empty<EthAddress>();
|
||||
}
|
||||
|
||||
public Request Request { get; }
|
||||
public EthAddress[] Hosts { get; private set; }
|
||||
public RequestState State { get; private set; }
|
||||
public bool RecentlyStarted { get; private set; }
|
||||
public bool RecentlyFininshed { get; private set; }
|
||||
|
||||
public void Update(ICodexContracts contracts)
|
||||
{
|
||||
Hosts = GetHosts(contracts);
|
||||
|
||||
var newState = contracts.GetRequestState(Request);
|
||||
|
||||
RecentlyStarted =
|
||||
State == RequestState.New &&
|
||||
newState == RequestState.Started;
|
||||
|
||||
RecentlyFininshed =
|
||||
State == RequestState.Started &&
|
||||
newState == RequestState.Finished;
|
||||
|
||||
State = newState;
|
||||
}
|
||||
|
||||
private EthAddress[] GetHosts(ICodexContracts contracts)
|
||||
{
|
||||
var result = new List<EthAddress>();
|
||||
|
||||
for (decimal i = 0; i < Request.Ask.Slots; i++)
|
||||
{
|
||||
var host = contracts.GetSlotHost(Request, i);
|
||||
if (host != null) result.Add(host);
|
||||
}
|
||||
|
||||
return result.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
using DiscordRewards;
|
||||
using GethPlugin;
|
||||
using Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Utils;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class Processor
|
||||
{
|
||||
private static readonly HistoricState historicState = new HistoricState();
|
||||
private static readonly RewardRepo rewardRepo = new RewardRepo();
|
||||
private readonly ILog log;
|
||||
|
||||
public Processor(ILog log)
|
||||
{
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
public async Task ProcessTimeSegment(TimeRange range)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connector = GethConnector.GethConnector.Initialize(log);
|
||||
if (connector == null) return;
|
||||
|
||||
var chainState = new ChainState(historicState, connector.CodexContracts, range);
|
||||
await ProcessTimeSegment(chainState);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.Error("Exception processing time segment: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessTimeSegment(ChainState chainState)
|
||||
{
|
||||
var outgoingRewards = new List<RewardUsersCommand>();
|
||||
foreach (var reward in rewardRepo.Rewards)
|
||||
{
|
||||
ProcessReward(outgoingRewards, reward, chainState);
|
||||
}
|
||||
|
||||
if (outgoingRewards.Any())
|
||||
{
|
||||
await SendRewardsCommand(outgoingRewards);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendRewardsCommand(List<RewardUsersCommand> outgoingRewards)
|
||||
{
|
||||
var cmd = new GiveRewardsCommand
|
||||
{
|
||||
Rewards = outgoingRewards.ToArray()
|
||||
};
|
||||
|
||||
log.Debug("Sending rewards: " + JsonConvert.SerializeObject(cmd));
|
||||
await Program.BotClient.SendRewards(cmd);
|
||||
}
|
||||
|
||||
private void ProcessReward(List<RewardUsersCommand> outgoingRewards, RewardConfig reward, ChainState chainState)
|
||||
{
|
||||
var winningAddresses = PerformCheck(reward, chainState);
|
||||
if (winningAddresses.Any())
|
||||
{
|
||||
outgoingRewards.Add(new RewardUsersCommand
|
||||
{
|
||||
RewardId = reward.RoleId,
|
||||
UserAddresses = winningAddresses.Select(a => a.Address).ToArray()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private EthAddress[] PerformCheck(RewardConfig reward, ChainState chainState)
|
||||
{
|
||||
var check = GetCheck(reward.CheckConfig);
|
||||
return check.Check(chainState);
|
||||
}
|
||||
|
||||
private ICheck GetCheck(CheckConfig config)
|
||||
{
|
||||
switch (config.Type)
|
||||
{
|
||||
case CheckType.FilledSlot:
|
||||
return new FilledAnySlotCheck();
|
||||
case CheckType.FinishedSlot:
|
||||
return new FinishedSlotCheck(config.MinSlotSize, config.MinDuration);
|
||||
case CheckType.PostedContract:
|
||||
return new PostedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
|
||||
case CheckType.StartedContract:
|
||||
return new StartedContractCheck(config.MinNumberOfHosts, config.MinSlotSize, config.MinDuration);
|
||||
}
|
||||
|
||||
throw new Exception("Unknown check type: " + config.Type);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using ArgsUniform;
|
||||
using GethConnector;
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static Configuration Config { get; private set; } = null!;
|
||||
public static ILog Log { get; private set; } = null!;
|
||||
public static CancellationToken CancellationToken { get; private set; }
|
||||
public static BotClient BotClient { get; private set; } = null!;
|
||||
private static Processor processor = null!;
|
||||
|
||||
public static Task Main(string[] args)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
CancellationToken = cts.Token;
|
||||
Console.CancelKeyPress += (sender, args) => cts.Cancel();
|
||||
|
||||
var uniformArgs = new ArgsUniform<Configuration>(PrintHelp, args);
|
||||
Config = uniformArgs.Parse(true);
|
||||
|
||||
Log = new LogSplitter(
|
||||
new FileLog(Path.Combine(Config.LogPath, "testnetrewarder")),
|
||||
new ConsoleLog()
|
||||
);
|
||||
|
||||
BotClient = new BotClient(Config, Log);
|
||||
processor = new Processor(Log);
|
||||
|
||||
EnsurePath(Config.DataPath);
|
||||
EnsurePath(Config.LogPath);
|
||||
|
||||
return new Program().MainAsync();
|
||||
}
|
||||
|
||||
public async Task MainAsync()
|
||||
{
|
||||
EnsureGethOnline();
|
||||
|
||||
Log.Log("Starting TestNet Rewarder...");
|
||||
var segmenter = new TimeSegmenter(Log, Config);
|
||||
|
||||
while (!CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await EnsureBotOnline();
|
||||
await segmenter.WaitForNextSegment(processor.ProcessTimeSegment);
|
||||
await Task.Delay(1000, CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureGethOnline()
|
||||
{
|
||||
Log.Log("Checking Geth...");
|
||||
var gc = GethConnector.GethConnector.Initialize(Log);
|
||||
if (gc == null) throw new Exception("Geth input incorrect");
|
||||
|
||||
var blockNumber = gc.GethNode.GetSyncedBlockNumber();
|
||||
if (blockNumber == null || blockNumber < 1) throw new Exception("Geth connection failed.");
|
||||
}
|
||||
|
||||
private static async Task EnsureBotOnline()
|
||||
{
|
||||
var start = DateTime.UtcNow;
|
||||
while (! await BotClient.IsOnline() && !CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(5000);
|
||||
|
||||
var elapsed = DateTime.UtcNow - start;
|
||||
if (elapsed.TotalMinutes > 10)
|
||||
{
|
||||
var msg = "Unable to connect to bot for " + Time.FormatDuration(elapsed);
|
||||
Log.Error(msg);
|
||||
throw new Exception(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintHelp()
|
||||
{
|
||||
Log.Log("TestNet Rewarder");
|
||||
}
|
||||
|
||||
private static void EnsurePath(string path)
|
||||
{
|
||||
if (Directory.Exists(path)) return;
|
||||
Directory.CreateDirectory(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Framework\ArgsUniform\ArgsUniform.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\DiscordRewards\DiscordRewards.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\GethConnector\GethConnector.csproj" />
|
||||
<ProjectReference Include="..\..\Framework\Logging\Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,51 @@
|
||||
using Logging;
|
||||
using Utils;
|
||||
|
||||
namespace TestNetRewarder
|
||||
{
|
||||
public class TimeSegmenter
|
||||
{
|
||||
private readonly ILog log;
|
||||
private readonly TimeSpan segmentSize;
|
||||
private DateTime start;
|
||||
|
||||
public TimeSegmenter(ILog log, Configuration configuration)
|
||||
{
|
||||
this.log = log;
|
||||
|
||||
if (configuration.Interval < 0) configuration.Interval = 15;
|
||||
if (configuration.CheckHistoryTimestamp == 0) throw new Exception("'check-history' unix timestamp is required. Set it to the start/launch moment of the testnet.");
|
||||
|
||||
segmentSize = TimeSpan.FromSeconds(configuration.Interval);
|
||||
start = DateTimeOffset.FromUnixTimeSeconds(configuration.CheckHistoryTimestamp).UtcDateTime;
|
||||
|
||||
log.Log("Starting time segments at " + start);
|
||||
log.Log("Segment size: " + Time.FormatDuration(segmentSize));
|
||||
}
|
||||
|
||||
public async Task WaitForNextSegment(Func<TimeRange, Task> onSegment)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var end = start + segmentSize;
|
||||
var waited = false;
|
||||
if (end > now)
|
||||
{
|
||||
// Wait for the entire time segment to be in the past.
|
||||
var delay = (end - now).Add(TimeSpan.FromSeconds(3));
|
||||
waited = true;
|
||||
await Task.Delay(delay, Program.CancellationToken);
|
||||
}
|
||||
|
||||
if (Program.CancellationToken.IsCancellationRequested) return;
|
||||
|
||||
var postfix = "(Catching up...)";
|
||||
if (waited) postfix = "(Real-time)";
|
||||
|
||||
log.Log($"Time segment [{start} to {end}] {postfix}");
|
||||
var range = new TimeRange(start, end);
|
||||
start = end;
|
||||
|
||||
await onSegment(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
# Variables
|
||||
ARG IMAGE=mcr.microsoft.com/dotnet/sdk:7.0
|
||||
ARG APP_HOME=/app
|
||||
|
||||
# Create
|
||||
FROM ${IMAGE}
|
||||
ARG APP_HOME
|
||||
|
||||
WORKDIR ${APP_HOME}
|
||||
COPY ./Tools/TestNetRewarder ./Tools/TestNetRewarder
|
||||
COPY ./Framework ./Framework
|
||||
COPY ./ProjectPlugins ./ProjectPlugins
|
||||
CMD ["dotnet", "run", "--project", "Tools/TestNetRewarder"]
|
||||
@@ -35,8 +35,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MetricsPlugin", "ProjectPlu
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexContinuousTests", "Tests\CodexContinuousTests\CodexContinuousTests.csproj", "{ADEC06CF-6F3A-44C5-AA57-EAB94124AC82}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexTestsLong", "Tests\CodexLongTests\CodexTestsLong.csproj", "{0C2D067F-053C-45A8-AE0D-4EB388E77C89}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CodexTests", "Tests\CodexTests\CodexTests.csproj", "{562EC700-6984-4C9A-83BF-3BF4E3EB1A64}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DistTestCore", "Tests\DistTestCore\DistTestCore.csproj", "{E849B7BA-FDCC-4CFF-998F-845ED2F1BF40}"
|
||||
@@ -53,6 +51,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DeployAndRunPlugin", "Proje
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrameworkTests", "Tests\FrameworkTests\FrameworkTests.csproj", "{25E72004-4D71-4D1E-A193-FC125D12FF96}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestNetRewarder", "Tools\TestNetRewarder\TestNetRewarder.csproj", "{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GethConnector", "Framework\GethConnector\GethConnector.csproj", "{F730DA73-1C92-4107-BCFB-D33759DAB0C3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DiscordRewards", "Framework\DiscordRewards\DiscordRewards.csproj", "{B07820C4-309F-4454-BCC1-1D4902C9C67B}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommitBlamer", "Tools\CommitBlamer\CommitBlamer.csproj", "{DC08EAA2-42F4-49EE-90C8-44673094BE0A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -107,10 +113,6 @@ Global
|
||||
{ADEC06CF-6F3A-44C5-AA57-EAB94124AC82}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{ADEC06CF-6F3A-44C5-AA57-EAB94124AC82}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{ADEC06CF-6F3A-44C5-AA57-EAB94124AC82}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C2D067F-053C-45A8-AE0D-4EB388E77C89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C2D067F-053C-45A8-AE0D-4EB388E77C89}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C2D067F-053C-45A8-AE0D-4EB388E77C89}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{0C2D067F-053C-45A8-AE0D-4EB388E77C89}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@@ -143,6 +145,22 @@ Global
|
||||
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25E72004-4D71-4D1E-A193-FC125D12FF96}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F730DA73-1C92-4107-BCFB-D33759DAB0C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B07820C4-309F-4454-BCC1-1D4902C9C67B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DC08EAA2-42F4-49EE-90C8-44673094BE0A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DC08EAA2-42F4-49EE-90C8-44673094BE0A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DC08EAA2-42F4-49EE-90C8-44673094BE0A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DC08EAA2-42F4-49EE-90C8-44673094BE0A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -160,7 +178,6 @@ Global
|
||||
{8B39F251-F948-40AE-8922-3D8C4E529A86} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
|
||||
{8DE8FF65-23CB-4FB3-8BE5-6C0BEC4BAA97} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
|
||||
{ADEC06CF-6F3A-44C5-AA57-EAB94124AC82} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{0C2D067F-053C-45A8-AE0D-4EB388E77C89} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{562EC700-6984-4C9A-83BF-3BF4E3EB1A64} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{E849B7BA-FDCC-4CFF-998F-845ED2F1BF40} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{3417D508-E2F4-4974-8988-BB124046D9E2} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
@@ -169,6 +186,10 @@ Global
|
||||
{3E38A906-C2FC-43DC-8CA2-FC07C79CF3CA} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
{1CC5AF82-8924-4C7E-BFF1-3125D86E53FB} = {8F1F1C2A-E313-4E0C-BE40-58FB0BA91124}
|
||||
{25E72004-4D71-4D1E-A193-FC125D12FF96} = {88C2A621-8A98-4D07-8625-7900FC8EF89E}
|
||||
{570C0DBE-0EF1-47B5-9A3B-E1F7895722A5} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
{F730DA73-1C92-4107-BCFB-D33759DAB0C3} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
|
||||
{B07820C4-309F-4454-BCC1-1D4902C9C67B} = {81AE04BC-CBFA-4E6F-B039-8208E9AFAAE7}
|
||||
{DC08EAA2-42F4-49EE-90C8-44673094BE0A} = {7591C5B3-D86E-4AE4-8ED2-B272D17FE7E3}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {237BF0AA-9EC4-4659-AD9A-65DEB974250C}
|
||||
|
||||
Reference in New Issue
Block a user