feat(WalletConnect): Implement sign request modal (#15520)

* feat(WalletConnect): Implement sign request modal

1. Implementing sign request modal based on SignTransactionModalBase
2. Adding storybook page
3. Integrate it in the app
4. Removing DAppRequestModal
5. Update RoundImageWithBadge to preserve aspect ratio between badge and main image

* fix(WalletConnect): Remove unneeded properties from WalletConnectService API

Removing `selectedAccountAddress` and `loginType`. These properties are now passed through DAppsWorkflow API

* fix(WalletConnect): Removing unnecessary changes
This commit is contained in:
Alex Jbanca 2024-07-12 00:00:15 +03:00 committed by GitHub
parent 2a41622298
commit ca8a0028a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 540 additions and 650 deletions

View File

@ -1,196 +0,0 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml 2.15
import Qt.labs.settings 1.0
import QtTest 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Utils 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups.Dialog 0.1
import Models 1.0
import Storybook 1.0
import shared.popups.walletconnect 1.0
import SortFilterProxyModel 0.2
import AppLayouts.Wallet.panels 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import utils 1.0
import shared.stores 1.0
Item {
id: root
function openModal() {
modal.open()
}
// qml Splitter
SplitView {
anchors.fill: parent
ColumnLayout {
SplitView.fillWidth: true
Component.onCompleted: root.openModal()
DAppRequestModal {
id: modal
anchors.centerIn: parent
spacing: 8
dappName: settings.dappName
dappUrl: settings.dappUrl
dappIcon: settings.dappIcon
payloadData: d.currentPayload ? d.currentPayload.payloadData : null
method: d.currentPayload ? d.currentPayload.method : ""
maxFeesText: d.currentPayload ? d.currentPayload.maxFeesText : ""
maxFeesEthText: d.currentPayload ? d.currentPayload.maxFeesEthText : ""
enoughFunds: settings.enoughFunds
estimatedTimeText: d.currentPayload ? d.currentPayload.estimatedTimeText : ""
account: d.selectedAccount
network: d.selectedNetwork
onSign: {
console.log("Sign button clicked")
}
onReject: {
console.log("Reject button clicked")
}
}
StatusButton {
id: openButton
Layout.alignment: Qt.AlignHCenter
Layout.margins: 20
text: "Open DAppRequestModal"
onClicked: root.openModal()
}
ColumnLayout {}
}
ColumnLayout {
id: optionsSpace
TextField {
id: dappNameTextField
text: settings.dappName
onTextChanged: settings.dappName = text
}
TextField {
id: dappUrlTextField
text: settings.dappUrl
onTextChanged: settings.dappUrl = text
}
TextField {
id: dappIconTextField
text: settings.dappIcon
onTextChanged: settings.dappIcon = text
}
TextField {
id: accountDisplayTextField
text: settings.accountDisplay
onTextChanged: settings.accountDisplay = text
}
StatusComboBox {
id: methodsComboBox
model: d.methodsModel
control.textRole: "method"
currentIndex: settings.payloadMethod
onCurrentIndexChanged: {
d.currentPayload = null
settings.payloadMethod = currentIndex
d.currentPayload = d.payloadOptions[currentIndex]
}
}
StatusCheckBox {
id: enoughFundsCheckBox
text: "Enough funds"
checked: settings.enoughFunds
onCheckedChanged: settings.enoughFunds = checked
}
Item { Layout.fillHeight: true }
}
}
Settings {
id: settings
property string dappName: "OpenSea"
property string dappUrl: "opensea.io"
property string dappIcon: "https://opensea.io/static/images/logos/opensea-logo.svg"
property string accountDisplay: "helloworld"
property int payloadMethod: 0
property bool enoughFunds: true
}
QtObject {
id: d
Component.onCompleted: methodsModel.append(payloadOptions)
readonly property var accountsModel: WalletAccountsModel{}
readonly property var selectedAccount: accountsModel.data[0]
readonly property var selectedNetwork: NetworksModel.flatNetworks.get(0)
readonly property ListModel methodsModel: ListModel {}
property var currentPayload: payloadOptions[settings.payloadMethod]
property string maxFeesText: ""
property string estimatedTimeText: ""
readonly property var payloadOptions: [
{
payloadData: {"message":"This is a message to sign.\nSigning this will prove ownership of the account."},
method: SessionRequest.methods.personalSign.name,
maxFeesText: "",
maxFeesEthText: "",
estimatedTimeText: ""
},
{
payloadData: {"message": "{\"types\":{\"EIP712Domain\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"version\",\"type\":\"string\"},{\"name\":\"chainId\",\"type\":\"uint256\"},{\"name\":\"verifyingContract\",\"type\":\"address\"}],\"Person\":[{\"name\":\"name\",\"type\":\"string\"},{\"name\":\"wallet\",\"type\":\"address\"}],\"Mail\":[{\"name\":\"from\",\"type\":\"Person\"},{\"name\":\"to\",\"type\":\"Person\"},{\"name\":\"contents\",\"type\":\"string\"}]},\"primaryType\":\"Mail\",\"domain\":{\"name\":\"Ether Mail\",\"version\":\"1\",\"chainId\":1,\"verifyingContract\":\"0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC\"},\"message\":{\"from\":{\"name\":\"Cow\",\"wallet\":\"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826\"},\"to\":{\"name\":\"Bob\",\"wallet\":\"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB\"},\"contents\":\"Hello, Bob!\"}}"},
method: SessionRequest.methods.signTypedData_v4.name,
maxFeesText: "",
maxFeesEthText: "",
estimatedTimeText: ""
},
{
payloadData: {"tx":{"data":"0x","from":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","gasLimit":"0x5208","gasPrice":"0x048ddbc5","nonce":"0x2a","to":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","value":"0x00"}},
method: SessionRequest.methods.signTransaction.name,
maxFeesText: "1.82 EUR",
maxFeesEthText: "0.0001 ETH",
estimatedTimeText: "3-5 mins"
},
{
payloadData: {"tx":{"data":"0x","from":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","gasLimit":"0x5208","gasPrice":"0x048ddbc5","nonce":"0x2a","to":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","value":"0x00"}},
method: SessionRequest.methods.sendTransaction.name,
maxFeesText: "0.92 EUR",
maxFeesEthText: "0.00005 ETH",
estimatedTimeText: "1-2 mins"
}
]
}
}
// category: Wallet

View File

@ -0,0 +1,137 @@
// category: Popups
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import shared.popups.walletconnect 1.0
import utils 1.0
import Storybook 1.0
SplitView {
id: root
PopupBackground {
SplitView.fillWidth: true
SplitView.fillHeight: true
Button {
anchors.centerIn: parent
text: "Open"
onClicked: dappSignRequestModal.visible = true
}
DAppSignRequestModal {
id: dappSignRequestModal
loginType: loginType.currentValue
visible: true
modal: false
closePolicy: Popup.NoAutoClose
dappUrl: "https://example.com"
dappIcon: "https://picsum.photos/200/200"
dappName: "OpenSea"
accountColor: "blue"
accountName: "Account Name"
accountAddress: "0xE2d622C817878dA5143bBE06866ca8E35273Ba8"
networkName: "Ethereum"
networkIconPath: "https://picsum.photos/200/200"
currentCurrency: "EUR"
fiatFees: fiatFees.text
cryptoFees: "0.001"
estimatedTime: "3-5 minutes"
feesLoading: feesLoading.checked
hasFees: hasFees.checked
enoughFundsForTransaction: enoughFeesForTransaction.checked
enoughFundsForFees: enoughFeesForGas.checked
// sun emoji
accountEmoji: "\u2600"
requestPayload: controls.contentToSign[contentToSignComboBox.currentIndex]
signingTransaction: signingTransaction.checked
onAccepted: print ("Accepted")
onRejected: print ("Rejected")
}
}
Pane {
id: controls
SplitView.preferredWidth: 300
SplitView.fillHeight: true
readonly property var contentToSign: ['{
"id": 1714038548266495,
"params": {
"chainld": "eip155:11155111",
"request": {
"expiryTimestamp": 1714038848,
"method": "eth_signTransaction",
"params": [{
"data": "0x",
"from": "0xE2d622C817878dA5143bBE06866ca8E35273Ba8",
"gasLimit": "0x5208",
"gasPrice": "0xa677ef31",
"nonce": "0x27",
"to": "0xE2d622C817878dA5143bBE06866ca8E35273Ba8a",
"value": "0x00"
}]
}
},
"topic": "a0f85b23a1f3a540d85760a523963165fb92169d57320c",
"verifyContext": {
"verified": {
"isScam": false,
"origin": "https://react-app.walletconnect.com/",
"validation": "VALID",
"verifyUrl": "https://verify.walletconnect.com/"
}
}
}',
'"tx":{"data":"0x","from":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","gasLimit":"0x5208","gasPrice":"0x048ddbc5","nonce":"0x2a","to":"0xE2d622C817878dA5143bBE06866ca8E35273Ba8a","value":"0x00"}',
""
]
ColumnLayout {
TextField {
id: fiatFees
text: "1.54"
}
ComboBox {
id: loginType
model: [{name: "Password", value: Constants.LoginType.Password}, {name: "Biometrics", value: Constants.LoginType.Biometrics}, {name: "Keycard", value: Constants.LoginType.Keycard}]
textRole: "name"
valueRole: "value"
currentIndex: 0
}
ComboBox {
id: contentToSignComboBox
model: ["Long content to sign", "Short content to sign", "Empty content to sign"]
currentIndex: 0
}
CheckBox {
id: enoughFeesForTransaction
text: "Enough fees for transaction"
checked: true
}
CheckBox {
id: enoughFeesForGas
text: "Enough fees for gas"
checked: true
}
CheckBox {
id: feesLoading
text: "Fees loading"
checked: false
}
CheckBox {
id: hasFees
text: "Has fees"
checked: true
}
CheckBox {
id: signingTransaction
text: "Signing transaction"
checked: false
}
}
}
}

