public BigInteger GetActiveValidators() { return(_entryList.Count()); }
private BigInteger GetBlockHeight() { var hashList = new StorageList(BlockHeightListTag, this.Storage); return(hashList.Count()); }
public void Update() { try { if (this.platforms == null) { if (!Nexus.HasGenesis) { return; } var platforms = Nexus.GetPlatforms(Nexus.RootStorage); this.platforms = platforms.Select(x => Nexus.GetPlatformInfo(Nexus.RootStorage, x)).ToArray(); if (this.platforms.Length == 0) { Logger.Warning("No interop platforms found. Make sure that the Nexus was created correctly."); return; } if (IsPlatformSupported(SwapPlatformChain.Neo)) { InitSwapper(SwapPlatformChain.Neo, new NeoInterop(this, neoAPI, _interopBlocks[SwapPlatformChain.Neo], Settings.Oracle.NeoQuickSync)); } if (IsPlatformSupported(SwapPlatformChain.Ethereum)) { InitSwapper(SwapPlatformChain.Ethereum, new EthereumInterop(this, ethAPI, _interopBlocks[SwapPlatformChain.Ethereum], Nexus.GetPlatformTokenHashes(EthereumWallet.EthereumPlatform, Nexus.RootStorage).Select(x => x.ToString().Substring(0, 40)).ToArray(), Settings.Oracle.EthConfirmations)); } if (IsPlatformSupported(SwapPlatformChain.BSC) && Nexus.PlatformExists(Nexus.RootStorage, BSCWallet.BSCPlatform)) { InitSwapper(SwapPlatformChain.BSC, new BSCInterop(this, bscAPI, _interopBlocks[SwapPlatformChain.BSC], Nexus.GetPlatformTokenHashes(BSCWallet.BSCPlatform, Nexus.RootStorage).Select(x => x.ToString().Substring(0, 40)).ToArray(), Settings.Oracle.EthConfirmations)); } Logger.Message("Available swap addresses:"); foreach (var x in SwapAddresses) { Logger.Message("platform: " + x.Key + " address: " + string.Join(", ", x.Value)); } } if (this.platforms.Length == 0) { return; } else { if (_taskMap.Count == 0) { foreach (var platform in Settings.Oracle.SwapPlatforms) { if (platform.Chain != SwapPlatformChain.Phantasma) // TODO is this IF necessary? { _taskMap.Add(platform.Chain, null); } } } } lock (StateModificationLock) { var pendingList = new StorageList(PendingTag, this.Storage); int i = 0; var count = pendingList.Count(); while (i < count) { var settlement = pendingList.Get <PendingFee>(i); if (UpdatePendingSettle(pendingList, i)) { pendingList.RemoveAt(i); count--; } else { i++; } } } ProcessCompletedTasks(); for (var j = 0; j < _taskMap.Count; j++) { var platform = _taskMap.Keys.ElementAt(j); var task = _taskMap[platform]; if (task == null) { if (_swappers.TryGetValue(platform, out ChainSwapper finder)) { _taskMap[platform] = new Task <IEnumerable <PendingSwap> >(() => { return(finder.Update()); }); } } } // start new tasks foreach (var entry in _taskMap) { var task = entry.Value; if (task != null && task.Status.Equals(TaskStatus.Created)) { task.ContinueWith(t => { Console.WriteLine($"===> task {task.ToString()} failed"); }, TaskContinuationOptions.OnlyOnFaulted); task.Start(); } } } catch (Exception e) { var logMessage = "TokenSwapper.Update() exception caught:\n" + e.Message; var inner = e.InnerException; while (inner != null) { logMessage += "\n---> " + inner.Message + "\n\n" + inner.StackTrace; inner = inner.InnerException; } logMessage += "\n\n" + e.StackTrace; Logger.Error(logMessage); } }
public void Queue(Address from, string symbol, BigInteger tableID) { Runtime.Expect(IsWitness(from), "invalid witness"); Runtime.Expect(tableID == 0, "invalid table"); var token = Runtime.Nexus.GetTokenInfo(Nexus.StakingTokenSymbol); Runtime.Expect(symbol == token.Symbol, "invalid symbol"); Runtime.Expect(!_matchMap.ContainsKey <Address>(from), "already in match"); var balances = Runtime.Chain.GetTokenBalances(token.Symbol); var fee = GetTableFee(tableID); Runtime.Expect(Runtime.Nexus.TransferTokens(token.Symbol, this.Storage, balances, from, Runtime.Chain.Address, fee), "fee transfer failed"); int queueIndex = -1; var count = _queue.Count(); for (var i = 0; i < count; i++) { var temp = _queue.Get <CasinoQueue>(i); if (temp.table == tableID) { queueIndex = i; break; } } if (queueIndex >= 0) { var other = _queue.Get <CasinoQueue>(queueIndex); _matchCount++; _matchMap.Set <Address, BigInteger>(from, _matchCount); _matchMap.Set <Address, BigInteger>(other.player, _matchCount); Runtime.Notify(EventKind.CasinoTableStart, from, other.player); Runtime.Notify(EventKind.CasinoTableStart, other.player, from); var match = new CasinoMatch() { amount = fee * 2, host = other.player, opponent = from, matchTurn = 0, hostTurn = 0, opponentTurn = 0, timestamp = Runtime.Time, cards = new byte[52], }; for (int j = 0; j < 2; j++) { for (int i = 0; i < 2; i++) { var card = DrawCard(match.cards, (byte)(i + 1)); Runtime.Expect(card >= 0, "card draw failed"); Runtime.Notify(EventKind.CasinoTableCard, i == 0 ? match.host : match.opponent, i); } } _matchData.Set <BigInteger, CasinoMatch>(_matchCount, match); } else { var entry = new CasinoQueue() { player = from, amount = fee, symbol = symbol, table = tableID }; _queue.Add <CasinoQueue>(entry); Runtime.Notify(EventKind.CasinoTableQueued, from, tableID); } }
public void AddBlock(Block block, IEnumerable <Transaction> transactions, BigInteger minimumFee) { /*if (CurrentEpoch != null && CurrentEpoch.IsSlashed(Timestamp.Now)) * { * return false; * }*/ var lastBlockHash = GetLastBlockHash(); var lastBlock = GetBlockByHash(lastBlockHash); if (lastBlock != null) { if (lastBlock.Height != block.Height - 1) { throw new BlockGenerationException($"height of block should be {lastBlock.Height + 1}"); } if (block.PreviousHash != lastBlock.Hash) { throw new BlockGenerationException($"previous hash should be {lastBlock.PreviousHash}"); } } var inputHashes = new HashSet <Hash>(transactions.Select(x => x.Hash)); foreach (var hash in block.TransactionHashes) { if (!inputHashes.Contains(hash)) { throw new BlockGenerationException($"missing in inputs transaction with hash {hash}"); } } var outputHashes = new HashSet <Hash>(block.TransactionHashes); foreach (var tx in transactions) { if (!outputHashes.Contains(tx.Hash)) { throw new BlockGenerationException($"missing in outputs transaction with hash {tx.Hash}"); } } // TODO avoid fetching this every time var expectedProtocol = Nexus.GetGovernanceValue(Nexus.RootStorage, Nexus.NexusProtocolVersionTag); if (block.Protocol != expectedProtocol) { throw new BlockGenerationException($"invalid protocol number {block.Protocol}, expected protocol {expectedProtocol}"); } foreach (var tx in transactions) { if (!tx.IsValid(this)) { throw new InvalidTransactionException(tx.Hash, $"invalid transaction with hash {tx.Hash}"); } } var changeSet = new StorageChangeSetContext(this.Storage); var oracle = Nexus.CreateOracleReader(); foreach (var tx in transactions) { byte[] result; try { if (ExecuteTransaction(tx, block.Timestamp, changeSet, block.Notify, oracle, minimumFee, out result)) { if (result != null) { block.SetResultForHash(tx.Hash, result); } } else { throw new InvalidTransactionException(tx.Hash, $"execution failed"); } } catch (Exception e) { if (e.InnerException != null) { e = e.InnerException; } throw new InvalidTransactionException(tx.Hash, e.Message); } } block.MergeOracle(oracle); var hashList = new StorageList(BlockHeightListTag, this.Storage); var expectedBlockHeight = hashList.Count() + 1; if (expectedBlockHeight != block.Height) { throw new ChainException("unexpected block height"); } // from here on, the block is accepted changeSet.Execute(); hashList.Add <Hash>(block.Hash); var blockMap = new StorageMap(BlockHashMapTag, this.Storage); blockMap.Set <Hash, Block>(block.Hash, block); var txMap = new StorageMap(TransactionHashMapTag, this.Storage); var txBlockMap = new StorageMap(TxBlockHashMapTag, this.Storage); foreach (Transaction tx in transactions) { txMap.Set <Hash, Transaction>(tx.Hash, tx); txBlockMap.Set <Hash, Hash>(tx.Hash, block.Hash); } var blockValidator = GetValidatorForBlock(block); if (blockValidator.IsNull) { throw new BlockGenerationException("no validator for this block"); } Nexus.PluginTriggerBlock(this, block); }
public void Update() { if (this.platforms == null) { if (!Nexus.HasGenesis) { return; } var platforms = Nexus.GetPlatforms(Nexus.RootStorage); this.platforms = platforms.Select(x => Nexus.GetPlatformInfo(Nexus.RootStorage, x)).ToArray(); if (this.platforms.Length == 0) { logger.Warning("No interop platforms found. Make sure that the Nexus was created correctly."); return; } _finders["neo"] = new NeoInterop(this, wifs["neo"], interopBlocks["neo"], neoscanAPI, logger); } if (this.platforms.Length == 0) { return; } var pendingList = new StorageList(PendingTag, this.Storage); int i = 0; var count = pendingList.Count(); while (i < count) { var settlement = pendingList.Get <PendingSettle>(i); if (UpdatePendingSettle(pendingList, i)) { pendingList.RemoveAt <PendingSettle>(i); count--; } else { i++; } } foreach (var finder in _finders.Values) { var swaps = finder.Update(); foreach (var swap in swaps) { if (_pendingSwaps.ContainsKey(swap.hash)) { continue; } logger.Message($"Detected {finder.PlatformName} swap: {swap.source} => {swap.destination}"); _pendingSwaps[swap.hash] = swap; MapSwap(swap.source, swap.hash); MapSwap(swap.destination, swap.hash); } } }
public void AddBlock(Block block, IEnumerable <Transaction> transactions, BigInteger minimumFee) { if (!block.IsSigned) { throw new BlockGenerationException($"block must be signed"); } StorageChangeSetContext changeSet; using (var m = new ProfileMarker("ValidateBlock")) changeSet = ValidateBlock(block, transactions, minimumFee); var unsignedBytes = block.ToByteArray(false); if (!block.Signature.Verify(unsignedBytes, block.Validator)) { throw new BlockGenerationException($"block signature does not match validator {block.Validator.Text}"); } var hashList = new StorageList(BlockHeightListTag, this.Storage); var expectedBlockHeight = hashList.Count() + 1; if (expectedBlockHeight != block.Height) { throw new ChainException("unexpected block height"); } // from here on, the block is accepted using (var m = new ProfileMarker("changeSet.Execute")) changeSet.Execute(); hashList.Add <Hash>(block.Hash); using (var m = new ProfileMarker("Compress")) { var blockMap = new StorageMap(BlockHashMapTag, this.Storage); var blockBytes = block.ToByteArray(true); blockBytes = CompressionUtils.Compress(blockBytes); blockMap.Set <Hash, byte[]>(block.Hash, blockBytes); var txMap = new StorageMap(TransactionHashMapTag, this.Storage); var txBlockMap = new StorageMap(TxBlockHashMapTag, this.Storage); foreach (Transaction tx in transactions) { var txBytes = tx.ToByteArray(true); txBytes = CompressionUtils.Compress(txBytes); txMap.Set <Hash, byte[]>(tx.Hash, txBytes); txBlockMap.Set <Hash, Hash>(tx.Hash, block.Hash); } } using (var m = new ProfileMarker("AddressBlockHashMapTag")) foreach (var transaction in transactions) { var addresses = new HashSet <Address>(); var events = block.GetEventsForTransaction(transaction.Hash); foreach (var evt in events) { if (evt.Address.IsSystem) { continue; } addresses.Add(evt.Address); } var addressTxMap = new StorageMap(AddressBlockHashMapTag, this.Storage); foreach (var address in addresses) { var addressList = addressTxMap.Get <Address, StorageList>(address); addressList.Add <Hash>(transaction.Hash); } } using (var m = new ProfileMarker("Nexus.PluginTriggerBlock")) Nexus.PluginTriggerBlock(this, block); }
public void SettleTransaction(Address from, string platform, Hash hash) { Runtime.Expect(platform != DomainSettings.PlatformName, "must be external platform"); Runtime.Expect(Runtime.PlatformExists(platform), "unsupported platform"); var platformInfo = Runtime.GetPlatform(platform); Runtime.Expect(Runtime.IsWitness(from), "invalid witness"); Runtime.Expect(from.IsUser, "must be user address"); var chainHashes = _hashes.Get <string, StorageSet>(platform); Runtime.Expect(!chainHashes.Contains <Hash>(hash), "hash already seen"); var interopTx = Runtime.ReadTransactionFromOracle(platform, DomainSettings.RootChainName, hash); Runtime.Expect(interopTx.Platform == platform, "unxpected platform name"); Runtime.Expect(interopTx.Hash == hash, "unxpected hash"); int swapCount = 0; foreach (var evt in interopTx.Events) { if (evt.Kind == EventKind.TokenReceive && evt.Address == platformInfo.Address) { Runtime.Expect(!evt.Address.IsNull, "invalid source address"); var transfer = evt.GetContent <TokenEventData>(); Runtime.Expect(transfer.value > 0, "amount must be positive and greater than zero"); Runtime.Expect(Runtime.TokenExists(transfer.symbol), "invalid token"); var token = this.Runtime.GetToken(transfer.symbol); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Transferable), "token must be transferable"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.External), "token must be external"); Address destination = Address.Null; foreach (var otherEvt in interopTx.Events) { if (otherEvt.Kind == EventKind.TokenSend) { var otherTransfer = otherEvt.GetContent <TokenEventData>(); if (otherTransfer.chainAddress == transfer.chainAddress && otherTransfer.symbol == transfer.symbol) { destination = otherEvt.Address; break; } } } if (destination.IsInterop) { destination = GetLink(destination, DomainSettings.PlatformName); } else { Runtime.Expect(destination.IsUser, "invalid destination address"); } Runtime.Expect(Runtime.TransferTokens(transfer.symbol, platformInfo.Address, destination, transfer.value), "mint failed"); Runtime.Notify(EventKind.TokenReceive, destination, new TokenEventData() { chainAddress = platformInfo.Address, value = transfer.value, symbol = transfer.symbol }); swapCount++; break; } if (evt.Kind == EventKind.TokenClaim) { var destination = evt.Address; Runtime.Expect(!destination.IsNull, "invalid destination"); var transfer = evt.GetContent <TokenEventData>(); Runtime.Expect(transfer.value > 0, "amount must be positive and greater than zero"); Runtime.Expect(Runtime.TokenExists(transfer.symbol), "invalid token"); var token = this.Runtime.GetToken(transfer.symbol); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Transferable), "token must be transferable"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.External), "token must be external"); var count = _withdraws.Count(); var index = -1; for (int i = 0; i < count; i++) { var entry = _withdraws.Get <InteropWithdraw>(i); if (entry.destination == destination && entry.transferAmount == transfer.value && entry.transferSymbol == transfer.symbol) { index = i; break; } } Runtime.Expect(index >= 0, "invalid withdraw, possible leak found"); var withdraw = _withdraws.Get <InteropWithdraw>(index); Runtime.Expect(withdraw.broker == from, "invalid broker"); _withdraws.RemoveAt <InteropWithdraw>(index); Runtime.Expect(Runtime.TransferTokens(withdraw.feeSymbol, this.Address, from, withdraw.feeAmount), "fee payment failed"); Runtime.Notify(EventKind.TokenReceive, from, new TokenEventData() { chainAddress = this.Runtime.Chain.Address, value = withdraw.feeAmount, symbol = withdraw.feeSymbol }); Runtime.Notify(EventKind.TokenReceive, destination, new TokenEventData() { chainAddress = platformInfo.Address, value = withdraw.transferAmount, symbol = withdraw.transferSymbol }); swapCount++; break; } } Runtime.Expect(swapCount > 0, "nothing to settle"); chainHashes.Add <Hash>(hash); }
public void SettleTransaction(Address from, string platform, string chain, Hash hash) { PlatformSwapAddress[] swapAddresses; if (platform != DomainSettings.PlatformName) { Runtime.Expect(Runtime.PlatformExists(platform), "unsupported platform"); var platformInfo = Runtime.GetPlatformByName(platform); swapAddresses = platformInfo.InteropAddresses; } else { swapAddresses = null; } Runtime.Expect(Runtime.IsWitness(from), "invalid witness"); Runtime.Expect(from.IsUser, "must be user address"); var chainHashes = _platformHashes.Get <string, StorageMap>(platform); Runtime.Expect(!chainHashes.ContainsKey <Hash>(hash), "hash already seen"); var interopTx = Runtime.ReadTransactionFromOracle(platform, chain, hash); Runtime.Expect(interopTx.Hash == hash, "unxpected hash"); int swapCount = 0; foreach (var transfer in interopTx.Transfers) { var count = _withdraws.Count(); var index = -1; for (int i = 0; i < count; i++) { var entry = _withdraws.Get <InteropWithdraw>(i); if (entry.destination == transfer.destinationAddress && entry.transferAmount == transfer.Value && entry.transferSymbol == transfer.Symbol) { index = i; break; } } if (index >= 0) { Runtime.Expect(Runtime.TokenExists(transfer.Symbol), "invalid token"); var token = this.Runtime.GetToken(transfer.Symbol); if (Runtime.ProtocolVersion >= 4) { if (token.Flags.HasFlag(TokenFlags.Fungible)) { Runtime.Expect(transfer.Value > 0, "amount must be positive and greater than zero"); } else { Runtime.Expect(Runtime.NFTExists(transfer.Symbol, transfer.Value), $"nft {transfer.Value} must exist"); } } else { Runtime.Expect(transfer.Value > 0, "amount must be positive and greater than zero"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); } Runtime.Expect(token.Flags.HasFlag(TokenFlags.Transferable), "token must be transferable"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Swappable), "transfer token must be swappable"); var withdraw = _withdraws.Get <InteropWithdraw>(index); _withdraws.RemoveAt(index); if (Runtime.ProtocolVersion >= 3) { var org = Runtime.GetOrganization(DomainSettings.ValidatorsOrganizationName); Runtime.Expect(org.IsMember(from), $"{from.Text} is not a validator node"); Runtime.TransferTokens(withdraw.feeSymbol, this.Address, from, withdraw.feeAmount); } else { Runtime.TransferTokens(withdraw.feeSymbol, this.Address, transfer.sourceAddress, withdraw.feeAmount); } RegisterHistory(hash, withdraw.hash, DomainSettings.PlatformName, Runtime.Chain.Name, transfer.sourceAddress, hash, platform, chain, withdraw.destination, transfer.Symbol, transfer.Value); swapCount++; } else if (swapAddresses != null) { foreach (var entry in swapAddresses) { if (transfer.destinationAddress == entry.LocalAddress) { Runtime.Expect(!transfer.sourceAddress.IsNull, "invalid source address"); // Here we detect if this transfer occurs between two swap addresses var isInternalTransfer = Runtime.IsPlatformAddress(transfer.sourceAddress); if (!isInternalTransfer) { Runtime.Expect(Runtime.TokenExists(transfer.Symbol), "invalid token"); var token = this.Runtime.GetToken(transfer.Symbol); if (Runtime.ProtocolVersion >= 4) { if (token.Flags.HasFlag(TokenFlags.Fungible)) { Runtime.Expect(transfer.Value > 0, "amount must be positive and greater than zero"); } else { Runtime.Expect(Runtime.NFTExists(transfer.Symbol, transfer.Value), $"nft {transfer.Value} must exist"); } } else { Runtime.Expect(transfer.Value > 0, "amount must be positive and greater than zero"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); } Runtime.Expect(token.Flags.HasFlag(TokenFlags.Transferable), "token must be transferable"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Swappable), "transfer token must be swappable"); Runtime.Expect(transfer.interopAddress.IsUser, "invalid destination address"); Runtime.SwapTokens(platform, transfer.sourceAddress, Runtime.Chain.Name, transfer.interopAddress, transfer.Symbol, transfer.Value); if (Runtime.ProtocolVersion >= 4 && !token.Flags.HasFlag(TokenFlags.Fungible)) { var externalNft = Runtime.ReadNFTFromOracle(platform, transfer.Symbol, transfer.Value); var ram = Serialization.Serialize(externalNft); var localNft = Runtime.ReadToken(transfer.Symbol, transfer.Value); Runtime.WriteToken(from, transfer.Symbol, transfer.Value, ram); // TODO "from" here might fail due to contract triggers, review this later } var settleHash = Runtime.Transaction.Hash; RegisterHistory(settleHash, hash, platform, chain, transfer.sourceAddress, settleHash, DomainSettings.PlatformName, Runtime.Chain.Name, transfer.interopAddress, transfer.Symbol, transfer.Value); swapCount++; } break; } } } } Runtime.Expect(swapCount > 0, "nothing to settle"); chainHashes.Set <Hash, Hash>(hash, Runtime.Transaction.Hash); Runtime.Notify(EventKind.ChainSwap, from, new TransactionSettleEventData(hash, platform, chain)); }
public void SettleTransaction(Address from, string platform, string chain, Hash hash) { PlatformSwapAddress[] swapAddresses; if (platform != DomainSettings.PlatformName) { Runtime.Expect(Runtime.PlatformExists(platform), "unsupported platform"); var platformInfo = Runtime.GetPlatformByName(platform); swapAddresses = platformInfo.InteropAddresses; } else { swapAddresses = null; } Runtime.Expect(Runtime.IsWitness(from), "invalid witness"); Runtime.Expect(from.IsUser, "must be user address"); var chainHashes = _hashes.Get <string, StorageMap>(platform); Runtime.Expect(!chainHashes.ContainsKey <Hash>(hash), "hash already seen"); var interopTx = Runtime.ReadTransactionFromOracle(platform, chain, hash); Runtime.Expect(interopTx.Hash == hash, "unxpected hash"); int swapCount = 0; foreach (var transfer in interopTx.Transfers) { var count = _withdraws.Count(); var index = -1; for (int i = 0; i < count; i++) { var entry = _withdraws.Get <InteropWithdraw>(i); if (entry.destination == transfer.destinationAddress && entry.transferAmount == transfer.Value && entry.transferSymbol == transfer.Symbol) { index = i; break; } } if (index >= 0) { Runtime.Expect(transfer.Value > 0, "amount must be positive and greater than zero"); Runtime.Expect(Runtime.TokenExists(transfer.Symbol), "invalid token"); var token = this.Runtime.GetToken(transfer.Symbol); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Transferable), "token must be transferable"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.External), "token must be external"); var withdraw = _withdraws.Get <InteropWithdraw>(index); _withdraws.RemoveAt <InteropWithdraw>(index); Runtime.TransferTokens(withdraw.feeSymbol, this.Address, transfer.sourceAddress, withdraw.feeAmount); swapCount++; } else if (swapAddresses != null) { foreach (var entry in swapAddresses) { if (transfer.destinationAddress == entry.LocalAddress) { Runtime.Expect(!transfer.sourceAddress.IsNull, "invalid source address"); Runtime.Expect(transfer.Value > 0, "amount must be positive and greater than zero"); Runtime.Expect(Runtime.TokenExists(transfer.Symbol), "invalid token"); var token = this.Runtime.GetToken(transfer.Symbol); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Fungible), "token must be fungible"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.Transferable), "token must be transferable"); Runtime.Expect(token.Flags.HasFlag(TokenFlags.External), "token must be external"); Runtime.Expect(transfer.interopAddress.IsUser, "invalid destination address"); // TODO support NFT Runtime.SwapTokens(platform, transfer.sourceAddress, Runtime.Chain.Name, transfer.interopAddress, transfer.Symbol, transfer.Value, null, null); //Runtime.Notify(EventKind.TokenSwap, destination, new TokenEventData(token.Symbol, amount, Runtime.Chain.Name)); swapCount++; break; } } } } Runtime.Expect(swapCount > 0, "nothing to settle"); chainHashes.Set <Hash, Hash>(hash, Runtime.Transaction.Hash); Runtime.Notify(EventKind.ChainSwap, from, new TransactionSettleEventData(hash, platform, chain)); }