private Task <PendingTransaction> IssueFundsAsync(INetworkSigningAccount networkSigningAccount, Issuance issuance, TransactionContext context) { this._logger.LogDebug($"Faucet Contract Balances: {issuance.SourceAccountBalance.ToFormattedUnitWithSymbol()}, {issuance.SourceTokenBalance.ToFormattedUnitWithSymbol()}"); EthereumAmount minEth = MinSourceNativeCurrencyAccountBalance(issuance.EthToIssue); Token minFun = MinSourceTokenAccountBalance(issuance.TokenToIssue); // don't allow the source account balance to go too low if (issuance.SourceAccountBalance < minEth) { string message = $"{issuance.Recipient.Network.Name}: Cannot issue {issuance.Recipient.Network.NativeCurrency} from {issuance.SourceName}. Faucet Contract {issuance.FundsSourceContract.Address} is low on ETH (Has {issuance.SourceAccountBalance.ToFormattedUnitWithSymbol()}. Minimum {minEth.ToFormattedUnitWithSymbol()})"; this._logger.LogCritical(message); throw new InsufficientTokenException(message); } if (issuance.SourceTokenBalance < minFun) { string message = $"{issuance.Recipient.Network.Name}: Cannot issue {this._tokenContract.Symbol} from {issuance.SourceName}. Faucet Contract {issuance.FundsSourceContract.Address} is low on FUN (Has {issuance.SourceTokenBalance.ToFormattedUnitWithSymbol()}. Minimum {minFun.ToFormattedUnitWithSymbol()})"; this._logger.LogCritical(message); throw new InsufficientTokenException(message); } return(issuance.SendFundsAsync(networkSigningAccount: networkSigningAccount, context: context, transactionExecutorFactory: this._transactionExecutorFactory)); }
/// <inheritdoc /> public async Task StartGameAsync(INetworkSigningAccount account, GameRound game, INetworkBlockHeader networkBlockHeader, CancellationToken cancellationToken) { PendingTransaction pendingTransaction; try { StartGameRoundInput input = new(roundId : game.GameRoundId, gameAddress : game.GameContract, entropyCommit : game.SeedCommit); pendingTransaction = await this._transactionService.SubmitAsync(account : account, transactionContext : new TransactionContext(contextType: @"GAMEROUND", game.GameRoundId.ToString()), input : input, cancellationToken : cancellationToken); } catch (TransactionWillAlwaysFailException exception) { this._logger.LogError(new EventId(exception.HResult), exception: exception, $"{networkBlockHeader.Network.Name}: Failed to start game {game.GameRoundId}: {exception.Message}"); await this._gameRoundDataManager.MarkAsBrokenAsync(gameRoundId : game.GameRoundId, closingBlockNumber : networkBlockHeader.Number, exceptionMessage : exception.Message); await this._gameStatisticsPublisher.GameRoundBrokenAsync(network : account.Network, gameRoundId : game.GameRoundId); return; } this._logger.LogInformation($"{pendingTransaction.Network.Name}: Created game {game.GameRoundId}: tx {pendingTransaction.TransactionHash}"); }
private async Task EndGameRoundAsync(INetworkBlockHeader blockHeader, GameRound gameRound, CancellationToken cancellationToken) { await using (IObjectLock <GameRoundId>?gameRoundLock = await this._gameRoundLockManager.TakeLockAsync(gameRound.GameRoundId)) { if (gameRoundLock == null) { // something else has the game round locked this._logger.LogInformation($"{gameRound.Network.Name}: could not get lock for {gameRound.GameRoundId}"); return; } try { INetworkSigningAccount signingAccount = this._ethereumAccountManager.GetAccount(new NetworkAccount(network: gameRound.Network, address: gameRound.CreatedByAccount)); this._logger.LogInformation($"{gameRound.Network.Name}: End using game round: {gameRound.GameRoundId}"); await this._gameManager.EndGameAsync(account : signingAccount, gameRoundId : gameRound.GameRoundId, networkBlockHeader : blockHeader, cancellationToken : cancellationToken); } catch (Exception exception) { this._logger.LogError(new EventId(exception.HResult), exception: exception, $"{gameRound.Network.Name}: Failed to end game {gameRound.GameRoundId}: {exception.Message}"); } } }
/// <inheritdoc /> public async Task TryToStartGameAsync(INetworkSigningAccount networkSigningAccount, ContractAddress gameContract, INetworkBlockHeader blockHeader, CancellationToken cancellationToken) { if (!this._contractInfo.Addresses.TryGetValue(key: networkSigningAccount.Network, out ContractAddress? gameManagerContractAddress)) { // Contract not supported on the network return; } await using (IObjectLock <EthereumAddress>?gameManagerLock = await this._gameManagerLockManager.TakeLockAsync(gameManagerContractAddress)) { if (gameManagerLock == null) { // something else has the game manager locked so is probably doing something important with it return; } bool canGameBeStarted = await this._gameRoundDataManager.CanStartAGameAsync(gameManagerContract: gameManagerContractAddress, (int)GameRoundParameters.InterGameDelay.TotalSeconds); if (!canGameBeStarted) { // Has active games don't start a new one return; } this._logger.LogInformation($"{blockHeader.Network.Name}: Starting new game of game contract {gameContract} using game manager: {gameManagerContractAddress}"); await this._gameManager.StartGameAsync(account : networkSigningAccount, gameContract : gameContract, networkBlockHeader : blockHeader, cancellationToken : cancellationToken); } }
private Task <PendingTransaction> IssueEthereumAndFunAsync(INetworkSigningAccount networkSigningAccount, TransactionContext context, ITransactionExecutorFactory transactionExecutorFactory) { return(this._contractInfo.SubmitTransactionAsync(transactionExecutorFactory: transactionExecutorFactory, account: networkSigningAccount, priority: TransactionPriority.NORMAL, input: new DistributeTokenAndEthInput(recipient: this.Recipient.Address, ethAmount: this.EthToIssue, tokenAmount: this.TokenToIssue), context: context, cancellationToken: CancellationToken.None)); }
/// <inheritdoc /> public async Task <FaucetDrip> OpenAsync(IPAddress ipAddress, INetworkAccount recipient, INetworkBlockHeader networkBlockHeader, CancellationToken cancellationToken) { const string sourceName = @"Test Network Faucet"; if (!this._faucetContract.Addresses.TryGetValue(key: recipient.Network, out ContractAddress? faucetContractAddress)) { this._logger.LogCritical($"{recipient.Network.Name}: Cannot issue {recipient.Network.NativeCurrency} from faucet. Faucet not available on network"); throw new InsufficientTokenException(); } INetworkSigningAccount networkSigningAccount = this._ethereumAccountManager.GetAccount(network: recipient.Network); (EthereumAmount sourceAccountBalance, EthereumAmount recipientEthBalance) = await this.GetNativeCurrencyBalancesAsync( recipient : recipient, faucetContractAddress : faucetContractAddress, networkBlockHeader : networkBlockHeader, cancellationToken : cancellationToken); (Token sourceBalanceForToken, Token recipientBalanceForToken) = await this.GetTokenBalancesAsync(recipient : recipient, networkBlockHeader : networkBlockHeader, faucetContractAddress : faucetContractAddress, cancellationToken : cancellationToken); (EthereumAmount nativeCurrencyAmount, Token tokenAmount) = this.CalculateFundsToIssue(recipient: recipient, recipientNativeCurrencyBalance: recipientEthBalance, recipientTokenBalance: recipientBalanceForToken, faucetNativeCurrencyBalance: sourceAccountBalance, faucetTokenBalance: sourceBalanceForToken); if (!await this.IsAllowedToIssueFromFaucetAsync(ipAddress: ipAddress, recipientAddress: recipient.Address)) { throw new TooFrequentTokenException(); } Issuance issuance = new(recipient : recipient, ethToIssue : nativeCurrencyAmount, tokenToIssue : tokenAmount, sourceAccountBalance : sourceAccountBalance, sourceTokenBalance : sourceBalanceForToken, sourceName : sourceName, new NetworkContract(network : recipient.Network, contractAddress : faucetContractAddress), contractInfo : this._faucetContract); try { PendingTransaction tx = await this.IssueFundsAsync(networkSigningAccount : networkSigningAccount, issuance : issuance, new TransactionContext(contextType : WellKnownContracts.Faucet, recipient.Address.ToString())); await this.RecordSuccessfulFaucetDripAsync(recipient : recipient, nativeCurrencyAmount : nativeCurrencyAmount, tokenAmount : tokenAmount, ipAddress : ipAddress); return(new FaucetDrip(transaction: tx, ethAmount: nativeCurrencyAmount, tokenAmount: tokenAmount)); } catch (TransactionWillAlwaysFailException exception) { this._logger.LogCritical(new EventId(exception.HResult), exception: exception, $"{issuance.Recipient.Network.Name}: Cannot issue {this._tokenContract.Symbol} from faucet: {exception.Message}"); throw new InsufficientTokenException(message: "Could not request funds from faucet", innerException: exception); } }
public Task <PendingTransaction> SendFundsAsync(INetworkSigningAccount networkSigningAccount, TransactionContext context, ITransactionExecutorFactory transactionExecutorFactory) { if (this.EthToIssue != EthereumAmount.Zero && this.TokenToIssue != Token.Zero) { return(this.IssueEthereumAndFunAsync(networkSigningAccount: networkSigningAccount, context: context, transactionExecutorFactory: transactionExecutorFactory)); } if (this.TokenToIssue != Token.Zero) { return(this.IssueFunAsync(networkSigningAccount: networkSigningAccount, context: context, transactionExecutorFactory: transactionExecutorFactory)); } return(this.IssueEthereumAsync(networkSigningAccount: networkSigningAccount, context: context, transactionExecutorFactory: transactionExecutorFactory)); }
/// <inheritdoc /> public async Task StartGameAsync(INetworkSigningAccount account, ContractAddress gameContract, INetworkBlockHeader networkBlockHeader, CancellationToken cancellationToken) { if (!this._lowBalanceWatcher.DoesAccountHaveEnoughBalance(account)) { this._logger.LogWarning($"{account.Network.Name}: There was no enough {account.Network.NativeCurrency} for house address {account.Address} to create a game"); return; } if (!this._gameManager.Addresses.TryGetValue(key: account.Network, out ContractAddress? gameManagerContract)) { return; } GameRoundId gameRoundId = new(bytes : this._randomSource.Generate(count : GameRoundId.RequiredByteLength)); // The commit that's public is generated by using the one way hash from the reveal. Seed seedReveal = new(bytes : this._randomSource.Generate(count : Seed.RequiredByteLength)); Seed seedCommit = new(this._hasher.Hash(seedReveal.ToSpan())); StartGameRoundInput input = new(roundId : gameRoundId, gameAddress : gameContract, entropyCommit : seedCommit); PendingTransaction pendingTransaction = await this._transactionService.SubmitAsync(account : account, transactionContext : new TransactionContext(contextType: @"GAMEROUND", gameRoundId.ToString()), input : input, cancellationToken : cancellationToken); this._logger.LogInformation($"{pendingTransaction.Network.Name}: Created game {gameRoundId} tx {pendingTransaction.TransactionHash}"); await this._gameRoundDataManager.SaveStartRoundAsync(gameRoundId : gameRoundId, createdByAccount : account.Address, network : account.Network, gameContract : gameContract, gameManagerContract : gameManagerContract, seedCommit : seedCommit, seedReveal : seedReveal, roundDuration : GameRoundParameters.RoundDuration, bettingCloseDuration : GameRoundParameters.BettingCloseDuration, roundTimeoutDuration : GameRoundParameters.RoundTimeoutDuration, blockNumberCreated : networkBlockHeader.Number, transactionHash : pendingTransaction.TransactionHash); await this._gameStatisticsPublisher.GameRoundStartingAsync(network : account.Network, gameRoundId : gameRoundId, transactionHash : pendingTransaction.TransactionHash); }
/// <inheritdoc /> public async Task <PendingTransaction> SubmitAsync <TTransactionInput>(INetworkSigningAccount account, TTransactionInput input, TransactionContext transactionContext, CancellationToken cancellationToken) where TTransactionInput : TransactionParameters { this._logger.LogInformation($"{account.Network.Name}: Submit transaction: {typeof(TTransactionInput)}"); PendingTransaction transaction = await this._contractInfo.SubmitTransactionAsync(transactionExecutorFactory : this._transactionExecutorFactory, account : account, input : input, amountToSend : EthereumAmount.Zero, priority : TransactionPriority.NORMAL, context : transactionContext, cancellationToken : cancellationToken); this._logger.LogInformation($"{account.Network.Name}: Transaction submitted: {typeof(TTransactionInput)}, hash: {transaction.TransactionHash}"); return(transaction); }
/// <inheritdoc /> public async Task EndGameAsync(INetworkSigningAccount account, GameRoundId gameRoundId, INetworkBlockHeader networkBlockHeader, CancellationToken cancellationToken) { GameRound?game = await this._gameRoundDataManager.GetAsync(gameRoundId); if (game == null) { throw new NotSupportedException(); } PendingTransaction pendingTransaction; try { EndGameRoundInput input = new(roundId : gameRoundId, entropyReveal : game.SeedReveal); pendingTransaction = await this._transactionService.SubmitAsync(account : account, transactionContext : new TransactionContext(contextType: @"GAMEROUND", gameRoundId.ToString()), input : input, cancellationToken : cancellationToken); } catch (TransactionWillAlwaysFailException exception) { this._logger.LogError(new EventId(exception.HResult), exception: exception, $"{account.Network.Name}: Failed to end game {gameRoundId}: {exception.Message}"); await this._gameRoundDataManager.MarkAsBrokenAsync(gameRoundId : gameRoundId, closingBlockNumber : networkBlockHeader.Number, exceptionMessage : exception.Message); await this._gameStatisticsPublisher.GameRoundBrokenAsync(network : account.Network, gameRoundId : gameRoundId); return; } this._logger.LogInformation($"{account.Network.Name}: Ending game {gameRoundId}: tx {pendingTransaction.TransactionHash}"); await this._gameRoundDataManager.BeginCompleteAsync(gameRoundId : gameRoundId, blockNumberCreated : networkBlockHeader.Number, transactionHash : pendingTransaction.TransactionHash); await this._gameStatisticsPublisher.GameRoundEndingAsync(network : account.Network, gameRoundId : gameRoundId, transactionHash : pendingTransaction.TransactionHash, seedReveal : game.SeedReveal); }
private async Task FixPendingGameAsync(INetworkBlockHeader blockHeader, GameRound game, CancellationToken cancellationToken) { this._logger.LogWarning($"{blockHeader.Network.Name}: {game.GameRoundId} - Needs fixing to start"); bool handled = await this.AttemptToResolveEventAsync <StartGameRoundEventHandler, StartGameRoundEvent, StartGameRoundEventOutput>( blockHeader : blockHeader, gameRoundId : game.GameRoundId, cancellationToken : cancellationToken); if (!handled) { try { INetworkSigningAccount account = this._ethereumAccountManager.GetAccount(new NetworkAccount(network: blockHeader.Network, address: game.CreatedByAccount)); await this._gameManager.StartGameAsync(account : account, game : game, networkBlockHeader : blockHeader, cancellationToken : cancellationToken); } catch { await this._gameRoundDataManager.MarkAsBrokenAsync(gameRoundId : game.GameRoundId, closingBlockNumber : blockHeader.Number, exceptionMessage : "Did not start"); } } }
/// <inheritdoc /> public Task StartGameAsync(EthereumNetwork network, ContractAddress gameContract, INetworkBlockHeader blockHeader, CancellationToken cancellationToken) { INetworkSigningAccount account = this._ethereumAccountManager.GetAccount(network); return(this.TryToStartGameAsync(networkSigningAccount: account, gameContract: gameContract, blockHeader: blockHeader, cancellationToken: cancellationToken)); }
/// <inheritdoc /> public async Task <FaucetDrip> OpenAsync(IPAddress ipAddress, INetworkAccount recipient, INetworkBlockHeader networkBlockHeader, CancellationToken cancellationToken) { const string sourceName = @"Test Network Faucet"; // ETH & FUN source (Faucet account - no need to be a signing account) if (!this._faucetContract.Addresses.TryGetValue(key: recipient.Network, out ContractAddress? contractAddress)) { this._logger.LogCritical($"{recipient.Network.Name}: Cannot issue ETH from faucet. Faucet not available on network"); throw new InsufficientTokenException(); } INetworkSigningAccount networkSigningAccount = this._ethereumAccountManager.GetAccount(network: recipient.Network); NetworkContract fundsSourceContract = new(network : recipient.Network, contractAddress : contractAddress); IReadOnlyList <EthereumAddress> networkAccounts = new EthereumAddress[] { contractAddress, recipient.Address }; IReadOnlyDictionary <EthereumAddress, EthereumAmount> accountBalances = await this._ethereumAccountBalanceSource.GetAccountBalancesAsync(networkAccounts : networkAccounts, networkBlockHeader : networkBlockHeader, cancellationToken : CancellationToken.None); // get the SOURCE account's balance if (!accountBalances.TryGetValue(key: contractAddress, out EthereumAmount? sourceAccountBalance)) { this._logger.LogCritical($"{recipient.Network.Name}: Could not retrieve balance for {networkSigningAccount.Address}"); throw new InsufficientTokenException(); } // get the account's current ETH balance if (!accountBalances.TryGetValue(key: recipient.Address, out EthereumAmount? recipientEthBalance)) { this._logger.LogCritical($"{recipient.Network.Name}: Could not retrieve balance for {recipient.Address}"); throw new InsufficientTokenException(); } IReadOnlyDictionary <EthereumAddress, Erc20TokenBalance> balances = await this._ethereumAccountBalanceSource.GetErc20TokenBalancesAsync( addresses : networkAccounts, networkBlockHeader : networkBlockHeader, tokenContract : this._tokenContract, cancellationToken : cancellationToken); // get the SOURCE account's current FUN balance if (!balances.TryGetValue(key: fundsSourceContract.Address, out Erc20TokenBalance sourceTokenBalance)) { this._logger.LogCritical($"{recipient.Network.Name}: Could not retrieve balance for {fundsSourceContract.Address}"); throw new InsufficientTokenException(); } Token sourceBalanceForToken = new(sourceTokenBalance); // get the recipient account's current FUN balance if (!balances.TryGetValue(key: recipient.Address, out Erc20TokenBalance recipientTokenBalance)) { this._logger.LogCritical($"{recipient.Network.Name}: Could not retrieve balance for {recipient.Address}"); throw new InsufficientTokenException(); } Token recipientBalanceForToken = new(recipientTokenBalance); bool giveToken = recipientBalanceForToken < this._maximumRecipientTokenBalance; bool giveEth = recipientEthBalance < this._maximumRecipientEthBalance; this._logger.LogInformation( $"{recipient.Network.Name}: Source: {sourceAccountBalance.ToFormattedUnitWithSymbol()} {sourceBalanceForToken.ToFormattedUnitWithSymbol()} Recipient: {recipientEthBalance.ToFormattedUnitWithSymbol()} {recipientBalanceForToken.ToFormattedUnitWithSymbol()} Issue: ETH: {giveEth} FUN: {giveToken} Max Eth: {this._maximumRecipientEthBalance.ToFormattedUnitWithSymbol()} Max FUN: {this._maximumRecipientTokenBalance.ToFormattedUnitWithSymbol()}"); if (!giveToken && !giveEth) { if (this._executionEnvironment.IsDevelopmentOrTest()) { this._logger.LogWarning($"{recipient.Network.Name}: Could not issue eth to {recipient.Address} - Recipient balance > max"); } throw new TooMuchTokenException(); } if (!await this.IsAllowedToIssueFromFaucetAsync(ipAddress: ipAddress, recipientAddress: recipient.Address)) { throw new TooFrequentTokenException(); } EthereumAmount ethAmount = giveEth ? this.CalculateAmountOfEthToIssueFromFaucet(recipientEthBalance) : EthereumAmount.Zero; Token tokenAmount = giveToken ? this.CalculateAmountOfFunToIssueFromFaucet(recipientBalanceForToken) : Token.Zero; Issuance issuance = new(recipient : recipient, ethToIssue : ethAmount, tokenToIssue : tokenAmount, sourceAccountBalance : sourceAccountBalance, sourceTokenBalance : sourceBalanceForToken, sourceName : sourceName, fundsSourceContract : fundsSourceContract, contractInfo : this._faucetContract); try { PendingTransaction tx = await this.IssueFundsAsync(networkSigningAccount : networkSigningAccount, issuance : issuance, new TransactionContext(contextType : WellKnownContracts.Faucet, recipient.Address.ToString())); return(new FaucetDrip(transaction: tx, ethAmount: ethAmount, tokenAmount: tokenAmount)); } catch (TransactionWillAlwaysFailException exception) { this._logger.LogCritical(new EventId(exception.HResult), exception: exception, $"{issuance.Recipient.Network.Name}: Cannot issue FUN from faucet: {exception.Message}"); throw new InsufficientTokenException(message: "Could not request fun from faucet", innerException: exception); } }