diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme index 0b3f7989e..8b71b4091 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme @@ -90,6 +90,34 @@ ReferencedContainer = "container:"> + + + + + + + + ())? - var onProposeResponse: ((String)->())? + var onProposeResponse: ((String, SessionProposal)->())? var onSessionRejected: ((Session.Proposal, SessionType.Reason)->())? private let proposalPayloadsStore: KeyValueStore @@ -71,7 +71,7 @@ final class PairingEngine { func propose(pairingTopic: String, namespaces: [String: ProposalNamespace], relay: RelayProtocolOptions) async throws { logger.debug("Propose Session on topic: \(pairingTopic)") - try Validator.validate(namespaces) + try Namespace.validate(namespaces) let publicKey = try! kms.createX25519KeyPair() let proposer = Participant( publicKey: publicKey.hexRepresentation, @@ -116,33 +116,34 @@ final class PairingEngine { // todo - delete pairing if inactive } - func respondSessionPropose(proposerPubKey: String) -> (String, SessionProposal)? { + func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) -> (String, SessionProposal)? { guard let payload = try? proposalPayloadsStore.get(key: proposerPubKey), case .sessionPropose(let proposal) = payload.wcRequest.params else { //TODO - throws return nil } + let proposedNamespaces = proposal.requiredNamespaces proposalPayloadsStore.delete(forKey: proposerPubKey) - - let selfPublicKey = try! kms.createX25519KeyPair() - var agreementKey: AgreementKeys! do { - agreementKey = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposal.proposer.publicKey) + try Namespace.validate(sessionNamespaces) + try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces) + let selfPublicKey = try! kms.createX25519KeyPair() + let agreementKey = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposal.proposer.publicKey) + //todo - extend pairing + let sessionTopic = agreementKey.derivedTopic() + + try! kms.setAgreementSecret(agreementKey, topic: sessionTopic) + guard let relay = proposal.relays.first else {return nil} + let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) + let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) + logger.debug("Responding session propose") + networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in } + return (sessionTopic, proposal) } catch { networkingInteractor.respondError(for: payload, reason: .missingOrInvalid("agreement keys")) return nil } - //todo - extend pairing - let sessionTopic = agreementKey.derivedTopic() - - try! kms.setAgreementSecret(agreementKey, topic: sessionTopic) - guard let relay = proposal.relays.first else {return nil} - let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) - let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) - logger.debug("Responding session propose") - networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in } - return (sessionTopic, proposal) } //MARK: - Private @@ -162,6 +163,12 @@ final class PairingEngine { private func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) { logger.debug("Received Session Proposal") + do { + try Namespace.validate(proposal.requiredNamespaces) + } catch { + // TODO: respond error + return + } try? proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey) onSessionProposal?(proposal.publicRepresentation()) } @@ -228,7 +235,7 @@ final class PairingEngine { try? kms.setAgreementSecret(agreementKeys, topic: sessionTopic) try! sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic) - onProposeResponse?(sessionTopic) + onProposeResponse?(sessionTopic, proposal) case .error(let error): if !pairing.active { diff --git a/Sources/WalletConnectAuth/Engine/Common/SessionEngine.swift b/Sources/WalletConnectAuth/Engine/Common/SessionEngine.swift index 4aaeaaecb..36741fee9 100644 --- a/Sources/WalletConnectAuth/Engine/Common/SessionEngine.swift +++ b/Sources/WalletConnectAuth/Engine/Common/SessionEngine.swift @@ -12,6 +12,8 @@ final class SessionEngine { var onSessionDelete: ((String, SessionType.Reason)->())? var onEventReceived: ((String, Session.Event, Blockchain?)->())? + var settlingProposal: SessionProposal? + private let sessionStore: WCSessionStorage private let pairingStore: WCPairingStorage private let sessionToPairingTopic: KeyValueStore @@ -59,6 +61,30 @@ final class SessionEngine { sessionStore.getAcknowledgedSessions().map{$0.publicRepresentation()} } + func settle(topic: String, proposal: SessionProposal, namespaces: [String: SessionNamespace]) throws { + let agreementKeys = try! kms.getAgreementSecret(for: topic)! + + let selfParticipant = Participant(publicKey: agreementKeys.publicKey.hexRepresentation, metadata: metadata) + + let expectedExpiryTimeStamp = Date().addingTimeInterval(TimeInterval(WCSession.defaultTimeToLive)) + guard let relay = proposal.relays.first else {return} + let settleParams = SessionType.SettleParams( + relay: relay, + controller: selfParticipant, + namespaces: namespaces, + expiry: Int64(expectedExpiryTimeStamp.timeIntervalSince1970))//todo - test expiration times + let session = WCSession( + topic: topic, + selfParticipant: selfParticipant, + peerParticipant: proposal.proposer, + settleParams: settleParams, + acknowledged: false) + logger.debug("Sending session settle request") + Task { try? await networkingInteractor.subscribe(topic: topic) } + sessionStore.setSession(session) + networkingInteractor.request(.wcSessionSettle(settleParams), onTopic: topic) + } + func delete(topic: String, reason: Reason) async throws { logger.debug("Will delete session for reason: message: \(reason.message) code: \(reason.code)") try await networkingInteractor.request(.wcSessionDelete(reason.internalRepresentation()), onTopic: topic) @@ -139,34 +165,24 @@ final class SessionEngine { } }.store(in: &publishers) } - - func settle(topic: String, proposal: SessionProposal, namespaces: [String: SessionNamespace]) throws { - try Validator.validate(namespaces) // FIXME: Validation should happen before responding proposal, before settlement - let agreementKeys = try! kms.getAgreementSecret(for: topic)! - - let selfParticipant = Participant(publicKey: agreementKeys.publicKey.hexRepresentation, metadata: metadata) - - let expectedExpiryTimeStamp = Date().addingTimeInterval(TimeInterval(WCSession.defaultTimeToLive)) - guard let relay = proposal.relays.first else {return} - let settleParams = SessionType.SettleParams( - relay: relay, - controller: selfParticipant, - namespaces: namespaces, - expiry: Int64(expectedExpiryTimeStamp.timeIntervalSince1970))//todo - test expiration times - let session = WCSession( - topic: topic, - selfParticipant: selfParticipant, - peerParticipant: proposal.proposer, - settleParams: settleParams, - acknowledged: false) - logger.debug("Sending session settle request") - Task { try? await networkingInteractor.subscribe(topic: topic) } - sessionStore.setSession(session) - networkingInteractor.request(.wcSessionSettle(settleParams), onTopic: topic) - } - + private func onSessionSettle(payload: WCRequestSubscriptionPayload, settleParams: SessionType.SettleParams) { logger.debug("Did receive session settle request") + guard let proposedNamespaces = settlingProposal?.requiredNamespaces else { + // TODO: respond error + return + } + settlingProposal = nil + let sessionNamespaces = settleParams.namespaces + do { + try Namespace.validate(proposedNamespaces) + try Namespace.validate(sessionNamespaces) + try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces) + } catch { + // TODO: respond error + return + } + let topic = payload.topic let agreementKeys = try! kms.getAgreementSecret(for: topic)! @@ -177,12 +193,12 @@ final class SessionEngine { updatePairingMetadata(topic: pairingTopic, metadata: settleParams.controller.metadata) } - // TODO: Validate namespaces - let session = WCSession(topic: topic, - selfParticipant: selfParticipant, - peerParticipant: settleParams.controller, - settleParams: settleParams, - acknowledged: true) + let session = WCSession( + topic: topic, + selfParticipant: selfParticipant, + peerParticipant: settleParams.controller, + settleParams: settleParams, + acknowledged: true) sessionStore.setSession(session) networkingInteractor.respondSuccess(for: payload) onSessionSettle?(session.publicRepresentation()) diff --git a/Sources/WalletConnectAuth/Engine/Common/SessionStateMachineValidating.swift b/Sources/WalletConnectAuth/Engine/Common/SessionStateMachineValidating.swift deleted file mode 100644 index 1099aab0f..000000000 --- a/Sources/WalletConnectAuth/Engine/Common/SessionStateMachineValidating.swift +++ /dev/null @@ -1,12 +0,0 @@ - -import Foundation - -protocol SessionStateMachineValidating { - func validateNamespaces(_ namespaces: Set) throws -} - -extension SessionStateMachineValidating { - func validateNamespaces(_ namespaces: Set) throws { - try Namespace.validate(namespaces) - } -} diff --git a/Sources/WalletConnectAuth/Engine/Controller/ControllerSessionStateMachine.swift b/Sources/WalletConnectAuth/Engine/Controller/ControllerSessionStateMachine.swift index 328146465..bccf2aca0 100644 --- a/Sources/WalletConnectAuth/Engine/Controller/ControllerSessionStateMachine.swift +++ b/Sources/WalletConnectAuth/Engine/Controller/ControllerSessionStateMachine.swift @@ -4,7 +4,7 @@ import WalletConnectUtils import WalletConnectKMS import Combine -final class ControllerSessionStateMachine: SessionStateMachineValidating { +final class ControllerSessionStateMachine { var onNamespacesUpdate: ((String, [String: SessionNamespace])->())? var onExpiryUpdate: ((String, Date)->())? @@ -27,11 +27,10 @@ final class ControllerSessionStateMachine: SessionStateMachineValidating { }.store(in: &publishers) } - // TODO: Change to new namespace spec func update(topic: String, namespaces: [String: SessionNamespace]) async throws { var session = try getSession(for: topic) try validateControlledAcknowledged(session) - try Validator.validate(namespaces) + try Namespace.validate(namespaces) logger.debug("Controller will update methods") session.updateNamespaces(namespaces) sessionStore.setSession(session) diff --git a/Sources/WalletConnectAuth/Engine/NonController/NonControllerSessionStateMachine.swift b/Sources/WalletConnectAuth/Engine/NonController/NonControllerSessionStateMachine.swift index 6a59c1fa6..8da7100b3 100644 --- a/Sources/WalletConnectAuth/Engine/NonController/NonControllerSessionStateMachine.swift +++ b/Sources/WalletConnectAuth/Engine/NonController/NonControllerSessionStateMachine.swift @@ -4,7 +4,7 @@ import WalletConnectUtils import WalletConnectKMS import Combine -final class NonControllerSessionStateMachine: SessionStateMachineValidating { +final class NonControllerSessionStateMachine { var onNamespacesUpdate: ((String, [String: SessionNamespace])->())? var onExpiryUpdate: ((String, Date) -> ())? @@ -42,7 +42,7 @@ final class NonControllerSessionStateMachine: SessionStateMachineValidating { // TODO: Update stored session namespaces private func onSessionUpdateNamespacesRequest(payload: WCRequestSubscriptionPayload, updateParams: SessionType.UpdateParams) { do { - try Validator.validate(updateParams.namespaces) + try Namespace.validate(updateParams.namespaces) } catch { networkingInteractor.respondError(for: payload, reason: .invalidUpdateNamespaceRequest) return diff --git a/Sources/WalletConnectAuth/Namespace.swift b/Sources/WalletConnectAuth/Namespace.swift index 29f7a5bc2..ebf5fe883 100644 --- a/Sources/WalletConnectAuth/Namespace.swift +++ b/Sources/WalletConnectAuth/Namespace.swift @@ -1,48 +1,13 @@ -// TODO: Remove type -public struct Namespace: Codable, Equatable, Hashable { - - public let chains: Set - public let methods: Set - public let events: Set - - public init(chains: Set, methods: Set, events: Set) { - self.chains = chains - self.methods = methods - self.events = events - } -} - -internal extension Namespace { - - static func validate(_ namespaces: Set) throws { - for namespace in namespaces { - guard !namespace.chains.isEmpty else { - throw WalletConnectError.namespaceHasEmptyChains - } - for method in namespace.methods { - if method.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - throw WalletConnectError.invalidMethod - } - } - for event in namespace.events { - if event.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - throw WalletConnectError.invalidEvent - } - } - } - } -} - public struct ProposalNamespace: Equatable, Codable { public let chains: Set public let methods: Set public let events: Set - public let `extension`: [Extension]? + public let extensions: [Extension]? public struct Extension: Equatable, Codable { public let chains: Set - public let methods: Set? - public let events: Set? + public let methods: Set + public let events: Set } } @@ -50,22 +15,80 @@ public struct SessionNamespace: Equatable, Codable { public let accounts: Set public let methods: Set public let events: Set - public let `extension`: [Extension]? + public let extensions: [Extension]? public struct Extension: Equatable, Codable { - public let chains: Set - public let methods: Set? - public let events: Set? + public let accounts: Set + public let methods: Set + public let events: Set } } -enum Validator { +enum Namespace { static func validate(_ namespaces: [String: ProposalNamespace]) throws { - // TODO + for (key, namespace) in namespaces { + if namespace.chains.isEmpty { + throw WalletConnectError.namespaceHasEmptyChains + } + for chain in namespace.chains { + if key != chain.namespace { + throw WalletConnectError.invalidNamespace + } + } + if let extensions = namespace.extensions { + for ext in extensions { + if ext.chains.isEmpty { + throw WalletConnectError.namespaceHasEmptyChains + } + } + } + } } static func validate(_ namespaces: [String: SessionNamespace]) throws { - // TODO + for (key, namespace) in namespaces { + if namespace.accounts.isEmpty { + throw WalletConnectError.invalidNamespace + } + for account in namespace.accounts { + if key != account.namespace { + throw WalletConnectError.invalidNamespace + } + } + if let extensions = namespace.extensions { + for ext in extensions { + if ext.accounts.isEmpty { + throw WalletConnectError.invalidNamespace + } + } + } + } + } + + static func validateApproved( + _ sessionNamespaces: [String: SessionNamespace], + against proposalNamespaces: [String: ProposalNamespace] + ) throws { + for (key, proposedNamespace) in proposalNamespaces { + guard let approvedNamespace = sessionNamespaces[key] else { + throw WalletConnectError.invalidNamespaceMatch + } + try proposedNamespace.chains.forEach { chain in + if !approvedNamespace.accounts.contains(where: { $0.blockchain == chain }) { + throw WalletConnectError.invalidNamespaceMatch + } + } + try proposedNamespace.methods.forEach { + if !approvedNamespace.methods.contains($0) { + throw WalletConnectError.invalidNamespaceMatch + } + } + try proposedNamespace.events.forEach { + if !approvedNamespace.events.contains($0) { + throw WalletConnectError.invalidNamespaceMatch + } + } + } } } diff --git a/Sources/WalletConnectAuth/WalletConnectError.swift b/Sources/WalletConnectAuth/WalletConnectError.swift index eb9d5217d..9e35fd760 100644 --- a/Sources/WalletConnectAuth/WalletConnectError.swift +++ b/Sources/WalletConnectAuth/WalletConnectError.swift @@ -6,6 +6,7 @@ enum WalletConnectError: Error { case noSessionMatchingTopic(String) case sessionNotAcknowledged(String) case pairingNotSettled(String) + case invalidNamespace case namespaceHasEmptyChains case invalidMethod case invalidEvent @@ -13,6 +14,7 @@ enum WalletConnectError: Error { case unauthorizedNonControllerCall case pairingAlreadyExist case topicGenerationFailed + case invalidNamespaceMatch // TODO: Refactor into actual cases case `internal`(_ reason: InternalReason) @@ -40,6 +42,8 @@ extension WalletConnectError { return "Pairing is not settled on topic \(topic)." case .invalidUpdateExpiryValue: return "Update expiry time is out of expected range" + case .invalidNamespace: + return "Namespace structure is invalid." case .namespaceHasEmptyChains: return "Namespace has an empty list of chain IDs." case .invalidMethod: @@ -52,6 +56,8 @@ extension WalletConnectError { return "Failed to generate topic from random bytes." case .pairingAlreadyExist: return "Pairing already exist" + case .invalidNamespaceMatch: + return "Invalid namespace approval." case .internal(_): // TODO: Remove internal case return "" } diff --git a/Tests/WalletConnectTests/NamespaceValidationTests.swift b/Tests/WalletConnectTests/NamespaceValidationTests.swift new file mode 100644 index 000000000..02300d750 --- /dev/null +++ b/Tests/WalletConnectTests/NamespaceValidationTests.swift @@ -0,0 +1,477 @@ +import XCTest +@testable import WalletConnectAuth + +final class NamespaceValidationTests: XCTestCase { + + let ethChain = Blockchain("eip155:1")! + let polyChain = Blockchain("eip155:137")! + let cosmosChain = Blockchain("cosmos:cosmoshub-4")! + + let ethAccount = Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! + let polyAccount = Account("eip155:137:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")! + let cosmosAccount = Account("cosmos:cosmoshub-4:cosmos1t2uflqwqe0fsj0shcfkrvpukewcw40yjj6hdc0")! + + // MARK: - Proposal namespace validation + + func testValidProposalNamespace() { + let namespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["method"], + events: ["event"], + extensions: [ + ProposalNamespace.Extension(chains: [Blockchain("eip155:137")!], methods: ["otherMethod"], events: ["otherEvent"]) + ] + ), + "cosmos": ProposalNamespace( + chains: [cosmosChain], + methods: ["someMethod"], + events: ["someEvent"], + extensions: nil + ) + ] + XCTAssertNoThrow(try Namespace.validate(namespace)) + } + + func testChainsMustNotNotBeEmpty() { + let namespace = [ + "eip155": ProposalNamespace( + chains: [], + methods: ["method"], + events: ["event"], + extensions: nil) + ] + XCTAssertThrowsError(try Namespace.validate(namespace)) + } + + func testChainAllowsEmptyMethods() { + let namespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: [], + events: ["event"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validate(namespace)) + } + + func testChainAllowsEmptyEvents() { + let namespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["method"], + events: [], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validate(namespace)) + } + + func testAllChainsContainsNamespacePrefix() { + let validNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, Blockchain("eip155:137")!, Blockchain("eip155:10")!], + methods: ["method"], + events: ["event"], + extensions: nil) + ] + let invalidNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, Blockchain("cosmos:cosmoshub-4")!], + methods: ["method"], + events: ["event"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validate(validNamespace)) + XCTAssertThrowsError(try Namespace.validate(invalidNamespace)) + } + + func testExtensionChainsMustNotBeEmpty() { + let namespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["method"], + events: ["event"], + extensions: [ + ProposalNamespace.Extension(chains: [], methods: ["otherMethod"], events: ["otherEvent"]) + ] + ) + ] + XCTAssertThrowsError(try Namespace.validate(namespace)) + } + + func testValidateAllProposalNamespaces() { + let namespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["method"], + events: ["event"], + extensions: nil), + "cosmos": ProposalNamespace( + chains: [], methods: [], events: [], extensions: nil) + ] + XCTAssertThrowsError(try Namespace.validate(namespace)) + } + + // MARK: - Session namespace validation + + func testValidSessionNamespace() { + let namespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["method"], + events: ["event"], + extensions: [ + SessionNamespace.Extension(accounts: [polyAccount], methods: ["otherMethod"], events: ["otherEvent"]) + ] + ) + ] + XCTAssertNoThrow(try Namespace.validate(namespace)) + } + + func testAccountsMustNotNotBeEmpty() { + let namespace = [ + "eip155": SessionNamespace( + accounts: [], + methods: ["method"], + events: ["event"], + extensions: nil) + ] + XCTAssertThrowsError(try Namespace.validate(namespace)) + } + + func testAccountAllowsEmptyMethods() { + let namespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: [], + events: ["event"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validate(namespace)) + } + + func testAccountAllowsEmptyEvents() { + let namespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["method"], + events: [], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validate(namespace)) + } + + func testAllAccountsContainsNamespacePrefix() { + let validNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: ["method"], + events: ["event"], + extensions: nil) + ] + let invalidNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, cosmosAccount], + methods: ["method"], + events: ["event"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validate(validNamespace)) + XCTAssertThrowsError(try Namespace.validate(invalidNamespace)) + } + + func testExtensionAccountsMustNotBeEmpty() { + let namespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["method"], + events: ["event"], + extensions: [ + SessionNamespace.Extension(accounts: [], methods: ["otherMethod"], events: ["otherEvent"]) + ] + ) + ] + XCTAssertThrowsError(try Namespace.validate(namespace)) + } + + func testValidateAllSessionNamespaces() { + let namespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["method"], + events: ["event"], + extensions: nil), + "cosmos": SessionNamespace( + accounts: [], methods: [], events: [], extensions: nil) + ] + XCTAssertThrowsError(try Namespace.validate(namespace)) + } + + // MARK: - Approval namespace validation + + func testNamespaceMustApproveAllMethods() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["eth_sign"], + events: [], + extensions: nil) + ] + let validSessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["eth_sign"], + events: [], + extensions: nil) + ] + let invalidSessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: [], + events: [], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(validSessionNamespace, against: proposalNamespace)) + XCTAssertThrowsError(try Namespace.validateApproved(invalidSessionNamespace, against: proposalNamespace)) + } + + func testApprovalMustHaveAtLeastOneAccountPerProposedChain() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, polyChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + let validSessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + let invalidSessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(validSessionNamespace, against: proposalNamespace)) + XCTAssertThrowsError(try Namespace.validateApproved(invalidSessionNamespace, against: proposalNamespace)) + } + + func testApprovalMayContainMultipleAccountsForSingleChain() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ + Account("eip155:1:0x25caCa7f7Bf3A77b1738A8c98A666dd9e4C69A0C")!, + Account("eip155:1:0x2Fe1cC9b1DCe6E8e16C48bc6A7ABbAB3d10DA954")!, + Account("eip155:1:0xEA674fdDe714fd979de3EdF0F56AA9716B898ec8")!, + Account("eip155:1:0xEB2F31B0224222D774541BfF89A221e7eb15a17E")!], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalMayExtendProposedMethodsAndEvents() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["eth_sign", "personalSign"], + events: ["accountsChanged", "someEvent"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalMayContainNonProposedChainAccounts() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalMustContainAllProposedNamespaces() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil), + "cosmos": ProposalNamespace( + chains: [cosmosChain], + methods: ["cosmos_signDirect"], + events: ["someEvent"], + extensions: nil) + ] + let validNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil), + "cosmos": SessionNamespace( + accounts: [cosmosAccount], + methods: ["cosmos_signDirect"], + events: ["someEvent"], + extensions: nil) + ] + let invalidNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: ["eth_sign", "cosmos_signDirect"], + events: ["accountsChanged", "someEvent"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(validNamespace, against: proposalNamespace)) + XCTAssertThrowsError(try Namespace.validateApproved(invalidNamespace, against: proposalNamespace)) + } + + func testExtensionsMayBeMerged() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, polyChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: [ + ProposalNamespace.Extension(chains: [polyChain], methods: ["personalSign"], events: []) + ] + ) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: ["eth_sign"], + events: ["accountsChanged", "personalSign"], + extensions: nil) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalMustContainAllEvents() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain], + methods: [], + events: ["chainChanged"], + extensions: nil) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount], + methods: [], + events: [], + extensions: nil) + ] + XCTAssertThrowsError(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalMayExtendoMethodsAndEventsInExtensions() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, polyChain], + methods: [], + events: ["chainChanged"], + extensions: [ + ProposalNamespace.Extension(chains: [polyChain], methods: ["eth_sign"], events: []) + ] + ) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: [], + events: ["chainChanged"], + extensions: [ + SessionNamespace.Extension( + accounts: [polyAccount], + methods: ["eth_sign", "personalSign"], + events: ["accountsChanged"] + ) + ] + ) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalExtensionsMayContainAccountsNotDefinedInProposal() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, polyChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: [ + ProposalNamespace.Extension(chains: [polyChain], methods: ["personalSign"], events: []) + ] + ) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: [ + SessionNamespace.Extension( + accounts: [polyAccount, Account("eip155:42:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")!], + methods: ["personalSign"], + events: [] + ) + ] + ) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } + + func testApprovalMayAddExtensionsNotDefinedInProposal() { + let proposalNamespace = [ + "eip155": ProposalNamespace( + chains: [ethChain, polyChain], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: nil) + ] + let sessionNamespace = [ + "eip155": SessionNamespace( + accounts: [ethAccount, polyAccount], + methods: ["eth_sign"], + events: ["accountsChanged"], + extensions: [ + SessionNamespace.Extension( + accounts: [polyAccount], + methods: ["personalSign"], + events: ["accountsChanged"] + ) + ] + ) + ] + XCTAssertNoThrow(try Namespace.validateApproved(sessionNamespace, against: proposalNamespace)) + } +} diff --git a/Tests/WalletConnectTests/PairingEngineTests.swift b/Tests/WalletConnectTests/PairingEngineTests.swift index faf3e4751..8863691d0 100644 --- a/Tests/WalletConnectTests/PairingEngineTests.swift +++ b/Tests/WalletConnectTests/PairingEngineTests.swift @@ -100,7 +100,7 @@ final class PairingEngineTests: XCTestCase { let request = WCRequest(method: .sessionPropose, params: .sessionPropose(proposal)) let payload = WCRequestSubscriptionPayload(topic: topicA, wcRequest: request) networkingInteractor.wcRequestPublisherSubject.send(payload) - let (topicB, _) = engine.respondSessionPropose(proposerPubKey: proposal.proposer.publicKey)! + let (topicB, _) = engine.approveProposal(proposerPubKey: proposal.proposer.publicKey, validating: SessionNamespace.stubDictionary())! XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") @@ -134,7 +134,7 @@ final class PairingEngineTests: XCTestCase { var sessionTopic: String! - engine.onProposeResponse = { topic in + engine.onProposeResponse = { topic, _ in sessionTopic = topic } networkingInteractor.onPairingResponse?(response) diff --git a/Tests/WalletConnectTests/SessionEngineTests.swift b/Tests/WalletConnectTests/SessionEngineTests.swift index 26366eb81..1b9b2cda0 100644 --- a/Tests/WalletConnectTests/SessionEngineTests.swift +++ b/Tests/WalletConnectTests/SessionEngineTests.swift @@ -72,9 +72,9 @@ final class SessionEngineTests: XCTestCase { didCallBackOnSessionApproved = true } + engine.settlingProposal = SessionProposal.stub() networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubSettle(topic: sessionTopic)) - XCTAssertTrue(storageMock.getSession(forTopic: sessionTopic)!.acknowledged, "Proposer must store acknowledged session on topic B") XCTAssertTrue(networkingInteractor.didRespondSuccess, "Proposer must send acknowledge on settle request") XCTAssertTrue(didCallBackOnSessionApproved, "Proposer's engine must call back with session") diff --git a/Tests/WalletConnectTests/Stub/Session+Stub.swift b/Tests/WalletConnectTests/Stub/Session+Stub.swift index 0ce1902c3..a6b3290df 100644 --- a/Tests/WalletConnectTests/Stub/Session+Stub.swift +++ b/Tests/WalletConnectTests/Stub/Session+Stub.swift @@ -36,7 +36,7 @@ extension SessionType.SettleParams { return SessionType.SettleParams( relay: RelayProtocolOptions.stub(), controller: Participant.stub(), - namespaces: [:], + namespaces: SessionNamespace.stubDictionary(), expiry: Int64(Date.distantFuture.timeIntervalSince1970)) } } diff --git a/Tests/WalletConnectTests/Stub/Stubs.swift b/Tests/WalletConnectTests/Stub/Stubs.swift index 899c2522c..84bc6dd9f 100644 --- a/Tests/WalletConnectTests/Stub/Stubs.swift +++ b/Tests/WalletConnectTests/Stub/Stubs.swift @@ -26,12 +26,6 @@ extension WCPairing { } } -extension Namespace { - static func stub() -> Namespace { - Namespace(chains: [Blockchain("eip155:1")!], methods: ["method"], events: ["event"]) - } -} - extension ProposalNamespace { static func stubDictionary() -> [String: ProposalNamespace] { return [ @@ -39,7 +33,7 @@ extension ProposalNamespace { chains: [Blockchain("eip155:1")!], methods: ["method"], events: ["event"], - extension: nil) + extensions: nil) ] } } @@ -51,7 +45,7 @@ extension SessionNamespace { accounts: [Account("eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb")!], methods: ["method"], events: ["event"], - extension: nil) + extensions: nil) ] } } @@ -101,7 +95,7 @@ extension WCRequestSubscriptionPayload { } extension SessionProposal { - static func stub(proposerPubKey: String) -> SessionProposal { + static func stub(proposerPubKey: String = "") -> SessionProposal { let relayOptions = RelayProtocolOptions(protocol: "waku", data: nil) return SessionType.ProposeParams( relays: [relayOptions],