diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb0c45b96..297e36c28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,15 +20,39 @@ jobs: runs-on: macos-latest strategy: matrix: - test-type: [ui-tests, unit-tests, integration-tests, build-example-wallet, build-example-dapp] + test-type: [unit-tests, integration-tests, build-example-wallet, build-example-dapp] steps: - uses: actions/checkout@v2 - name: Setup Xcode Version uses: maxim-lobanov/setup-xcode@v1 + + - uses: actions/cache@v2 + with: + path: | + .build + SourcePackages + key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }} + restore-keys: | + ${{ runner.os }}-spm- + + - uses: ./.github/actions/ci with: - xcode-version: '13.2' + type: ${{ matrix.test-type }} + + test-ui: + if: github.ref == 'refs/heads/main' + runs-on: macos-latest + strategy: + matrix: + test-type: [ui-tests] + + steps: + - uses: actions/checkout@v2 + + - name: Setup Xcode Version + uses: maxim-lobanov/setup-xcode@v1 - uses: actions/cache@v2 with: diff --git a/.github/workflows/intake.yml b/.github/workflows/intake.yml new file mode 100644 index 000000000..4b79bc326 --- /dev/null +++ b/.github/workflows/intake.yml @@ -0,0 +1,43 @@ +# This workflow moves issues to the Swift board +# when they receive the "accepted" label +# When WalletConnect Org members create issues they +# are automatically "accepted". +# Else they need to manually receive that label during intake. +name: intake + +on: + issues: + types: [opened, labeled] + pull_request: + types: [opened, labeled] + +jobs: + add-to-project: + name: Add issue to board + if: github.event.action == 'labeled' && github.event.label.name == 'accepted' + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.1.0 + with: + project-url: https://github.com/orgs/WalletConnect/projects/5 + github-token: ${{ secrets.ASSIGN_TO_PROJECT_GITHUB_TOKEN }} + labeled: accepted + label-operator: OR + auto-promote: + name: auto-promote + if: github.event.action == 'opened' + runs-on: ubuntu-latest + steps: + - name: Check if organization member + id: is_organization_member + if: github.event.action == 'opened' + uses: JamesSingleton/is-organization-member@1.0.0 + with: + organization: WalletConnect + username: ${{ github.event_name != 'pull_request' && github.event.issue.user.login || github.event.sender.login }} + token: ${{ secrets.ASSIGN_TO_PROJECT_GITHUB_TOKEN }} + - name: Label issues + uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90 + with: + add-labels: "accepted" + repo-token: ${{ secrets.ASSIGN_TO_PROJECT_GITHUB_TOKEN }} diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AuthTests.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AuthTests.xcscheme new file mode 100644 index 000000000..b6a508a85 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AuthTests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme index 3f9debd20..75284ea37 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnect.xcscheme @@ -262,6 +262,16 @@ ReferencedContainer = "container:"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/ExampleApp.xcodeproj/project.pbxproj b/Example/ExampleApp.xcodeproj/project.pbxproj index 8b9592e3e..6018e9231 100644 --- a/Example/ExampleApp.xcodeproj/project.pbxproj +++ b/Example/ExampleApp.xcodeproj/project.pbxproj @@ -23,6 +23,7 @@ 765056272821989600F9AE79 /* Color+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 765056262821989600F9AE79 /* Color+Extension.swift */; }; 76744CF726FE4D5400B77ED9 /* ActiveSessionItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76744CF626FE4D5400B77ED9 /* ActiveSessionItem.swift */; }; 76744CF926FE4D7400B77ED9 /* ActiveSessionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76744CF826FE4D7400B77ED9 /* ActiveSessionCell.swift */; }; + 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 767DC83428997F8E00080FA9 /* EthSendTransaction.swift */; }; 7694A5262874296A0001257E /* RegistryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7694A5252874296A0001257E /* RegistryTests.swift */; }; 76B149F02821C03B00F05F91 /* Proposal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B149EF2821C03B00F05F91 /* Proposal.swift */; }; 76B6E39F2807A3B6004DF775 /* WalletViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B6E39E2807A3B6004DF775 /* WalletViewController.swift */; }; @@ -182,6 +183,7 @@ 765056262821989600F9AE79 /* Color+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = ""; }; 76744CF626FE4D5400B77ED9 /* ActiveSessionItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionItem.swift; sourceTree = ""; }; 76744CF826FE4D7400B77ED9 /* ActiveSessionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveSessionCell.swift; sourceTree = ""; }; + 767DC83428997F8E00080FA9 /* EthSendTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthSendTransaction.swift; sourceTree = ""; }; 7694A5252874296A0001257E /* RegistryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistryTests.swift; sourceTree = ""; }; 76B149EF2821C03B00F05F91 /* Proposal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Proposal.swift; sourceTree = ""; }; 76B6E39E2807A3B6004DF775 /* WalletViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletViewController.swift; sourceTree = ""; }; @@ -436,6 +438,15 @@ name = Frameworks; sourceTree = ""; }; + 767DC83328997F7600080FA9 /* Helpers */ = { + isa = PBXGroup; + children = ( + A5E03DF8286465C700888481 /* ClientDelegate.swift */, + 767DC83428997F8E00080FA9 /* EthSendTransaction.swift */, + ); + path = Helpers; + sourceTree = ""; + }; 8460DCFD274F98A90081F94C /* Request */ = { isa = PBXGroup; children = ( @@ -862,7 +873,7 @@ A5E03E0928646A8100888481 /* Sign */ = { isa = PBXGroup; children = ( - A5E03DF8286465C700888481 /* ClientDelegate.swift */, + 767DC83328997F7600080FA9 /* Helpers */, A5E03DF9286465C700888481 /* SignClientTests.swift */, ); path = Sign; @@ -1248,6 +1259,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */, A5E03DFB286465C700888481 /* ClientDelegate.swift in Sources */, A5E03E03286466F400888481 /* ChatTests.swift in Sources */, 7694A5262874296A0001257E /* RegistryTests.swift in Sources */, diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift index 87093f6ef..812b34e05 100644 --- a/Example/IntegrationTests/Chat/ChatTests.swift +++ b/Example/IntegrationTests/Chat/ChatTests.swift @@ -16,25 +16,27 @@ final class ChatTests: XCTestCase { registry = KeyValueRegistry() invitee = makeClient(prefix: "🦖 Registered") inviter = makeClient(prefix: "🍄 Inviter") + + waitClientsConnected() } - private func waitClientsConnected() async { - let group = DispatchGroup() - group.enter() + private func waitClientsConnected() { + let expectation = expectation(description: "Wait Clients Connected") + expectation.expectedFulfillmentCount = 2 + invitee.socketConnectionStatusPublisher.sink { status in if status == .connected { - group.leave() + expectation.fulfill() } }.store(in: &publishers) - group.enter() inviter.socketConnectionStatusPublisher.sink { status in if status == .connected { - group.leave() + expectation.fulfill() } }.store(in: &publishers) - group.wait() - return + + wait(for: [expectation], timeout: 5) } func makeClient(prefix: String) -> ChatClient { @@ -43,11 +45,10 @@ final class ChatTests: XCTestCase { let projectId = "8ba9ee138960775e5231b70cc5ef1c3a" let keychain = KeychainStorageMock() let relayClient = RelayClient(relayHost: relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger) - return ChatClient(registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), logger: logger, keyValueStorage: RuntimeKeyValueStorage()) + return ChatClientFactory.create(registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), logger: logger, keyValueStorage: RuntimeKeyValueStorage()) } func testInvite() async { - await waitClientsConnected() let inviteExpectation = expectation(description: "invitation expectation") let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")! @@ -59,61 +60,65 @@ final class ChatTests: XCTestCase { wait(for: [inviteExpectation], timeout: 4) } -// func testAcceptAndCreateNewThread() async { -// await waitClientsConnected() -// let newThreadInviterExpectation = expectation(description: "new thread on inviting client expectation") -// let newThreadinviteeExpectation = expectation(description: "new thread on invitee client expectation") -// let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! -// let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")! -// let pubKey = try! await invitee.register(account: inviteeAccount) -// -// try! await inviter.invite(publicKey: pubKey, peerAccount: inviteeAccount, openingMessage: "opening message", account: inviterAccount) -// -// invitee.invitePublisher.sink { [unowned self] invite in -// Task {try! await invitee.accept(inviteId: invite.id)} -// }.store(in: &publishers) -// -// invitee.newThreadPublisher.sink { _ in -// newThreadinviteeExpectation.fulfill() -// }.store(in: &publishers) -// -// inviter.newThreadPublisher.sink { _ in -// newThreadInviterExpectation.fulfill() -// }.store(in: &publishers) -// -// wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: 4) -// } -// -// func testMessage() async { -// await waitClientsConnected() -// let messageExpectation = expectation(description: "message received") -// messageExpectation.expectedFulfillmentCount = 2 -// let message = "message" -// let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! -// let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")! -// let pubKey = try! await invitee.register(account: inviteeAccount) -// try! await inviter.invite(publicKey: pubKey, peerAccount: inviteeAccount, openingMessage: "opening message", account: inviterAccount) -// -// invitee.invitePublisher.sink { [unowned self] invite in -// Task {try! await invitee.accept(inviteId: invite.id)} -// }.store(in: &publishers) -// -// invitee.newThreadPublisher.sink { [unowned self] thread in -// Task {try! await invitee.message(topic: thread.topic, message: message)} -// }.store(in: &publishers) -// -// inviter.newThreadPublisher.sink { [unowned self] thread in -// Task {try! await inviter.message(topic: thread.topic, message: message)} -// }.store(in: &publishers) -// -// inviter.messagePublisher.sink { _ in -// messageExpectation.fulfill() -// }.store(in: &publishers) -// -// invitee.messagePublisher.sink { _ in -// messageExpectation.fulfill() -// }.store(in: &publishers) -// -// wait(for: [messageExpectation], timeout: 35) -// } + func testAcceptAndCreateNewThread() { + let newThreadInviterExpectation = expectation(description: "new thread on inviting client expectation") + let newThreadinviteeExpectation = expectation(description: "new thread on invitee client expectation") + let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! + let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")! + + Task(priority: .background) { + let pubKey = try! await invitee.register(account: inviteeAccount) + + try! await inviter.invite(publicKey: pubKey, peerAccount: inviteeAccount, openingMessage: "opening message", account: inviterAccount) + } + + invitee.invitePublisher.sink { [unowned self] invite in + Task {try! await invitee.accept(inviteId: invite.id)} + }.store(in: &publishers) + + invitee.newThreadPublisher.sink { _ in + newThreadinviteeExpectation.fulfill() + }.store(in: &publishers) + + inviter.newThreadPublisher.sink { _ in + newThreadInviterExpectation.fulfill() + }.store(in: &publishers) + + wait(for: [newThreadinviteeExpectation, newThreadInviterExpectation], timeout: 10) + } + + func testMessage() { + let messageExpectation = expectation(description: "message received") + messageExpectation.expectedFulfillmentCount = 4 // expectedFulfillmentCount 4 because onMessage() called on send too + + let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")! + let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")! + + Task(priority: .background) { + let pubKey = try! await invitee.register(account: inviteeAccount) + try! await inviter.invite(publicKey: pubKey, peerAccount: inviteeAccount, openingMessage: "opening message", account: inviterAccount) + } + + invitee.invitePublisher.sink { [unowned self] invite in + Task {try! await invitee.accept(inviteId: invite.id)} + }.store(in: &publishers) + + invitee.newThreadPublisher.sink { [unowned self] thread in + Task {try! await invitee.message(topic: thread.topic, message: "message")} + }.store(in: &publishers) + + inviter.newThreadPublisher.sink { [unowned self] thread in + Task {try! await inviter.message(topic: thread.topic, message: "message")} + }.store(in: &publishers) + + inviter.messagePublisher.sink { _ in + messageExpectation.fulfill() + }.store(in: &publishers) + + invitee.messagePublisher.sink { _ in + messageExpectation.fulfill() + }.store(in: &publishers) + + wait(for: [messageExpectation], timeout: 10) + } } diff --git a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift index 12baaea83..44f329ac4 100644 --- a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift +++ b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift @@ -22,7 +22,7 @@ final class RelayClientEndToEndTests: XCTestCase { ) let urlFactory = RelayUrlFactory(socketAuthenticator: socketAuthenticator) let socket = WebSocket(url: urlFactory.create(host: relayHost, projectId: projectId)) - + let logger = ConsoleLogger() let dispatcher = Dispatcher(socket: socket, socketConnectionHandler: ManualSocketConnectionHandler(socket: socket), logger: logger) return RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage()) diff --git a/Example/IntegrationTests/Sign/ClientDelegate.swift b/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift similarity index 100% rename from Example/IntegrationTests/Sign/ClientDelegate.swift rename to Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift diff --git a/Example/IntegrationTests/Sign/Helpers/EthSendTransaction.swift b/Example/IntegrationTests/Sign/Helpers/EthSendTransaction.swift new file mode 100644 index 000000000..6e4db5b62 --- /dev/null +++ b/Example/IntegrationTests/Sign/Helpers/EthSendTransaction.swift @@ -0,0 +1,26 @@ +import Foundation + +struct EthSendTransaction: Codable, Equatable { + let from: String + let data: String + let value: String + let to: String + let gasPrice: String + let nonce: String + + static func stub() -> EthSendTransaction { + try! JSONDecoder().decode(EthSendTransaction.self, from: ethSendTransactionJSON.data(using: .utf8)!) + } + + private static let ethSendTransactionJSON = """ +{ + "from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155", + "to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567", + "data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675", + "gas":"0x76c0", + "gasPrice":"0x9184e72a000", + "value":"0x9184e72a", + "nonce":"0x117" +} +""" +} diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift index e2cd14006..cbf6566cf 100644 --- a/Example/IntegrationTests/Sign/SignClientTests.swift +++ b/Example/IntegrationTests/Sign/SignClientTests.swift @@ -8,8 +8,8 @@ final class SignClientTests: XCTestCase { let defaultTimeout: TimeInterval = 5 - var proposer: ClientDelegate! - var responder: ClientDelegate! + var dapp: ClientDelegate! + var wallet: ClientDelegate! static private func makeClientDelegate( name: String, @@ -27,7 +27,7 @@ final class SignClientTests: XCTestCase { socketConnectionType: .automatic, logger: logger ) - let client = SignClient( + let client = SignClientFactory.create( metadata: AppMetadata(name: name, description: "", url: "", icons: [""]), logger: logger, keyValueStorage: RuntimeKeyValueStorage(), @@ -40,11 +40,11 @@ final class SignClientTests: XCTestCase { private func listenForConnection() async { let group = DispatchGroup() group.enter() - proposer.onConnected = { + dapp.onConnected = { group.leave() } group.enter() - responder.onConnected = { + wallet.onConnected = { group.leave() } group.wait() @@ -52,27 +52,29 @@ final class SignClientTests: XCTestCase { } override func setUp() async throws { - proposer = Self.makeClientDelegate(name: "🍏P") - responder = Self.makeClientDelegate(name: "🍎R") + dapp = Self.makeClientDelegate(name: "🍏P") + wallet = Self.makeClientDelegate(name: "🍎R") await listenForConnection() } override func tearDown() { - proposer = nil - responder = nil + dapp = nil + wallet = nil } func testSessionPropose() async throws { - let dapp = proposer! - let wallet = responder! let dappSettlementExpectation = expectation(description: "Dapp expects to settle a session") let walletSettlementExpectation = expectation(description: "Wallet expects to settle a session") let requiredNamespaces = ProposalNamespace.stubRequired() let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) - wallet.onSessionProposal = { proposal in + wallet.onSessionProposal = { [unowned self] proposal in Task(priority: .high) { - do { try await wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) } catch { XCTFail("\(error)") } + do { + try await wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) + } catch { + XCTFail("\(error)") + } } } dapp.onSessionSettled = { _ in @@ -88,8 +90,6 @@ final class SignClientTests: XCTestCase { } func testSessionReject() async throws { - let dapp = proposer! - let wallet = responder! let sessionRejectExpectation = expectation(description: "Proposer is notified on session rejection") class Store { var rejectedProposal: Session.Proposal? } @@ -98,7 +98,7 @@ final class SignClientTests: XCTestCase { let uri = try await dapp.client.connect(requiredNamespaces: ProposalNamespace.stubRequired()) try await wallet.client.pair(uri: uri!) - wallet.onSessionProposal = { proposal in + wallet.onSessionProposal = { [unowned self] proposal in Task(priority: .high) { do { try await wallet.client.reject(proposalId: proposal.id, reason: .disapprovedChains) // TODO: Review reason @@ -114,18 +114,16 @@ final class SignClientTests: XCTestCase { } func testSessionDelete() async throws { - let dapp = proposer! - let wallet = responder! let sessionDeleteExpectation = expectation(description: "Wallet expects session to be deleted") let requiredNamespaces = ProposalNamespace.stubRequired() let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) - wallet.onSessionProposal = { proposal in + wallet.onSessionProposal = { [unowned self] proposal in Task(priority: .high) { do { try await wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) } catch { XCTFail("\(error)") } } } - dapp.onSessionSettled = { settledSession in + dapp.onSessionSettled = { [unowned self] settledSession in Task(priority: .high) { try await dapp.client.disconnect(topic: settledSession.topic) } @@ -140,8 +138,6 @@ final class SignClientTests: XCTestCase { } func testNewPairingPing() async throws { - let dapp = proposer! - let wallet = responder! let pongResponseExpectation = expectation(description: "Ping sender receives a pong response") let uri = try await dapp.client.connect(requiredNamespaces: ProposalNamespace.stubRequired())! @@ -155,6 +151,99 @@ final class SignClientTests: XCTestCase { wait(for: [pongResponseExpectation], timeout: defaultTimeout) } + func testSessionRequest() async throws { + let requestExpectation = expectation(description: "Wallet expects to receive a request") + let responseExpectation = expectation(description: "Dapp expects to receive a response") + let requiredNamespaces = ProposalNamespace.stubRequired() + let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) + + let requestMethod = "eth_sendTransaction" + let requestParams = [EthSendTransaction.stub()] + let responseParams = "0xdeadbeef" + let chain = Blockchain("eip155:1")! + + wallet.onSessionProposal = { [unowned self] proposal in + Task(priority: .high) { + do { + try await wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) } + catch { + XCTFail("\(error)") + } + } + } + dapp.onSessionSettled = { [unowned self] settledSession in + Task(priority: .high) { + let request = Request(id: 0, topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) + try await dapp.client.request(params: request) + } + } + wallet.onSessionRequest = { [unowned self] sessionRequest in + let receivedParams = try! sessionRequest.params.get([EthSendTransaction].self) + XCTAssertEqual(receivedParams, requestParams) + XCTAssertEqual(sessionRequest.method, requestMethod) + requestExpectation.fulfill() + Task(priority: .high) { + let jsonrpcResponse = JSONRPCResponse(id: sessionRequest.id, result: AnyCodable(responseParams)) + try await wallet.client.respond(topic: sessionRequest.topic, response: .response(jsonrpcResponse)) + } + } + dapp.onSessionResponse = { response in + switch response.result { + case .response(let response): + XCTAssertEqual(try! response.result.get(String.self), responseParams) + case .error: + XCTFail() + } + responseExpectation.fulfill() + } + + let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces) + try await wallet.client.pair(uri: uri!) + wait(for: [requestExpectation, responseExpectation], timeout: defaultTimeout) + } + + func testSessionRequestFailureResponse() async throws { + let expectation = expectation(description: "Dapp expects to receive an error response") + let requiredNamespaces = ProposalNamespace.stubRequired() + let sessionNamespaces = SessionNamespace.make(toRespond: requiredNamespaces) + + let requestMethod = "eth_sendTransaction" + let requestParams = [EthSendTransaction.stub()] + let error = JSONRPCErrorResponse.Error(code: 0, message: "error") + let chain = Blockchain("eip155:1")! + + wallet.onSessionProposal = { [unowned self] proposal in + Task(priority: .high) { + try await wallet.client.approve(proposalId: proposal.id, namespaces: sessionNamespaces) + } + } + dapp.onSessionSettled = { [unowned self] settledSession in + Task(priority: .high) { + let request = Request(id: 0, topic: settledSession.topic, method: requestMethod, params: requestParams, chainId: chain) + try await dapp.client.request(params: request) + } + } + wallet.onSessionRequest = { [unowned self] sessionRequest in + Task(priority: .high) { + let response = JSONRPCErrorResponse(id: sessionRequest.id, error: error) + try await wallet.client.respond(topic: sessionRequest.topic, response: .error(response)) + } + } + dapp.onSessionResponse = { response in + switch response.result { + case .response: + XCTFail() + case .error(let errorResponse): + XCTAssertEqual(error, errorResponse.error) + } + expectation.fulfill() + } + + let uri = try await dapp.client.connect(requiredNamespaces: requiredNamespaces) + try await wallet.client.pair(uri: uri!) + wait(for: [expectation], timeout: defaultTimeout) + } + // // func testNewSessionOnExistingPairing() async { // await waitClientsConnected() @@ -186,82 +275,6 @@ final class SignClientTests: XCTestCase { // wait(for: [proposerSettlesSessionExpectation, responderSettlesSessionExpectation], timeout: defaultTimeout) // } // -// -// func testProposerRequestSessionRequest() async { -// await waitClientsConnected() -// let requestExpectation = expectation(description: "Responder receives request") -// let responseExpectation = expectation(description: "Proposer receives response") -// let method = "eth_sendTransaction" -// let params = [try! JSONDecoder().decode(EthSendTransaction.self, from: ethSendTransaction.data(using: .utf8)!)] -// let responseParams = "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c" -// let uri = try! await proposer.client.connect(namespaces: [Namespace.stub(methods: [method])])! -// -// _ = try! await responder.client.pair(uri: uri) -// responder.onSessionProposal = {[unowned self] proposal in -// try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: proposal.namespaces) -// } -// proposer.onSessionSettled = {[unowned self] settledSession in -// let requestParams = Request(id: 0, topic: settledSession.topic, method: method, params: AnyCodable(params), chainId: Blockchain("eip155:1")!) -// Task { -// try await self.proposer.client.request(params: requestParams) -// } -// } -// proposer.onSessionResponse = { response in -// switch response.result { -// case .response(let jsonRpcResponse): -// let response = try! jsonRpcResponse.result.get(String.self) -// XCTAssertEqual(response, responseParams) -// responseExpectation.fulfill() -// case .error(_): -// XCTFail() -// } -// } -// responder.onSessionRequest = {[unowned self] sessionRequest in -// XCTAssertEqual(sessionRequest.method, method) -// let ethSendTrancastionParams = try! sessionRequest.params.get([EthSendTransaction].self) -// XCTAssertEqual(ethSendTrancastionParams, params) -// let jsonrpcResponse = JSONRPCResponse(id: sessionRequest.id, result: AnyCodable(responseParams)) -// self.responder.client.respond(topic: sessionRequest.topic, response: .response(jsonrpcResponse)) -// requestExpectation.fulfill() -// } -// wait(for: [requestExpectation, responseExpectation], timeout: defaultTimeout) -// } -// -// -// func testSessionRequestFailureResponse() async { -// await waitClientsConnected() -// let failureResponseExpectation = expectation(description: "Proposer receives failure response") -// let method = "eth_sendTransaction" -// let params = [try! JSONDecoder().decode(EthSendTransaction.self, from: ethSendTransaction.data(using: .utf8)!)] -// let error = JSONRPCErrorResponse.Error(code: 0, message: "error_message") -// let uri = try! await proposer.client.connect(namespaces: [Namespace.stub(methods: [method])])! -// _ = try! await responder.client.pair(uri: uri) -// responder.onSessionProposal = {[unowned self] proposal in -// try? self.responder.client.approve(proposalId: proposal.id, accounts: [], namespaces: proposal.namespaces) -// } -// proposer.onSessionSettled = {[unowned self] settledSession in -// let requestParams = Request(id: 0, topic: settledSession.topic, method: method, params: AnyCodable(params), chainId: Blockchain("eip155:1")!) -// Task { -// try await self.proposer.client.request(params: requestParams) -// } -// } -// proposer.onSessionResponse = { response in -// switch response.result { -// case .response(_): -// XCTFail() -// case .error(let errorResponse): -// XCTAssertEqual(error, errorResponse.error) -// failureResponseExpectation.fulfill() -// } -// -// } -// responder.onSessionRequest = {[unowned self] sessionRequest in -// let jsonrpcErrorResponse = JSONRPCErrorResponse(id: sessionRequest.id, error: error) -// self.responder.client.respond(topic: sessionRequest.topic, response: .error(jsonrpcErrorResponse)) -// } -// wait(for: [failureResponseExpectation], timeout: defaultTimeout) -// } -// // func testSessionPing() async { // await waitClientsConnected() // let proposerReceivesPingResponseExpectation = expectation(description: "Proposer receives ping response") @@ -368,26 +381,3 @@ final class SignClientTests: XCTestCase { // wait(for: [proposerReceivesEventExpectation], timeout: defaultTimeout) // } } - -// public struct EthSendTransaction: Codable, Equatable { -// public let from: String -// public let data: String -// public let value: String -// public let to: String -// public let gasPrice: String -// public let nonce: String -// } -// -// -// fileprivate let ethSendTransaction = """ -// { -// "from":"0xb60e8dd61c5d32be8058bb8eb970870f07233155", -// "to":"0xd46e8dd67c5d32be8058bb8eb970870f07244567", -// "data":"0xd46e8dd67c5d32be8d46e8dd67c5d32be8058bb8eb970870f072445675058bb8eb970870f072445675", -// "gas":"0x76c0", -// "gasPrice":"0x9184e72a000", -// "value":"0x9184e72a", -// "nonce":"0x117" -// } -// """ -// diff --git a/Example/IntegrationTests/Stubs/Stubs.swift b/Example/IntegrationTests/Stubs/Stubs.swift index 78f06afca..d4f1f9eda 100644 --- a/Example/IntegrationTests/Stubs/Stubs.swift +++ b/Example/IntegrationTests/Stubs/Stubs.swift @@ -5,7 +5,7 @@ extension ProposalNamespace { return [ "eip155": ProposalNamespace( chains: [Blockchain("eip155:1")!], - methods: ["personal_sign"], + methods: ["personal_sign", "eth_sendTransaction"], events: []) ] } diff --git a/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift b/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift index 33eeaadf5..fe4d61fa7 100644 --- a/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift +++ b/Example/Showcase/Classes/DomainLayer/Chat/ChatFactory.swift @@ -2,6 +2,7 @@ import Foundation import Chat import WalletConnectKMS import WalletConnectRelay +import WalletConnectUtils class ChatFactory { @@ -12,10 +13,11 @@ class ChatFactory { let client = HTTPClient(host: "keys.walletconnect.com") let registry = KeyserverRegistryProvider(client: client) let relayClient = RelayClient(relayHost: relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory()) - return ChatClient( + return ChatClientFactory.create( registry: registry, relayClient: relayClient, kms: KeyManagementService(keychain: keychain), + logger: ConsoleLogger(), keyValueStorage: UserDefaults.standard ) } diff --git a/Example/Showcase/Classes/PresentationLayer/Import/ImportInteractor.swift b/Example/Showcase/Classes/PresentationLayer/Import/ImportInteractor.swift index a6bcca7e8..81d70a023 100644 --- a/Example/Showcase/Classes/PresentationLayer/Import/ImportInteractor.swift +++ b/Example/Showcase/Classes/PresentationLayer/Import/ImportInteractor.swift @@ -1,4 +1,3 @@ - final class ImportInteractor { private let registerService: RegisterService private let accountStorage: AccountStorage diff --git a/Package.swift b/Package.swift index 537aeed2a..a94e15017 100644 --- a/Package.swift +++ b/Package.swift @@ -15,18 +15,25 @@ let package = Package( targets: ["WalletConnectSign"]), .library( name: "WalletConnectChat", - targets: ["Chat"]) + targets: ["Chat"]), + .library( + name: "WalletConnectPairing", + targets: ["WalletConnectPairing"]) ], dependencies: [], targets: [ .target( name: "WalletConnectSign", - dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS"], + dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS", "WalletConnectPairing"], path: "Sources/WalletConnectSign"), .target( name: "Chat", dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS"], path: "Sources/Chat"), + .target( + name: "Auth", + dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS", "WalletConnectPairing"], + path: "Sources/Auth"), .target( name: "WalletConnectRelay", dependencies: ["WalletConnectUtils", "WalletConnectKMS"], @@ -35,9 +42,12 @@ let package = Package( name: "WalletConnectKMS", dependencies: ["WalletConnectUtils"], path: "Sources/WalletConnectKMS"), + .target( + name: "WalletConnectPairing", + dependencies: ["WalletConnectUtils"]), .target( name: "WalletConnectUtils", - dependencies: ["Commons"]), + dependencies: ["Commons", "JSONRPC"]), .target( name: "JSONRPC", dependencies: ["Commons"]), @@ -50,6 +60,9 @@ let package = Package( .testTarget( name: "ChatTests", dependencies: ["Chat", "WalletConnectUtils", "TestingUtils"]), + .testTarget( + name: "AuthTests", + dependencies: ["Auth", "WalletConnectUtils", "TestingUtils"]), .testTarget( name: "RelayerTests", dependencies: ["WalletConnectRelay", "WalletConnectUtils", "TestingUtils"]), @@ -58,11 +71,11 @@ let package = Package( dependencies: ["WalletConnectKMS", "WalletConnectUtils", "TestingUtils"]), .target( name: "TestingUtils", - dependencies: ["WalletConnectUtils", "WalletConnectKMS"], + dependencies: ["WalletConnectUtils", "WalletConnectKMS", "JSONRPC"], path: "Tests/TestingUtils"), .testTarget( name: "WalletConnectUtilsTests", - dependencies: ["WalletConnectUtils"]), + dependencies: ["WalletConnectUtils", "TestingUtils"]), .testTarget( name: "JSONRPCTests", dependencies: ["JSONRPC", "TestingUtils"]), diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift new file mode 100644 index 000000000..a9c751104 --- /dev/null +++ b/Sources/Auth/AuthClient.swift @@ -0,0 +1,31 @@ +import Foundation + +class AuthClient { + enum Errors: Error { + case malformedPairingURI + } + + private let appPairService: AppPairService + private let appRequestService: AuthRequestService + + private let walletPairService: WalletPairService + + init(appPairService: AppPairService, appRequestService: AuthRequestService, walletPairService: WalletPairService) { + self.appPairService = appPairService + self.appRequestService = appRequestService + self.walletPairService = walletPairService + } + + func request(params: RequestParams) async throws -> String { + let uri = try await appPairService.create() + try await appRequestService.request(params: params, topic: uri.topic) + return uri.absoluteString + } + + func pair(uri: String) async throws { + guard let pairingURI = WalletConnectURI(string: uri) else { + throw Errors.malformedPairingURI + } + try await walletPairService.pair(pairingURI) + } +} diff --git a/Sources/Auth/Services/App/AppPairService.swift b/Sources/Auth/Services/App/AppPairService.swift new file mode 100644 index 000000000..05a309653 --- /dev/null +++ b/Sources/Auth/Services/App/AppPairService.swift @@ -0,0 +1,25 @@ +import Foundation +import WalletConnectKMS +import WalletConnectPairing + +actor AppPairService { + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let pairingStorage: WCPairingStorage + + init(networkingInteractor: NetworkInteracting, kms: KeyManagementServiceProtocol, pairingStorage: WCPairingStorage) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStorage = pairingStorage + } + + func create() async throws -> WalletConnectURI { + let topic = String.generateTopic() + try await networkingInteractor.subscribe(topic: topic) + let symKey = try! kms.createSymmetricKey(topic) + let pairing = WCPairing(topic: topic) + let uri = WalletConnectURI(topic: topic, symKey: symKey.hexRepresentation, relay: pairing.relay) + pairingStorage.setPairing(pairing) + return uri + } +} diff --git a/Sources/Auth/Services/App/AuthRequestService.swift b/Sources/Auth/Services/App/AuthRequestService.swift new file mode 100644 index 000000000..b82d5259b --- /dev/null +++ b/Sources/Auth/Services/App/AuthRequestService.swift @@ -0,0 +1,30 @@ +import Foundation +import WalletConnectUtils +import WalletConnectKMS +import JSONRPC + +actor AuthRequestService { + private let networkingInteractor: NetworkInteracting + private let appMetadata: AppMetadata + private let kms: KeyManagementService + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementService, + appMetadata: AppMetadata) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.appMetadata = appMetadata + } + + func request(params: RequestParams, topic: String) async throws { + let pubKey = try kms.createX25519KeyPair() + let responseTopic = pubKey.rawRepresentation.sha256().toHexString() + let requester = AuthRequestParams.Requester(publicKey: pubKey.hexRepresentation, metadata: appMetadata) + let issueAt = ISO8601DateFormatter().string(from: Date()) + let payload = AuthPayload(requestParams: params, iat: issueAt) + let params = AuthRequestParams(requester: requester, payloadParams: payload) + let request = RPCRequest(method: "wc_authRequest", params: params) + try await networkingInteractor.request(request, topic: topic, tag: AuthRequestParams.tag) + try await networkingInteractor.subscribe(topic: responseTopic) + } +} diff --git a/Sources/Auth/Services/Common/CacaoFormatter.swift b/Sources/Auth/Services/Common/CacaoFormatter.swift new file mode 100644 index 000000000..43ddb9058 --- /dev/null +++ b/Sources/Auth/Services/Common/CacaoFormatter.swift @@ -0,0 +1,12 @@ +import Foundation +import WalletConnectUtils + +protocol CacaoFormatting { + func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ issuer: Account) -> Cacao +} + +class CacaoFormatter: CacaoFormatting { + func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ issuer: Account) -> Cacao { + fatalError("not implemented") + } +} diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/Auth/Services/Common/NetworkingInteractor.swift new file mode 100644 index 000000000..c81420b26 --- /dev/null +++ b/Sources/Auth/Services/Common/NetworkingInteractor.swift @@ -0,0 +1,53 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import Combine +import WalletConnectKMS +import JSONRPC + +protocol NetworkInteracting { + var requestPublisher: AnyPublisher {get} + func subscribe(topic: String) async throws + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws + func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws +} + +extension NetworkInteracting { + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType = .type0) async throws { + try await self.request(request, topic: topic, tag: tag, envelopeType: envelopeType) + } +} + +class NetworkingInteractor: NetworkInteracting { + private let relayClient: RelayClient + private let serializer: Serializing + private let rpcHistory: RPCHistory + var requestPublisher: AnyPublisher { + requestPublisherSubject.eraseToAnyPublisher() + } + private let requestPublisherSubject = PassthroughSubject() + + init(relayClient: RelayClient, + serializer: Serializing, + rpcHistory: RPCHistory) { + self.relayClient = relayClient + self.serializer = serializer + self.rpcHistory = rpcHistory + } + + func subscribe(topic: String) async throws { + try await relayClient.subscribe(topic: topic) + } + + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + try rpcHistory.set(request, forTopic: topic, emmitedBy: .local) + let message = try! serializer.serialize(topic: topic, encodable: request, envelopeType: envelopeType) + try await relayClient.publish(topic: topic, payload: message, tag: tag) + } + + func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + try rpcHistory.resolve(response) + let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType) + try await relayClient.publish(topic: topic, payload: message, tag: tag) + } +} diff --git a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift new file mode 100644 index 000000000..edf489f74 --- /dev/null +++ b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift @@ -0,0 +1,11 @@ +import Foundation + +protocol SIWEMessageFormatting { + func formatMessage(from request: AuthRequestParams) throws -> String +} + +struct SIWEMessageFormatter: SIWEMessageFormatting { + func formatMessage(from request: AuthRequestParams) throws -> String { + fatalError("not implemented") + } +} diff --git a/Sources/Auth/Services/Wallet/AuthRequestSubscriber.swift b/Sources/Auth/Services/Wallet/AuthRequestSubscriber.swift new file mode 100644 index 000000000..d65a56bae --- /dev/null +++ b/Sources/Auth/Services/Wallet/AuthRequestSubscriber.swift @@ -0,0 +1,39 @@ +import Combine +import Foundation +import WalletConnectUtils +import JSONRPC + +class AuthRequestSubscriber { + private let networkingInteractor: NetworkInteracting + private let logger: ConsoleLogging + private var publishers = [AnyCancellable]() + private let messageFormatter: SIWEMessageFormatting + var onRequest: ((_ id: RPCID, _ message: String)->())? + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + messageFormatter: SIWEMessageFormatting) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.messageFormatter = messageFormatter + subscribeForRequest() + } + + private func subscribeForRequest() { + networkingInteractor.requestPublisher.sink { [unowned self] subscriptionPayload in + guard subscriptionPayload.request.method == "wc_authRequest" else { return } + guard let authRequestParams = try? subscriptionPayload.request.params?.get(AuthRequestParams.self) else { + logger.debug("Malformed auth request params") + return + } + do { + let message = try messageFormatter.formatMessage(from: authRequestParams) + guard let requestId = subscriptionPayload.request.id else { return } + onRequest?(requestId, message) + } catch { + logger.debug(error) + } + }.store(in: &publishers) + } + +} diff --git a/Sources/Auth/Services/Wallet/AuthRespondService.swift b/Sources/Auth/Services/Wallet/AuthRespondService.swift new file mode 100644 index 000000000..006459d37 --- /dev/null +++ b/Sources/Auth/Services/Wallet/AuthRespondService.swift @@ -0,0 +1,41 @@ +import Foundation +import WalletConnectKMS +import JSONRPC +import WalletConnectUtils + +actor AuthRespondService { + enum Errors: Error { + case recordForIdNotFound + case malformedAuthRequestParams + } + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementService + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + + init(networkingInteractor: NetworkInteracting, + logger: ConsoleLogging, + kms: KeyManagementService, + rpcHistory: RPCHistory) { + self.networkingInteractor = networkingInteractor + self.logger = logger + self.kms = kms + self.rpcHistory = rpcHistory + } + + func respond(respondParams: RespondParams, issuer: Account) async throws { + guard let request = rpcHistory.get(recordId: RPCID(respondParams.id))?.request else { throw Errors.recordForIdNotFound } + guard let authRequestParams = try? request.params?.get(AuthRequestParams.self) else { throw Errors.malformedAuthRequestParams } + + let peerPubKey = authRequestParams.requester.publicKey + let responseTopic = peerPubKey.rawRepresentation.sha256().toHexString() + let selfPubKey = try kms.createX25519KeyPair() + let agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey) + try kms.setAgreementSecret(agreementKeys, topic: responseTopic) + + let cacao = CacaoFormatter().format(authRequestParams, respondParams.signature, issuer) + let response = RPCResponse(id: request.id!, result: cacao) + + try await networkingInteractor.respond(topic: respondParams.topic, response: response, tag: AuthResponseParams.tag, envelopeType: .type1(pubKey: selfPubKey.rawRepresentation)) + } +} diff --git a/Sources/Auth/Services/Wallet/WalletPairService.swift b/Sources/Auth/Services/Wallet/WalletPairService.swift new file mode 100644 index 000000000..ef4fc2e28 --- /dev/null +++ b/Sources/Auth/Services/Wallet/WalletPairService.swift @@ -0,0 +1,37 @@ +import Foundation +import WalletConnectKMS +import WalletConnectPairing + +actor WalletPairService { + enum Errors: Error { + case pairingAlreadyExist + } + + private let networkingInteractor: NetworkInteracting + private let kms: KeyManagementServiceProtocol + private let pairingStorage: WCPairingStorage + + init(networkingInteractor: NetworkInteracting, + kms: KeyManagementServiceProtocol, + pairingStorage: WCPairingStorage) { + self.networkingInteractor = networkingInteractor + self.kms = kms + self.pairingStorage = pairingStorage + } + + func pair(_ uri: WalletConnectURI) async throws { + guard !hasPairing(for: uri.topic) else { + throw Errors.pairingAlreadyExist + } + var pairing = WCPairing(uri: uri) + try await networkingInteractor.subscribe(topic: pairing.topic) + let symKey = try SymmetricKey(hex: uri.symKey) + try kms.setSymmetricKey(symKey, for: pairing.topic) + pairing.activate() + pairingStorage.setPairing(pairing) + } + + func hasPairing(for topic: String) -> Bool { + return pairingStorage.hasPairing(forTopic: topic) + } +} diff --git a/Sources/Auth/Types/AppMetadata.swift b/Sources/Auth/Types/AppMetadata.swift new file mode 100644 index 000000000..bd646f855 --- /dev/null +++ b/Sources/Auth/Types/AppMetadata.swift @@ -0,0 +1,3 @@ +import WalletConnectPairing + +public typealias AppMetadata = WalletConnectPairing.AppMetadata diff --git a/Sources/Auth/Types/AuthPayload.swift b/Sources/Auth/Types/AuthPayload.swift new file mode 100644 index 000000000..b281c19d0 --- /dev/null +++ b/Sources/Auth/Types/AuthPayload.swift @@ -0,0 +1,31 @@ +import Foundation + +struct AuthPayload: Codable, Equatable { + let type: String + let chainId: String + let domain: String + let aud: String + let version: String + let nonce: String + let iat: String + let nbf: String? + let exp: String? + let statement: String? + let requestId: String? + let resources: String? + + init(requestParams: RequestParams, iat: String) { + self.type = "eip4361" + self.chainId = requestParams.chainId + self.domain = requestParams.domain + self.aud = requestParams.aud + self.version = "1" + self.nonce = requestParams.nonce + self.iat = iat + self.nbf = requestParams.nbf + self.exp = requestParams.exp + self.statement = requestParams.statement + self.requestId = requestParams.requestId + self.resources = requestParams.resources + } +} diff --git a/Sources/Auth/Types/Cacao/Cacao.swift b/Sources/Auth/Types/Cacao/Cacao.swift new file mode 100644 index 000000000..95a2d59c5 --- /dev/null +++ b/Sources/Auth/Types/Cacao/Cacao.swift @@ -0,0 +1,7 @@ +import Foundation + +struct Cacao: Codable, Equatable { + let header: CacaoHeader + let payload: CacaoPayload + let signature: CacaoSignature +} diff --git a/Sources/Auth/Types/Cacao/CacaoHeader.swift b/Sources/Auth/Types/Cacao/CacaoHeader.swift new file mode 100644 index 000000000..1461f3ae4 --- /dev/null +++ b/Sources/Auth/Types/Cacao/CacaoHeader.swift @@ -0,0 +1,5 @@ +import Foundation + +struct CacaoHeader: Codable, Equatable { + let t: String +} diff --git a/Sources/Auth/Types/Cacao/CacaoPayload.swift b/Sources/Auth/Types/Cacao/CacaoPayload.swift new file mode 100644 index 000000000..c339bd18d --- /dev/null +++ b/Sources/Auth/Types/Cacao/CacaoPayload.swift @@ -0,0 +1,15 @@ +import Foundation + +struct CacaoPayload: Codable, Equatable { + let iss: String + let domain: String + let aud: String + let version: String + let nonce: String + let iat: String + let nbf: String + let exp: String + let statement: String + let requestId: String + let resources: String +} diff --git a/Sources/Auth/Types/Cacao/CacaoSignature.swift b/Sources/Auth/Types/Cacao/CacaoSignature.swift new file mode 100644 index 000000000..97c04c142 --- /dev/null +++ b/Sources/Auth/Types/Cacao/CacaoSignature.swift @@ -0,0 +1,7 @@ +import Foundation + +struct CacaoSignature: Codable, Equatable { + let t: String + let s: String + let m: String +} diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift new file mode 100644 index 000000000..c39ba3489 --- /dev/null +++ b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift @@ -0,0 +1,18 @@ +import Foundation +import WalletConnectUtils + +struct AuthRequestParams: Codable, Equatable { + let requester: Requester + let payloadParams: AuthPayload + + static var tag: Int { + return 3000 + } +} + +extension AuthRequestParams { + struct Requester: Codable, Equatable { + let publicKey: String + let metadata: AppMetadata + } +} diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift new file mode 100644 index 000000000..a0d64b152 --- /dev/null +++ b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift @@ -0,0 +1,12 @@ +import Foundation +import WalletConnectUtils + +struct AuthResponseParams: Codable, Equatable { + let header: CacaoHeader + let payload: CacaoPayload + let signature: CacaoSignature + + static var tag: Int { + return 3001 + } +} diff --git a/Sources/Auth/Types/RelayProtocolOptions.swift b/Sources/Auth/Types/RelayProtocolOptions.swift new file mode 100644 index 000000000..d5b4b6c4f --- /dev/null +++ b/Sources/Auth/Types/RelayProtocolOptions.swift @@ -0,0 +1,4 @@ +import Foundation +import WalletConnectUtils + +typealias RelayProtocolOptions = WalletConnectUtils.RelayProtocolOptions diff --git a/Sources/Auth/Types/RequestParams.swift b/Sources/Auth/Types/RequestParams.swift new file mode 100644 index 000000000..7a6082f52 --- /dev/null +++ b/Sources/Auth/Types/RequestParams.swift @@ -0,0 +1,13 @@ +import Foundation + +struct RequestParams { + let domain: String + let chainId: String + let nonce: String + let aud: String + let nbf: String? + let exp: String? + let statement: String? + let requestId: String? + let resources: String? +} diff --git a/Sources/Auth/Types/RequestSubscriptionPayload.swift b/Sources/Auth/Types/RequestSubscriptionPayload.swift new file mode 100644 index 000000000..b7865d715 --- /dev/null +++ b/Sources/Auth/Types/RequestSubscriptionPayload.swift @@ -0,0 +1,7 @@ +import Foundation +import JSONRPC + +struct RequestSubscriptionPayload: Codable { + let id: Int64 + let request: RPCRequest +} diff --git a/Sources/Auth/Types/RespondParams.swift b/Sources/Auth/Types/RespondParams.swift new file mode 100644 index 000000000..fcd9b3293 --- /dev/null +++ b/Sources/Auth/Types/RespondParams.swift @@ -0,0 +1,7 @@ +import Foundation + +struct RespondParams { + let id: Int64 + let topic: String + let signature: CacaoSignature +} diff --git a/Sources/Auth/Types/WalletConnectURI.swift b/Sources/Auth/Types/WalletConnectURI.swift new file mode 100644 index 000000000..a2cfe2de9 --- /dev/null +++ b/Sources/Auth/Types/WalletConnectURI.swift @@ -0,0 +1,4 @@ +import Foundation +import WalletConnectUtils + +typealias WalletConnectURI = WalletConnectUtils.WalletConnectURI diff --git a/Sources/Chat/ChatClient.swift b/Sources/Chat/ChatClient.swift index 25fad4bb0..1c44bbeb6 100644 --- a/Sources/Chat/ChatClient.swift +++ b/Sources/Chat/ChatClient.swift @@ -35,48 +35,39 @@ public class ChatClient { messagePublisherSubject.eraseToAnyPublisher() } - public init(registry: Registry, - relayClient: RelayClient, + // MARK: - Initialization + + init(registry: Registry, + registryService: RegistryService, + messagingService: MessagingService, + invitationHandlingService: InvitationHandlingService, + inviteService: InviteService, + leaveService: LeaveService, + resubscriptionService: ResubscriptionService, kms: KeyManagementService, - logger: ConsoleLogging = ConsoleLogger(loggingLevel: .debug), - keyValueStorage: KeyValueStorage) { - let topicToRegistryRecordStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) + threadStore: Database, + messagesStore: Database, + invitePayloadStore: CodableStore<(RequestSubscriptionPayload)>, + socketConnectionStatusPublisher: AnyPublisher + ) { self.registry = registry + self.registryService = registryService + self.messagingService = messagingService + self.invitationHandlingService = invitationHandlingService + self.inviteService = inviteService + self.leaveService = leaveService + self.resubscriptionService = resubscriptionService self.kms = kms - let serialiser = Serializer(kms: kms) - let jsonRpcHistory = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) - let networkingInteractor = NetworkingInteractor( - relayClient: relayClient, - serializer: serialiser, - logger: logger, - jsonRpcHistory: jsonRpcHistory) - self.invitePayloadStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) - self.registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) - threadStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue) - self.resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, threadStore: threadStore, logger: logger) - self.invitationHandlingService = InvitationHandlingService(registry: registry, - networkingInteractor: networkingInteractor, - kms: kms, - logger: logger, - topicToRegistryRecordStore: topicToRegistryRecordStore, - invitePayloadStore: invitePayloadStore, - threadsStore: threadStore) - self.inviteService = InviteService( - networkingInteractor: networkingInteractor, - kms: kms, - threadStore: threadStore, - logger: logger) - self.leaveService = LeaveService() - self.messagesStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.messages.rawValue) - self.messagingService = MessagingService( - networkingInteractor: networkingInteractor, - messagesStore: messagesStore, - threadStore: threadStore, - logger: logger) - socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher + self.threadStore = threadStore + self.messagesStore = messagesStore + self.invitePayloadStore = invitePayloadStore + self.socketConnectionStatusPublisher = socketConnectionStatusPublisher + setUpEnginesCallbacks() } + // MARK: - Public interface + /// Registers a new record on Chat keyserver, /// record is a blockchain account with a client generated public key /// - Parameter account: CAIP10 blockchain account diff --git a/Sources/Chat/ChatClientFactory.swift b/Sources/Chat/ChatClientFactory.swift new file mode 100644 index 000000000..89f4907b1 --- /dev/null +++ b/Sources/Chat/ChatClientFactory.swift @@ -0,0 +1,45 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS + +public struct ChatClientFactory { + + public static func create( + registry: Registry, + relayClient: RelayClient, + kms: KeyManagementService, + logger: ConsoleLogging, + keyValueStorage: KeyValueStorage) -> ChatClient { + let topicToRegistryRecordStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.topicToInvitationPubKey.rawValue) + let serialiser = Serializer(kms: kms) + let jsonRpcHistory = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) + let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serialiser, logger: logger, jsonRpcHistory: jsonRpcHistory) + let invitePayloadStore = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.invite.rawValue) + let registryService = RegistryService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore) + let threadStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.threads.rawValue) + let resubscriptionService = ResubscriptionService(networkingInteractor: networkingInteractor, threadStore: threadStore, logger: logger) + let invitationHandlingService = InvitationHandlingService(registry: registry, networkingInteractor: networkingInteractor, kms: kms, logger: logger, topicToRegistryRecordStore: topicToRegistryRecordStore, invitePayloadStore: invitePayloadStore, threadsStore: threadStore) + let inviteService = InviteService(networkingInteractor: networkingInteractor, kms: kms, threadStore: threadStore, logger: logger) + let leaveService = LeaveService() + let messagesStore = Database(keyValueStorage: keyValueStorage, identifier: StorageDomainIdentifiers.messages.rawValue) + let messagingService = MessagingService(networkingInteractor: networkingInteractor, messagesStore: messagesStore, threadStore: threadStore, logger: logger) + + let client = ChatClient( + registry: registry, + registryService: registryService, + messagingService: messagingService, + invitationHandlingService: invitationHandlingService, + inviteService: inviteService, + leaveService: leaveService, + resubscriptionService: resubscriptionService, + kms: kms, + threadStore: threadStore, + messagesStore: messagesStore, + invitePayloadStore: invitePayloadStore, + socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher + ) + + return client + } +} diff --git a/Sources/Commons/Either.swift b/Sources/Commons/Either.swift index 71e7b8dde..3c5f1fbdf 100644 --- a/Sources/Commons/Either.swift +++ b/Sources/Commons/Either.swift @@ -63,3 +63,15 @@ extension Either: Codable where L: Codable, R: Codable { } } } + +extension Either: CustomStringConvertible { + + public var description: String { + switch self { + case let .left(left): + return "\(left)" + case let .right(right): + return "\(right)" + } + } +} diff --git a/Sources/JSONRPC/RPCID.swift b/Sources/JSONRPC/RPCID.swift index a4a7bec7e..9d52511d9 100644 --- a/Sources/JSONRPC/RPCID.swift +++ b/Sources/JSONRPC/RPCID.swift @@ -1,6 +1,6 @@ import Commons -public typealias RPCID = Either +public typealias RPCID = Either public protocol IdentifierGenerator { func next() -> RPCID @@ -9,6 +9,6 @@ public protocol IdentifierGenerator { struct IntIdentifierGenerator: IdentifierGenerator { func next() -> RPCID { - return RPCID(Int.random(in: Int.min...Int.max)) + return RPCID(Int64.random(in: Int64.min...Int64.max)) } } diff --git a/Sources/JSONRPC/RPCRequest.swift b/Sources/JSONRPC/RPCRequest.swift index 473550899..351d05a0d 100644 --- a/Sources/JSONRPC/RPCRequest.swift +++ b/Sources/JSONRPC/RPCRequest.swift @@ -37,7 +37,7 @@ public struct RPCRequest: Equatable { try self.init(method: method, checkedParams: params, id: idGenerator.next()) } - public init(method: String, checkedParams params: C, id: Int) throws where C: Codable { + public init(method: String, checkedParams params: C, id: Int64) throws where C: Codable { try self.init(method: method, checkedParams: params, id: .right(id)) } @@ -49,10 +49,14 @@ public struct RPCRequest: Equatable { self.init(method: method, params: AnyCodable(params), id: idGenerator.next()) } - public init(method: String, params: C, id: Int) where C: Codable { + public init(method: String, params: C, id: Int64) where C: Codable { self.init(method: method, params: AnyCodable(params), id: .right(id)) } + public init(method: String, params: C, rpcid: RPCID) where C: Codable { + self.init(method: method, params: AnyCodable(params), id: rpcid) + } + public init(method: String, params: C, id: String) where C: Codable { self.init(method: method, params: AnyCodable(params), id: .left(id)) } @@ -61,7 +65,7 @@ public struct RPCRequest: Equatable { self.init(method: method, params: nil, id: idGenerator.next()) } - public init(method: String, id: Int) { + public init(method: String, id: Int64) { self.init(method: method, params: nil, id: .right(id)) } @@ -72,11 +76,11 @@ public struct RPCRequest: Equatable { extension RPCRequest { - static func notification(method: String, params: C) -> RPCRequest where C: Codable { + public static func notification(method: String, params: C) -> RPCRequest where C: Codable { return RPCRequest(method: method, params: AnyCodable(params), id: nil) } - static func notification(method: String) -> RPCRequest { + public static func notification(method: String) -> RPCRequest { return RPCRequest(method: method, params: nil, id: nil) } diff --git a/Sources/JSONRPC/RPCResponse.swift b/Sources/JSONRPC/RPCResponse.swift index 0934adb7e..e0e60f06f 100644 --- a/Sources/JSONRPC/RPCResponse.swift +++ b/Sources/JSONRPC/RPCResponse.swift @@ -19,7 +19,7 @@ public struct RPCResponse: Equatable { return nil } - private let outcome: Result + public let outcome: Result internal init(id: RPCID?, outcome: Result) { self.jsonrpc = "2.0" @@ -27,7 +27,15 @@ public struct RPCResponse: Equatable { self.outcome = outcome } - public init(id: Int, result: C) where C: Codable { + public init(matchingRequest: RPCRequest, result: C) where C: Codable { + self.init(id: matchingRequest.id, outcome: .success(AnyCodable(result))) + } + + public init(matchingRequest: RPCRequest, error: JSONRPCError) { + self.init(id: matchingRequest.id, outcome: .failure(error)) + } + + public init(id: Int64, result: C) where C: Codable { self.init(id: RPCID(id), outcome: .success(AnyCodable(result))) } @@ -35,7 +43,11 @@ public struct RPCResponse: Equatable { self.init(id: RPCID(id), outcome: .success(AnyCodable(result))) } - public init(id: Int, error: JSONRPCError) { + public init(id: RPCID, result: C) where C: Codable { + self.init(id: id, outcome: .success(AnyCodable(result))) + } + + public init(id: Int64, error: JSONRPCError) { self.init(id: RPCID(id), outcome: .failure(error)) } @@ -43,7 +55,7 @@ public struct RPCResponse: Equatable { self.init(id: RPCID(id), outcome: .failure(error)) } - public init(id: Int, errorCode: Int, message: String, associatedData: AnyCodable? = nil) { + public init(id: Int64, errorCode: Int, message: String, associatedData: AnyCodable? = nil) { self.init(id: RPCID(id), outcome: .failure(JSONRPCError(code: errorCode, message: message, data: associatedData))) } diff --git a/Sources/WalletConnectKMS/Crypto/Hash.swift b/Sources/WalletConnectKMS/Crypto/Hash.swift index f38504b32..61220058d 100644 --- a/Sources/WalletConnectKMS/Crypto/Hash.swift +++ b/Sources/WalletConnectKMS/Crypto/Hash.swift @@ -1,5 +1,3 @@ -// - import Foundation import CryptoKit diff --git a/Sources/WalletConnectKMS/Keychain/KeychainStorage.swift b/Sources/WalletConnectKMS/Keychain/KeychainStorage.swift index 5a3e9b548..c8c3ee083 100644 --- a/Sources/WalletConnectKMS/Keychain/KeychainStorage.swift +++ b/Sources/WalletConnectKMS/Keychain/KeychainStorage.swift @@ -28,6 +28,10 @@ public final class KeychainStorage: KeychainStorageProtocol { let status = secItem.add(query as CFDictionary, nil) + guard status != errSecDuplicateItem else { + return try update(data: data, forKey: key) + } + guard status == errSecSuccess else { throw KeychainError(status) } diff --git a/Sources/WalletConnectSign/Storage/PairingStorage.swift b/Sources/WalletConnectPairing/Storage/PairingStorage.swift similarity index 58% rename from Sources/WalletConnectSign/Storage/PairingStorage.swift rename to Sources/WalletConnectPairing/Storage/PairingStorage.swift index 211994e03..297ff35fd 100644 --- a/Sources/WalletConnectSign/Storage/PairingStorage.swift +++ b/Sources/WalletConnectPairing/Storage/PairingStorage.swift @@ -1,4 +1,7 @@ -protocol WCPairingStorage: AnyObject { +import Foundation +import WalletConnectUtils + +public protocol WCPairingStorage: AnyObject { var onPairingExpiration: ((WCPairing) -> Void)? { get set } func hasPairing(forTopic topic: String) -> Bool func setPairing(_ pairing: WCPairing) @@ -8,40 +11,40 @@ protocol WCPairingStorage: AnyObject { func deleteAll() } -final class PairingStorage: WCPairingStorage { +public final class PairingStorage: WCPairingStorage { - var onPairingExpiration: ((WCPairing) -> Void)? { + public var onPairingExpiration: ((WCPairing) -> Void)? { get { storage.onSequenceExpiration } set { storage.onSequenceExpiration = newValue } } private let storage: SequenceStore - init(storage: SequenceStore) { + public init(storage: SequenceStore) { self.storage = storage } - func hasPairing(forTopic topic: String) -> Bool { + public func hasPairing(forTopic topic: String) -> Bool { storage.hasSequence(forTopic: topic) } - func setPairing(_ pairing: WCPairing) { + public func setPairing(_ pairing: WCPairing) { storage.setSequence(pairing) } - func getPairing(forTopic topic: String) -> WCPairing? { + public func getPairing(forTopic topic: String) -> WCPairing? { try? storage.getSequence(forTopic: topic) } - func getAll() -> [WCPairing] { + public func getAll() -> [WCPairing] { storage.getAll() } - func delete(topic: String) { + public func delete(topic: String) { storage.delete(topic: topic) } - func deleteAll() { + public func deleteAll() { storage.deleteAll() } } diff --git a/Sources/WalletConnectSign/Types/Common/AppMetadata.swift b/Sources/WalletConnectPairing/Types/AppMetadata.swift similarity index 100% rename from Sources/WalletConnectSign/Types/Common/AppMetadata.swift rename to Sources/WalletConnectPairing/Types/AppMetadata.swift diff --git a/Sources/WalletConnectSign/Types/Pairing/PairingProposal.swift b/Sources/WalletConnectPairing/Types/PairingProposal.swift similarity index 100% rename from Sources/WalletConnectSign/Types/Pairing/PairingProposal.swift rename to Sources/WalletConnectPairing/Types/PairingProposal.swift diff --git a/Sources/WalletConnectPairing/Types/PairingType.swift b/Sources/WalletConnectPairing/Types/PairingType.swift new file mode 100644 index 000000000..a7d279407 --- /dev/null +++ b/Sources/WalletConnectPairing/Types/PairingType.swift @@ -0,0 +1,18 @@ +import Foundation + +// Internal namespace for pairing payloads. +public enum PairingType { + + public struct DeleteParams: Codable, Equatable { + let reason: Reason + } + + public struct Reason: Codable, Equatable { + let code: Int + let message: String + } + + public struct PingParams: Codable, Equatable { + public init() { } + } +} diff --git a/Sources/WalletConnectSign/Types/Pairing/WCPairing.swift b/Sources/WalletConnectPairing/Types/WCPairing.swift similarity index 51% rename from Sources/WalletConnectSign/Types/Pairing/WCPairing.swift rename to Sources/WalletConnectPairing/Types/WCPairing.swift index 794f36c86..b15a45e64 100644 --- a/Sources/WalletConnectSign/Types/Pairing/WCPairing.swift +++ b/Sources/WalletConnectPairing/Types/WCPairing.swift @@ -1,28 +1,33 @@ import Foundation -import WalletConnectKMS +import WalletConnectUtils -struct WCPairing: SequenceObject { - let topic: String - let relay: RelayProtocolOptions - var peerMetadata: AppMetadata? - private (set) var expiryDate: Date - private (set) var active: Bool +public struct WCPairing: SequenceObject { + enum Errors: Error { + case invalidUpdateExpiryValue + } + + public let topic: String + public let relay: RelayProtocolOptions + public var peerMetadata: AppMetadata? + + public private (set) var expiryDate: Date + public private (set) var active: Bool #if DEBUG - static var dateInitializer: () -> Date = Date.init + public static var dateInitializer: () -> Date = Date.init #else private static var dateInitializer: () -> Date = Date.init #endif - static var timeToLiveInactive: TimeInterval { + public static var timeToLiveInactive: TimeInterval { 5 * .minute } - static var timeToLiveActive: TimeInterval { + public static var timeToLiveActive: TimeInterval { 30 * .day } - init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, expiryDate: Date) { + public init(topic: String, relay: RelayProtocolOptions, peerMetadata: AppMetadata, isActive: Bool = false, expiryDate: Date) { self.topic = topic self.relay = relay self.peerMetadata = peerMetadata @@ -30,31 +35,31 @@ struct WCPairing: SequenceObject { self.expiryDate = expiryDate } - init(topic: String) { + public init(topic: String) { self.topic = topic - self.relay = RelayProtocolOptions(protocol: "iridium", data: nil) + self.relay = RelayProtocolOptions(protocol: "irn", data: nil) self.active = false self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive) } - init(uri: WalletConnectURI) { + public init(uri: WalletConnectURI) { self.topic = uri.topic self.relay = uri.relay self.active = false self.expiryDate = Self.dateInitializer().advanced(by: Self.timeToLiveInactive) } - mutating func activate() { + public mutating func activate() { active = true try? updateExpiry() } - mutating func updateExpiry(_ ttl: TimeInterval = WCPairing.timeToLiveActive) throws { + public mutating func updateExpiry(_ ttl: TimeInterval = WCPairing.timeToLiveActive) throws { let now = Self.dateInitializer() let newExpiryDate = now.advanced(by: ttl) let maxExpiryDate = now.advanced(by: Self.timeToLiveActive) guard newExpiryDate > expiryDate && newExpiryDate <= maxExpiryDate else { - throw WalletConnectError.invalidUpdateExpiryValue + throw Errors.invalidUpdateExpiryValue } expiryDate = newExpiryDate } diff --git a/Sources/WalletConnectRelay/EnvironmentInfo.swift b/Sources/WalletConnectRelay/EnvironmentInfo.swift index 767dc8752..f726b7578 100644 --- a/Sources/WalletConnectRelay/EnvironmentInfo.swift +++ b/Sources/WalletConnectRelay/EnvironmentInfo.swift @@ -1,4 +1,7 @@ +#if os(iOS) import UIKit +#endif +import Foundation enum EnvironmentInfo { @@ -15,10 +18,15 @@ enum EnvironmentInfo { } static var sdkVersion: String { - "v0.9.1-rc.0" + "v0.9.2-rc.0" } static var operatingSystem: String { - "\(UIDevice.current.systemName)-\(UIDevice.current.systemVersion)" +#if os(iOS) + return "\(UIDevice.current.systemName)-\(UIDevice.current.systemVersion)" +#elseif os(macOS) + let systemVersion = ProcessInfo.processInfo.operatingSystemVersion + return "macOS-\(systemVersion)" +#endif } } diff --git a/Sources/WalletConnectRelay/RPC/Methods.swift b/Sources/WalletConnectRelay/RPC/Methods.swift new file mode 100644 index 000000000..be94ac20e --- /dev/null +++ b/Sources/WalletConnectRelay/RPC/Methods.swift @@ -0,0 +1,65 @@ +struct Subscribe: RelayRPC { + + struct Params: Codable { + let topic: String + } + + let params: Params + + var method: String { + "subscribe" + } +} + +struct Unsubscribe: RelayRPC { + + struct Params: Codable { + let id: String + let topic: String + } + + let params: Params + + var method: String { + "unsubscribe" + } +} + +struct Publish: RelayRPC { + + struct Params: Codable { + let topic: String + let message: String + let ttl: Int + let prompt: Bool? + let tag: Int? + } + + let params: Params + + var method: String { + "publish" + } +} + +struct Subscription: RelayRPC { + + struct Params: Codable { + struct Contents: Codable { + let topic: String + let message: String + } + let id: String + let data: Contents + } + + let params: Params + + var method: String { + "subscription" + } + + init(id: String, topic: String, message: String) { + self.params = Params(id: id, data: Params.Contents(topic: topic, message: message)) + } +} diff --git a/Sources/WalletConnectRelay/RPC/RPCMethod.swift b/Sources/WalletConnectRelay/RPC/RPCMethod.swift new file mode 100644 index 000000000..1f104f7c7 --- /dev/null +++ b/Sources/WalletConnectRelay/RPC/RPCMethod.swift @@ -0,0 +1,5 @@ +protocol RPCMethod { + associatedtype Parameters + var method: String { get } + var params: Parameters { get } +} diff --git a/Sources/WalletConnectRelay/RPC/RelayRPC.swift b/Sources/WalletConnectRelay/RPC/RelayRPC.swift new file mode 100644 index 000000000..6950c42b5 --- /dev/null +++ b/Sources/WalletConnectRelay/RPC/RelayRPC.swift @@ -0,0 +1,34 @@ +import JSONRPC + +protocol RelayRPC: RPCMethod {} + +extension RelayRPC where Parameters: Codable { + + var idGenerator: IdentifierGenerator { + return WalletConnectRPCID() + } + + func wrapToIRN() -> PrefixDecorator { + return PrefixDecorator(rpcMethod: self, prefix: "irn") + } + + func asRPCRequest() -> RPCRequest { + RPCRequest(method: self.method, params: self.params, idGenerator: self.idGenerator) + } +} + +struct PrefixDecorator: RelayRPC where T: RelayRPC { + + typealias Parameters = T.Parameters + + let rpcMethod: T + let prefix: String + + var method: String { + "\(prefix)_\(rpcMethod.method)" + } + + var params: Parameters { + rpcMethod.params + } +} diff --git a/Sources/WalletConnectRelay/RPC/WalletConnectRPCID.swift b/Sources/WalletConnectRelay/RPC/WalletConnectRPCID.swift new file mode 100644 index 000000000..b33eb4959 --- /dev/null +++ b/Sources/WalletConnectRelay/RPC/WalletConnectRPCID.swift @@ -0,0 +1,11 @@ +import Foundation +import JSONRPC + +struct WalletConnectRPCID: IdentifierGenerator { + + func next() -> RPCID { + let timestamp = Int64(Date().timeIntervalSince1970 * 1000) * 1000 + let random = Int64.random(in: 0..<1000) + return .right(Int64(timestamp + random)) + } +} diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift index 47dc706b8..6be813de9 100644 --- a/Sources/WalletConnectRelay/RelayClient.swift +++ b/Sources/WalletConnectRelay/RelayClient.swift @@ -2,41 +2,45 @@ import Foundation import Combine import WalletConnectUtils import WalletConnectKMS +import JSONRPC public enum SocketConnectionStatus { case connected case disconnected } + public final class RelayClient { - enum RelyerError: Error { + + enum Errors: Error { case subscriptionIdNotFound } - private typealias SubscriptionRequest = JSONRPCRequest - private typealias SubscriptionResponse = JSONRPCResponse - private typealias RequestAcknowledgement = JSONRPCResponse - private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.relay_client", - attributes: .concurrent) - let jsonRpcSubscriptionsHistory: JsonRpcHistory + + static let historyIdentifier = "com.walletconnect.sdk.relayer_client.subscription_json_rpc_record" + public var onMessage: ((String, String) -> Void)? - private var dispatcher: Dispatching - var subscriptions: [String: String] = [:] + let defaultTtl = 6*Time.hour + var subscriptions: [String: String] = [:] public var socketConnectionStatusPublisher: AnyPublisher { socketConnectionStatusPublisherSubject.eraseToAnyPublisher() } private let socketConnectionStatusPublisherSubject = PassthroughSubject() - private var subscriptionResponsePublisher: AnyPublisher, Never> { + private let subscriptionResponsePublisherSubject = PassthroughSubject<(RPCID?, String), Never>() + private var subscriptionResponsePublisher: AnyPublisher<(RPCID?, String), Never> { subscriptionResponsePublisherSubject.eraseToAnyPublisher() } - private let subscriptionResponsePublisherSubject = PassthroughSubject, Never>() - private var requestAcknowledgePublisher: AnyPublisher, Never> { + private let requestAcknowledgePublisherSubject = PassthroughSubject() + private var requestAcknowledgePublisher: AnyPublisher { requestAcknowledgePublisherSubject.eraseToAnyPublisher() } - private let requestAcknowledgePublisherSubject = PassthroughSubject, Never>() - let logger: ConsoleLogging - static let historyIdentifier = "com.walletconnect.sdk.relayer_client.subscription_json_rpc_record" + + private var dispatcher: Dispatching + private let rpcHistory: RPCHistory + private let logger: ConsoleLogging + + private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.relay_client", attributes: .concurrent) init( dispatcher: Dispatching, @@ -45,14 +49,22 @@ public final class RelayClient { ) { self.logger = logger self.dispatcher = dispatcher - - self.jsonRpcSubscriptionsHistory = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: Self.historyIdentifier)) + self.rpcHistory = RPCHistory(keyValueStore: CodableStore(defaults: keyValueStorage, identifier: Self.historyIdentifier)) setUpBindings() } + private func setUpBindings() { + dispatcher.onMessage = { [weak self] payload in + self?.handlePayloadMessage(payload) + } + dispatcher.onConnect = { [unowned self] in + self.socketConnectionStatusPublisherSubject.send(.connected) + } + } + /// Instantiates Relay Client /// - Parameters: - /// - relayHost: proxy server host that your application will use to connect to Iridium Network. If you register your project at `www.walletconnect.com` you can use `relay.walletconnect.com` + /// - relayHost: proxy server host that your application will use to connect to Relay Network. If you register your project at `www.walletconnect.com` you can use `relay.walletconnect.com` /// - projectId: an optional parameter used to access the public WalletConnect infrastructure. Go to `www.walletconnect.com` for info. /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults /// - socketConnectionType: socket connection type @@ -98,65 +110,69 @@ public final class RelayClient { /// Completes when networking client sends a request, error if it fails on client side public func publish(topic: String, payload: String, tag: Int, prompt: Bool = false) async throws { - let params = RelayJSONRPC.PublishParams(topic: topic, message: payload, ttl: defaultTtl, prompt: prompt, tag: tag) - let request = JSONRPCRequest(method: RelayJSONRPC.Method.publish.method, params: params) - logger.debug("Publishing Payload on Topic: \(topic)") - let requestJson = try request.json() - try await dispatcher.send(requestJson) + let request = Publish(params: .init(topic: topic, message: payload, ttl: defaultTtl, prompt: prompt, tag: tag)) + .wrapToIRN() + .asRPCRequest() + let message = try request.asJSONEncodedString() + logger.debug("Publishing payload on topic: \(topic)") + try await dispatcher.send(message) } /// Completes with an acknowledgement from the relay network. - @discardableResult public func publish( + public func publish( topic: String, payload: String, tag: Int, prompt: Bool = false, - onNetworkAcknowledge: @escaping ((Error?) -> Void)) -> Int64 { - let params = RelayJSONRPC.PublishParams(topic: topic, message: payload, ttl: defaultTtl, prompt: prompt, tag: tag) - let request = JSONRPCRequest(method: RelayJSONRPC.Method.publish.method, params: params) - let requestJson = try! request.json() - logger.debug("iridium: Publishing Payload on Topic: \(topic)") + onNetworkAcknowledge: @escaping ((Error?) -> Void) + ) { + let rpc = Publish(params: .init(topic: topic, message: payload, ttl: defaultTtl, prompt: prompt, tag: tag)) + let request = rpc + .wrapToIRN() + .asRPCRequest() + let message = try! request.asJSONEncodedString() + logger.debug("Publishing Payload on Topic: \(topic)") var cancellable: AnyCancellable? - dispatcher.send(requestJson) { [weak self] error in + cancellable = requestAcknowledgePublisher + .filter { $0 == request.id } + .sink { (_) in + cancellable?.cancel() + onNetworkAcknowledge(nil) + } + dispatcher.send(message) { [weak self] error in if let error = error { self?.logger.debug("Failed to Publish Payload, error: \(error)") cancellable?.cancel() onNetworkAcknowledge(error) } } - cancellable = requestAcknowledgePublisher - .filter {$0.id == request.id} - .sink { (_) in - cancellable?.cancel() - onNetworkAcknowledge(nil) - } - return request.id } @available(*, renamed: "subscribe(topic:)") public func subscribe(topic: String, completion: @escaping (Error?) -> Void) { - logger.debug("iridium: Subscribing on Topic: \(topic)") - let params = RelayJSONRPC.SubscribeParams(topic: topic) - let request = JSONRPCRequest(method: RelayJSONRPC.Method.subscribe.method, params: params) - let requestJson = try! request.json() + logger.debug("Relay: Subscribing to topic: \(topic)") + let rpc = Subscribe(params: .init(topic: topic)) + let request = rpc + .wrapToIRN() + .asRPCRequest() + let message = try! request.asJSONEncodedString() var cancellable: AnyCancellable? - dispatcher.send(requestJson) { [weak self] error in + cancellable = subscriptionResponsePublisher + .filter { $0.0 == request.id } + .sink { [weak self] subscriptionInfo in + cancellable?.cancel() + self?.concurrentQueue.async(flags: .barrier) { + self?.subscriptions[topic] = subscriptionInfo.1 + } + completion(nil) + } + dispatcher.send(message) { [weak self] error in if let error = error { - self?.logger.debug("Failed to Subscribe on Topic \(error)") + self?.logger.debug("Failed to subscribe to topic \(error)") cancellable?.cancel() completion(error) - } else { - completion(nil) } } - cancellable = subscriptionResponsePublisher - .filter {$0.id == request.id} - .sink { [weak self] (subscriptionResponse) in - cancellable?.cancel() - self?.concurrentQueue.async(flags: .barrier) { - self?.subscriptions[topic] = subscriptionResponse.result - } - } } public func subscribe(topic: String) async throws { @@ -171,20 +187,28 @@ public final class RelayClient { } } - @discardableResult public func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) -> Int64? { + public func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) { guard let subscriptionId = subscriptions[topic] else { - completion(RelyerError.subscriptionIdNotFound) - return nil + completion(Errors.subscriptionIdNotFound) + return } - logger.debug("iridium: Unsubscribing on Topic: \(topic)") - let params = RelayJSONRPC.UnsubscribeParams(id: subscriptionId, topic: topic) - let request = JSONRPCRequest(method: RelayJSONRPC.Method.unsubscribe.method, params: params) - let requestJson = try! request.json() + logger.debug("Relay: Unsubscribing from topic: \(topic)") + let rpc = Unsubscribe(params: .init(id: subscriptionId, topic: topic)) + let request = rpc + .wrapToIRN() + .asRPCRequest() + let message = try! request.asJSONEncodedString() + rpcHistory.deleteAll(forTopic: topic) var cancellable: AnyCancellable? - jsonRpcSubscriptionsHistory.delete(topic: topic) - dispatcher.send(requestJson) { [weak self] error in + cancellable = requestAcknowledgePublisher + .filter { $0 == request.id } + .sink { (_) in + cancellable?.cancel() + completion(nil) + } + dispatcher.send(message) { [weak self] error in if let error = error { - self?.logger.debug("Failed to Unsubscribe on Topic") + self?.logger.debug("Failed to unsubscribe from topic") cancellable?.cancel() completion(error) } else { @@ -194,48 +218,38 @@ public final class RelayClient { completion(nil) } } - cancellable = requestAcknowledgePublisher - .filter {$0.id == request.id} - .sink { (_) in - cancellable?.cancel() - completion(nil) - } - return request.id - } - - private func setUpBindings() { - dispatcher.onMessage = { [weak self] payload in - self?.handlePayloadMessage(payload) - } - dispatcher.onConnect = { [unowned self] in - self.socketConnectionStatusPublisherSubject.send(.connected) - } } + // FIXME: Parse data to string once before trying to decode -> respond error on fail private func handlePayloadMessage(_ payload: String) { - if let request = tryDecode(SubscriptionRequest.self, from: payload), validate(request: request, method: .subscription) { - do { - try jsonRpcSubscriptionsHistory.set(topic: request.params.data.topic, request: request) - onMessage?(request.params.data.topic, request.params.data.message) - acknowledgeSubscription(requestId: request.id) - } catch { - logger.info("Relay Client Info: Json Rpc Duplicate Detected") + if let request = tryDecode(RPCRequest.self, from: payload) { + if let params = try? request.params?.get(Subscription.Params.self) { + do { + try rpcHistory.set(request, forTopic: params.data.topic, emmitedBy: .remote) + try acknowledgeRequest(request) + onMessage?(params.data.topic, params.data.message) + } catch { + logger.error("[RelayClient] RPC History 'set()' error: \(error)") + } + } else { + logger.error("Unexpected request from network") + } + } else if let response = tryDecode(RPCResponse.self, from: payload) { + switch response.outcome { + case .success(let anyCodable): + if let _ = try? anyCodable.get(Bool.self) { // TODO: Handle success vs. error + requestAcknowledgePublisherSubject.send(response.id) + } else if let subscriptionId = try? anyCodable.get(String.self) { + subscriptionResponsePublisherSubject.send((response.id, subscriptionId)) + } + case .failure(let rpcError): + logger.error("Received RPC error from relay network: \(rpcError)") } - } else if let response = tryDecode(RequestAcknowledgement.self, from: payload) { - requestAcknowledgePublisherSubject.send(response) - } else if let response = tryDecode(SubscriptionResponse.self, from: payload) { - subscriptionResponsePublisherSubject.send(response) - } else if let response = tryDecode(JSONRPCErrorResponse.self, from: payload) { - logger.error("Received error message from iridium network, code: \(response.error.code), message: \(response.error.message)") } else { logger.error("Unexpected response from network") } } - private func validate(request: JSONRPCRequest, method: RelayJSONRPC.Method) -> Bool { - return request.method.contains(method.name) - } - private func tryDecode(_ type: T.Type, from payload: String) -> T? { if let data = payload.data(using: .utf8), let response = try? JSONDecoder().decode(T.self, from: data) { @@ -245,13 +259,13 @@ public final class RelayClient { } } - private func acknowledgeSubscription(requestId: Int64) { - let response = JSONRPCResponse(id: requestId, result: AnyCodable(true)) - let responseJson = try! response.json() - _ = try? jsonRpcSubscriptionsHistory.resolve(response: JsonRpcResult.response(response)) - dispatcher.send(responseJson) { [weak self] error in - if let error = error { - self?.logger.debug("Failed to Respond for request id: \(requestId), error: \(error)") + private func acknowledgeRequest(_ request: RPCRequest) throws { + let response = RPCResponse(matchingRequest: request, result: true) + try rpcHistory.resolve(response) + let message = try response.asJSONEncodedString() + dispatcher.send(message) { [weak self] in + if let error = $0 { + self?.logger.debug("Failed to dispatch response: \(response), error: \(error)") } } } diff --git a/Sources/WalletConnectRelay/RelayJSONRPC.swift b/Sources/WalletConnectRelay/RelayJSONRPC.swift deleted file mode 100644 index e83de4427..000000000 --- a/Sources/WalletConnectRelay/RelayJSONRPC.swift +++ /dev/null @@ -1,63 +0,0 @@ -// - -import Foundation - -enum RelayJSONRPC { - enum Method { - case subscribe - case publish - case subscription - case unsubscribe - } - - struct PublishParams: Codable, Equatable { - let topic: String - let message: String - let ttl: Int - let prompt: Bool? - let tag: Int? - } - - struct SubscribeParams: Codable, Equatable { - let topic: String - } - - struct SubscriptionData: Codable, Equatable { - let topic: String - let message: String - } - - struct SubscriptionParams: Codable, Equatable { - let id: String - let data: SubscriptionData - } - - struct UnsubscribeParams: Codable, Equatable { - let id: String - let topic: String - } -} - -extension RelayJSONRPC.Method { - - var prefix: String { - return "iridium" - } - - var name: String { - switch self { - case .subscribe: - return "subscribe" - case .publish: - return "publish" - case .subscription: - return "subscription" - case .unsubscribe: - return "unsubscribe" - } - } - - var method: String { - return "\(prefix)_\(name)" - } -} diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift index 0b602f839..1211c63f5 100644 --- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift @@ -2,6 +2,7 @@ import Foundation import Combine import WalletConnectUtils import WalletConnectKMS +import WalletConnectPairing final class ApproveEngine { enum Errors: Error { @@ -71,7 +72,6 @@ final class ApproveEngine { peerPublicKey: proposal.proposer.publicKey ) else { throw Errors.agreementMissingOrInvalid } - // TODO: Extend pairing let sessionTopic = agreementKey.derivedTopic() try kms.setAgreementSecret(agreementKey, topic: sessionTopic) @@ -82,7 +82,15 @@ final class ApproveEngine { let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation) let response = JSONRPCResponse(id: payload.wcRequest.id, result: AnyCodable(proposeResponse)) + guard var pairing = pairingStore.getPairing(forTopic: payload.topic) else { + throw Errors.pairingNotFound + } + try await networkingInteractor.respond(topic: payload.topic, response: .response(response), tag: payload.wcRequest.responseTag) + + try pairing.updateExpiry() + pairingStore.setPairing(pairing) + try await settle(topic: sessionTopic, proposal: proposal, namespaces: sessionNamespaces) } diff --git a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift index 7b2231c82..106652d6e 100644 --- a/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift +++ b/Sources/WalletConnectSign/Engine/Common/PairingEngine.swift @@ -1,5 +1,6 @@ import Foundation import Combine +import WalletConnectPairing import WalletConnectUtils import WalletConnectKMS diff --git a/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift b/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift index f455738cb..969d4302b 100644 --- a/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift +++ b/Sources/WalletConnectSign/Engine/Controller/PairEngine.swift @@ -1,5 +1,6 @@ import Foundation import WalletConnectKMS +import WalletConnectPairing actor PairEngine { private let networkingInteractor: NetworkInteracting diff --git a/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift b/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift index 8892ff6a7..2ea62420d 100644 --- a/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift +++ b/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift @@ -11,9 +11,9 @@ protocol NetworkRelaying { func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws func publish(topic: String, payload: String, tag: Int, prompt: Bool) async throws /// - returns: request id - @discardableResult func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) -> Int64 + func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) func subscribe(topic: String, completion: @escaping (Error?) -> Void) func subscribe(topic: String) async throws /// - returns: request id - @discardableResult func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) -> Int64? + func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) } diff --git a/Sources/WalletConnectSign/Request.swift b/Sources/WalletConnectSign/Request.swift index a5182cb19..da55722b2 100644 --- a/Sources/WalletConnectSign/Request.swift +++ b/Sources/WalletConnectSign/Request.swift @@ -17,10 +17,10 @@ public struct Request: Codable, Equatable { } public init(topic: String, method: String, params: AnyCodable, chainId: Blockchain) { - self.id = JsonRpcID.generate() - self.topic = topic - self.method = method - self.params = params - self.chainId = chainId + self.init(id: JsonRpcID.generate(), topic: topic, method: method, params: params, chainId: chainId) + } + + internal init(id: Int64, topic: String, method: String, params: C, chainId: Blockchain) where C: Codable { + self.init(id: id, topic: topic, method: method, params: AnyCodable(params), chainId: chainId) } } diff --git a/Sources/WalletConnectSign/Services/CelanupService.swift b/Sources/WalletConnectSign/Services/CelanupService.swift index e0e083796..0058c179b 100644 --- a/Sources/WalletConnectSign/Services/CelanupService.swift +++ b/Sources/WalletConnectSign/Services/CelanupService.swift @@ -1,6 +1,7 @@ import Foundation import WalletConnectKMS import WalletConnectUtils +import WalletConnectPairing final class CleanupService { diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift index b2c653610..f00ae09be 100644 --- a/Sources/WalletConnectSign/Sign/Sign.swift +++ b/Sources/WalletConnectSign/Sign/Sign.swift @@ -23,7 +23,10 @@ public class Sign { socketFactory: config.socketFactory, socketConnectionType: config.socketConnectionType ) - client = SignClient(metadata: config.metadata, relayClient: relayClient) + client = SignClientFactory.create( + metadata: config.metadata, + relayClient: relayClient + ) client.delegate = self } diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift index 64f269a52..1dc88cae2 100644 --- a/Sources/WalletConnectSign/Sign/SignClient.swift +++ b/Sources/WalletConnectSign/Sign/SignClient.swift @@ -20,10 +20,10 @@ import UIKit /// - delegate: The object that acts as the delegate of WalletConnect Client /// - logger: An object for logging messages public final class SignClient { - public weak var delegate: SignClientDelegate? public let logger: ConsoleLogging + private let relayClient: RelayClient private let pairingEngine: PairingEngine private let pairEngine: PairEngine private let sessionEngine: SessionEngine @@ -34,48 +34,31 @@ public final class SignClient { private let cleanupService: CleanupService private var publishers = [AnyCancellable]() - // MARK: - Initializers + // MARK: - Initialization - /// Initializes and returns newly created WalletConnect Client Instance. Establishes a network connection with the relay - /// - /// - Parameters: - /// - metadata: describes your application and will define pairing appearance in a web browser. - /// - projectId: an optional parameter used to access the public WalletConnect infrastructure. Go to `www.walletconnect.com` for info. - /// - relayHost: proxy server host that your application will use to connect to Iridium Network. If you register your project at `www.walletconnect.com` you can use `relay.walletconnect.com` - /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults - /// - /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. - public convenience init(metadata: AppMetadata, relayClient: RelayClient) { - let logger = ConsoleLogger(loggingLevel: .off) - let keyValueStorage = UserDefaults.standard - let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") - self.init( - metadata: metadata, - logger: logger, - keyValueStorage: keyValueStorage, - keychainStorage: keychainStorage, - relayClient: relayClient - ) - } - - init(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) { + init(logger: ConsoleLogging, + relayClient: RelayClient, + pairingEngine: PairingEngine, + pairEngine: PairEngine, + sessionEngine: SessionEngine, + approveEngine: ApproveEngine, + nonControllerSessionStateMachine: NonControllerSessionStateMachine, + controllerSessionStateMachine: ControllerSessionStateMachine, + history: JsonRpcHistory, + cleanupService: CleanupService + ) { self.logger = logger - let kms = KeyManagementService(keychain: keychainStorage) - let serializer = Serializer(kms: kms) - self.history = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) - let networkingInteractor = NetworkInteractor(relayClient: relayClient, serializer: serializer, logger: logger, jsonRpcHistory: history) - let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) - let sessionStore = SessionStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.sessions.rawValue))) - let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) - let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) - self.pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) - self.sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - self.nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - self.controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) - self.pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) - self.approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) - self.cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) - setUpConnectionObserving(relayClient: relayClient) + self.relayClient = relayClient + self.pairingEngine = pairingEngine + self.pairEngine = pairEngine + self.sessionEngine = sessionEngine + self.approveEngine = approveEngine + self.nonControllerSessionStateMachine = nonControllerSessionStateMachine + self.controllerSessionStateMachine = controllerSessionStateMachine + self.history = history + self.cleanupService = cleanupService + + setUpConnectionObserving() setUpEnginesCallbacks() } @@ -292,7 +275,7 @@ public final class SignClient { } } - private func setUpConnectionObserving(relayClient: RelayClient) { + private func setUpConnectionObserving() { relayClient.socketConnectionStatusPublisher.sink { [weak self] status in self?.delegate?.didChangeSocketConnectionStatus(status) }.store(in: &publishers) diff --git a/Sources/WalletConnectSign/Sign/SignClientFactory.swift b/Sources/WalletConnectSign/Sign/SignClientFactory.swift new file mode 100644 index 000000000..8ab60ee52 --- /dev/null +++ b/Sources/WalletConnectSign/Sign/SignClientFactory.swift @@ -0,0 +1,57 @@ +import Foundation +import WalletConnectRelay +import WalletConnectUtils +import WalletConnectKMS +import WalletConnectPairing + +public struct SignClientFactory { + + /// Initializes and returns newly created WalletConnect Client Instance + /// + /// - Parameters: + /// - metadata: describes your application and will define pairing appearance in a web browser. + /// - projectId: an optional parameter used to access the public WalletConnect infrastructure. Go to `www.walletconnect.com` for info. + /// - relayHost: proxy server host that your application will use to connect to Iridium Network. If you register your project at `www.walletconnect.com` you can use `relay.walletconnect.com` + /// - keyValueStorage: by default WalletConnect SDK will store sequences in UserDefaults + /// + /// WalletConnect Client is not a singleton but once you create an instance, you should not deinitialize it. Usually only one instance of a client is required in the application. + public static func create(metadata: AppMetadata, relayClient: RelayClient) -> SignClient { + let logger = ConsoleLogger(loggingLevel: .off) + let keyValueStorage = UserDefaults.standard + let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk") + return SignClientFactory.create(metadata: metadata, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient) + } + + static func create(metadata: AppMetadata, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) -> SignClient { + let kms = KeyManagementService(keychain: keychainStorage) + let serializer = Serializer(kms: kms) + let history = JsonRpcHistory(logger: logger, keyValueStore: CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)) + let networkingInteractor = NetworkInteractor(relayClient: relayClient, serializer: serializer, logger: logger, jsonRpcHistory: history) + let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue))) + let sessionStore = SessionStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.sessions.rawValue))) + let sessionToPairingTopic = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.sessionToPairingTopic.rawValue) + let proposalPayloadsStore = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue) + let pairingEngine = PairingEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore, metadata: metadata, logger: logger) + let sessionEngine = SessionEngine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) + let nonControllerSessionStateMachine = NonControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) + let controllerSessionStateMachine = ControllerSessionStateMachine(networkingInteractor: networkingInteractor, kms: kms, sessionStore: sessionStore, logger: logger) + let pairEngine = PairEngine(networkingInteractor: networkingInteractor, kms: kms, pairingStore: pairingStore) + let approveEngine = ApproveEngine(networkingInteractor: networkingInteractor, proposalPayloadsStore: proposalPayloadsStore, sessionToPairingTopic: sessionToPairingTopic, metadata: metadata, kms: kms, logger: logger, pairingStore: pairingStore, sessionStore: sessionStore) + let cleanupService = CleanupService(pairingStore: pairingStore, sessionStore: sessionStore, kms: kms, sessionToPairingTopic: sessionToPairingTopic) + + let client = SignClient( + logger: logger, + relayClient: relayClient, + pairingEngine: pairingEngine, + pairEngine: pairEngine, + sessionEngine: sessionEngine, + approveEngine: approveEngine, + nonControllerSessionStateMachine: nonControllerSessionStateMachine, + controllerSessionStateMachine: controllerSessionStateMachine, + history: history, + cleanupService: cleanupService + ) + + return client + } +} diff --git a/Sources/WalletConnectSign/Sign/SignConfig.swift b/Sources/WalletConnectSign/Sign/SignConfig.swift index b6167c473..4ba4edeaa 100644 --- a/Sources/WalletConnectSign/Sign/SignConfig.swift +++ b/Sources/WalletConnectSign/Sign/SignConfig.swift @@ -1,5 +1,5 @@ -import WalletConnectRelay import Foundation +import WalletConnectRelay public extension Sign { struct Config { diff --git a/Sources/WalletConnectSign/Storage/SessionStorage.swift b/Sources/WalletConnectSign/Storage/SessionStorage.swift index 1b08d90f7..d24a535ef 100644 --- a/Sources/WalletConnectSign/Storage/SessionStorage.swift +++ b/Sources/WalletConnectSign/Storage/SessionStorage.swift @@ -1,3 +1,6 @@ +import Foundation +import WalletConnectUtils + protocol WCSessionStorage: AnyObject { var onSessionExpiration: ((WCSession) -> Void)? { get set } @discardableResult diff --git a/Sources/WalletConnectSign/Types/AppMetadata.swift b/Sources/WalletConnectSign/Types/AppMetadata.swift new file mode 100644 index 000000000..bd646f855 --- /dev/null +++ b/Sources/WalletConnectSign/Types/AppMetadata.swift @@ -0,0 +1,3 @@ +import WalletConnectPairing + +public typealias AppMetadata = WalletConnectPairing.AppMetadata diff --git a/Sources/WalletConnectSign/Types/Common/Participant.swift b/Sources/WalletConnectSign/Types/Common/Participant.swift index 0f0f4502a..fc81036d5 100644 --- a/Sources/WalletConnectSign/Types/Common/Participant.swift +++ b/Sources/WalletConnectSign/Types/Common/Participant.swift @@ -1,3 +1,5 @@ +import Foundation + struct Participant: Codable, Equatable { let publicKey: String let metadata: AppMetadata diff --git a/Sources/WalletConnectSign/Types/Common/RelayProtocolOptions.swift b/Sources/WalletConnectSign/Types/Common/RelayProtocolOptions.swift deleted file mode 100644 index 2d0009aa9..000000000 --- a/Sources/WalletConnectSign/Types/Common/RelayProtocolOptions.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Foundation - -struct RelayProtocolOptions: Codable, Equatable { - let `protocol`: String - let data: String? -} diff --git a/Sources/WalletConnectSign/Types/Pairing/PairingType.swift b/Sources/WalletConnectSign/Types/Pairing/PairingType.swift deleted file mode 100644 index d814e4449..000000000 --- a/Sources/WalletConnectSign/Types/Pairing/PairingType.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -// Internal namespace for pairing payloads. -internal enum PairingType { - - struct DeleteParams: Codable, Equatable { - let reason: Reason - } - - struct Reason: Codable, Equatable { - let code: Int - let message: String - } - - struct PingParams: Codable, Equatable {} -} diff --git a/Sources/WalletConnectSign/Types/RelayProtocolOptions.swift b/Sources/WalletConnectSign/Types/RelayProtocolOptions.swift new file mode 100644 index 000000000..d5b4b6c4f --- /dev/null +++ b/Sources/WalletConnectSign/Types/RelayProtocolOptions.swift @@ -0,0 +1,4 @@ +import Foundation +import WalletConnectUtils + +typealias RelayProtocolOptions = WalletConnectUtils.RelayProtocolOptions diff --git a/Sources/WalletConnectSign/Types/Session/WCSession.swift b/Sources/WalletConnectSign/Types/Session/WCSession.swift index 4c4109bc6..cb0771d43 100644 --- a/Sources/WalletConnectSign/Types/Session/WCSession.swift +++ b/Sources/WalletConnectSign/Types/Session/WCSession.swift @@ -57,7 +57,7 @@ struct WCSession: SequenceObject, Equatable { peerParticipant: Participant, namespaces: [String: SessionNamespace], requiredNamespaces: [String: ProposalNamespace], - events: Set, + events: Set, accounts: Set, acknowledged: Bool, expiry: Int64 diff --git a/Sources/WalletConnectSign/Types/WCMethod.swift b/Sources/WalletConnectSign/Types/WCMethod.swift index cbe97b754..e2002188a 100644 --- a/Sources/WalletConnectSign/Types/WCMethod.swift +++ b/Sources/WalletConnectSign/Types/WCMethod.swift @@ -1,3 +1,6 @@ +import Foundation +import WalletConnectPairing + enum WCMethod { case wcPairingPing case wcSessionPropose(SessionType.ProposeParams) diff --git a/Sources/WalletConnectSign/Types/WCRequest.swift b/Sources/WalletConnectSign/Types/WCRequest.swift index 74ad5bebb..ec9edfdf8 100644 --- a/Sources/WalletConnectSign/Types/WCRequest.swift +++ b/Sources/WalletConnectSign/Types/WCRequest.swift @@ -1,4 +1,5 @@ import Foundation +import WalletConnectPairing import WalletConnectUtils struct WCRequest: Codable { diff --git a/Sources/WalletConnectSign/Types/WalletConnectURI.swift b/Sources/WalletConnectSign/Types/WalletConnectURI.swift index 0d43287a6..a2cfe2de9 100644 --- a/Sources/WalletConnectSign/Types/WalletConnectURI.swift +++ b/Sources/WalletConnectSign/Types/WalletConnectURI.swift @@ -1,50 +1,4 @@ import Foundation +import WalletConnectUtils -public struct WalletConnectURI: Equatable { - - let topic: String - let version: String - let symKey: String - let relay: RelayProtocolOptions - - init(topic: String, symKey: String, relay: RelayProtocolOptions) { - self.version = "2" - self.topic = topic - self.symKey = symKey - self.relay = relay - } - - public init?(string: String) { - guard string.hasPrefix("wc:") else { - return nil - } - let urlString = !string.hasPrefix("wc://") ? string.replacingOccurrences(of: "wc:", with: "wc://") : string - guard let components = URLComponents(string: urlString) else { - return nil - } - let query: [String: String]? = components.queryItems?.reduce(into: [:]) { $0[$1.name] = $1.value } - - guard let topic = components.user, - let version = components.host, - let symKey = query?["symKey"], - let relayProtocol = query?["relay-protocol"] - else { return nil } - let relayData = query?["relay-data"] - self.version = version - self.topic = topic - self.symKey = symKey - self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData) - } - - public var absoluteString: String { - return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)" - } - - private var relayQuery: String { - var query = "relay-protocol=\(relay.protocol)" - if let relayData = relay.data { - query = "\(query)&relay-data=\(relayData)" - } - return query - } -} +typealias WalletConnectURI = WalletConnectUtils.WalletConnectURI diff --git a/Sources/WalletConnectUtils/Encodable.swift b/Sources/WalletConnectUtils/Encodable.swift index 74d14671c..4d18c65b9 100644 --- a/Sources/WalletConnectUtils/Encodable.swift +++ b/Sources/WalletConnectUtils/Encodable.swift @@ -8,6 +8,8 @@ public enum DataConversionError: Error { } public extension Encodable { + + // TODO: Migrate func json() throws -> String { let data = try JSONEncoder().encode(self) guard let string = String(data: data, encoding: .utf8) else { @@ -15,4 +17,12 @@ public extension Encodable { } return string } + + func asJSONEncodedString() throws -> String { + let data = try JSONEncoder().encode(self) + guard let string = String(data: data, encoding: .utf8) else { + throw DataConversionError.dataToStringFailed + } + return string + } } diff --git a/Sources/WalletConnectUtils/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory.swift new file mode 100644 index 000000000..8112cacc3 --- /dev/null +++ b/Sources/WalletConnectUtils/RPCHistory.swift @@ -0,0 +1,67 @@ +import JSONRPC + +public final class RPCHistory { + + public struct Record: Codable { + public enum Origin: String, Codable { + case local + case remote + } + let id: RPCID + let topic: String + let origin: Origin + public let request: RPCRequest + var response: RPCResponse? + } + + enum HistoryError: Error { + case unidentifiedRequest + case unidentifiedResponse + case requestDuplicateNotAllowed + case responseDuplicateNotAllowed + case requestMatchingResponseNotFound + } + + private let storage: CodableStore + + public init(keyValueStore: CodableStore) { + self.storage = keyValueStore + } + + public func get(recordId: RPCID) -> Record? { + try? storage.get(key: "\(recordId)") + } + + public func set(_ request: RPCRequest, forTopic topic: String, emmitedBy origin: Record.Origin) throws { + guard let id = request.id else { + throw HistoryError.unidentifiedRequest + } + guard get(recordId: id) == nil else { + throw HistoryError.requestDuplicateNotAllowed + } + let record = Record(id: id, topic: topic, origin: origin, request: request) + storage.set(record, forKey: "\(record.id)") + } + + public func resolve(_ response: RPCResponse) throws { + guard let id = response.id else { + throw HistoryError.unidentifiedResponse + } + guard var record = get(recordId: id) else { + throw HistoryError.requestMatchingResponseNotFound + } + guard record.response == nil else { + throw HistoryError.responseDuplicateNotAllowed + } + record.response = response + storage.set(record, forKey: "\(record.id)") + } + + public func deleteAll(forTopic topic: String) { + storage.getAll().forEach { record in + if record.topic == topic { + storage.delete(forKey: "\(record.id)") + } + } + } +} diff --git a/Sources/WalletConnectUtils/RelayProtocolOptions.swift b/Sources/WalletConnectUtils/RelayProtocolOptions.swift new file mode 100644 index 000000000..e437e9342 --- /dev/null +++ b/Sources/WalletConnectUtils/RelayProtocolOptions.swift @@ -0,0 +1,11 @@ +import Foundation + +public struct RelayProtocolOptions: Codable, Equatable { + public let `protocol`: String + public let data: String? + + public init(protocol: String, data: String?) { + self.protocol = `protocol` + self.data = data + } +} diff --git a/Sources/WalletConnectSign/Storage/SequenceStore.swift b/Sources/WalletConnectUtils/SequenceStore.swift similarity index 63% rename from Sources/WalletConnectSign/Storage/SequenceStore.swift rename to Sources/WalletConnectUtils/SequenceStore.swift index 7bbe62348..738831b74 100644 --- a/Sources/WalletConnectSign/Storage/SequenceStore.swift +++ b/Sources/WalletConnectUtils/SequenceStore.swift @@ -1,51 +1,45 @@ import Foundation -import WalletConnectUtils -protocol Expirable { +public protocol SequenceObject: Codable { var expiryDate: Date { get } -} - -protocol Entitled { var topic: String { get } } -typealias SequenceObject = Entitled & Expirable & Codable - -final class SequenceStore where T: SequenceObject { +public final class SequenceStore where T: SequenceObject { - var onSequenceExpiration: ((_ sequence: T) -> Void)? + public var onSequenceExpiration: ((_ sequence: T) -> Void)? private let store: CodableStore private let dateInitializer: () -> Date - init(store: CodableStore, dateInitializer: @escaping () -> Date = Date.init) { + public init(store: CodableStore, dateInitializer: @escaping () -> Date = Date.init) { self.store = store self.dateInitializer = dateInitializer } - func hasSequence(forTopic topic: String) -> Bool { + public func hasSequence(forTopic topic: String) -> Bool { (try? getSequence(forTopic: topic)) != nil } - func setSequence(_ sequence: T) { + public func setSequence(_ sequence: T) { store.set(sequence, forKey: sequence.topic) } - func getSequence(forTopic topic: String) throws -> T? { + public func getSequence(forTopic topic: String) throws -> T? { guard let value = try store.get(key: topic) else { return nil } return verifyExpiry(on: value) } - func getAll() -> [T] { + public func getAll() -> [T] { let values = store.getAll() return values.compactMap { verifyExpiry(on: $0) } } - func delete(topic: String) { + public func delete(topic: String) { store.delete(forKey: topic) } - func deleteAll() { + public func deleteAll() { store.deleteAll() } } diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift new file mode 100644 index 000000000..d149f7f70 --- /dev/null +++ b/Sources/WalletConnectUtils/WalletConnectURI.swift @@ -0,0 +1,50 @@ +import Foundation + +public struct WalletConnectURI: Equatable { + + public let topic: String + public let version: String + public let symKey: String + public let relay: RelayProtocolOptions + + public init(topic: String, symKey: String, relay: RelayProtocolOptions) { + self.version = "2" + self.topic = topic + self.symKey = symKey + self.relay = relay + } + + public init?(string: String) { + guard string.hasPrefix("wc:") else { + return nil + } + let urlString = !string.hasPrefix("wc://") ? string.replacingOccurrences(of: "wc:", with: "wc://") : string + guard let components = URLComponents(string: urlString) else { + return nil + } + let query: [String: String]? = components.queryItems?.reduce(into: [:]) { $0[$1.name] = $1.value } + + guard let topic = components.user, + let version = components.host, + let symKey = query?["symKey"], + let relayProtocol = query?["relay-protocol"] + else { return nil } + let relayData = query?["relay-data"] + self.version = version + self.topic = topic + self.symKey = symKey + self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData) + } + + public var absoluteString: String { + return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)" + } + + private var relayQuery: String { + var query = "relay-protocol=\(relay.protocol)" + if let relayData = relay.data { + query = "\(query)&relay-data=\(relayData)" + } + return query + } +} diff --git a/Tests/AuthTests/AuthRequstSubscriberTests.swift b/Tests/AuthTests/AuthRequstSubscriberTests.swift new file mode 100644 index 000000000..c457d9246 --- /dev/null +++ b/Tests/AuthTests/AuthRequstSubscriberTests.swift @@ -0,0 +1,42 @@ +import Foundation +import XCTest +@testable import Auth +import WalletConnectUtils +@testable import WalletConnectKMS +@testable import TestingUtils +import JSONRPC + +class AuthRequstSubscriberTests: XCTestCase { + var networkingInteractor: NetworkingInteractorMock! + var sut: AuthRequestSubscriber! + var messageFormatter: SIWEMessageFormatterMock! + let defaultTimeout: TimeInterval = 0.01 + + override func setUp() { + networkingInteractor = NetworkingInteractorMock() + messageFormatter = SIWEMessageFormatterMock() + sut = AuthRequestSubscriber(networkingInteractor: networkingInteractor, + logger: ConsoleLoggerMock(), + messageFormatter: messageFormatter) + } + + func testSubscribeRequest() { + let expectedMessage = "Expected Message" + let expectedRequestId: RPCID = RPCID(1234) + let messageExpectation = expectation(description: "receives formatted message") + messageFormatter.formattedMessage = expectedMessage + var messageId: RPCID! + var message: String! + sut.onRequest = { id, formattedMessage in + messageId = id + message = formattedMessage + messageExpectation.fulfill() + } + + networkingInteractor.requestPublisherSubject.send(RequestSubscriptionPayload.stub(id: expectedRequestId)) + + wait(for: [messageExpectation], timeout: defaultTimeout) + XCTAssertEqual(message, expectedMessage) + XCTAssertEqual(messageId, expectedRequestId) + } +} diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift new file mode 100644 index 000000000..32e8abe85 --- /dev/null +++ b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift @@ -0,0 +1,26 @@ +import Foundation +import Combine +@testable import Auth +import JSONRPC +import WalletConnectKMS + +struct NetworkingInteractorMock: NetworkInteracting { + + let requestPublisherSubject = PassthroughSubject() + var requestPublisher: AnyPublisher { + requestPublisherSubject.eraseToAnyPublisher() + } + + func subscribe(topic: String) async throws { + + } + + func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + + func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws { + + } + +} diff --git a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift new file mode 100644 index 000000000..b576bfc68 --- /dev/null +++ b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift @@ -0,0 +1,9 @@ +import Foundation +@testable import Auth + +class SIWEMessageFormatterMock: SIWEMessageFormatting { + var formattedMessage: String! + func formatMessage(from request: AuthRequestParams) throws -> String { + return formattedMessage + } +} diff --git a/Tests/AuthTests/Stubs/RequestParams.swift b/Tests/AuthTests/Stubs/RequestParams.swift new file mode 100644 index 000000000..9a3890289 --- /dev/null +++ b/Tests/AuthTests/Stubs/RequestParams.swift @@ -0,0 +1,8 @@ +import Foundation +@testable import Auth + +extension RequestParams { + static func stub() -> RequestParams { + return RequestParams(domain: "", chainId: "", nonce: "", aud: "", nbf: nil, exp: nil, statement: nil, requestId: nil, resources: nil) + } +} diff --git a/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift new file mode 100644 index 000000000..afeb1c281 --- /dev/null +++ b/Tests/AuthTests/Stubs/RequestSubscriptionPayload.swift @@ -0,0 +1,15 @@ +import Foundation +@testable import Auth +import JSONRPC + +extension RequestSubscriptionPayload { + static func stub(id: RPCID) -> RequestSubscriptionPayload { + let appMetadata = AppMetadata(name: "", description: "", url: "", icons: []) + let requester = AuthRequestParams.Requester(publicKey: "", metadata: appMetadata) + let issueAt = ISO8601DateFormatter().string(from: Date()) + let payload = AuthPayload(requestParams: RequestParams.stub(), iat: issueAt) + let params = AuthRequestParams(requester: requester, payloadParams: payload) + let request = RPCRequest(method: "wc_authRequest", params: params, rpcid: id) + return RequestSubscriptionPayload(id: 123, request: request) + } +} diff --git a/Tests/RelayerTests/DispatcherTests.swift b/Tests/RelayerTests/DispatcherTests.swift index b8d6e1040..78103a30c 100644 --- a/Tests/RelayerTests/DispatcherTests.swift +++ b/Tests/RelayerTests/DispatcherTests.swift @@ -7,7 +7,6 @@ import Combine class WebSocketMock: WebSocketConnecting { var request: URLRequest = URLRequest(url: URL(string: "wss://relay.walletconnect.com")!) - var onText: ((String) -> Void)? var onConnect: (() -> Void)? var onDisconnect: ((Error?) -> Void)? diff --git a/Tests/RelayerTests/IridiumRelayTests.swift b/Tests/RelayerTests/IridiumRelayTests.swift deleted file mode 100644 index 4ce71e96f..000000000 --- a/Tests/RelayerTests/IridiumRelayTests.swift +++ /dev/null @@ -1,90 +0,0 @@ -import WalletConnectUtils -import Foundation -import Combine -import XCTest -@testable import WalletConnectRelay - -class IridiumRelayTests: XCTestCase { - var iridiumRelay: RelayClient! - var dispatcher: DispatcherMock! - - override func setUp() { - dispatcher = DispatcherMock() - let logger = ConsoleLogger() - iridiumRelay = RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage()) - } - - override func tearDown() { - iridiumRelay = nil - dispatcher = nil - } - - func testNotifyOnSubscriptionRequest() { - let subscriptionExpectation = expectation(description: "notifies with encoded message on a iridium subscription event") - let topic = "0987" - let message = "qwerty" - let subscriptionId = "sub-id" - let subscriptionParams = RelayJSONRPC.SubscriptionParams(id: subscriptionId, data: RelayJSONRPC.SubscriptionData(topic: topic, message: message)) - let subscriptionRequest = JSONRPCRequest(id: 12345, method: RelayJSONRPC.Method.subscription.method, params: subscriptionParams) - iridiumRelay.onMessage = { subscriptionTopic, subscriptionMessage in - XCTAssertEqual(subscriptionMessage, message) - XCTAssertEqual(subscriptionTopic, topic) - subscriptionExpectation.fulfill() - } - dispatcher.onMessage?(try! subscriptionRequest.json()) - waitForExpectations(timeout: 0.001, handler: nil) - } - - func testPublishRequestAcknowledge() { - let acknowledgeExpectation = expectation(description: "completion with no error on iridium request acknowledge after publish") - let requestId = iridiumRelay.publish(topic: "", payload: "{}", tag: 0, onNetworkAcknowledge: { error in - acknowledgeExpectation.fulfill() - XCTAssertNil(error) - }) - let response = try! JSONRPCResponse(id: requestId, result: true).json() - dispatcher.onMessage?(response) - waitForExpectations(timeout: 0.001, handler: nil) - } - - func testUnsubscribeRequestAcknowledge() { - let acknowledgeExpectation = expectation(description: "completion with no error on iridium request acknowledge after unsubscribe") - let topic = "1234" - iridiumRelay.subscriptions[topic] = "" - let requestId = iridiumRelay.unsubscribe(topic: topic) { error in - XCTAssertNil(error) - acknowledgeExpectation.fulfill() - } - let response = try! JSONRPCResponse(id: requestId!, result: true).json() - dispatcher.onMessage?(response) - waitForExpectations(timeout: 0.001, handler: nil) - } - - func testSubscriptionRequestDeliveredOnce() { - let expectation = expectation(description: "Request duplicate not delivered") - let subscriptionParams = RelayJSONRPC.SubscriptionParams(id: "sub_id", data: RelayJSONRPC.SubscriptionData(topic: "topic", message: "message")) - let subscriptionRequest = JSONRPCRequest(id: 12345, method: RelayJSONRPC.Method.subscription.method, params: subscriptionParams) - iridiumRelay.onMessage = { _, _ in - expectation.fulfill() - } - dispatcher.onMessage?(try! subscriptionRequest.json()) - dispatcher.onMessage?(try! subscriptionRequest.json()) - waitForExpectations(timeout: 0.001, handler: nil) - } - - func testSendOnPublish() { - iridiumRelay.publish(topic: "", payload: "", tag: 0, onNetworkAcknowledge: { _ in}) - XCTAssertTrue(dispatcher.sent) - } - - func testSendOnSubscribe() { - iridiumRelay.subscribe(topic: "") {_ in } - XCTAssertTrue(dispatcher.sent) - } - - func testSendOnUnsubscribe() { - let topic = "123" - iridiumRelay.subscriptions[topic] = "" - iridiumRelay.unsubscribe(topic: topic) {_ in } - XCTAssertTrue(dispatcher.sent) - } -} diff --git a/Tests/RelayerTests/Mocks/DispatcherMock.swift b/Tests/RelayerTests/Mocks/DispatcherMock.swift index 4efc9c222..97ddac5dc 100644 --- a/Tests/RelayerTests/Mocks/DispatcherMock.swift +++ b/Tests/RelayerTests/Mocks/DispatcherMock.swift @@ -1,17 +1,32 @@ import Foundation +import JSONRPC @testable import WalletConnectRelay class DispatcherMock: Dispatching { + var onConnect: (() -> Void)? var onDisconnect: (() -> Void)? var onMessage: ((String) -> Void)? + + func connect() {} + func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) {} + var sent = false + var lastMessage: String = "" + func send(_ string: String, completion: @escaping (Error?) -> Void) { sent = true + lastMessage = string } func send(_ string: String) async throws { send(string, completion: { _ in }) } - func connect() {} - func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) {} +} + +extension DispatcherMock { + + func getLastRequestSent() -> RPCRequest { + let data = lastMessage.data(using: .utf8)! + return try! JSONDecoder().decode(RPCRequest.self, from: data) + } } diff --git a/Tests/RelayerTests/RelayClientTests.swift b/Tests/RelayerTests/RelayClientTests.swift new file mode 100644 index 000000000..d45bb61fe --- /dev/null +++ b/Tests/RelayerTests/RelayClientTests.swift @@ -0,0 +1,106 @@ +import WalletConnectUtils +import Foundation +import Combine +import JSONRPC +import XCTest +@testable import WalletConnectRelay + +final class RelayClientTests: XCTestCase { + + var sut: RelayClient! + var dispatcher: DispatcherMock! + + override func setUp() { + dispatcher = DispatcherMock() + let logger = ConsoleLogger() + sut = RelayClient(dispatcher: dispatcher, logger: logger, keyValueStorage: RuntimeKeyValueStorage()) + } + + override func tearDown() { + sut = nil + dispatcher = nil + } + + func testNotifyOnSubscriptionRequest() { + let expectation = expectation(description: "Relay must notify listener on a Subscription request") + let topic = "0987" + let message = "qwerty" + let subscriptionId = "sub-id" + let subscription = Subscription(id: subscriptionId, topic: topic, message: message) + let request = subscription.asRPCRequest() + + sut.onMessage = { subscriptionTopic, subscriptionMessage in + XCTAssertEqual(subscriptionMessage, message) + XCTAssertEqual(subscriptionTopic, topic) + expectation.fulfill() + } + dispatcher.onMessage?(try! request.asJSONEncodedString()) + waitForExpectations(timeout: 0.001, handler: nil) + } + + func testSubscribeRequestAcknowledge() { + let acknowledgeExpectation = expectation(description: "") + sut.subscribe(topic: "") { error in + XCTAssertNil(error) + acknowledgeExpectation.fulfill() + } + let request = dispatcher.getLastRequestSent() + let response = RPCResponse(matchingRequest: request, result: "id") + dispatcher.onMessage?(try! response.asJSONEncodedString()) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testPublishRequestAcknowledge() { + let expectation = expectation(description: "Publish must callback on relay server acknowledgement") + sut.publish(topic: "", payload: "{}", tag: 0) { error in + XCTAssertNil(error) + expectation.fulfill() + } + let request = dispatcher.getLastRequestSent() + let response = RPCResponse(matchingRequest: request, result: true) + dispatcher.onMessage?(try! response.asJSONEncodedString()) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testUnsubscribeRequestAcknowledge() { + let expectation = expectation(description: "Unsubscribe must callback on relay server acknowledgement") + let topic = String.randomTopic() + sut.subscriptions[topic] = "" + sut.unsubscribe(topic: topic) { error in + XCTAssertNil(error) + expectation.fulfill() + } + let request = dispatcher.getLastRequestSent() + let response = RPCResponse(matchingRequest: request, result: true) + dispatcher.onMessage?(try! response.asJSONEncodedString()) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testSubscriptionRequestDeliveredOnce() { + let expectation = expectation(description: "Duplicate Subscription requests must notify only the first time") + let request = Subscription.init(id: "sub_id", topic: "topic", message: "message").asRPCRequest() + sut.onMessage = { _, _ in + expectation.fulfill() + } + dispatcher.onMessage?(try! request.asJSONEncodedString()) + dispatcher.onMessage?(try! request.asJSONEncodedString()) + waitForExpectations(timeout: 0.1, handler: nil) + } + + func testSendOnPublish() { + sut.publish(topic: "", payload: "", tag: 0, onNetworkAcknowledge: { _ in}) + XCTAssertTrue(dispatcher.sent) + } + + func testSendOnSubscribe() { + sut.subscribe(topic: "") {_ in } + XCTAssertTrue(dispatcher.sent) + } + + func testSendOnUnsubscribe() { + let topic = "123" + sut.subscriptions[topic] = "" + sut.unsubscribe(topic: topic) {_ in } + XCTAssertTrue(dispatcher.sent) + } +} diff --git a/Tests/TestingUtils/Mocks/RPC.swift b/Tests/TestingUtils/Mocks/RPC.swift new file mode 100644 index 000000000..a8255b7a0 --- /dev/null +++ b/Tests/TestingUtils/Mocks/RPC.swift @@ -0,0 +1,12 @@ +import JSONRPC + +public extension RPCRequest { + + static func stub() -> RPCRequest { + RPCRequest(method: "method", params: EmptyCodable()) + } + + static func stub(method: String, id: Int64) -> RPCRequest { + RPCRequest(method: method, params: EmptyCodable(), id: id) + } +} diff --git a/Tests/WalletConnectKMSTests/KeychainStorageTests.swift b/Tests/WalletConnectKMSTests/KeychainStorageTests.swift index 19c57bdaf..422f23bcf 100644 --- a/Tests/WalletConnectKMSTests/KeychainStorageTests.swift +++ b/Tests/WalletConnectKMSTests/KeychainStorageTests.swift @@ -32,13 +32,12 @@ final class KeychainStorageTests: XCTestCase { XCTAssertNoThrow(try sut.add(privateKey, forKey: "id-2")) } - func testAddDuplicateItemError() { + func testAddDuplicateItem() throws { let privateKey = Curve25519.KeyAgreement.PrivateKey() - try? sut.add(privateKey, forKey: defaultIdentifier) - XCTAssertThrowsError(try sut.add(privateKey, forKey: defaultIdentifier)) { error in - guard let error = error as? KeychainError else { XCTFail(); return } - XCTAssertEqual(error.status, errSecDuplicateItem) - } + try sut.add(privateKey, forKey: defaultIdentifier) + let newPrivateKey = Curve25519.KeyAgreement.PrivateKey() + XCTAssertNoThrow(try sut.add(newPrivateKey, forKey: defaultIdentifier)) + XCTAssertEqual(try sut.read(key: defaultIdentifier), newPrivateKey) } func testAddUnknownFailure() { diff --git a/Tests/WalletConnectSignTests/ApproveEngineTests.swift b/Tests/WalletConnectSignTests/ApproveEngineTests.swift index ac6098c34..88e7a541c 100644 --- a/Tests/WalletConnectSignTests/ApproveEngineTests.swift +++ b/Tests/WalletConnectSignTests/ApproveEngineTests.swift @@ -1,9 +1,10 @@ import XCTest import Combine +import WalletConnectUtils +import WalletConnectPairing @testable import WalletConnectSign @testable import TestingUtils @testable import WalletConnectKMS -import WalletConnectUtils final class ApproveEngineTests: XCTestCase { @@ -47,6 +48,8 @@ final class ApproveEngineTests: XCTestCase { func testApproveProposal() async throws { // Client receives a proposal let topicA = String.generateTopic() + let pairing = WCPairing.stub(expiryDate: Date(timeIntervalSinceNow: 10000), topic: topicA) + pairingStorageMock.setPairing(pairing) let proposerPubKey = AgreementPrivateKey().publicKey.hexRepresentation let proposal = SessionProposal.stub(proposerPubKey: proposerPubKey) let request = WCRequest(method: .sessionPropose, params: .sessionPropose(proposal)) @@ -57,9 +60,11 @@ final class ApproveEngineTests: XCTestCase { let topicB = networkingInteractor.subscriptions.last! + let extendedPairing = pairingStorageMock.getPairing(forTopic: topicA)! XCTAssertTrue(networkingInteractor.didCallSubscribe) XCTAssert(cryptoMock.hasAgreementSecret(for: topicB), "Responder must store agreement key for topic B") XCTAssertEqual(networkingInteractor.didRespondOnTopic!, topicA, "Responder must respond on topic A") + XCTAssertEqual(extendedPairing.expiryDate.timeIntervalSince1970, Date(timeIntervalSinceNow: 2_592_000).timeIntervalSince1970, accuracy: 1, "pairing expiry has been extended by 30 days") } func testReceiveProposal() { diff --git a/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift b/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift index 01f4ed5fc..dd98115f7 100644 --- a/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift +++ b/Tests/WalletConnectSignTests/JsonRpcHistoryTests.swift @@ -2,6 +2,7 @@ import Foundation import XCTest import TestingUtils import WalletConnectUtils +import WalletConnectPairing @testable import WalletConnectSign final class JsonRpcHistoryTests: XCTestCase { diff --git a/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift b/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift index b8de74a1a..98f57f0ef 100644 --- a/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift +++ b/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift @@ -18,18 +18,17 @@ class MockedRelayClient: NetworkRelaying { var onMessage: ((String, String) -> Void)? var error: Error? var prompt = false - func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) -> Int64 { + func publish(topic: String, payload: String, tag: Int, prompt: Bool, onNetworkAcknowledge: @escaping ((Error?) -> Void)) { self.prompt = prompt onNetworkAcknowledge(error) - return 0 } func subscribe(topic: String, completion: @escaping (Error?) -> Void) { } - func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) -> Int64? { - return 0 + func unsubscribe(topic: String, completion: @escaping ((Error?) -> Void)) { } + func connect() { } diff --git a/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift b/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift index 302233b0f..49d220625 100644 --- a/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift +++ b/Tests/WalletConnectSignTests/Mocks/NetworkingInteractorMock.swift @@ -1,6 +1,7 @@ import Foundation import Combine import WalletConnectUtils +import WalletConnectPairing @testable import WalletConnectSign @testable import TestingUtils diff --git a/Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift b/Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift index 62b8b34c5..f3c11550e 100644 --- a/Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift +++ b/Tests/WalletConnectSignTests/Mocks/WCPairingStorageMock.swift @@ -1,3 +1,4 @@ +import WalletConnectPairing @testable import WalletConnectSign final class WCPairingStorageMock: WCPairingStorage { diff --git a/Tests/WalletConnectSignTests/Stub/Stubs.swift b/Tests/WalletConnectSignTests/Stub/Stubs.swift index 813243610..67c329247 100644 --- a/Tests/WalletConnectSignTests/Stub/Stubs.swift +++ b/Tests/WalletConnectSignTests/Stub/Stubs.swift @@ -3,6 +3,7 @@ import Foundation import WalletConnectKMS import WalletConnectUtils import TestingUtils +import WalletConnectPairing extension AppMetadata { static func stub() -> AppMetadata { @@ -16,14 +17,14 @@ extension AppMetadata { } extension Pairing { - static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000)) -> Pairing { - Pairing(topic: String.generateTopic(), peer: nil, expiryDate: expiryDate) + static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), topic: String = String.generateTopic()) -> Pairing { + Pairing(topic: topic, peer: nil, expiryDate: expiryDate) } } extension WCPairing { - static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), isActive: Bool = true) -> WCPairing { - WCPairing(topic: String.generateTopic(), relay: RelayProtocolOptions.stub(), peerMetadata: AppMetadata.stub(), isActive: isActive, expiryDate: expiryDate) + static func stub(expiryDate: Date = Date(timeIntervalSinceNow: 10000), isActive: Bool = true, topic: String = String.generateTopic()) -> WCPairing { + WCPairing(topic: topic, relay: RelayProtocolOptions.stub(), peerMetadata: AppMetadata.stub(), isActive: isActive, expiryDate: expiryDate) } } @@ -97,7 +98,7 @@ extension WCRequestSubscriptionPayload { extension SessionProposal { static func stub(proposerPubKey: String = "") -> SessionProposal { - let relayOptions = RelayProtocolOptions(protocol: "iridium", data: nil) + let relayOptions = RelayProtocolOptions(protocol: "irn", data: nil) return SessionType.ProposeParams( relays: [relayOptions], proposer: Participant(publicKey: proposerPubKey, metadata: AppMetadata.stub()), diff --git a/Tests/WalletConnectSignTests/WCPairingTests.swift b/Tests/WalletConnectSignTests/WCPairingTests.swift index 951fdb430..6826add6d 100644 --- a/Tests/WalletConnectSignTests/WCPairingTests.swift +++ b/Tests/WalletConnectSignTests/WCPairingTests.swift @@ -1,4 +1,5 @@ import XCTest +import WalletConnectPairing @testable import WalletConnectSign final class WCPairingTests: XCTestCase { diff --git a/Tests/WalletConnectSignTests/WCRelayTests.swift b/Tests/WalletConnectSignTests/WCRelayTests.swift index 0196de9a4..500967141 100644 --- a/Tests/WalletConnectSignTests/WCRelayTests.swift +++ b/Tests/WalletConnectSignTests/WCRelayTests.swift @@ -2,6 +2,7 @@ import Foundation import Combine import XCTest import WalletConnectUtils +import WalletConnectPairing @testable import TestingUtils @testable import WalletConnectSign @@ -57,7 +58,7 @@ private let testPayload = { "id":1630300527198334, "jsonrpc":"2.0", - "method":"iridium_subscription", + "method":"irn_subscription", "params":{ "id":"0847f4e1dd19cf03a43dc7525f39896b630e9da33e4683c8efbc92ea671b5e07", "data":{ diff --git a/Tests/WalletConnectSignTests/WalletConnectURITests.swift b/Tests/WalletConnectSignTests/WalletConnectURITests.swift index f2331c626..e1e218a4b 100644 --- a/Tests/WalletConnectSignTests/WalletConnectURITests.swift +++ b/Tests/WalletConnectSignTests/WalletConnectURITests.swift @@ -3,9 +3,9 @@ import XCTest private let stubTopic = "8097df5f14871126866252c1b7479a14aefb980188fc35ec97d130d24bd887c8" private let stubSymKey = "587d5484ce2a2a6ee3ba1962fdd7e8588e06200c46823bd18fbd67def96ad303" -private let stubProtocol = "iridium" +private let stubProtocol = "irn" -private let stubURI = "wc:7f6e504bfad60b485450578e05678ed3e8e8c4751d3c6160be17160d63ec90f9@2?symKey=587d5484ce2a2a6ee3ba1962fdd7e8588e06200c46823bd18fbd67def96ad303&relay-protocol=iridium" +private let stubURI = "wc:7f6e504bfad60b485450578e05678ed3e8e8c4751d3c6160be17160d63ec90f9@2?symKey=587d5484ce2a2a6ee3ba1962fdd7e8588e06200c46823bd18fbd67def96ad303&relay-protocol=irn" final class WalletConnectURITests: XCTestCase { @@ -13,7 +13,7 @@ final class WalletConnectURITests: XCTestCase { let inputURI = WalletConnectURI( topic: "8097df5f14871126866252c1b7479a14aefb980188fc35ec97d130d24bd887c8", symKey: "19c5ecc857963976fabb98ed6a3e0a6ab6b0d65c018b6e25fbdcd3a164def868", - relay: RelayProtocolOptions(protocol: "iridium", data: nil)) + relay: RelayProtocolOptions(protocol: "irn", data: nil)) let uriString = inputURI.absoluteString let outputURI = WalletConnectURI(string: uriString) XCTAssertEqual(inputURI, outputURI) diff --git a/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift b/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift new file mode 100644 index 000000000..10bfbc049 --- /dev/null +++ b/Tests/WalletConnectUtilsTests/RPCHistoryTests.swift @@ -0,0 +1,110 @@ +import XCTest +import JSONRPC +import TestingUtils +@testable import WalletConnectUtils + +final class RPCHistoryTests: XCTestCase { + + var sut: RPCHistory! + + override func setUp() { + let storage = CodableStore(defaults: RuntimeKeyValueStorage(), identifier: "") + sut = RPCHistory(keyValueStore: storage) + } + + override func tearDown() { + sut = nil + } + + // MARK: History Storage Tests + + func testRoundTrip() throws { + let request = RPCRequest.stub() + try sut.set(request, forTopic: String.randomTopic(), emmitedBy: .local) + let record = sut.get(recordId: request.id!) + XCTAssertNil(record?.response) + XCTAssertEqual(record?.request, request) + } + + func testResolveSuccessAndError() throws { + let requestA = RPCRequest.stub() + let requestB = RPCRequest.stub() + let responseA = RPCResponse(matchingRequest: requestA, result: true) + let responseB = RPCResponse(matchingRequest: requestB, error: .internalError) + try sut.set(requestA, forTopic: String.randomTopic(), emmitedBy: .remote) + try sut.set(requestB, forTopic: String.randomTopic(), emmitedBy: .local) + try sut.resolve(responseA) + try sut.resolve(responseB) + let recordA = sut.get(recordId: requestA.id!) + let recordB = sut.get(recordId: requestB.id!) + XCTAssertEqual(recordA?.response, responseA) + XCTAssertEqual(recordB?.response, responseB) + } + + func testDelete() throws { + let requests = (1...5).map { _ in RPCRequest.stub() } + let topic = String.randomTopic() + try requests.forEach { try sut.set($0, forTopic: topic, emmitedBy: .local) } + sut.deleteAll(forTopic: topic) + requests.forEach { + XCTAssertNil(sut.get(recordId: $0.id!)) + } + } + + // MARK: Error Cases Tests + + func testSetUnidentifiedRequest() { + let expectedError = RPCHistory.HistoryError.unidentifiedRequest + + let request = RPCRequest.notification(method: "notify") + XCTAssertThrowsError(try sut.set(request, forTopic: String.randomTopic(), emmitedBy: .local)) { error in + XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) + } + } + + func testSetDuplicateRequest() throws { + let expectedError = RPCHistory.HistoryError.requestDuplicateNotAllowed + + let id = Int.random() + let requestA = RPCRequest.stub(method: "method-1", id: id) + let requestB = RPCRequest.stub(method: "method-2", id: id) + let topic = String.randomTopic() + + try sut.set(requestA, forTopic: topic, emmitedBy: .local) + XCTAssertThrowsError(try sut.set(requestB, forTopic: topic, emmitedBy: .local)) { error in + XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) + } + } + + func testResolveResponseWithoutRequest() throws { + let expectedError = RPCHistory.HistoryError.requestMatchingResponseNotFound + + let response = RPCResponse(id: 0, result: true) + XCTAssertThrowsError(try sut.resolve(response)) { error in + XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) + } + } + + func testResolveUnidentifiedResponse() throws { + let expectedError = RPCHistory.HistoryError.unidentifiedResponse + + let response = RPCResponse(errorWithoutID: JSONRPCError.internalError) + XCTAssertThrowsError(try sut.resolve(response)) { error in + XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) + } + } + + func testResolveDuplicateResponse() throws { + let expectedError = RPCHistory.HistoryError.responseDuplicateNotAllowed + + let request = RPCRequest.stub() + let responseA = RPCResponse(matchingRequest: request, result: true) + let responseB = RPCResponse(matchingRequest: request, result: false) + + try sut.set(request, forTopic: String.randomTopic(), emmitedBy: .local) + try sut.resolve(responseA) + XCTAssertThrowsError(try sut.resolve(responseB)) { error in + XCTAssertEqual(expectedError, error as? RPCHistory.HistoryError) + } + } +}