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],