Example #1
0
        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));
        }
Example #2
0
        /// <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}");
                }
            }
        }
Example #4
0
        /// <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);
            }
        }
Example #5
0
 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));
 }
Example #6
0
        /// <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);
            }
        }
Example #7
0
            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));
            }
Example #8
0
        /// <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);
        }
Example #10
0
        /// <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");
                }
            }
        }
Example #12
0
        /// <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);
            }
        }