Skip to content

Commit

Permalink
feat(WalletConnect): Handle sign request expiration
Browse files Browse the repository at this point in the history
Implementing the user-story for sign request expiry and add qml tests
+ other minor fixes

## Acceptance Criteria

```
//Always show the expiration
Given the sign/transaction request dialog is shown
When request has an expiration date
Then the user sees a 1 minute countdown in the dialog
```

```
// Show 1 minute timer
Given the sign/transaction request dialog is shown
When the request has 1 minute or less before expiring
Then the user sees a 1 second countdown in the dialog
```

```
Given the sign/transaction dialog is open
When the request expires
Then the Accept button is removed
And the only option for the user is to close the dialog
```

```
Given the sign/transaction request dialog is open
When the request expired
Then the `Sign` and `Reject` buttons are removed
And the `Close` button is visible
```

```
Given the sign/transaction request expired
Then a toast message is showing
And it contains the "<dapp domain> sign request timed out" message
```

```
Given the sign/transaction request dialog is open
When the request expired
Then the sign/transaction request dialog is still visible
```

```
Given the sign/transaction request expires
Then a console message is shown
And it contains 'WC WalletConnectSDK.onSessionRequestExpire; id: ${id}`'
```
  • Loading branch information
alexjba committed Oct 9, 2024
1 parent 63bcc8f commit c498580
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 44 deletions.
3 changes: 2 additions & 1 deletion storybook/qmlTests/tests/helpers/wallet_connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,14 @@ function formatApproveSessionResponse(networksArray, accountsArray, custom) {

function formatSessionRequest(chainId, method, params, topic, requestId) {
const reqId = requestId || 1717149885151715
const expiry = Date.now() / 1000 + 6000
let paramsStr = params.map(param => `${param}`).join(',')
return `{
"id": ${reqId},
"params": {
"chainId": "eip155:${chainId}",
"request": {
"expiryTimestamp": 1717150185,
"expiryTimestamp": ${expiry},
"method": "${method}",
"params": [${paramsStr}]
}
Expand Down
224 changes: 220 additions & 4 deletions storybook/qmlTests/tests/tst_DAppsWorkflow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Item {
chainId: network,
data: "hello world",
preparedData: "hello world",
expirationTimestamp: Date.now() + 1000,
expirationTimestamp: (Date.now() + 10000) / 1000,
sourceId: Constants.DAppConnectors.WalletConnect
})

Expand Down Expand Up @@ -114,8 +114,8 @@ Item {
}

property var onDisplayToastMessageTriggers: []
onDisplayToastMessage: function(message, error) {
onDisplayToastMessageTriggers.push({message, error})
onDisplayToastMessage: function(message, type) {
onDisplayToastMessageTriggers.push({message, type})
}

property var onPairingValidatedTriggers: []
Expand Down Expand Up @@ -444,6 +444,150 @@ Item {
compare(request.haveEnoughFees, data.expect.haveEnoughForFees, "expected haveEnoughForFees to be set")
verify(!!request.feesInfo, "expected feesInfo to be set")
}

function test_sessionRequestExpiryInTheFuture() {
const sdk = handler.sdk
const testAddressUpper = "0x3A"
const chainId = 2
const method = "personal_sign"
const message = "hello world"
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))

verify(session.params.request.expiryTimestamp > Date.now() / 1000, "expected expiryTimestamp to be in the future")

// Expect to have calls to getActiveSessions from service initialization
const prevRequests = sdk.getActiveSessionsCallbacks.length
sdk.sessionRequestEvent(session)

verify(handler.requestsModel.count === 1, "expected a request to be added")
const request = handler.requestsModel.findRequest(topic, session.id)
verify(!!request, "expected request to be found")
verify(!request.isExpired(), "expected request to not be expired")
}

function test_sessionRequestExpiryInThePast()
{
const sdk = handler.sdk
const testAddressUpper = "0x3A"
const chainId = 2
const method = "personal_sign"
const message = "hello world"
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
session.params.request.expiryTimestamp = (Date.now() - 10000) / 1000

verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")

sdk.sessionRequestEvent(session)

verify(handler.requestsModel.count === 1, "expected a request to be added")
const request = handler.requestsModel.findRequest(topic, session.id)
verify(!!request, "expected request to be found")
verify(request.isExpired(), "expected request to be expired")
verify(displayToastMessageSpy.count === 0, "no toast message should be displayed")
}

function test_wcSignalsSessionRequestExpiry()
{
const sdk = handler.sdk
const testAddressUpper = "0x3A"
const chainId = 2
const method = "personal_sign"
const message = "hello world"
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))

verify(session.params.request.expiryTimestamp > Date.now() / 1000, "expected expiryTimestamp to be in the future")
sdk.sessionRequestEvent(session)
const request = handler.requestsModel.findRequest(topic, session.id)
verify(!!request, "expected request to be found")
verify(!request.isExpired(), "expected request to not be expired")

sdk.sessionRequestExpired(session.id)
verify(request.isExpired(), "expected request to be expired")
verify(displayToastMessageSpy.count === 0, "no toast message should be displayed")
}

function test_acceptExpiredSessionRequest()
{
const sdk = handler.sdk
const testAddressUpper = "0x3A"
const chainId = 2
const method = "personal_sign"
const message = "hello world"
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
session.params.request.expiryTimestamp = (Date.now() - 10000) / 1000

verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")

sdk.sessionRequestEvent(session)

verify(handler.requestsModel.count === 1, "expected a request to be added")
const request = handler.requestsModel.findRequest(topic, session.id)
request.resolveDappInfoFromSession({peer: {metadata: {name: "Test DApp", url: "https://test.dapp", icons:[]}}})
verify(!!request, "expected request to be found")
verify(request.isExpired(), "expected request to be expired")
verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")

ignoreWarning("Error: request expired")
handler.store.userAuthenticated(topic, session.id, "1234", "", message)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
sdk.sessionRequestUserAnswerResult(topic, session.id, false, "")
verify(displayToastMessageSpy.count === 1, "expected a toast message to be displayed")
compare(displayToastMessageSpy.signalArguments[0][0], "test.dapp sign request timed out")
}

