diff --git a/Sources/WalletConnect/Engine/PairingEngine.swift b/Sources/WalletConnect/Engine/PairingEngine.swift index 41e364c9c..7335a723b 100644 --- a/Sources/WalletConnect/Engine/PairingEngine.swift +++ b/Sources/WalletConnect/Engine/PairingEngine.swift @@ -164,76 +164,25 @@ final class PairingEngine { private func setUpWCRequestHandling() { wcSubscriber.onReceivePayload = { [unowned self] subscriptionPayload in - let requestId = subscriptionPayload.wcRequest.id - let topic = subscriptionPayload.topic switch subscriptionPayload.wcRequest.params { case .pairingApprove(let approveParams): - handlePairingApprove(approveParams: approveParams, pendingPairingTopic: topic, requestId: requestId) + wcPairingApprove(subscriptionPayload, approveParams: approveParams) case .pairingUpdate(let updateParams): - handlePairingUpdate(params: updateParams, topic: topic, requestId: requestId) + wcPairingUpdate(subscriptionPayload, updateParams: updateParams) case .pairingPayload(let pairingPayload): - self.handlePairingPayload(pairingPayload, for: topic, requestId: requestId) + wcPairingPayload(subscriptionPayload, payloadParams: pairingPayload) case .pairingPing(_): - self.handlePairingPing(topic: topic, requestId: requestId) + wcPairingPing(subscriptionPayload) default: logger.warn("Warning: Pairing Engine - Unexpected method type: \(subscriptionPayload.wcRequest.method) received from subscriber") } } } - private func handlePairingUpdate(params: PairingType.UpdateParams,topic: String, requestId: Int64) { - guard var pairing = try? sequencesStore.getSequence(forTopic: topic) else { - logger.debug("Could not find pairing for topic \(topic)") - return - } - guard pairing.peerIsController else { - let error = WalletConnectError.unauthrorized(.unauthorizedUpdateRequest) - logger.error(error) - respond(error: error, requestId: requestId, topic: topic) - return - } - let response = JSONRPCResponse(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 { - pairing.settled?.state = params.state - sequencesStore.setSequence(pairing) - onPairingUpdate?(topic, params.state.metadata) - } - } - } - - private func handlePairingPing(topic: String, requestId: Int64) { - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { error in - //todo - } - } - - private func handlePairingPayload(_ payload: PairingType.PayloadParams, for topic: String, requestId: Int64) { - logger.debug("Will handle pairing payload") - guard sequencesStore.hasSequence(forTopic: topic) else { - logger.error("Pairing for the topic: \(topic) does not exist") - return - } - guard payload.request.method == PairingType.PayloadMethods.sessionPropose else { - logger.error("Forbidden WCPairingPayload method") - return - } - let sessionProposal = payload.request.params - if let pairingAgreementSecret = try? crypto.getAgreementSecret(for: sessionProposal.signal.params.topic) { - try? crypto.setAgreementSecret(pairingAgreementSecret, topic: sessionProposal.topic) - } - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [weak self] error in - self?.onSessionProposal?(sessionProposal) - } - } - - private func handlePairingApprove(approveParams: PairingType.ApprovalParams, pendingPairingTopic: String, requestId: Int64) { - logger.debug("Responder Client approved pairing on topic: \(pendingPairingTopic)") + private func wcPairingApprove(_ payload: WCRequestSubscriptionPayload, approveParams: PairingType.ApprovalParams) { + let pendingPairingTopic = payload.topic guard let pairing = try? sequencesStore.getSequence(forTopic: pendingPairingTopic), let pendingPairing = pairing.pending else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .pairing, topic: pendingPairingTopic)) return } @@ -255,15 +204,55 @@ final class PairingEngine { } sessionPermissions[pendingPairingTopic] = nil - // TODO: Move JSON-RPC responding to networking layer - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: proposal.topic, response: JsonRpcResult.response(response)) { [weak self] error in - if let error = error { - self?.logger.error("Could not respond with error: \(error)") - } + relayer.respondSuccess(for: payload) + onPairingApproved?(Pairing(topic: settledPairing.topic, peer: nil), permissions, settledPairing.relay) + } + + private func wcPairingUpdate(_ payload: WCRequestSubscriptionPayload, updateParams: PairingType.UpdateParams) { + let topic = payload.topic + guard var pairing = try? sequencesStore.getSequence(forTopic: topic) else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .pairing, topic: topic)) + return + } + guard pairing.peerIsController else { + relayer.respondError(for: payload, reason: .unauthorizedUpdateRequest(context: .pairing)) + return } - onPairingApproved?(Pairing(topic: settledPairing.topic, peer: nil), permissions, settledPairing.relay) + pairing.settled?.state = updateParams.state + sequencesStore.setSequence(pairing) + + relayer.respondSuccess(for: payload) + onPairingUpdate?(topic, updateParams.state.metadata) + } + + private func wcPairingPayload(_ payload: WCRequestSubscriptionPayload, payloadParams: PairingType.PayloadParams) { + guard sequencesStore.hasSequence(forTopic: payload.topic) else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .pairing, topic: payload.topic)) + return + } + guard payloadParams.request.method == PairingType.PayloadMethods.sessionPropose else { + relayer.respondError(for: payload, reason: .unauthorizedRPCMethod(payloadParams.request.method.rawValue)) + return + } + let sessionProposal = payloadParams.request.params + do { + if let pairingAgreementSecret = try crypto.getAgreementSecret(for: sessionProposal.signal.params.topic) { + try crypto.setAgreementSecret(pairingAgreementSecret, topic: sessionProposal.topic) + } else { + relayer.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) + return + } + } catch { + relayer.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) + return + } + relayer.respondSuccess(for: payload) + onSessionProposal?(sessionProposal) + } + + private func wcPairingPing(_ payload: WCRequestSubscriptionPayload) { + relayer.respondSuccess(for: payload) } private func removeRespondedPendingPairings() { @@ -290,18 +279,6 @@ final class PairingEngine { } } - private func respond(error: WalletConnectError, requestId: Int64, topic: String) { - let jsonrpcError = JSONRPCErrorResponse.Error(code: error.code, message: error.description) - let response = JSONRPCErrorResponse(id: requestId, error: jsonrpcError) - relayer.respond(topic: topic, response: .error(response)) { [weak self] responseError in - if let responseError = responseError { - self?.logger.error("Could not respond with error: \(responseError)") - } else { - self?.logger.debug("successfully responded with error") - } - } - } - private func handleReponse(_ response: WCResponse) { switch response.requestParams { case .pairingApprove: diff --git a/Sources/WalletConnect/Engine/SessionEngine.swift b/Sources/WalletConnect/Engine/SessionEngine.swift index 3fa684863..a6ac0be19 100644 --- a/Sources/WalletConnect/Engine/SessionEngine.swift +++ b/Sources/WalletConnect/Engine/SessionEngine.swift @@ -202,7 +202,7 @@ final class SessionEngine { throw WalletConnectError.internal(.notApproved) // TODO: Use a suitable error cases } } - if !session.isController || session.settled?.status != .acknowledged { + if !session.selfIsController || session.settled?.status != .acknowledged { throw WalletConnectError.unauthrorized(.unauthorizedUpdateRequest) } session.update(accounts) @@ -218,7 +218,7 @@ final class SessionEngine { guard session.isSettled else { throw WalletConnectError.sessionNotSettled(topic) } - guard session.isController else { + guard session.selfIsController else { throw WalletConnectError.unauthorizedNonControllerCall } guard validatePermissions(permissions) else { @@ -256,65 +256,85 @@ final class SessionEngine { private func setUpWCRequestHandling() { wcSubscriber.onReceivePayload = { [unowned self] subscriptionPayload in - let requestId = subscriptionPayload.wcRequest.id - let topic = subscriptionPayload.topic switch subscriptionPayload.wcRequest.params { case .sessionApprove(let approveParams): - handleSessionApprove(approveParams, topic: topic, requestId: requestId) + wcSessionApprove(subscriptionPayload, approveParams: approveParams) case .sessionReject(let rejectParams): - handleSessionReject(rejectParams, topic: topic) + wcSessionReject(subscriptionPayload, rejectParams: rejectParams) case .sessionUpdate(let updateParams): - handleSessionUpdate(payload: subscriptionPayload, updateParams: updateParams) + wcSessionUpdate(payload: subscriptionPayload, updateParams: updateParams) case .sessionUpgrade(let upgradeParams): - handleSessionUpgrade(payload: subscriptionPayload, upgradeParams: upgradeParams) + wcSessionUpgrade(payload: subscriptionPayload, upgradeParams: upgradeParams) case .sessionDelete(let deleteParams): - handleSessionDelete(deleteParams, topic: topic) + wcSessionDelete(subscriptionPayload, deleteParams: deleteParams) case .sessionPayload(let sessionPayloadParams): - handleSessionPayload(payloadParams: sessionPayloadParams, topic: topic, requestId: requestId) + wcSessionPayload(subscriptionPayload, payloadParams: sessionPayloadParams) case .sessionPing(_): - handleSessionPing(topic: topic, requestId: requestId) + wcSessionPing(subscriptionPayload) case .sessionNotification(let notificationParams): - handleSessionNotification(topic: topic, notificationParams: notificationParams, requestId: requestId) + wcSessionNotification(subscriptionPayload, notificationParams: notificationParams) default: logger.warn("Warning: Session Engine - Unexpected method type: \(subscriptionPayload.wcRequest.method) received from subscriber") } } } - private func handleSessionNotification(topic: String, notificationParams: SessionType.NotificationParams, requestId: Int64) { - guard let session = sequencesStore.getSequence(forTopic: topic), session.isSettled else { + private func wcSessionApprove(_ payload: WCRequestSubscriptionPayload, approveParams: SessionType.ApproveParams) { + let topic = payload.topic + guard let session = sequencesStore.getSequence(forTopic: topic), let pendingSession = session.pending else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: topic)) + return + } + guard !session.selfIsController else { + // TODO: Replace generic reason with a valid code. + relayer.respondError(for: payload, reason: .generic(message: "wcSessionApproval received by a controller")) return } + + let settledTopic: String + let agreementKeys: AgreementSecret do { - try validateNotification(session: session, params: notificationParams) - let response = JSONRPCResponse(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 { - let notification = Session.Notification(type: notificationParams.type, data: notificationParams.data) - onNotificationReceived?(topic, notification) - } - } - } catch let error as WalletConnectError { - logger.error(error) - respond(error: error, requestId: requestId, topic: topic) - //on unauthorized notification received? - } catch {} + let publicKey = try session.getPublicKey() + agreementKeys = try crypto.performKeyAgreement(selfPublicKey: publicKey, peerPublicKey: approveParams.responder.publicKey) + settledTopic = agreementKeys.derivedTopic() + try crypto.setAgreementSecret(agreementKeys, topic: settledTopic) + } catch { + relayer.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) + return + } + + let proposal = pendingSession.proposal + let settledSession = SessionSequence.buildAcknowledged(approval: approveParams, proposal: proposal, agreementKeys: agreementKeys, metadata: metadata) + sequencesStore.delete(topic: proposal.topic) + sequencesStore.setSequence(settledSession) + wcSubscriber.setSubscription(topic: settledTopic) + wcSubscriber.removeSubscription(topic: proposal.topic) + + let approvedSession = Session( + topic: settledTopic, + peer: approveParams.responder.metadata, + permissions: Session.Permissions( + blockchains: pendingSession.proposal.permissions.blockchain.chains, + methods: pendingSession.proposal.permissions.jsonrpc.methods), accounts: settledSession.settled!.state.accounts) + + logger.debug("Responder Client approved session on topic: \(topic)") + relayer.respondSuccess(for: payload) + onSessionApproved?(approvedSession) } - private func validateNotification(session: SessionSequence, params: SessionType.NotificationParams) throws { - if session.isController { + private func wcSessionReject(_ payload: WCRequestSubscriptionPayload, rejectParams: SessionType.RejectParams) { + let topic = payload.topic + guard sequencesStore.hasSequence(forTopic: topic) else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: topic)) return - } else { - guard let notifications = session.settled?.permissions.notifications, - notifications.types.contains(params.type) else { - throw WalletConnectError.unauthrorized(.unauthorizedNotificationType) - } } + sequencesStore.delete(topic: topic) + wcSubscriber.removeSubscription(topic: topic) + relayer.respondSuccess(for: payload) + onSessionRejected?(topic, rejectParams.reason) } - private func handleSessionUpdate(payload: WCRequestSubscriptionPayload, updateParams: SessionType.UpdateParams) { + private func wcSessionUpdate(payload: WCRequestSubscriptionPayload, updateParams: SessionType.UpdateParams) { for account in updateParams.state.accounts { if !String.conformsToCAIP10(account) { relayer.respondError(for: payload, reason: .invalidUpdateRequest(context: .session)) @@ -336,7 +356,7 @@ final class SessionEngine { onSessionUpdate?(topic, updateParams.state.accounts) } - private func handleSessionUpgrade(payload: WCRequestSubscriptionPayload, upgradeParams: SessionType.UpgradeParams) { + private func wcSessionUpgrade(payload: WCRequestSubscriptionPayload, upgradeParams: SessionType.UpgradeParams) { guard validatePermissions(upgradeParams.permissions) else { relayer.respondError(for: payload, reason: .invalidUpgradeRequest(context: .session)) return @@ -356,120 +376,75 @@ final class SessionEngine { onSessionUpgrade?(session.topic, newPermissions) } - private func handleSessionPing(topic: String, requestId: Int64) { - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: .response(response)) { error in - //todo - } - } - - private func handleSessionDelete(_ deleteParams: SessionType.DeleteParams, topic: String) { + private func wcSessionDelete(_ payload: WCRequestSubscriptionPayload, deleteParams: SessionType.DeleteParams) { + let topic = payload.topic guard sequencesStore.hasSequence(forTopic: topic) else { - logger.debug("Could not find session for topic \(topic)") + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: topic)) return } sequencesStore.delete(topic: topic) wcSubscriber.removeSubscription(topic: topic) + relayer.respondSuccess(for: payload) onSessionDelete?(topic, deleteParams.reason) } - private func handleSessionReject(_ rejectParams: SessionType.RejectParams, topic: String) { - guard sequencesStore.hasSequence(forTopic: topic) else { - logger.debug("Could not find session for topic \(topic)") - return - } - sequencesStore.delete(topic: topic) - wcSubscriber.removeSubscription(topic: topic) - onSessionRejected?(topic, rejectParams.reason) - } - - private func handleSessionPayload(payloadParams: SessionType.PayloadParams, topic: String, requestId: Int64) { - let jsonRpcRequest = JSONRPCRequest(id: requestId, method: payloadParams.request.method, params: payloadParams.request.params) + private func wcSessionPayload(_ payload: WCRequestSubscriptionPayload, payloadParams: SessionType.PayloadParams) { + let topic = payload.topic + let jsonRpcRequest = JSONRPCRequest(id: payload.wcRequest.id, method: payloadParams.request.method, params: payloadParams.request.params) let request = Request( id: jsonRpcRequest.id, topic: topic, method: jsonRpcRequest.method, params: jsonRpcRequest.params, chainId: payloadParams.chainId) - do { - try validatePayload(request) - onSessionPayloadRequest?(request) - } catch let error as WalletConnectError { - logger.error(error) - respond(error: error, requestId: jsonRpcRequest.id, topic: topic) - } catch {} - } - - private func respond(error: WalletConnectError, requestId: Int64, topic: String) { - let jsonrpcError = JSONRPCErrorResponse.Error(code: error.code, message: error.description) - let response = JSONRPCErrorResponse(id: requestId, error: jsonrpcError) - relayer.respond(topic: topic, response: JsonRpcResult.error(response)) { [weak self] responseError in - if let responseError = responseError { - self?.logger.error("Could not respond with error: \(responseError)") - } else { - self?.logger.debug("successfully responded with error") - } - } - } - - private func validatePayload(_ sessionRequest: Request) throws { - guard let session = sequencesStore.getSequence(forTopic: sessionRequest.topic) else { - throw WalletConnectError.internal(.noSequenceForTopic) + + guard let session = sequencesStore.getSequence(forTopic: topic) else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: topic)) + return } - if let chainId = sessionRequest.chainId { + if let chainId = request.chainId { guard session.hasPermission(forChain: chainId) else { - throw WalletConnectError.unauthrorized(.unauthorizedJsonRpcMethod) + relayer.respondError(for: payload, reason: .unauthorizedTargetChain(chainId)) + return } } - guard session.hasPermission(forMethod: sessionRequest.method) else { - throw WalletConnectError.unauthrorized(.unauthorizedJsonRpcMethod) + guard session.hasPermission(forMethod: request.method) else { + relayer.respondError(for: payload, reason: .unauthorizedRPCMethod(request.method)) + return } + onSessionPayloadRequest?(request) } - private func handleSessionApprove(_ approveParams: SessionType.ApproveParams, topic: String, requestId: Int64) { - logger.debug("Responder Client approved session on topic: \(topic)") - guard let session = sequencesStore.getSequence(forTopic: topic), - let pendingSession = session.pending else { - logger.error("Could not find pending session for topic: \(topic)") - return - } - logger.debug("isController: \(session.isController)") - - guard !session.isController else { - logger.warn("Warning: Session Engine - Unexpected handleSessionApprove method call by non Controller client") + private func wcSessionPing(_ payload: WCRequestSubscriptionPayload) { + relayer.respondSuccess(for: payload) + } + + private func wcSessionNotification(_ payload: WCRequestSubscriptionPayload, notificationParams: SessionType.NotificationParams) { + let topic = payload.topic + guard let session = sequencesStore.getSequence(forTopic: topic), session.isSettled else { + relayer.respondError(for: payload, reason: .noContextWithTopic(context: .session, topic: payload.topic)) return } - logger.debug("handleSessionApprove") - - let agreementKeys = try! crypto.performKeyAgreement(selfPublicKey: try! session.getPublicKey(), peerPublicKey: approveParams.responder.publicKey) - - let settledTopic = agreementKeys.derivedTopic() - - try! crypto.setAgreementSecret(agreementKeys, topic: settledTopic) - - let proposal = pendingSession.proposal - let settledSession = SessionSequence.buildAcknowledged(approval: approveParams, proposal: proposal, agreementKeys: agreementKeys, metadata: metadata) - - sequencesStore.delete(topic: proposal.topic) - sequencesStore.setSequence(settledSession) - - wcSubscriber.setSubscription(topic: settledTopic) - wcSubscriber.removeSubscription(topic: proposal.topic) - - let approvedSession = Session( - topic: settledTopic, - peer: approveParams.responder.metadata, - permissions: Session.Permissions( - blockchains: pendingSession.proposal.permissions.blockchain.chains, - methods: pendingSession.proposal.permissions.jsonrpc.methods), accounts: settledSession.settled!.state.accounts) - - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - relayer.respond(topic: topic, response: JsonRpcResult.response(response)) { [unowned self] error in - if let error = error { - logger.error(error) + if session.selfIsController { + guard session.hasPermission(forNotification: notificationParams.type) else { + relayer.respondError(for: payload, reason: .unauthorizedNotificationType(notificationParams.type)) + return + } + } + let notification = Session.Notification(type: notificationParams.type, data: notificationParams.data) + relayer.respondSuccess(for: payload) + onNotificationReceived?(topic, notification) + } + + private func validateNotification(session: SessionSequence, params: SessionType.NotificationParams) throws { + if session.selfIsController { + return + } else { + guard let notifications = session.settled?.permissions.notifications, + notifications.types.contains(params.type) else { + throw WalletConnectError.unauthrorized(.unauthorizedNotificationType) } } - onSessionApproved?(approvedSession) } private func setupExpirationHandling() { @@ -528,7 +503,7 @@ final class SessionEngine { } switch result { case .response: - guard let settledSession = try? sequencesStore.getSequence(forTopic: settledTopic) else {return} + guard let settledSession = sequencesStore.getSequence(forTopic: settledTopic) else {return} crypto.deleteAgreementSecret(for: topic) wcSubscriber.removeSubscription(topic: topic) sequencesStore.delete(topic: topic) diff --git a/Sources/WalletConnect/Types/ReasonCode.swift b/Sources/WalletConnect/Types/ReasonCode.swift index 187095d47..4dcb1aa09 100644 --- a/Sources/WalletConnect/Types/ReasonCode.swift +++ b/Sources/WalletConnect/Types/ReasonCode.swift @@ -9,11 +9,15 @@ enum ReasonCode { case generic(message: String) // 1000 (Internal) + case missingOrInvalid(String) case invalidUpdateRequest(context: Context) case invalidUpgradeRequest(context: Context) case noContextWithTopic(context: Context, topic: String) // 3000 (Unauthorized) + case unauthorizedTargetChain(String) + case unauthorizedRPCMethod(String) + case unauthorizedNotificationType(String) case unauthorizedUpdateRequest(context: Context) case unauthorizedUpgradeRequest(context: Context) case unauthorizedMatchingController(isController: Bool) @@ -21,9 +25,13 @@ enum ReasonCode { var code: Int { switch self { case .generic: return 0 + case .missingOrInvalid: return 1000 case .invalidUpdateRequest: return 1003 case .invalidUpgradeRequest: return 1004 case .noContextWithTopic: return 1301 + case .unauthorizedTargetChain: return 3000 + case .unauthorizedRPCMethod: return 3001 + case .unauthorizedNotificationType: return 3002 case .unauthorizedUpdateRequest: return 3003 case .unauthorizedUpgradeRequest: return 3004 case .unauthorizedMatchingController: return 3005 @@ -34,12 +42,20 @@ enum ReasonCode { switch self { case .generic(let message): return message + case .missingOrInvalid(let name): + return "Missing or invalid \(name)" 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 .unauthorizedTargetChain(let chainId): + return "Unauthorized target chain id requested: \(chainId)" + case .unauthorizedRPCMethod(let method): + return "Unauthorized JSON-RPC method requested: \(method)" + case .unauthorizedNotificationType(let type): + return "Unauthorized notification type requested: \(type)" case .unauthorizedUpdateRequest(let context): return "Unauthorized \(context) update request" case .unauthorizedUpgradeRequest(let context): diff --git a/Sources/WalletConnect/Types/Session/SessionSequence.swift b/Sources/WalletConnect/Types/Session/SessionSequence.swift index b54cf5158..0ae11da31 100644 --- a/Sources/WalletConnect/Types/Session/SessionSequence.swift +++ b/Sources/WalletConnect/Types/Session/SessionSequence.swift @@ -42,7 +42,7 @@ struct SessionSequence: ExpirableSequence { settled?.status == .acknowledged } - var isController: Bool { + var selfIsController: Bool { guard let controller = settled?.permissions.controller else { return false } return selfParticipant.publicKey == controller.publicKey } @@ -73,6 +73,11 @@ struct SessionSequence: ExpirableSequence { return settled.permissions.jsonrpc.methods.contains(method) } + func hasPermission(forNotification type: String) -> Bool { + guard let notificationPermissions = settled?.permissions.notifications else { return false } + return notificationPermissions.types.contains(type) + } + mutating func upgrade(_ permissions: SessionPermissions) { settled?.permissions.upgrade(with: permissions) }