public InteropCoordinationPayload(string requestId, int transactionId, string signature, DestinationChain targetChain) { this.requestId = requestId; this.transactionId = transactionId; this.signature = signature; this.DestinationChain = targetChain; }
public async Task <IActionResult> MultisigConfirmationsAsync(DestinationChain destinationChain, int transactionId) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); List <string> owners = await client.GetOwnersAsync().ConfigureAwait(false); var ownersConfirmed = new List <string>(); foreach (string multisig in owners) { bool confirmed = await client.AddressConfirmedTransactionAsync(transactionId, multisig).ConfigureAwait(false); if (confirmed) { ownersConfirmed.Add(multisig); } } return(this.Json(ownersConfirmed)); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> MultisigTransactionAsync(DestinationChain destinationChain, int transactionId, bool raw) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); if (raw) { return(this.Json(await client.GetRawMultisigTransactionAsync(transactionId).ConfigureAwait(false))); } TransactionDTO transaction = await client.GetMultisigTransactionAsync(transactionId).ConfigureAwait(false); var response = new TransactionResponseModel() { Destination = transaction.Destination, Value = transaction.Value.ToString(), Data = Encoders.Hex.EncodeData(transaction.Data), Executed = transaction.Executed }; return(this.Json(response)); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public IActionResult ChangeRequirement(DestinationChain destinationChain, int requirement, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); string data = client.EncodeChangeRequirementParams(requirement); ETHInteropSettings settings = this.interopSettings.GetSettingsByChain(destinationChain); // TODO: Maybe for convenience the gas price could come from the external API poller return(this.Json(client.SubmitTransactionAsync(settings.MultisigWalletAddress, 0, data).GetAwaiter().GetResult())); } catch (Exception e) { this.logger.Error("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
/// <inheritdoc /> public IETHClient GetClientForChain(DestinationChain chain) { if (!this.supportedChains.ContainsKey(chain)) { throw new NotImplementedException("Provided chain type not supported: " + chain); } return(this.supportedChains[chain]); }
/// <summary> /// Creates filters that the RPC interfaces uses to listen for events against the desired contract. /// In this case the filter is specifically listening for Transfer events emitted by the wrapped strax /// contracts deployed on supported chains. /// </summary> private async Task CreateEventFiltersIfRequired(DestinationChain targetChain, IETHClient chainClient) { if (this.eventFilterCreationRequired[DestinationChain.ETH]) { // The filter should only be set up once IBD completes. await chainClient.CreateTransferEventFilterAsync().ConfigureAwait(false); this.eventFilterCreationRequired[targetChain] = false; } }
/// <inheritdoc /> public bool IsChainSupportedAndEnabled(DestinationChain chain) { bool supported = this.IsChainSupported(chain); if (!supported) { return(false); } return(this.interopSettings.GetSettingsByChain(chain).InteropEnabled); }
public ETHInteropSettings GetSettingsByChain(DestinationChain chain) { switch (chain) { case DestinationChain.ETH: { return(this.ETHSettings); } case DestinationChain.BNB: { return(this.BNBSettings); } } throw new NotImplementedException("Provided chain type not supported: " + chain); }
/// <inheritdoc /> public IDeposit ExtractDepositFromTransaction(Transaction transaction, int blockHeight, uint256 blockHash) { // Coinbase transactions can't have deposits. if (transaction.IsCoinBase) { return(null); } // Deposits have a certain structure. if (transaction.Outputs.Count != ExpectedNumberOfOutputsNoChange && transaction.Outputs.Count != ExpectedNumberOfOutputsChange) { return(null); } var depositsToMultisig = transaction.Outputs.Where(output => output.ScriptPubKey == this.depositScript && output.Value >= FederatedPegSettings.CrossChainTransferMinimum).ToList(); if (!depositsToMultisig.Any()) { return(null); } // Check the common case first. bool conversionTransaction = false; DestinationChain targetChain = DestinationChain.STRAX; if (!this.opReturnDataReader.TryGetTargetAddress(transaction, out string targetAddress)) { byte[] opReturnBytes = OpReturnDataReader.SelectBytesContentFromOpReturn(transaction).FirstOrDefault(); if (opReturnBytes != null && InterFluxOpReturnEncoder.TryDecode(opReturnBytes, out int destinationChain, out targetAddress)) { targetChain = (DestinationChain)destinationChain; } else { return(null); } conversionTransaction = true; }
public async Task <IActionResult> BalanceAsync(DestinationChain destinationChain, string account) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); return(this.Json((await client.GetErc20BalanceAsync(account).ConfigureAwait(false)).ToString())); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public IActionResult ConfirmTransaction(DestinationChain destinationChain, int transactionId, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); return(this.Json(client.ConfirmTransactionAsync(transactionId).GetAwaiter().GetResult())); } catch (Exception e) { this.logger.Error("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> ConfirmTransactionAsync(DestinationChain destinationChain, int transactionId, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); // TODO: Maybe for convenience the gas price could come from the external API poller return(this.Json(await client.ConfirmTransactionAsync(transactionId, gasPrice).ConfigureAwait(false))); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
public async Task <IActionResult> RemoveOwnerAsync(DestinationChain destinationChain, string existingOwnerAddress, int gasPrice) { try { if (!this.ethCompatibleClientProvider.IsChainSupportedAndEnabled(destinationChain)) { return(this.Json($"{destinationChain} not enabled or supported!")); } IETHClient client = this.ethCompatibleClientProvider.GetClientForChain(destinationChain); string data = client.EncodeRemoveOwnerParams(existingOwnerAddress); ETHInteropSettings settings = this.interopSettings.GetSettingsByChain(destinationChain); // TODO: Maybe for convenience the gas price could come from the external API poller return(this.Json(await client.SubmitTransactionAsync(settings.MultisigWalletAddress, 0, data, gasPrice).ConfigureAwait(false))); } catch (Exception e) { this.logger.LogError("Exception occurred: {0}", e.ToString()); return(ErrorHelpers.BuildErrorResponse(HttpStatusCode.BadRequest, e.Message, e.ToString())); } }
private async Task BroadcastCoordinationAsync(string requestId, BigInteger transactionId, DestinationChain targetChain) { string signature = this.federationManager.CurrentFederationKey.SignMessage(requestId + ((int)transactionId)); await this.federatedPegBroadcaster.BroadcastAsync(new InteropCoordinationPayload(requestId, (int)transactionId, signature, targetChain)).ConfigureAwait(false); }
public static string Encode(DestinationChain destinationChain, string address) { return(InterFluxPrefix + (int)destinationChain + "_" + address); }
/// <inheritdoc /> public bool IsChainSupported(DestinationChain chain) { return(this.supportedChains.ContainsKey(chain)); }
/// <summary> /// Retrieves any Transfer events from the logs of the Wrapped Strax contract deployed on specified chain. /// Transfers with the zero (0x0000...) address as their destination can be considered to be burn transactions and are saved for processing as withdrawals on the mainchain. /// </summary> private async Task CheckForContractEventsAsync(DestinationChain targetChain, IETHClient chainClient) { await this.CreateEventFiltersIfRequired(targetChain, chainClient).ConfigureAwait(false); // Check for all Transfer events against the WrappedStrax contract since the last time we checked. // In future this could also poll for other events as the need arises. List <EventLog <TransferEventDTO> > transferEvents = await chainClient.GetTransferEventsForWrappedStraxAsync().ConfigureAwait(false); foreach (EventLog <TransferEventDTO> transferEvent in transferEvents) { // Will probably never be the case, but check anyway. if (string.IsNullOrWhiteSpace(transferEvent.Log.TransactionHash)) { continue; } // These could be mints or something else, either way ignore them. if (transferEvent.Event.From == ETHClient.ETHClient.ZeroAddress) { continue; } // Transfers can only be burns if they are made with the zero address as the destination. if (transferEvent.Event.To != ETHClient.ETHClient.ZeroAddress) { continue; } this.logger.LogInformation("Conversion burn transaction {0} received from contract events, sender {1}.", transferEvent.Log.TransactionHash, transferEvent.Event.From); if (this.conversionRequestRepository.Get(transferEvent.Log.TransactionHash) != null) { this.logger.LogInformation("Conversion burn transaction {0} already exists, ignoring.", transferEvent.Log.TransactionHash); continue; } if (transferEvent.Event.Value == BigInteger.Zero) { this.logger.LogInformation("Ignoring zero-valued burn transaction {0}.", transferEvent.Log.TransactionHash); continue; } if (transferEvent.Event.Value < BigInteger.Zero) { this.logger.LogInformation("Ignoring negative-valued burn transaction {0}.", transferEvent.Log.TransactionHash); continue; } this.logger.LogInformation("Conversion burn transaction {0} has value {1}.", transferEvent.Log.TransactionHash, transferEvent.Event.Value); // Look up the desired destination address for this account. string destinationAddress = await chainClient.GetDestinationAddressAsync(transferEvent.Event.From).ConfigureAwait(false); this.logger.LogInformation("Conversion burn transaction {0} has destination address {1}.", transferEvent.Log.TransactionHash, destinationAddress); try { // Validate that it is a mainchain address here before bothering to add it to the repository. BitcoinAddress.Create(destinationAddress, this.counterChainNetwork); } catch (Exception) { this.logger.LogWarning("Error validating destination address {0} for transaction {1}.", destinationAddress, transferEvent.Log.TransactionHash); continue; } // Schedule this transaction to be processed at the next block height that is divisible by 10. If the current block height is divisible by 10, add a further 10 to it. // In this way, burns will always be scheduled at a predictable future time across the multisig. // This is because we cannot predict exactly when each node is polling the Ethereum chain for events. ulong blockHeight = (ulong)this.chainIndexer.Tip.Height - ((ulong)this.chainIndexer.Tip.Height % 10) + 10; if (blockHeight <= 0) { blockHeight = 10; } this.conversionRequestRepository.Save(new ConversionRequest() { RequestId = transferEvent.Log.TransactionHash, RequestType = ConversionRequestType.Burn, Processed = false, RequestStatus = ConversionRequestStatus.Unprocessed, Amount = this.ConvertWeiToSatoshi(transferEvent.Event.Value), BlockHeight = (int)blockHeight, DestinationAddress = destinationAddress, DestinationChain = targetChain }); } }
public static ConversionRequestPayload Reply(string requestId, int transactionId, string signature, DestinationChain destinationChain) { return(new ConversionRequestPayload(requestId, transactionId, signature, destinationChain, false)); }
public Deposit(uint256 id, DepositRetrievalType retrievalType, Money amount, string targetAddress, DestinationChain targetChain, int blockNumber, uint256 blockHash) { this.Id = id; this.RetrievalType = retrievalType; this.Amount = amount; this.TargetAddress = targetAddress; this.TargetChain = targetChain; this.BlockNumber = blockNumber; this.BlockHash = blockHash; }
private ConversionRequestPayload(string requestId, int transactionId, string signature, DestinationChain destinationChain, bool isRequesting) { this.requestId = requestId; this.transactionId = transactionId; this.signature = signature; this.destinationChain = (int)destinationChain; this.isRequesting = isRequesting; }