function test_rejectExpiredSessionRequest()
{
const sdk = handler.sdk
const testAddressUpper = "0x3A"
const chainId = 2
const method = "personal_sign"
const message = "hello world"
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
session.params.request.expiryTimestamp = (Date.now() - 10000) / 1000

verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")

sdk.sessionRequestEvent(session)

verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")

ignoreWarning("Error: request expired")
handler.store.userAuthenticationFailed(topic, session.id)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
}

function test_signFailedAuthOnExpiredRequest()
{
const sdk = handler.sdk
const testAddressUpper = "0x3A"
const chainId = 2
const method = "personal_sign"
const message = "hello world"
const params = [`"${DAppsHelpers.strToHex(message)}"`, `"${testAddressUpper}"`]
const topic = "b536a"
const session = JSON.parse(Testing.formatSessionRequest(chainId, method, params, topic))
session.params.request.expiryTimestamp = (Date.now() - 10000) / 1000

verify(session.params.request.expiryTimestamp < Date.now() / 1000, "expected expiryTimestamp to be in the past")

sdk.sessionRequestEvent(session)

verify(sdk.rejectSessionRequestCalls.length === 0, "expected no call to sdk.rejectSessionRequest")

ignoreWarning("Error: request expired")
handler.store.userAuthenticationFailed(topic, session.id)
verify(sdk.rejectSessionRequestCalls.length === 1, "expected a call to sdk.rejectSessionRequest")
}
}

TestCase {
Expand Down Expand Up @@ -561,7 +705,7 @@ Item {
verify(service.onApproveSessionResultTriggers[0].session, "expected session to be set")

compare(service.onDisplayToastMessageTriggers.length, 1, "expected a success message to be displayed")
verify(!service.onDisplayToastMessageTriggers[0].error, "expected no error")
verify(service.onDisplayToastMessageTriggers[0].type !== Constants.ephemeralNotificationType.danger, "expected no error")
verify(service.onDisplayToastMessageTriggers[0].message, "expected message to be set")
}

Expand Down Expand Up @@ -1027,5 +1171,77 @@ Item {
verify(!popup.opened)
verify(!popup.visible)
}

function test_SignRequestExpired() {
const topic = "abcd"
const requestId = "12345"
let popup = showRequestModal(topic, requestId)

const request = controlUnderTest.sessionRequestsModel.findRequest(topic, requestId)
verify(!!request)

const countDownPill = findChild(popup, "countdownPill")
verify(!!countDownPill)
tryVerify(() => countDownPill.remainingSeconds > 0)
// Hackish -> countdownPill internals ask for a refresh before going to expired state
const remainingSeconds = countDownPill.remainingSeconds
tryVerify(() => countDownPill.visible)
tryVerify(() => countDownPill.remainingSeconds !== remainingSeconds)

request.setExpired()
tryVerify(() => countDownPill.isExpired)
verify(countDownPill.visible)

const signButton = findChild(popup, "signButton")
const rejectButton = findChild(popup, "rejectButton")
const closeButton = findChild(popup, "closeButton")

tryVerify(() => !signButton.visible)
verify(!rejectButton.visible)
verify(closeButton.visible)
}

function test_SignRequestDoesWithoutExpiry()
{
const topic = "abcd"
const requestId = "12345"
let popup = showRequestModal(topic, requestId)

const request = controlUnderTest.sessionRequestsModel.findRequest(topic, requestId)
verify(!!request)
request.expirationTimestamp = 0

const countDownPill = findChild(popup, "countdownPill")
verify(!!countDownPill)
tryVerify(() => !countDownPill.visible)

request.setExpired()
tryVerify(() => !countDownPill.visible)

const signButton = findChild(popup, "signButton")
const rejectButton = findChild(popup, "rejectButton")
const closeButton = findChild(popup, "closeButton")

verify(signButton.visible)
verify(rejectButton.visible)
verify(!closeButton.visible)
}

function test_SignRequestModalAfterModelRemove()
{
const topic = "abcd"
const requestId = "12345"
let popup = showRequestModal(topic, requestId)

const request = controlUnderTest.sessionRequestsModel.findRequest(topic, requestId)
verify(!!request)

controlUnderTest.sessionRequestsModel.removeRequest(topic, requestId)
verify(!controlUnderTest.sessionRequestsModel.findRequest(topic, requestId))

waitForRendering(controlUnderTest)
popup = findChild(controlUnderTest, "dappsRequestModal")
verify(!popup)
}
}
}
2 changes: 2 additions & 0 deletions ui/app/AppLayouts/Wallet/panels/DAppsWorkflow.qml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,8 @@ DappsComboBox {
signingTransaction: !!request.method && (request.method === SessionRequest.methods.signTransaction.name
|| request.method === SessionRequest.methods.sendTransaction.name)
requestPayload: request.preparedData
expirationSeconds: request.expirationTimestamp ? request.expirationTimestamp - requestTimestamp.getTime() / 1000
: 0

onClosed: {
Qt.callLater(rejectRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ StatusDialog {

CountdownPill {
id: countdownPill
objectName: "countdownPill"
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Style.current.padding
Expand Down
Loading

0 comments on commit c498580

Please sign in to comment.