diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim index bbca5320c2..33cb4880ac 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, json, sequtils, sugar, chronicles, strformat, stint, httpclient, net, strutils, os, times +import NimQml, Tables, json, sequtils, sugar, chronicles, strformat, stint, httpclient, net, strutils, os, times, algorithm import web3/[ethtypes, conversions] import ../settings/service as settings_service @@ -38,6 +38,15 @@ const SIGNAL_WALLET_ACCOUNT_TOKENS_REBUILT* = "walletAccount/tokensRebuilt" var balanceCache {.threadvar.}: Table[string, float64] +proc priorityTokenCmp(a, b: WalletTokenDto): int = + for symbol in @["ETH", "SNT", "DAI", "STT"]: + if a.symbol == symbol: + return -1 + if b.symbol == symbol: + return 1 + + cmp(a.name, b.name) + proc hex2Balance*(input: string, decimals: int): string = var value = fromHex(Stuint[256], input) @@ -399,6 +408,8 @@ QtObject: var tokens: seq[WalletTokenDto] if(responseObj.getProp(wAddress, tokensArr)): tokens = map(tokensArr.getElems(), proc(x: JsonNode): WalletTokenDto = x.toWalletTokenDto()) + + tokens.sort(priorityTokenCmp) self.walletAccounts[wAddress].tokens = tokens data.accountsTokens[wAddress] = tokens diff --git a/test/ui-test/src/screens/SettingsScreen.py b/test/ui-test/src/screens/SettingsScreen.py index 3dd1b4d6d5..d7a84001c5 100644 --- a/test/ui-test/src/screens/SettingsScreen.py +++ b/test/ui-test/src/screens/SettingsScreen.py @@ -10,6 +10,9 @@ from enum import Enum +import random +import time +import string from drivers.SquishDriver import * from drivers.SquishDriverVerification import * from .StatusMainScreen import MainScreenComponents @@ -25,11 +28,21 @@ class SidebarComponents(Enum): SIGN_OUT_AND_QUIT_OPTION: str = "sign_out_Quit_StatusNavigationListItem" COMMUNITIES_OPTION: str = "communities_StatusNavigationListItem" PROFILE_OPTION: str = "profile_StatusNavigationListItem" + ENS_ITEM: str = "settings_Sidebar_ENS_Item" class AdvancedOptionScreen(Enum): ACTIVATE_OR_DEACTIVATE_WALLET: str = "walletSettingsLineButton" I_UNDERSTAND_POP_UP: str = "i_understand_StatusBaseText" +class ENSScreen(Enum): + START_BUTTON :str = "settings_ENS_Start_Button" + ENS_SEARCH_INPUT: str = "settings_ENS_Search_Input" + NEXT_BUTTON: str = "settings_ENS_Search_Next_Button" + AGREE_TERMS: str = "settings_ENS_Terms_Agree" + OPEN_TRANSACTION: str = "settings_ENS_Terms_Open_Transaction" + TRANSACTION_NEXT_BUTTON: str = "settings_ENS_Terms_Transaction_Next_Button" + PASSWORD_INPUT: str = "settings_ENS_Terms_Transaction_Password_Input" + class WalletSettingsScreen(Enum): GENERATED_ACCOUNTS: str = "settings_Wallet_MainView_GeneratedAccounts" DELETE_ACCOUNT: str = "settings_Wallet_AccountView_DeleteAccount" @@ -114,6 +127,26 @@ class SettingsScreen: def open_language_and_currency_settings(self): click_obj_by_name(SidebarComponents.LANGUAGE_CURRENCY_OPTION.value) + + def register_random_ens_name(self, password: str): + click_obj_by_name(SidebarComponents.ENS_ITEM.value) + get_and_click_obj(ENSScreen.START_BUTTON.value) + + name = "" + for _ in range(4): + name += string.ascii_lowercase[random.randrange(26)] + + type(ENSScreen.ENS_SEARCH_INPUT.value, name) + time.sleep(1) + + click_obj_by_name(ENSScreen.NEXT_BUTTON.value) + click_obj_by_name(ENSScreen.AGREE_TERMS.value) + click_obj_by_name(ENSScreen.OPEN_TRANSACTION.value) + click_obj_by_name(ENSScreen.TRANSACTION_NEXT_BUTTON.value) + click_obj_by_name(ENSScreen.TRANSACTION_NEXT_BUTTON.value) + + type(ENSScreen.PASSWORD_INPUT.value, password) + click_obj_by_name(ENSScreen.TRANSACTION_NEXT_BUTTON.value) def _find_account_index(self, account_name: str) -> int: accounts = get_obj(WalletSettingsScreen.GENERATED_ACCOUNTS.value) diff --git a/test/ui-test/src/screens/StatusMainScreen.py b/test/ui-test/src/screens/StatusMainScreen.py index c53609952a..41dbdbdf84 100644 --- a/test/ui-test/src/screens/StatusMainScreen.py +++ b/test/ui-test/src/screens/StatusMainScreen.py @@ -9,6 +9,7 @@ # *****************************************************************************/ +import time from enum import Enum from drivers.SquishDriver import * from drivers.SquishDriverVerification import * @@ -55,6 +56,7 @@ class StatusMainScreen: def open_settings(self): click_obj_by_name(MainScreenComponents.SETTINGS_BUTTON.value) + time.sleep(0.5) def open_start_chat_view(self): click_obj_by_name(MainScreenComponents.START_CHAT_BTN.value) diff --git a/test/ui-test/src/screens/StatusWalletScreen.py b/test/ui-test/src/screens/StatusWalletScreen.py index 5308779579..d60cc51165 100644 --- a/test/ui-test/src/screens/StatusWalletScreen.py +++ b/test/ui-test/src/screens/StatusWalletScreen.py @@ -45,6 +45,8 @@ class SendPopup(Enum): NETWORKS_LIST: str = "mainWallet_Send_Popup_Networks_List" SEND_BUTTON: str = "mainWallet_Send_Popup_Send_Button" PASSWORD_INPUT: str = "mainWallet_Send_Popup_Password_Input" + ASSET_SELECTOR: str = "mainWallet_Send_Popup_Asset_Selector" + ASSET_LIST: str = "mainWallet_Send_Popup_Asset_List" class AddAccountPopup(Enum): SCROLL_BAR: str = "mainWallet_Add_Account_Popup_Main" @@ -145,9 +147,14 @@ class StatusWalletScreen: self._click_repeater(SendPopup.HEADER_ACCOUNTS_LIST.value, account_name) time.sleep(1) type(SendPopup.AMOUNT_INPUT.value, amount) - + if token != "ETH": - print("TODO: switch token") + click_obj_by_name(SendPopup.ASSET_SELECTOR.value) + asset_list = get_obj(SendPopup.ASSET_LIST.value) + for index in range(asset_list.count): + if(asset_list.itemAtIndex(index).objectName == token): + click_obj(asset_list.itemAtIndex(index)) + break click_obj_by_name(SendPopup.MY_ACCOUNTS_TAB.value) @@ -237,7 +244,7 @@ class StatusWalletScreen: for index in range(list.count): if list.itemAtIndex(index).objectName == symbol: balance = list.itemAtIndex(index).children.at(2).text - assert balance != f"0 {symbol}", "balance is not positive" + assert balance != f"0 {symbol}", f"balance is not positive, balance: {balance}" return assert False, "symbol not found" diff --git a/test/ui-test/testSuites/suite_status/shared/scripts/sections/settings_names.py b/test/ui-test/testSuites/suite_status/shared/scripts/sections/settings_names.py index be623e8498..627d4c1f49 100644 --- a/test/ui-test/testSuites/suite_status/shared/scripts/sections/settings_names.py +++ b/test/ui-test/testSuites/suite_status/shared/scripts/sections/settings_names.py @@ -32,6 +32,16 @@ settings_navbar_settings_icon_StatusIcon = {"container": navBarListView_Settings mainWindow_ScrollView = {"container": statusDesktop_mainWindow, "type": "StatusScrollView", "unnamed": 1, "visible": True} mainWindow_ScrollView_2 = {"container": statusDesktop_mainWindow, "occurrence": 2, "type": "StatusScrollView", "unnamed": 1, "visible": True} settingsSave_StatusButton = {"container": statusDesktop_mainWindow, "objectName": "settingsDirtyToastMessageSaveButton", "type": "StatusButton", "visible": True} +settings_Sidebar_ENS_Item = {"container": mainWindow_ScrollView, "objectName": "ENS usernames-MainMenu", "type": "StatusNavigationListItem"} + +# ENS view; +settings_ENS_Start_Button = {"container": statusDesktop_mainWindow, "objectName": "ensStartButton", "type": "StatusButton"} +settings_ENS_Search_Input = {"container": statusDesktop_mainWindow, "objectName": "ensUsernameInput", "type": "StyledTextField"} +settings_ENS_Search_Next_Button = {"container": statusDesktop_mainWindow, "objectName": "ensNextButton", "type": "StatusRoundButton"} +settings_ENS_Terms_Agree = {"container": statusDesktop_mainWindow, "objectName": "ensAgreeTerms", "type": "StatusCheckBox"} +settings_ENS_Terms_Open_Transaction = {"container": statusDesktop_mainWindow, "objectName": "ensStartTransaction", "type": "StatusButton"} +settings_ENS_Terms_Transaction_Next_Button = {"container": statusDesktop_mainWindow, "objectName": "sendNextButton", "type": "StatusButton"} +settings_ENS_Terms_Transaction_Password_Input = {"container": statusDesktop_mainWindow, "objectName": "transactionSignerPasswordInput", "type": "StyledTextField"} # Side bar items: wallet_StatusNavigationListItem = {"container": mainWindow_ScrollView, "objectName": SettingsSubsection.WALLET.value, "type": "StatusNavigationListItem", "visible": True} @@ -62,6 +72,7 @@ customUrl_popup_TextEdit = {"container": customUrl_popup_StatusInput, "type": "T settings_Wallet_MainView_GeneratedAccounts = {"container": statusDesktop_mainWindow, "objectName":'generatedAccounts', "type": 'ListView'} settings_Wallet_AccountView_DeleteAccount = {"container": statusDesktop_mainWindow, "type": "StatusButton", "objectName": "deleteAccountButton"} settings_Wallet_AccountView_DeleteAccount_Confirm = {"container": statusDesktop_mainWindow, "type": "StatusButton", "objectName": "confirmDeleteAccountButton"} +mainWindow_ScrollView_2 = {"container": statusDesktop_mainWindow, "occurrence": 2, "type": "StatusScrollView", "unnamed": 1, "visible": True} settings_Wallet_MainView_Networks = {"container": statusDesktop_mainWindow, "objectName": "networksItem", "type": "StatusListItem"} settings_Wallet_NetworksView_TestNet_Toggle = {"container": statusDesktop_mainWindow, "objectName": "testnetModeSwitch", "type": "StatusSwitch"} settings_Wallet_AccountView_EditAccountButton = {"container": statusDesktop_mainWindow, "type": "StatusFlatRoundButton", "objectName": "walletAccountViewEditAccountButton"} diff --git a/test/ui-test/testSuites/suite_status/shared/scripts/sections/wallet_names.py b/test/ui-test/testSuites/suite_status/shared/scripts/sections/wallet_names.py index ec1ae363df..8a13eecab5 100644 --- a/test/ui-test/testSuites/suite_status/shared/scripts/sections/wallet_names.py +++ b/test/ui-test/testSuites/suite_status/shared/scripts/sections/wallet_names.py @@ -15,7 +15,7 @@ mainWallet_Right_Side_Tab_Bar = {"container": statusDesktop_mainWindow, "objectN mainWallet_Assets_View_List = {"container": statusDesktop_mainWindow, "objectName": "assetViewStatusListView", "type": "StatusListView"} # Network selector popup -mainWallet_Network_Popup_Chain_Repeater_1 = {"container": statusDesktop_mainWindow, "objectName": "chainRepeaterLayer1", "type": "Repeater"} +mainWallet_Network_Popup_Chain_Repeater_1 = {"container": statusDesktop_mainWindow, "objectName": "networkSelectPopupChainRepeaterLayer1", "type": "Repeater"} # Send popup: mainWallet_Footer_Send_Button = {"container": statusDesktop_mainWindow, "objectName": "walletFooterSendButton", "type": "StatusFlatButton"} @@ -27,6 +27,8 @@ mainWallet_Send_Popup_Header_Accounts = {"container": statusDesktop_mainWindow, mainWallet_Send_Popup_Networks_List = {"container": statusDesktop_mainWindow, "objectName": "networksList", "type": "Repeater"} mainWallet_Send_Popup_Send_Button = {"container": statusDesktop_mainWindow, "objectName": "sendModalFooterSendButton", "type": "StatusFlatButton"} mainWallet_Send_Popup_Password_Input = {"container": statusDesktop_mainWindow, "objectName": "transactionSignerPasswordInput", "type": "StyledTextField"} +mainWallet_Send_Popup_Asset_Selector = {"container": statusDesktop_mainWindow, "objectName": "assetSelectorButton", "type": "StatusComboBox"} +mainWallet_Send_Popup_Asset_List = {"container": statusDesktop_mainWindow, "objectName": "assetSelectorList", "type": "StatusListView"} # Add account popup: mainWallet_Add_Account_Popup_Main = {"container": statusDesktop_mainWindow, "objectName": "AddAccountModalContent", "type": "StatusScrollView", "visible": True} diff --git a/test/ui-test/testSuites/suite_status/shared/steps/settingsSteps.py b/test/ui-test/testSuites/suite_status/shared/steps/settingsSteps.py index 16ddf6f05f..bcc51d5006 100644 --- a/test/ui-test/testSuites/suite_status/shared/steps/settingsSteps.py +++ b/test/ui-test/testSuites/suite_status/shared/steps/settingsSteps.py @@ -41,6 +41,10 @@ def step(context: any): def step(context: any, account_name: str, account_color: str): _settingsScreen.edit_account(account_name, account_color) +@When("the user registers a random ens name with password |any|") +def step(context, password): + _statusMain.open_settings() + _settingsScreen.register_random_ens_name(password) @When("the user clicks on Language & Currency") def step(context): diff --git a/test/ui-test/testSuites/suite_status/shared/steps/walletSteps.py b/test/ui-test/testSuites/suite_status/shared/steps/walletSteps.py index 70bd61767b..49651958dc 100644 --- a/test/ui-test/testSuites/suite_status/shared/steps/walletSteps.py +++ b/test/ui-test/testSuites/suite_status/shared/steps/walletSteps.py @@ -46,7 +46,7 @@ def step(context, name): @When("the user toggles the network |any|") def step(context, network_name): _walletScreen.toggle_network(network_name) - + @Then("the user has a positive balance of |any|") def step(context, symbol): _walletScreen.verify_positive_balance(symbol) diff --git a/test/ui-test/testSuites/suite_status/tst_transaction/test.feature b/test/ui-test/testSuites/suite_status/tst_transaction/test.feature index df1834af54..8c849f9255 100644 --- a/test/ui-test/testSuites/suite_status/tst_transaction/test.feature +++ b/test/ui-test/testSuites/suite_status/tst_transaction/test.feature @@ -1,24 +1,31 @@ Feature: Status Desktop Transaction - As a user I want to perform transaction + As a user I want to perform transaction - Background: Sign up & Enable wallet section & Toggle test networks - Given A first time user lands on the status desktop and generates new key - When user signs up with username tester123 and password TesTEr16843/!@00 + Background: Sign up & Enable wallet section & Toggle test networks + Given A first time user lands on the status desktop and navigates to import seed phrase + When The user inputs the seed phrase pelican chief sudden oval media rare swamp elephant lawsuit wheat knife initial + And user clicks on the following ui-component seedPhraseView_Submit_Button + When user signs up with username tester123 and password qqqqqqqqqq Then the user lands on the signed in app When the user opens app settings screen When the user activates wallet and opens the wallet settings When the user toggles test networks When the user opens wallet screen When the user accepts the signing phrase - When the user imports a seed phrase with one and TesTEr16843/!@00 and pelican chief sudden oval media rare swamp elephant lawsuit wheat knife initial - @mayfail - Scenario Outline: User sends a transaction - When the user sends a transaction to himself from account one of on with password TesTEr16843/!@00 - Then the transaction is in progress +# Scenario Outline: User sends a transaction +# When the user sends a transaction to himself from account Status account of on with password TesTEr16843/!@00 +# Then the transaction is in progress - Examples: - | amount | token | chain_name | - | 0.000001 | ETH | Ropsten | - | 0 | ETH | Ropsten | \ No newline at end of file +# Examples: +# | amount | token | chain_name | +# | 0.000001 | ETH | Ropsten | +# | 0 | ETH | Ropsten | +# | 1 | STT | Goerli | +# | 0 | STT | Goerli | + + +# Scenario: User registers a ENS name +# When the user registers a random ens name with password qqqqqqqqqq +# Then the transaction is in progress diff --git a/ui/StatusQ b/ui/StatusQ index f15b0d4de4..a22d8252b1 160000 --- a/ui/StatusQ +++ b/ui/StatusQ @@ -1 +1 @@ -Subproject commit f15b0d4de4dd3c262d38f1eb0602c82079885194 +Subproject commit a22d8252b104b25459d4cb7c387e1ca5cf4fc00a diff --git a/ui/app/AppLayouts/Profile/views/EnsSearchView.qml b/ui/app/AppLayouts/Profile/views/EnsSearchView.qml index cb3591971f..cf728175e3 100644 --- a/ui/app/AppLayouts/Profile/views/EnsSearchView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsSearchView.qml @@ -137,6 +137,7 @@ Item { Input { id: ensUsername + textField.objectName: "ensUsernameInput" placeholderText: !isStatus ? "vitalik94.domain.eth" : "vitalik94" anchors.left: parent.left anchors.top: circleAt.bottom @@ -181,6 +182,7 @@ Item { anchors.topMargin: Style.current.bigPadding anchors.right: parent.right type: StatusQControls.StatusRoundButton.Type.Secondary + objectName: "ensNextButton" icon.name: "arrow-right" icon.width: 18 icon.height: 14 diff --git a/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml b/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml index 5728722d58..b71433a397 100644 --- a/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsTermsAndConditionsView.qml @@ -293,6 +293,7 @@ Item { StatusCheckBox { id: termsAndConditionsCheckbox + objectName: "ensAgreeTerms" anchors.top: keyLbl.bottom anchors.topMargin: Style.current.padding anchors.left: parent.left @@ -367,6 +368,7 @@ Item { StatusButton { id: startBtn + objectName: "ensStartTransaction" anchors.bottom: parent.bottom anchors.bottomMargin: Style.current.padding anchors.right: parent.right diff --git a/ui/app/AppLayouts/Profile/views/EnsWelcomeView.qml b/ui/app/AppLayouts/Profile/views/EnsWelcomeView.qml index 64356b46bd..1082f5f3a6 100644 --- a/ui/app/AppLayouts/Profile/views/EnsWelcomeView.qml +++ b/ui/app/AppLayouts/Profile/views/EnsWelcomeView.qml @@ -279,6 +279,7 @@ Item { StatusButton { id: startBtn + objectName: "ensStartButton" anchors.bottom: parent.bottom anchors.bottomMargin: Style.current.padding anchors.horizontalCenter: parent.horizontalCenter diff --git a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml index a793bbd6cc..bb9313ee54 100644 --- a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml @@ -58,7 +58,7 @@ Popup { Repeater { id: chainRepeater1 - objectName: "chainRepeaterLayer1" + objectName: "networkSelectPopupChainRepeaterLayer1" model: popup.layer1Networks delegate: chainItem diff --git a/ui/imports/shared/controls/GasValidator.qml b/ui/imports/shared/controls/GasValidator.qml index aa0dcc9396..287253b746 100644 --- a/ui/imports/shared/controls/GasValidator.qml +++ b/ui/imports/shared/controls/GasValidator.qml @@ -40,7 +40,7 @@ Column { if (selectedAsset && selectedAsset.symbol && selectedAsset.symbol.toUpperCase() === "ETH") { gasTotal += selectedAmount } - const chainId = (selectedNetwork && selectedNetwork.chainId) || Global.currentChainId + const chainId = selectedNetwork && selectedNetwork.chainId const currAcctGasAsset = Utils.findAssetByChainAndSymbol(chainId, selectedAccount.assets, "ETH") if (currAcctGasAsset && currAcctGasAsset.totalBalance > gasTotal) { diff --git a/ui/imports/shared/panels/StatusAssetSelector.qml b/ui/imports/shared/panels/StatusAssetSelector.qml index ed882f2a94..0e0701d476 100644 --- a/ui/imports/shared/panels/StatusAssetSelector.qml +++ b/ui/imports/shared/panels/StatusAssetSelector.qml @@ -45,7 +45,7 @@ Item { StatusComboBox { id: comboBox - + objectName: "assetSelectorButton" width: parent.width height: parent.height @@ -53,6 +53,8 @@ Item { control.popup.width: 342 control.popup.height: 416 control.popup.x: width - control.popup.width + + popupContentItemObjectName: "assetSelectorList" model: root.assets @@ -95,7 +97,7 @@ Item { width: comboBox.control.popup.width highlighted: index === comboBox.control.highlightedIndex padding: 16 - + objectName: symbol onClicked: { // TODO: move this out of StatusQ, this involves dependency on BE code // WARNING: Wrong ComboBox value processing. Check `StatusAccountSelector` for more info. diff --git a/ui/imports/shared/popups/SendModal.qml b/ui/imports/shared/popups/SendModal.qml index d8555b4c02..2717ff5745 100644 --- a/ui/imports/shared/popups/SendModal.qml +++ b/ui/imports/shared/popups/SendModal.qml @@ -50,7 +50,7 @@ StatusDialog { gasSelector.selectedTipLimit, gasSelector.selectedOverallLimit, transactionSigner.enteredPassword, - networkSelector.selectedNetwork.chainId || Global.currentChainId, + networkSelector.selectedNetwork.chainId, stack.uuid, gasSelector.suggestedFees.eip1559Enabled, ) @@ -411,7 +411,7 @@ StatusDialog { recipientSelector.selectedRecipient.address, assetSelector.selectedAsset.symbol, amountToSendInput.text, - chainID || Global.currentChainId, + chainID, "")) if (!gasEstimate.success) { diff --git a/ui/imports/shared/status/StatusETHTransactionModal.qml b/ui/imports/shared/status/StatusETHTransactionModal.qml index 24182fbafd..c44a37e822 100644 --- a/ui/imports/shared/status/StatusETHTransactionModal.qml +++ b/ui/imports/shared/status/StatusETHTransactionModal.qml @@ -139,6 +139,9 @@ ModalPopup { selectedAsset: root.asset selectedAmount: 0 selectedGasEthValue: gasSelector.selectedGasEthValue + selectedNetwork: { + return {chainId: root.chainId} + } } } TransactionFormGroup { diff --git a/ui/imports/shared/status/StatusSNTTransactionModal.qml b/ui/imports/shared/status/StatusSNTTransactionModal.qml index 16a8be39f6..03c05b0bb1 100644 --- a/ui/imports/shared/status/StatusSNTTransactionModal.qml +++ b/ui/imports/shared/status/StatusSNTTransactionModal.qml @@ -121,6 +121,7 @@ ModalPopup { contactsStore: root.contactsStore selectedRecipient: { "address": contractAddress, "type": RecipientSelector.Type.Address } readOnly: true + isValid: true onSelectedRecipientChanged: if (isValid) { gasSelector.estimateGas() } } @@ -156,6 +157,9 @@ ModalPopup { selectedAsset: root.asset selectedAmount: parseFloat(root.assetPrice) selectedGasEthValue: gasSelector.selectedGasEthValue + selectedNetwork: { + return {chainId: root.chainId} + } } } TransactionFormGroup { @@ -224,6 +228,7 @@ ModalPopup { id: btnNext anchors.right: parent.right text: qsTr("Next") + objectName: "sendNextButton" enabled: stack.currentGroup.isValid && !stack.currentGroup.isPending loading: stack.currentGroup.isPending onClicked: {