Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#36 Upgrade method permissions checks #62

Merged
merged 8 commits into from
Feb 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 65 additions & 37 deletions Sources/WalletConnect/Engine/SessionEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -213,25 +213,24 @@ final class SessionEngine {
relayer.request(.wcSessionUpdate(SessionType.UpdateParams(accounts: accounts)), onTopic: topic)
}

func upgrade(topic: String, permissions: Session.Permissions) {
func upgrade(topic: String, permissions: Session.Permissions) throws {
let permissions = SessionPermissions(permissions: permissions)
guard var session = sequencesStore.getSequence(forTopic: topic) else {
logger.debug("Could not find session for topic \(topic)")
return
throw WalletConnectError.noSessionMatchingTopic(topic)
}
session.upgrade(permissions)
guard let newPermissions = session.settled?.permissions else {
return
guard session.isSettled else {
throw WalletConnectError.sessionNotSettled(topic)
}
relayer.request(.wcSessionUpgrade(SessionType.UpgradeParams(permissions: newPermissions)), onTopic: topic) { [unowned self] result in
switch result {
case .success(_):
sequencesStore.setSequence(session)
onSessionUpgrade?(session.topic, newPermissions)
case .failure(_):
return
//TODO
}
guard isController else {
throw WalletConnectError.unauthorizedNonControllerCall
}
guard validatePermissions(permissions) else {
throw WalletConnectError.invalidPermissions
}
session.upgrade(permissions)
let newPermissions = session.settled!.permissions // We know session is settled
sequencesStore.setSequence(session)
relayer.request(.wcSessionUpgrade(SessionType.UpgradeParams(permissions: newPermissions)), onTopic: topic)
}

func notify(topic: String, params: Session.Notification, completion: ((Error?)->())?) {
Expand Down Expand Up @@ -270,7 +269,7 @@ final class SessionEngine {
case .sessionUpdate(let updateParams):
handleSessionUpdate(payload: subscriptionPayload, updateParams: updateParams)
case .sessionUpgrade(let upgradeParams):
handleSessionUpgrade(topic: topic, upgradeParams: upgradeParams, requestId: requestId)
handleSessionUpgrade(payload: subscriptionPayload, upgradeParams: upgradeParams)
case .sessionDelete(let deleteParams):
handleSessionDelete(deleteParams, topic: topic)
case .sessionPayload(let sessionPayloadParams):
Expand Down Expand Up @@ -338,40 +337,34 @@ final class SessionEngine {
relayer.respondError(for: payload, reason: .unauthorizedMatchingController(isController: isController))
return
}

session.settled?.state = updateParams.state
sequencesStore.setSequence(session)
relayer.respondSuccess(for: payload)
onSessionUpdate?(topic, updateParams.state.accounts)
}

private func handleSessionUpgrade(topic: String, upgradeParams: SessionType.UpgradeParams, requestId: Int64) {
guard var session = sequencesStore.getSequence(forTopic: topic) else {
logger.debug("Could not find session for topic \(topic)")
private func handleSessionUpgrade(payload: WCRequestSubscriptionPayload, upgradeParams: SessionType.UpgradeParams) {
guard validatePermissions(upgradeParams.permissions) else {
relayer.respondError(for: payload, reason: .invalidUpgradeRequest(context: .session))
return
}
guard session.peerIsController else {
let error = WalletConnectError.unauthrorized(.unauthorizedUpgradeRequest)
logger.error(error)
respond(error: error, requestId: requestId, topic: topic)
guard var session = sequencesStore.getSequence(forTopic: payload.topic) else {
relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: payload.topic))
return
}
let permissions = Session.Permissions(
blockchains: upgradeParams.permissions.blockchain.chains,
methods: upgradeParams.permissions.jsonrpc.methods)
session.upgrade(permissions)
guard let newPermissions = session.settled?.permissions else {
guard session.peerIsController else {
relayer.respondError(for: payload, reason: .unauthorizedUpgradeRequest(context: .session))
return
}
let response = JSONRPCResponse<AnyCodable>(id: requestId, result: AnyCodable(true))
relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [unowned self] error in
if let error = error {
logger.error(error)
} else {
sequencesStore.setSequence(session)
onSessionUpgrade?(session.topic, newPermissions)
}
guard !isController else {
relayer.respondError(for: payload, reason: .unauthorizedMatchingController(isController: isController))
return
}
session.upgrade(upgradeParams.permissions)
sequencesStore.setSequence(session)
let newPermissions = session.settled!.permissions // We know session is settled
relayer.respondSuccess(for: payload)
onSessionUpgrade?(session.topic, newPermissions)
}

private func handleSessionPing(topic: String, requestId: Int64) {
Expand Down Expand Up @@ -513,6 +506,8 @@ final class SessionEngine {
handleApproveResponse(topic: response.topic, result: response.result)
case .sessionUpdate:
handleUpdateResponse(topic: response.topic, result: response.result)
case .sessionUpgrade:
handleUpgradeResponse(topic: response.topic, result: response.result)
case .sessionPayload(_):
let response = Response(topic: response.topic, chainId: response.chainId, result: response.result)
onSessionPayloadResponse?(response)
Expand Down Expand Up @@ -576,4 +571,37 @@ final class SessionEngine {
logger.error("Peer failed to update state.")
}
}

private func handleUpgradeResponse(topic: String, result: JsonRpcResult) {
guard let session = sequencesStore.getSequence(forTopic: topic), let permissions = session.settled?.permissions else {
return
}
switch result {
case .response:
onSessionUpgrade?(session.topic, permissions)
case .error:
logger.error("Peer failed to upgrade permissions.")
}
}

private func validatePermissions(_ permissions: SessionPermissions) -> Bool {
for chainId in permissions.blockchain.chains {
if !String.conformsToCAIP2(chainId) {
return false
}
}
for method in permissions.jsonrpc.methods {
if method.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return false
}
}
if let notificationTypes = permissions.notifications?.types {
for notification in notificationTypes {
if notification.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return false
}
}
}
return true
}
}
13 changes: 13 additions & 0 deletions Sources/WalletConnect/Types/ReasonCode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,27 @@ enum ReasonCode {
case session = "session"
}

// 0 (Generic)
case generic(message: String)

// 1000 (Internal)
case invalidUpdateRequest(context: Context)
case invalidUpgradeRequest(context: Context)
case noContextWithTopic(context: Context, topic: String)

// 3000 (Unauthorized)
case unauthorizedUpdateRequest(context: Context)
case unauthorizedUpgradeRequest(context: Context)
case unauthorizedMatchingController(isController: Bool)

var code: Int {
switch self {
case .generic: return 0
case .invalidUpdateRequest: return 1003
case .invalidUpgradeRequest: return 1004
case .noContextWithTopic: return 1301
case .unauthorizedUpdateRequest: return 3003
case .unauthorizedUpgradeRequest: return 3004
case .unauthorizedMatchingController: return 3005
}
}
Expand All @@ -27,10 +36,14 @@ enum ReasonCode {
return message
case .invalidUpdateRequest(let context):
return "Invalid \(context) update request"
case .invalidUpgradeRequest(let context):
return "Invalid \(context) upgrade request"
case .noContextWithTopic(let context, let topic):
return "No matching \(context) with topic: \(topic)"
case .unauthorizedUpdateRequest(let context):
return "Unauthorized \(context) update request"
case .unauthorizedUpgradeRequest(let context):
return "Unauthorized \(context) upgrade request"
case .unauthorizedMatchingController(let isController):
return "Unauthorized: peer is also \(isController ? "" : "non-")controller"
}
Expand Down
6 changes: 3 additions & 3 deletions Sources/WalletConnect/Types/Session/SessionPermissions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ struct SessionPermissions: Codable, Equatable {
self.controller = nil
}

mutating func upgrade(with sessionPermissions: Session.Permissions) {
blockchain.chains.formUnion(sessionPermissions.blockchains)
jsonrpc.methods.formUnion(sessionPermissions.methods)
mutating func upgrade(with permissions: SessionPermissions) {
blockchain.chains.formUnion(permissions.blockchain.chains)
jsonrpc.methods.formUnion(permissions.jsonrpc.methods)
}
}
2 changes: 1 addition & 1 deletion Sources/WalletConnect/Types/Session/SessionSequence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ struct SessionSequence: ExpirableSequence {
return settled.permissions.jsonrpc.methods.contains(method)
}

mutating func upgrade(_ permissions: Session.Permissions) {
mutating func upgrade(_ permissions: SessionPermissions) {
settled?.permissions.upgrade(with: permissions)
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/WalletConnect/WalletConnectClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ public final class WalletConnectClient {
/// - Parameters:
/// - topic: Topic of the session that is intended to be upgraded.
/// - permissions: Sets of permissions that will be combined with existing ones.
public func upgrade(topic: String, permissions: Session.Permissions) {
sessionEngine.upgrade(topic: topic, permissions: permissions)
public func upgrade(topic: String, permissions: Session.Permissions) throws {
try sessionEngine.upgrade(topic: topic, permissions: permissions)
}

/// For the proposer to send JSON-RPC requests to responding peer.
Expand Down
23 changes: 16 additions & 7 deletions Sources/WalletConnect/WalletConnectError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,18 @@

import Foundation

// TODO: Migrate protocol errors to ReasonCode enum over time. Use WalletConnectError for client errors only.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

enum WalletConnectError: Error {

// 1000 (Internal)
case noSessionMatchingTopic(String)
case sessionNotSettled(String)
case invalidPermissions
case unauthorizedNonControllerCall

case `internal`(_ reason: InternalReason)

// 2000 (Timeout)
// 3000 (Unauthorized)
case unauthrorized(_ reason: UnauthorizedReason)

// 4000 (EIP-1193)
// 5000 (CAIP-25)
// 9000 (Unknown)

enum InternalReason: Error {
case notApproved
case malformedPairingURI
Expand Down Expand Up @@ -50,11 +49,21 @@ extension WalletConnectError: CustomStringConvertible {
return reason.code
case .unauthrorized(let reason):
return reason.code
default:
return 0
}
}

var localizedDescription: String {
switch self {
case .noSessionMatchingTopic(let topic):
return "No session found matching topic \(topic)."
case .sessionNotSettled(let topic):
return "Session is not settled on topic \(topic)."
case .invalidPermissions:
return "Permission set is invalid."
case .unauthorizedNonControllerCall:
return "Method must be called by a controller client."
case .internal(let reason):
return reason.description
case .unauthrorized(let reason):
Expand Down
27 changes: 1 addition & 26 deletions Tests/IntegrationTests/ClientTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ final class ClientTests: XCTestCase {
self.responder.client.approve(proposal: proposal, accounts: [account])
}
responder.onSessionSettled = { [unowned self] sessionSettled in
responder.client.upgrade(topic: sessionSettled.topic, permissions: upgradePermissions)
try? responder.client.upgrade(topic: sessionSettled.topic, permissions: upgradePermissions)
}
proposer.onSessionUpgrade = { topic, permissions in
XCTAssertTrue(permissions.blockchains.isSuperset(of: upgradePermissions.blockchains))
Expand All @@ -269,31 +269,6 @@ final class ClientTests: XCTestCase {
waitForExpectations(timeout: defaultTimeout, handler: nil)
}

func testSessionUpgradeFailsOnNonControllerRequest() {
let proposerSessionUpgradeExpectation = expectation(description: "Proposer upgrades session")
proposerSessionUpgradeExpectation.isInverted = true
let responderSessionUpgradeExpectation = expectation(description: "Responder upgrades session")
responderSessionUpgradeExpectation.isInverted = true
let account = "0x022c0c42a80bd19EA4cF0F94c4F9F96645759716"
let permissions = Session.Permissions.stub()
let upgradePermissions = Session.Permissions(blockchains: ["eip155:42"], methods: ["eth_sendTransaction"])
let uri = try! proposer.client.connect(sessionPermissions: permissions)!
try! responder.client.pair(uri: uri)
responder.onSessionProposal = { [unowned self] proposal in
self.responder.client.approve(proposal: proposal, accounts: [account])
}
proposer.onSessionSettled = { [unowned self] sessionSettled in
proposer.client.upgrade(topic: sessionSettled.topic, permissions: upgradePermissions)
}
proposer.onSessionUpgrade = { topic, permissions in
proposerSessionUpgradeExpectation.fulfill()
}
responder.onSessionUpgrade = { topic, permissions in
responderSessionUpgradeExpectation.fulfill()
}
waitForExpectations(timeout: 3.0, handler: nil)
}

func testSuccessfulSessionUpdate() {
let proposerSessionUpdateExpectation = expectation(description: "Proposer updates session on responder request")
let responderSessionUpdateExpectation = expectation(description: "Responder updates session on proposer response")
Expand Down
27 changes: 27 additions & 0 deletions Tests/WalletConnectTests/Helpers/Error+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,30 @@ extension NSError {
NSError(domain: "com.walletconnect.sdk.tests.error", code: code, userInfo: nil)
}
}

extension Error {

var wcError: WalletConnectError? {
self as? WalletConnectError
}

var isNoSessionMatchingTopicError: Bool {
guard case .noSessionMatchingTopic = wcError else { return false }
return true
}

var isSessionNotSettledError: Bool {
guard case .sessionNotSettled = wcError else { return false }
return true
}

var isInvalidPermissionsError: Bool {
guard case .invalidPermissions = wcError else { return false }
return true
}

var isUnauthorizedNonControllerCallError: Bool {
guard case .unauthorizedNonControllerCall = wcError else { return false }
return true
}
}
Loading