View File

@ -61,6 +61,8 @@ Item {
spacing: 8
wcService: walletConnectService
loginType: Constants.LoginType.Biometrics
selectedAccountAddress: ""
}
}
ColumnLayout {}
@ -373,7 +375,6 @@ Item {
}
walletRootStore: QObject {
property string selectedAddress: ""
property var filteredFlatModel: SortFilterProxyModel {
sourceModel: NetworksModel.flatNetworks
filters: ValueFilter { roleName: "isTest"; value: settings.testNetworks; }

View File

@ -121,7 +121,6 @@ Item {
id: walletStoreComponent
QtObject {
property string selectedAddress: ""
readonly property ListModel filteredFlatModel: ListModel {
ListElement { chainId: 1 }
ListElement {
@ -503,6 +502,7 @@ Item {
Component {
id: componentUnderTest
DAppsWorkflow {
loginType: Constants.LoginType.Password
}
}
@ -605,9 +605,9 @@ Item {
verify(popup.visible)
compare(popup.dappName, td.session.peer.metadata.name)
compare(popup.account.name, td.account.name)
compare(popup.account.address, td.account.address)
compare(popup.network.chainId, td.network.chainId)
compare(popup.accountName, td.account.name)
compare(popup.accountAddress, td.account.address)
compare(popup.networkName, td.network.chainName)
popup.close()
waitForRendering(controlUnderTest)

View File

@ -93,16 +93,16 @@ Item {
.arg(controlUnderTest.formatBigNumber(controlUnderTest.fromTokenAmount)).arg(controlUnderTest.fromTokenSymbol)
.arg(controlUnderTest.accountName).arg(controlUnderTest.serviceProviderURL).arg(controlUnderTest.networkName))
const fromImageHidden = findChild(controlUnderTest.contentItem, "fromImage")
const fromImageHidden = findChild(controlUnderTest.contentItem, "fromImageIdenticon")
compare(fromImageHidden.visible, false)
const fromImage = findChild(controlUnderTest.contentItem, "fromImageIdenticon")
verify(!!fromImage)
compare(fromImage.asset.emoji, controlUnderTest.accountEmoji)
compare(fromImage.asset.color, controlUnderTest.accountColor)
const toImage = findChild(controlUnderTest.contentItem, "toImage")
const toImage = findChild(controlUnderTest.contentItem, "toImageIdenticon")
verify(!!toImage)
compare(toImage.image.source, Constants.tokenIcon(controlUnderTest.fromTokenSymbol))
compare(toImage.asset.name, Constants.tokenIcon(controlUnderTest.fromTokenSymbol))
// spending cap box
const spendingCapBox = findChild(controlUnderTest.contentItem, "spendingCapBox")

View File

@ -95,12 +95,12 @@ Item {
const headerText = findChild(controlUnderTest.contentItem, "headerText")
verify(!!headerText)
compare(headerText.text, qsTr("Swap 1000.123456789 SNT to 1.42 ETH in %1 on %2").arg(controlUnderTest.accountName).arg(controlUnderTest.networkName))
const fromImage = findChild(controlUnderTest.contentItem, "fromImage")
const fromImage = findChild(controlUnderTest.contentItem, "fromImageIdenticon")
verify(!!fromImage)
compare(fromImage.image.source, Constants.tokenIcon(controlUnderTest.fromTokenSymbol))
const toImage = findChild(controlUnderTest.contentItem, "toImage")
compare(fromImage.asset.name, Constants.tokenIcon(controlUnderTest.fromTokenSymbol))
const toImage = findChild(controlUnderTest.contentItem, "toImageIdenticon")
verify(!!toImage)
compare(toImage.image.source, Constants.tokenIcon(controlUnderTest.toTokenSymbol))
compare(toImage.asset.name, Constants.tokenIcon(controlUnderTest.toTokenSymbol))
// pay box
const payBox = findChild(controlUnderTest.contentItem, "payBox")

View File

@ -165,6 +165,7 @@
<file>assets/img/icons/checkmark.svg</file>
<file>assets/img/icons/chevron-down.svg</file>
<file>assets/img/icons/chevron-up.svg</file>
<file>assets/img/icons/collapse.svg</file>
<file>assets/img/icons/clear.svg</file>
<file>assets/img/icons/close-circle.svg</file>
<file>assets/img/icons/close.svg</file>
@ -187,6 +188,7 @@
<file>assets/img/icons/emojis.svg</file>
<file>assets/img/icons/ETH.png</file>
<file>assets/img/icons/exchange.svg</file>
<file>assets/img/icons/expand.svg</file>
<file>assets/img/icons/external.svg</file>
<file>assets/img/icons/external-link.svg</file>
<file>assets/img/icons/face-id.svg</file>

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Arrow / Collapse">
<g id="Icon">
<path d="M12.7959 4.65686C12.7959 4.24264 13.1317 3.90686 13.5459 3.90686C13.9602 3.90686 14.2959 4.24264 14.2959 4.65686L14.2959 7.29594C14.2959 7.74139 14.8345 7.96448 15.1495 7.6495L19.3293 3.46967C19.6222 3.17678 20.0971 3.17678 20.39 3.46967C20.6829 3.76256 20.6829 4.23744 20.39 4.53033L16.2102 8.71016C15.8952 9.02514 16.1183 9.56371 16.5637 9.56371L19.2028 9.56371C19.617 9.56371 19.9528 9.89949 19.9528 10.3137C19.9528 10.7279 19.617 11.0637 19.2028 11.0637L13.5459 11.0637C13.1317 11.0637 12.7959 10.7279 12.7959 10.3137L12.7959 4.65686Z" fill="#939BA1"/>
<path d="M4.54594 12.7647C4.13173 12.7647 3.79594 13.1005 3.79594 13.5147C3.79594 13.9289 4.13173 14.2647 4.54594 14.2647L7.18503 14.2647C7.63048 14.2647 7.85356 14.8033 7.53858 15.1183L3.35875 19.2981C3.06586 19.591 3.06586 20.0659 3.35876 20.3588C3.65165 20.6517 4.12652 20.6517 4.41942 20.3588L8.59924 16.1789C8.91422 15.864 9.45279 16.087 9.45279 16.5325L9.45279 19.1716C9.45279 19.5858 9.78858 19.9216 10.2028 19.9216C10.617 19.9216 10.9528 19.5858 10.9528 19.1716V13.5147C10.9528 13.1005 10.617 12.7647 10.2028 12.7647L4.54594 12.7647Z" fill="#939BA1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Arrow / Expand">
<g id="Icon">
<path d="M3.21016 14.2426C3.21016 13.8284 3.54594 13.4926 3.96016 13.4926C4.37437 13.4926 4.71016 13.8284 4.71016 14.2426L4.71015 16.8817C4.71015 17.3272 5.24873 17.5503 5.56371 17.2353L9.74353 13.0555C10.0364 12.7626 10.5113 12.7626 10.8042 13.0555C11.0971 13.3484 11.0971 13.8232 10.8042 14.1161L6.62437 18.2959C6.30939 18.6109 6.53247 19.1495 6.97792 19.1495L9.61701 19.1495C10.0312 19.1495 10.367 19.4853 10.367 19.8995C10.367 20.3137 10.0312 20.6495 9.61701 20.6495L3.96016 20.6495C3.54594 20.6495 3.21016 20.3137 3.21016 19.8995L3.21016 14.2426Z" fill="#939BA1"/>
<path d="M13.9602 3.35051C13.5459 3.35051 13.2102 3.68629 13.2102 4.10051C13.2102 4.51472 13.5459 4.85051 13.9602 4.85051L16.5992 4.85051C17.0447 4.85051 17.2678 5.38908 16.9528 5.70406L12.773 9.88388C12.4801 10.1768 12.4801 10.6517 12.773 10.9445C13.0659 11.2374 13.5407 11.2374 13.8336 10.9445L18.0135 6.76472C18.3284 6.44974 18.867 6.67282 18.867 7.11828L18.867 9.75736C18.867 10.1716 19.2028 10.5074 19.617 10.5074C20.0312 10.5074 20.367 10.1716 20.367 9.75736L20.367 4.10051C20.367 3.68629 20.0312 3.35051 19.617 3.35051L13.9602 3.35051Z" fill="#939BA1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -11,13 +11,15 @@ import shared.popups.walletconnect 1.0
import AppLayouts.Wallet.services.dapps 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
import shared.stores 1.0
import utils 1.0
DappsComboBox {
id: root
required property WalletConnectService wcService
// Values mapped to Constants.LoginType
required property int loginType
property string selectedAccountAddress
signal pairWCReady()
@ -85,7 +87,7 @@ DappsComboBox {
}
]
}
selectedAccountAddress: root.wcService.selectedAccountAddress
selectedAccountAddress: root.selectedAccountAddress
dAppUrl: proposalMedatada.url
dAppName: proposalMedatada.name
@ -116,26 +118,62 @@ DappsComboBox {
property SessionRequestResolved request: null
sourceComponent: DAppRequestModal {
account: request.account
network: request.network
dappName: request.dappName
dappUrl: request.dappUrl
dappIcon: request.dappIcon
payloadData: request.data
method: request.method
maxFeesText: request.maxFeesText
maxFeesEthText: request.maxFeesEthText
enoughFunds: request.enoughFunds
estimatedTimeText: request.estimatedTimeText
sourceComponent: DAppSignRequestModal {
objectName: "dappsRequestModal"
loginType: request.account.migragedToKeycard ? Constants.LoginType.Keycard : root.loginType
visible: true
onClosed: sessionRequestLoader.active = false
dappUrl: request.dappUrl
dappIcon: request.dappIcon
dappName: request.dappName
onSign: {
accountColor: request.account.color
accountName: request.account.name
accountAddress: request.account.address
accountEmoji: request.account.emoji
networkName: request.network.chainName
networkIconPath: Style.svg(request.network.iconUrl)
currentCurrency: ""
fiatFees: request.maxFeesText
cryptoFees: request.maxFeesEthText
estimatedTime: request.estimatedTimeText
feesLoading: !request.maxFeesText || !request.maxFeesEthText
hasFees: signingTransaction
enoughFundsForTransaction: request.enoughFunds
enoughFundsForFees: request.enoughFunds
signingTransaction: request.method === SessionRequest.methods.signTransaction.name || request.method === SessionRequest.methods.sendTransaction.name
requestPayload: {
switch(request.method) {
case SessionRequest.methods.personalSign.name:
return SessionRequest.methods.personalSign.getMessageFromData(request.data)
case SessionRequest.methods.sign.name: {
return SessionRequest.methods.sign.getMessageFromData(request.data)
}
case SessionRequest.methods.signTypedData_v4.name: {
const stringPayload = SessionRequest.methods.signTypedData_v4.getMessageFromData(request.data)
return JSON.stringify(JSON.parse(stringPayload), null, 2)
}
case SessionRequest.methods.signTypedData.name: {
const stringPayload = SessionRequest.methods.signTypedData.getMessageFromData(root.payloadData)
return JSON.stringify(JSON.parse(stringPayload), null, 2)
}
case SessionRequest.methods.signTransaction.name: {
const jsonPayload = SessionRequest.methods.signTransaction.getTxObjFromData(request.data)
return JSON.stringify(jsonPayload, null, 2)
}
case SessionRequest.methods.sendTransaction.name: {
const jsonPayload = SessionRequest.methods.sendTransaction.getTxObjFromData(request.data)
return JSON.stringify(jsonPayload, null, 2)
}
}
}
onClosed: Qt.callLater( () => sessionRequestLoader.active = false)
onAccepted: {
if (!request) {
console.error("Error signing: request is null")
return
@ -143,28 +181,32 @@ DappsComboBox {
root.wcService.requestHandler.authenticate(request)
}
onReject: {
onRejected: {
let userRejected = true
root.wcService.requestHandler.rejectSessionRequest(request, userRejected)
close()
}
Connections {
target: root.wcService.requestHandler
function onMaxFeesUpdated(maxFees, maxFeesWei, haveEnoughFunds, symbol) {
maxFeesText = `${maxFees.toFixed(2)} ${symbol}`
fiatFees = maxFees
currentCurrency = symbol
var ethStr = "?"
try {
ethStr = globalUtils.wei2Eth(maxFeesWei, 9)
} catch (e) {
// ignore error in case of tests and storybook where we don't have access to globalUtils
}
maxFeesEthText = `${ethStr} ETH`
enoughFunds = haveEnoughFunds
cryptoFees = ethStr
enoughFundsForTransaction = haveEnoughFunds
enoughFundsForFees = haveEnoughFunds
feesLoading = false
hasFees = !!maxFees
}
function onEstimatedTimeUpdated(minMinutes, maxMinutes) {
estimatedTimeText = qsTr("%1-%2mins").arg(minMinutes).arg(maxMinutes)
estimatedTime = qsTr("%1-%2mins").arg(minMinutes).arg(maxMinutes)
}
}
}

View File

@ -128,6 +128,8 @@ Item {
enabled: !!Global.walletConnectService
wcService: Global.walletConnectService
loginType: root.store.loginType
selectedAccountAddress: root.walletStore.selectedAddress
}
StatusButton {

View File

@ -2,6 +2,7 @@ import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import QtGraphicalEffects 1.15
import StatusQ 0.1
import StatusQ.Core 0.1
@ -24,6 +25,7 @@ StatusDialog {
property Component headerIconComponent
property bool feesLoading
property bool signButtonEnabled: true
property ObjectModel leftFooterContents
property ObjectModel rightFooterContents: ObjectModel {
@ -39,7 +41,7 @@ StatusDialog {
StatusButton {
objectName: "signButton"
id: signButton
interactive: !root.feesLoading
interactive: !root.feesLoading && root.signButtonEnabled
icon.name: Constants.authenticationIconByType[root.loginType]
text: qsTr("Sign")
onClicked: root.accept() // close and emit accepted() signal
@ -51,11 +53,14 @@ StatusDialog {
property url fromImageSource
property alias fromImageSmartIdenticon: fromImageSmartIdenticon
property url toImageSource
readonly property alias toImageSmartIdenticon: toImageSmartIdenticon
property alias headerMainText: headerMainText.text
property alias headerSubTextLayout: headerSubTextLayout.children
readonly property alias headerSubTextLayout: headerSubTextLayout.children
property string infoTagText
readonly property alias infoTag: infoTag
property bool showHeaderDivider: true
default property alias contents: contentsLayout.children
default property alias contents: contentsLayout.data
width: 480
padding: 0
@ -127,26 +132,48 @@ StatusDialog {
id: fromImageSmartIdenticon
width: 40
height: 40
asset.name: root.fromImageSource
asset.width: 40
asset.height: 40
asset.bgWidth: 40
asset.bgHeight: 40
asset.color: "transparent"
asset.bgColor: "transparent"
visible: !!asset.name
layer.enabled: toImageSmartIdenticon.visible
layer.effect: OpacityMask {
id: mask
invert: true
maskSource: Item {
width: mask.width + 4
height: mask.height + 4
Rectangle {
anchors.centerIn: parent
anchors.horizontalCenterOffset: toImageSmartIdenticon.width - 10
width: parent.width
height: width
radius: width / 2
}
}
}
}
StatusRoundedImage {
objectName: "fromImage"
width: 42
height: 42
border.width: 2
border.color: "transparent"
image.source: root.fromImageSource
visible: root.fromImageSource.toString() !== ""
}
StatusRoundedImage {
objectName: "toImage"
width: 42
height: 42
border.width: 2
border.color: Theme.palette.statusBadge.foregroundColor
image.source: root.toImageSource
StatusSmartIdenticon {
objectName: "toImageIdenticon"
id: toImageSmartIdenticon
width: 40
height: 40
asset.bgWidth: 40
asset.bgHeight: 40
visible: !!asset.name || !!asset.source
asset.name: root.toImageSource
asset.width: 40
asset.height: 40
asset.color: "transparent"
asset.bgColor: "transparent"
}
}
@ -182,6 +209,7 @@ StatusDialog {
StatusDialogDivider {
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
visible: root.showHeaderDivider
}
ColumnLayout {

View File

@ -159,6 +159,13 @@ QObject {
obj.resolveDappInfoFromSession(session)
root.sessionRequest(obj)
// TODO #15192: update maxFees
if (!event.params.request.params[0].gasLimit || !event.params.request.params[0].gasPrice) {
root.maxFeesUpdated(0, 0, true, "")
root.estimatedTimeUpdated(0, 0)
return
}
let gasLimit = parseFloat(parseInt(event.params.request.params[0].gasLimit, 16));
let gasPrice = parseFloat(parseInt(event.params.request.params[0].gasPrice, 16));
let maxFees = gasLimit * gasPrice

View File

@ -32,8 +32,6 @@ QObject {
required property DAppsStore store
required property var walletRootStore
readonly property string selectedAccountAddress: walletRootStore.selectedAddress
readonly property alias dappsModel: dappsProvider.dappsModel
readonly property alias requestHandler: requestHandler

View File

@ -1,362 +0,0 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ.Core 0.1
import StatusQ.Popups.Dialog 0.1
import StatusQ.Controls 0.1
import StatusQ.Components 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as StatusQ
import shared.popups.walletconnect.panels 1.0
import utils 1.0
import AppLayouts.Wallet.services.dapps.types 1.0
StatusDialog {
id: root
objectName: "dappsRequestModal"
implicitWidth: 480
required property string dappName
required property string dappUrl
required property url dappIcon
required property string method
required property var payloadData
property string maxFeesText: ""
property string maxFeesEthText: ""
property bool enoughFunds: false
property string estimatedTimeText: ""
required property var account
property var network: null
signal sign()
signal reject()
title: qsTr("Sign request")
padding: 20
onPayloadDataChanged: d.updateDisplay()
onMethodChanged: d.updateDisplay()
Component.onCompleted: d.updateDisplay()
contentItem: StatusScrollView {
id: scrollView
padding: 0
ColumnLayout {
spacing: 20
clip: true
width: scrollView.availableWidth
IntentionPanel {
Layout.fillWidth: true
dappName: root.dappName
dappIcon: root.dappIcon
account: root.account
userDisplayNaming: d.userDisplayNaming
}
ContentPanel {
Layout.fillWidth: true
Layout.maximumHeight: 340
payloadToDisplay: d.payloadToDisplay
}
// TODO: externalize as a TargetPanel
ColumnLayout {
spacing: 8
StatusBaseText {
text: qsTr("Sign with")
font.pixelSize: 13
color: Theme.palette.directColor1
}
// TODO #14762: implement proper control to display the accounts details
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 76
radius: 8
border.width: 1
border.color: Theme.palette.baseColor2
color: "transparent"
RowLayout {
spacing: 12
anchors.fill: parent
anchors.margins: 16
StatusSmartIdenticon {
width: 40
height: 40
asset: StatusAssetSettings {
color: Theme.palette.primaryColor1
isImage: false
isLetterIdenticon: true
useAcronymForLetterIdenticon: false
emoji: root.account.emoji
}
}
ColumnLayout {
Layout.alignment: Qt.AlignLeft
StatusBaseText {
text: root.account.name
Layout.alignment: Qt.AlignLeft
font.pixelSize: 13
}
StatusBaseText {
text: StatusQ.Utils.elideAndFormatWalletAddress(root.account.address, 6, 4)
Layout.alignment: Qt.AlignLeft
font.pixelSize: 13
color: Theme.palette.baseColor1
}
}
Item {Layout.fillWidth: true }
}
}
StatusBaseText {
text: qsTr("Network")
font.pixelSize: 13
color: Theme.palette.directColor1
}
// TODO #14762: implement proper control to display the chain
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 76
visible: root.network !== null
radius: 8
border.width: 1
border.color: Theme.palette.baseColor2
color: "transparent"
RowLayout {
spacing: 12
anchors.fill: parent
anchors.margins: 16
StatusSmartIdenticon {
width: 40
height: 40
asset: StatusAssetSettings {
isImage: true
name: !!root.network ? Style.svg("tiny/" + root.network.iconUrl) : ""
}
}
StatusBaseText {
text: !!root.network ? root.network.chainName : ""
Layout.alignment: Qt.AlignLeft
font.pixelSize: 13
}
Item {Layout.fillWidth: true }
}
}
StatusBaseText {
text: qsTr("Fees")
font.pixelSize: 13
color: Theme.palette.directColor1
visible: d.isTransaction()
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 76
visible: root.network !== null && d.isTransaction()
radius: 8
border.width: 1
border.color: Theme.palette.baseColor2
color: "transparent"
RowLayout {
spacing: 12
anchors.fill: parent
anchors.margins: 16
StatusBaseText {
text: qsTr("Max. fees on %1").arg(!!root.network && root.network.chainName)
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
font.pixelSize: 13
color: Theme.palette.baseColor1
}
Item {Layout.fillWidth: true }
ColumnLayout {
StatusBaseText {
text: root.maxFeesText
Layout.alignment: Qt.AlignRight
font.pixelSize: 13
color: root.enoughFunds ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
StatusBaseText {
text: root.maxFeesEthText
Layout.alignment: Qt.AlignRight
font.pixelSize: 13
color: root.enoughFunds ? Theme.palette.baseColor1 : Theme.palette.dangerColor1
}
}
}
}
}
}
}
header: StatusDialogHeader {
leftComponent: Item {
width: 46
height: 46
StatusSmartIdenticon {
anchors.fill: parent
anchors.margins: 3
asset: StatusAssetSettings {
width: 40
height: 40
bgRadius: bgWidth / 2
imgIsIdenticon: false
isImage: true
useAcronymForLetterIdenticon: false
name: root.dappIcon
}
bridgeBadge.visible: true
bridgeBadge.width: 16
bridgeBadge.height: 16
bridgeBadge.image.source: "assets/sign.svg"
bridgeBadge.border.width: 3
bridgeBadge.border.color: "transparent"
bridgeBadge.color: Theme.palette.miscColor1
}
}
headline.title: qsTr("Sign request")
headline.subtitle: root.dappUrl
}
footer: StatusDialogFooter {
id: footer
leftButtons: ObjectModel {
MaxFeesDisplay {
maxFeesText: root.maxFeesText
feesTextColor: root.enoughFunds ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
Item {
width: 20
}
EstimatedTimeDisplay {
estimatedTimeText: root.estimatedTimeText
visible: !!root.estimatedTimeText
}
}
rightButtons: ObjectModel {
StatusButton {
objectName: "rejectButton"
height: 44
text: qsTr("Reject")
onClicked: {
root.reject()
}
}
StatusButton {
height: 44
text: qsTr("Sign")
onClicked: {
root.sign()
}
}
}
}
QtObject {
id: d
property string payloadToDisplay: ""
property string userDisplayNaming: ""
function isTransaction() {
return root.method === SessionRequest.methods.signTransaction.name || root.method === SessionRequest.methods.sendTransaction.name
}
function updateDisplay() {
if (!root.payloadData)
return
switch (root.method) {
case SessionRequest.methods.personalSign.name: {
payloadToDisplay = SessionRequest.methods.personalSign.getMessageFromData(root.payloadData)
userDisplayNaming = SessionRequest.methods.personalSign.requestDisplay
break
}
case SessionRequest.methods.sign.name: {
payloadToDisplay = SessionRequest.methods.sign.getMessageFromData(root.payloadData)
userDisplayNaming = SessionRequest.methods.sign.requestDisplay
break
}
case SessionRequest.methods.signTypedData_v4.name: {
let messageObject = SessionRequest.methods.signTypedData_v4.getMessageFromData(root.payloadData)
payloadToDisplay = JSON.stringify(JSON.parse(messageObject), null, 2)
userDisplayNaming = SessionRequest.methods.signTypedData_v4.requestDisplay
break
}
case SessionRequest.methods.signTypedData.name: {
let messageObject = SessionRequest.methods.signTypedData.getMessageFromData(root.payloadData)
payloadToDisplay = JSON.stringify(JSON.parse(messageObject), null, 2)
userDisplayNaming = SessionRequest.methods.signTypedData.requestDisplay
break
}
case SessionRequest.methods.signTransaction.name: {
let tx = SessionRequest.methods.signTransaction.getTxObjFromData(root.payloadData)
payloadToDisplay = JSON.stringify(tx, null, 2)
userDisplayNaming = SessionRequest.methods.signTransaction.requestDisplay
break
}
case SessionRequest.methods.sendTransaction.name: {
let tx = SessionRequest.methods.sendTransaction.getTxObjFromData(root.payloadData)
payloadToDisplay = JSON.stringify(tx, null, 2)
userDisplayNaming = SessionRequest.methods.sendTransaction.requestDisplay
break
}
}
}
}
}

View File

@ -0,0 +1,192 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtQml.Models 2.15
import StatusQ 0.1
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Core.Utils 0.1 as SQUtils
import AppLayouts.Wallet.popups 1.0
import AppLayouts.Wallet.panels 1.0
import shared.popups.walletconnect.panels 1.0
import utils 1.0
SignTransactionModalBase {
id: root
required property bool signingTransaction
// DApp info
required property url dappUrl
required property url dappIcon
required property string dappName
// Payload to sign
required property string requestPayload
// Account
required property color accountColor
required property string accountName
required property string accountEmoji
required property string accountAddress
// Network
required property string networkName
required property string networkIconPath
// Fees
required property string currentCurrency
required property string fiatFees
required property string cryptoFees
required property string estimatedTime
required property bool hasFees
property bool enoughFundsForTransaction: true
property bool enoughFundsForFees: false
signButtonEnabled: enoughFundsForTransaction && enoughFundsForFees
title: qsTr("Sign Request")
subtitle: SQUtils.StringUtils.extractDomainFromLink(root.dappUrl)
headerIconComponent: RoundImageWithBadge {
imageUrl: root.dappIcon
width: 40
height: 40
}
gradientColor: Utils.setColorAlpha(root.accountColor, 0.05) // 5% of wallet color
headerMainText: root.signingTransaction ? qsTr("%1 wants you to sign this transaction with %2").arg(root.dappName).arg(root.accountName) :
qsTr("%1 wants you to sign this message with %2").arg(root.dappName).arg(root.accountName)
fromImageSmartIdenticon.asset.name: "filled-account"
fromImageSmartIdenticon.asset.emoji: root.accountEmoji
fromImageSmartIdenticon.asset.color: root.accountColor
fromImageSmartIdenticon.asset.isLetterIdenticon: !!root.accountEmoji
toImageSmartIdenticon.asset.name: Style.svg("sign")
toImageSmartIdenticon.asset.bgColor: Theme.palette.primaryColor3
toImageSmartIdenticon.asset.width: 24
toImageSmartIdenticon.asset.height: 24
toImageSmartIdenticon.asset.color: Theme.palette.primaryColor1
infoTagText: qsTr("Only sign if you trust the dApp")
infoTag.states: [
State {
name: "insufficientFunds"
when: !root.enoughFundsForTransaction
PropertyChanges {
target: infoTag
asset.color: Theme.palette.dangerColor1
tagPrimaryLabel.color: Theme.palette.dangerColor1
backgroundColor: Theme.palette.dangerColor3
bgBorderColor: Theme.palette.dangerColor2
tagPrimaryLabel.text: qsTr("Insufficient funds for transaction")
}
}
]
showHeaderDivider: !root.requestPayload
leftFooterContents: ObjectModel {
RowLayout {
Layout.leftMargin: 4
spacing: Style.current.bigPadding
ColumnLayout {
spacing: 2
StatusBaseText {
text: qsTr("Max fees:")
color: Theme.palette.baseColor1
font.pixelSize: Style.current.additionalTextSize
}
StatusTextWithLoadingState {
Layout.fillWidth: true
objectName: "footerFiatFeesText"
text: "%1 %2".arg(formatBigNumber(root.fiatFees)).arg(root.currentCurrency)
loading: root.feesLoading
customColor: root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
elide: Qt.ElideMiddle
Binding on text {
when: !root.hasFees
value: qsTr("No fees")
}
}
}
ColumnLayout {
spacing: 2
visible: root.hasFees
StatusBaseText {
text: qsTr("Est. time:")
color: Theme.palette.baseColor1
font.pixelSize: Style.current.additionalTextSize
}
StatusTextWithLoadingState {
objectName: "footerEstimatedTime"
text: root.estimatedTime
loading: root.feesLoading
}
}
}
}
// Payload
ContentPanel {
Layout.fillWidth: true
Layout.fillHeight: true
payloadToDisplay: root.requestPayload
visible: !!root.requestPayload
}
// Account
SignInfoBox {
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
objectName: "accountBox"
caption: qsTr("Sign with")
primaryText: root.accountName
secondaryText: SQUtils.Utils.elideAndFormatWalletAddress(root.accountAddress)
asset.name: "filled-account"
asset.emoji: root.accountEmoji
asset.color: root.accountColor
asset.isLetterIdenticon: !!root.accountEmoji
}
// Network
SignInfoBox {
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
objectName: "networkBox"
caption: qsTr("Network")
primaryText: root.networkName
icon: root.networkIconPath
}
// Fees
SignInfoBox {
Layout.fillWidth: true
Layout.bottomMargin: Style.current.bigPadding
objectName: "feesBox"
caption: qsTr("Fees")
primaryText: qsTr("Max. fees on %1").arg(root.networkName)
secondaryText: " "
enabled: false
visible: root.hasFees
components: [
ColumnLayout {
spacing: 2
StatusTextWithLoadingState {
objectName: "fiatFeesText"
Layout.alignment: Qt.AlignRight
text: "%1 %2".arg(formatBigNumber(root.fiatFees)).arg(root.currentCurrency)
horizontalAlignment: Text.AlignRight
font.pixelSize: Style.current.additionalTextSize
loading: root.feesLoading
customColor: root.enoughFundsForFees ? Theme.palette.directColor1 : Theme.palette.dangerColor1
}
StatusTextWithLoadingState {
objectName: "cryptoFeesText"
Layout.alignment: Qt.AlignRight
text: "%1 ETH".arg(formatBigNumber(root.cryptoFees))
horizontalAlignment: Text.AlignRight
font.pixelSize: Style.current.additionalTextSize
customColor: root.enoughFundsForFees ? Theme.palette.baseColor1 : Theme.palette.dangerColor1
loading: root.feesLoading
}
}
]
}
}

View File

@ -39,15 +39,15 @@ Item {
anchors.fill: mainImage
active: !mainImage.visible
sourceComponent: StatusRoundedComponent {
id: imageWrapper
color: Theme.palette.primaryColor3
StatusIcon {
anchors.fill: parent
anchors.margins: Style.current.padding
anchors.fill: imageWrapper
anchors.margins: imageWrapper.width / 4.5
color: Theme.palette.primaryColor1
icon: "dapp"
}
}
}
} }
layer.enabled: true
layer.effect: OpacityMask {
@ -72,7 +72,7 @@ Item {
StatusRoundIcon {
id: badge
width: (root.width / 2) - Style.current.padding
width: root.width / 3.6
height: width
anchors.bottom: parent.bottom
anchors.right: parent.right

View File

@ -1,9 +1,13 @@
import QtQuick 2.15
import QtQuick.Layouts 1.15
import QtGraphicalEffects 1.15
import StatusQ.Controls 0.1
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import utils 1.0
Rectangle {
id: root
@ -14,40 +18,60 @@ Rectangle {
color: "transparent"
radius: 8
implicitHeight: contentScrollView.implicitHeight + (2 * contentText.anchors.margins)
implicitHeight: d.expanded ? contentText.implicitHeight + (2 * contentText.anchors.margins) :
Math.min(contentText.implicitHeight + (2 * contentText.anchors.margins), 200)
MouseArea {
anchors.fill: parent
cursorShape: contentScrollView.enabled || !enabled ? undefined : Qt.PointingHandCursor
enabled: contentScrollView.height < contentScrollView.contentHeight
onClicked: {
contentScrollView.enabled = !contentScrollView.enabled
}
z: contentScrollView.z + 1
HoverHandler {
id: hoverHandler
target: root
}
StatusScrollView {
id: contentScrollView
StatusBaseText {
id: contentText
objectName: "textContent"
anchors.fill: parent
anchors.margins: 20
contentWidth: availableWidth
contentHeight: contentText.contentHeight
text: root.payloadToDisplay
font.pixelSize: Style.current.additionalTextSize
lineHeightMode: Text.FixedHeight
lineHeight: 18
padding: 0
wrapMode: Text.WrapAnywhere
enabled: false
StatusBaseText {
id: contentText
anchors.fill: parent
anchors.margins: 20
width: contentScrollView.availableWidth
text: root.payloadToDisplay
wrapMode: Text.WrapAnywhere
StatusFlatButton {
objectName: "expandButton"
anchors.top: parent.top
anchors.right: parent.right
icon.name: d.expanded ? "collapse" : "expand"
icon.color: hovered ? Theme.palette.directColor1 : Theme.palette.baseColor1
hoverColor: "transparent"
visible: d.canExpand && hoverHandler.hovered
onClicked: {
d.expanded = !d.expanded
}
}
layer.enabled: !d.expanded && d.canExpand
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0 }
GradientStop { position: (root.height - 60) / root.height }
GradientStop { position: 1; color: "transparent" }
}
}
}
}
QtObject {
id: d
readonly property int maxContentHeight: 350
property bool expanded: false
property bool canExpand: contentText.implicitHeight > maxContentHeight
}
}

View File

@ -2,6 +2,6 @@ PairWCModal 1.0 PairWCModal.qml
DAppsListPopup 1.0 DAppsListPopup.qml
ConnectDAppModal 1.0 ConnectDAppModal.qml
ConnectionStatusTag 1.0 ConnectionStatusTag.qml
DAppRequestModal 1.0 DAppRequestModal.qml
DAppSignRequestModal 1.0 DAppSignRequestModal.qml
DAppsUriCopyInstructionsPopup 1.0 DAppsUriCopyInstructionsPopup.qml
RoundImageWithBadge 1.0 RoundImageWithBadge.qml

View File

@ -6,7 +6,6 @@ QObject {
id: root
required property var controller
/// \c dappsJson serialized from status-go.wallet.GetDapps
signal dappsListReceived(string dappsJson)
signal userAuthenticated(string topic, string id, string password, string pin)