/// <summary> /// Creates a new child context based on the current context instance. /// Includes an optional configuration method that can be immediately /// applied to the new context. This method is used internally to create /// contexts for cloned clients and network method calls having custom /// configuration callbacks. /// </summary> private GossipContextStack CreateChildContext(Action <IContext>?configure) { var context = new GossipContextStack(_context); configure?.Invoke(context); return(context); }
internal TransactionBody(GossipContextStack context, TransactionID transactionId) { OnConstruction(); TransactionID = transactionId; NodeAccountID = new AccountID(RequireInContext.Gateway(context)); TransactionFee = (ulong)context.FeeLimit; TransactionValidDuration = new Proto.Duration(context.TransactionDuration); Memo = context.Memo ?? ""; }
/// <summary> /// Internal Helper function used to wait for conesnsus regardless of the reported /// transaction outcome. We do not know if the transaction in question has come /// to consensus so we need to get the receipt first (and wait if necessary). /// The Receipt status returned does notmatter in this case. /// We may be retrieving a failed record (the status would not equal OK). private async Task WaitForConsensusReceipt(GossipContextStack context, TransactionID transactionId) { var query = new Query { TransactionGetReceipt = new TransactionGetReceiptQuery { TransactionID = transactionId } }; await Transactions.ExecuteNetworkRequestWithRetryAsync(context, query, query.InstantiateNetworkRequestMethod, shouldRetry);
private static Action <IMessage> InstantiateOnSendingRequestHandler(this GossipContextStack context) { var handlers = context.GetAll <Action <IMessage> >(nameof(context.OnSendingRequest)).Where(h => h != null).ToArray(); if (handlers.Length > 0) { return((IMessage request) => ExecuteHandlers(handlers, request)); } else { return(NoOp); }
internal static TransactionID CreateTransactionID(this GossipContextStack context, Address payer) { var(seconds, nanos) = Epoch.UniqueSecondsAndNanos(context.AdjustForLocalClockDrift); return(new TransactionID { AccountID = new AccountID(payer), TransactionValidStart = new Proto.Timestamp { Seconds = seconds, Nanos = nanos } }); }
/// <summary> /// Internal implementation of client creation. Accounts for newly created /// clients and cloning of clients alike. /// </summary> /// <param name="configure"> /// The optional <see cref="IContext"/> callback method, passed in from public /// instantiation or a <see cref="Client.Clone(Action{IContext})"/> method call. /// </param> /// <param name="parent"> /// The parent <see cref="GossipContextStack"/> if this creation is a result of a /// <see cref="Client.Clone(Action{IContext})"/> method call. /// </param> private Client(Action <IContext>?configure, GossipContextStack?parent) { if (parent is null) { // Create a Context with System Defaults // that are unreachable and can't be "Reset". parent = new GossipContextStack(null) { FeeLimit = 2_900_000_000, TransactionDuration = TimeSpan.FromSeconds(120), RetryCount = 5, RetryDelay = TimeSpan.FromMilliseconds(200), AdjustForLocalClockDrift = false }; } _context = new GossipContextStack(parent); configure?.Invoke(_context); }
internal static ISignatory GatherSignatories(this GossipContextStack context, params Signatory?[] extraSignatories) { var signatories = new List <Signatory>(1 + extraSignatories.Length); var contextSignatory = context.Signatory; if (contextSignatory is not null) { signatories.Add(contextSignatory); } foreach (var extraSignatory in extraSignatories) { if (extraSignatory is not null) { signatories.Add(extraSignatory); } } return(signatories.Count == 1 ? signatories[0] : new Signatory(signatories.ToArray())); }
/// <summary> /// Internal Helper function to retrieve receipt record provided by /// the network following network consensus regarding a query or transaction. /// </summary> private async Task <Proto.TransactionReceipt> GetReceiptAsync(GossipContextStack context, TransactionID transactionId) { var query = new Query { TransactionGetReceipt = new TransactionGetReceiptQuery { TransactionID = transactionId } }; var response = await Transactions.ExecuteNetworkRequestWithRetryAsync(context, query, getServerMethod, shouldRetry); var responseCode = response.TransactionGetReceipt.Header.NodeTransactionPrecheckCode; switch (responseCode) { case ResponseCodeEnum.Ok: break; case ResponseCodeEnum.Busy: throw new ConsensusException("Network failed to respond to request for a transaction receipt, it is too busy. It is possible the network may still reach concensus for this transaction.", Protobuf.FromTransactionId(transactionId), (ResponseCode)responseCode); case ResponseCodeEnum.Unknown: case ResponseCodeEnum.ReceiptNotFound: throw new TransactionException($"Network failed to return a transaction receipt, Status Code Returned: {responseCode}", Protobuf.FromTransactionId(transactionId), (ResponseCode)responseCode); } var status = response.TransactionGetReceipt.Receipt.Status; switch (status) { case ResponseCodeEnum.Unknown: throw new ConsensusException("Network failed to reach concensus within the configured retry time window, It is possible the network may still reach concensus for this transaction.", Protobuf.FromTransactionId(transactionId), (ResponseCode)status); case ResponseCodeEnum.TransactionExpired: throw new ConsensusException("Network failed to reach concensus before transaction request expired.", Protobuf.FromTransactionId(transactionId), (ResponseCode)status); case ResponseCodeEnum.RecordNotFound: throw new ConsensusException("Network failed to find a receipt for given transaction.", Protobuf.FromTransactionId(transactionId), (ResponseCode)status); default: return(response.TransactionGetReceipt.Receipt); }
internal static TransactionID GetOrCreateTransactionID(this GossipContextStack context) { var preExistingTransaction = context.Transaction; if (preExistingTransaction is null) { var payer = context.Payer; if (payer is null) { throw new InvalidOperationException("The Payer address has not been configured. Please check that 'Payer' is set in the Client context."); } return(CreateTransactionID(context, payer)); } else if (preExistingTransaction.Pending) { throw new ArgumentException("Can not set the context's Transaction ID's Pending field of a transaction to true.", nameof(context.Transaction)); } else { return(new TransactionID(preExistingTransaction)); } }
internal static TransactionID GetOrCreateTransactionID(GossipContextStack context) { var preExistingTransaction = context.Transaction; if (preExistingTransaction is null) { var(seconds, nanos) = Epoch.UniqueSecondsAndNanos(context.AdjustForLocalClockDrift); return(new TransactionID { AccountID = new AccountID(RequireInContext.Payer(context)), TransactionValidStart = new Proto.Timestamp { Seconds = seconds, Nanos = nanos } }); } else { return(new TransactionID(preExistingTransaction)); } }
/// <summary> /// Internal Helper function used to wait for conesnsus regardless of the reported /// transaction outcome. We do not know if the transaction in question has come /// to consensus so we need to get the receipt first (and wait if necessary). /// The Receipt status returned does notmatter in this case. /// We may be retrieving a failed record (the status would not equal OK). private async Task WaitForConsensusReceipt(GossipContextStack context, TransactionID transactionId) { var query = new TransactionGetReceiptQuery(transactionId, false, false) as INetworkQuery; await context.ExecuteNetworkRequestWithRetryAsync(query.CreateEnvelope(), query.InstantiateNetworkRequestMethod, shouldRetry).ConfigureAwait(false);
internal static async Task <TResponse> ExecuteNetworkRequestWithRetryAsync <TRequest, TResponse>(this GossipContextStack context, TRequest request, Func <Channel, Func <TRequest, Metadata?, DateTime?, CancellationToken, AsyncUnaryCall <TResponse> > > instantiateRequestMethod, Func <TResponse, bool> shouldRetryRequest) where TRequest : IMessage where TResponse : IMessage { try { var retryCount = 0; var maxRetries = context.RetryCount; var retryDelay = context.RetryDelay; var callOnSendingHandlers = InstantiateOnSendingRequestHandler(context); var callOnResponseReceivedHandlers = InstantiateOnResponseReceivedHandler(context); var sendRequest = instantiateRequestMethod(context.GetChannel()); callOnSendingHandlers(request); for (; retryCount < maxRetries; retryCount++) { try { var tenativeResponse = await sendRequest(request, null, null, default); callOnResponseReceivedHandlers(retryCount, tenativeResponse); if (!shouldRetryRequest(tenativeResponse)) { return(tenativeResponse); } } catch (RpcException rpcex) when(rpcex.StatusCode == StatusCode.Unavailable || rpcex.StatusCode == StatusCode.Unknown) { var channel = context.GetChannel(); var message = channel.State == ChannelState.Connecting ? $"Unable to communicate with network node {channel.ResolvedTarget}, it may be down or not reachable." : $"Unable to communicate with network node {channel.ResolvedTarget}: {rpcex.Status}"; callOnResponseReceivedHandlers(retryCount, new StringValue { Value = message }); // If this was a transaction, it may have actully successfully been processed, in which case // the receipt will already be in the system. Check to see if it is there. if (request is Transaction transaction) { await Task.Delay(retryDelay *retryCount).ConfigureAwait(false); var receiptResponse = await CheckForReceipt(transaction).ConfigureAwait(false); callOnResponseReceivedHandlers(retryCount, receiptResponse); if (receiptResponse.NodeTransactionPrecheckCode != ResponseCodeEnum.ReceiptNotFound && receiptResponse is TResponse tenativeResponse && !shouldRetryRequest(tenativeResponse)) { return(tenativeResponse); } } } await Task.Delay(retryDelay *(retryCount + 1)).ConfigureAwait(false); } var finalResponse = await sendRequest(request, null, null, default); callOnResponseReceivedHandlers(maxRetries, finalResponse); return(finalResponse); async Task <TransactionResponse> CheckForReceipt(Transaction transaction) { // In the case we submitted a transaction, the receipt may actually // be in the system. Unpacking the transaction is not necessarily efficient, // however we are here due to edge case error condition due to poor network // performance or grpc connection issues already. if (transaction != null) { var signedTransaction = SignedTransaction.Parser.ParseFrom(transaction.SignedTransactionBytes); var transactionBody = TransactionBody.Parser.ParseFrom(signedTransaction.BodyBytes); var transactionId = transactionBody.TransactionID; var query = new Query { TransactionGetReceipt = new TransactionGetReceiptQuery { TransactionID = transactionId } }; for (; retryCount < maxRetries; retryCount++) { try { var client = new CryptoService.CryptoServiceClient(context.GetChannel()); var receipt = await client.getTransactionReceiptsAsync(query); return(new TransactionResponse { NodeTransactionPrecheckCode = receipt.TransactionGetReceipt.Header.NodeTransactionPrecheckCode }); } catch (RpcException rpcex) when(rpcex.StatusCode == StatusCode.Unavailable) { var channel = context.GetChannel(); var message = channel.State == ChannelState.Connecting ? $"Unable to communicate with network node {channel.ResolvedTarget}, it may be down or not reachable." : $"Unable to communicate with network node {channel.ResolvedTarget}: {rpcex.Status}"; callOnResponseReceivedHandlers(retryCount, new StringValue { Value = message }); } await Task.Delay(retryDelay *(retryCount + 1)).ConfigureAwait(false); } } return(new TransactionResponse { NodeTransactionPrecheckCode = ResponseCodeEnum.Unknown }); } } catch (RpcException rpcex) { var channel = context.GetChannel(); var message = rpcex.StatusCode == StatusCode.Unavailable && channel.State == ChannelState.Connecting ? $"Unable to communicate with network node {channel.ResolvedTarget}, it may be down or not reachable." : $"Unable to communicate with network node {channel.ResolvedTarget}: {rpcex.Status}"; throw new PrecheckException(message, TxId.None, ResponseCode.RpcError, 0, rpcex); } }
internal static Task <TResponse> ExecuteSignedRequestWithRetryImplementationAsync <TRequest, TResponse>(this GossipContextStack context, TRequest request, Func <Channel, Func <TRequest, Metadata?, DateTime?, CancellationToken, AsyncUnaryCall <TResponse> > > instantiateRequestMethod, Func <TResponse, ResponseCodeEnum> getResponseCode) where TRequest : IMessage where TResponse : IMessage { var trackTimeDrift = context.AdjustForLocalClockDrift && context.Transaction is null; var startingInstant = trackTimeDrift ? Epoch.UniqueClockNanos() : 0; return(ExecuteNetworkRequestWithRetryAsync(context, request, instantiateRequestMethod, shouldRetryRequest)); bool shouldRetryRequest(TResponse response) { var code = getResponseCode(response); if (trackTimeDrift && code == ResponseCodeEnum.InvalidTransactionStart) { Epoch.AddToClockDrift(Epoch.UniqueClockNanos() - startingInstant); } return (code == ResponseCodeEnum.Busy || code == ResponseCodeEnum.InvalidTransactionStart); } }
internal async Task<TransactionResponse> SignAndSubmitWithRetryAsync(ISignatory signatory, GossipContextStack context) { var request = await SignAsync(signatory, context.SignaturePrefixTrimLimit); return await Transactions.ExecuteSignedRequestWithRetryImplementationAsync(context, request, InstantiateNetworkRequestMethod, getResponseCode);
internal async Task<TransactionReceipt> SignAndExecuteWithRetryAsync(ISignatory signatory, GossipContextStack context) { var precheck = await SignAndSubmitWithRetryAsync(signatory, context); ValidateResult.PreCheck(TransactionID, precheck); var receipt = await Transactions.GetReceiptAsync(context, TransactionID); return receipt; }