diff --git a/.github/actions/ci/action.yml b/.github/actions/ci/action.yml
index 1e887a78c..75ed3bd4f 100644
--- a/.github/actions/ci/action.yml
+++ b/.github/actions/ci/action.yml
@@ -15,7 +15,7 @@ runs:
run: "xcodebuild \
-project Example/ExampleApp.xcodeproj \
-scheme WalletConnect \
- -clonedSourcePackagesDirPath SourcePackages \
+ -clonedSourcePackagesDirPath SourcePackagesCache \
-destination 'platform=iOS Simulator,name=iPhone 13' \
test"
@@ -26,7 +26,7 @@ runs:
run: "xcodebuild \
-project Example/ExampleApp.xcodeproj \
-scheme IntegrationTests \
- -clonedSourcePackagesDirPath SourcePackages \
+ -clonedSourcePackagesDirPath SourcePackagesCache \
-destination 'platform=iOS Simulator,name=iPhone 13' test"
# Wallet build
@@ -36,7 +36,7 @@ runs:
run: "xcodebuild \
-project Example/ExampleApp.xcodeproj \
-scheme Wallet \
- -clonedSourcePackagesDirPath SourcePackages \
+ -clonedSourcePackagesDirPath SourcePackagesCache \
-sdk iphonesimulator"
# DApp build
@@ -46,7 +46,7 @@ runs:
run: "xcodebuild \
-project Example/ExampleApp.xcodeproj \
-scheme DApp \
- -clonedSourcePackagesDirPath SourcePackages \
+ -clonedSourcePackagesDirPath SourcePackagesCache \
-sdk iphonesimulator"
# UI tests
@@ -56,6 +56,6 @@ runs:
run: "xcodebuild \
-project Example/ExampleApp.xcodeproj \
-scheme UITests \
- -clonedSourcePackagesDirPath SourcePackages \
+ -clonedSourcePackagesDirPath SourcePackagesCache \
-destination 'platform=iOS Simulator,name=iPhone 13' test"
continue-on-error: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 297e36c28..bcbcf09cf 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,15 +24,21 @@ jobs:
steps:
- uses: actions/checkout@v2
-
+
- name: Setup Xcode Version
uses: maxim-lobanov/setup-xcode@v1
+ - name: Resolve Dependencies
+ shell: bash
+ run: "
+ xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme DApp -clonedSourcePackagesDirPath SourcePackagesCache; \
+ xcodebuild -resolvePackageDependencies -project Example/ExampleApp.xcodeproj -scheme WalletConnect -clonedSourcePackagesDirPath SourcePackagesCache"
+
- uses: actions/cache@v2
with:
path: |
.build
- SourcePackages
+ SourcePackagesCache
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
@@ -58,7 +64,7 @@ jobs:
with:
path: |
.build
- SourcePackages
+ SourcePackagesCache
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
diff --git a/.github/workflows/intake.yml b/.github/workflows/intake.yml
index 4b79bc326..210a8e04e 100644
--- a/.github/workflows/intake.yml
+++ b/.github/workflows/intake.yml
@@ -1,4 +1,4 @@
-# This workflow moves issues to the Swift board
+# This workflow moves issues to the board
# when they receive the "accepted" label
# When WalletConnect Org members create issues they
# are automatically "accepted".
@@ -28,15 +28,20 @@ jobs:
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
+ - name: Check Core Team membership
+ uses: tspascoal/get-user-teams-membership@v1
+ id: is-core-team
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 }}
+ team: "Core Team"
+ GITHUB_TOKEN: ${{ secrets.ASSIGN_TO_PROJECT_GITHUB_TOKEN }}
+ - name: Print result
+ env:
+ CREATOR: ${{ github.event_name != 'pull_request' && github.event.issue.user.login || github.event.sender.login }}
+ IS_TEAM_MEMBER: ${{ steps.is-core-team.outputs.isTeamMember }}
+ run: echo "$CREATOR (Core Team Member $IS_TEAM_MEMBER) created this issue/PR"
- name: Label issues
+ if: ${{ steps.is-core-team.outputs.isTeamMember == 'true' }}
uses: andymckay/labeler@e6c4322d0397f3240f0e7e30a33b5c5df2d39e90
with:
add-labels: "accepted"
diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectAuth.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectAuth.xcscheme
index 5b05c7da2..10b74b10a 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectAuth.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/WalletConnectAuth.xcscheme
@@ -28,6 +28,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
+
+
+
+
private let viewModel: SessionDetailViewModel
- init(session: Session, client: Sign) {
+ init(session: Session, client: SignClient) {
self.viewModel = SessionDetailViewModel(session: session, client: client)
super.init(rootView: SessionDetailView(viewModel: viewModel))
diff --git a/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift b/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift
index 00e2c18d2..f29dea255 100644
--- a/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift
+++ b/Example/ExampleApp/SessionDetails/SessionDetailViewModel.swift
@@ -5,7 +5,7 @@ import WalletConnectSign
@MainActor
final class SessionDetailViewModel: ObservableObject {
private let session: Session
- private let client: Sign
+ private let client: SignClient
enum Fields {
case accounts
@@ -18,7 +18,7 @@ final class SessionDetailViewModel: ObservableObject {
@Published var pingSuccess: Bool = false
@Published var pingFailed: Bool = false
- init(session: Session, client: Sign) {
+ init(session: Session, client: SignClient) {
self.session = session
self.client = client
self.namespaces = session.namespaces
diff --git a/Example/ExampleApp/Wallet/WalletViewController.swift b/Example/ExampleApp/Wallet/WalletViewController.swift
index cc9b001e6..5553c823e 100644
--- a/Example/ExampleApp/Wallet/WalletViewController.swift
+++ b/Example/ExampleApp/Wallet/WalletViewController.swift
@@ -1,6 +1,7 @@
import UIKit
import WalletConnectSign
import WalletConnectUtils
+import WalletConnectRouter
import Web3
import CryptoSwift
import Combine
@@ -23,6 +24,7 @@ final class WalletViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Wallet"
+ navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: self, action: #selector(goBack))
walletView.scanButton.addTarget(self, action: #selector(showScanner), for: .touchUpInside)
walletView.pasteButton.addTarget(self, action: #selector(showTextInput), for: .touchUpInside)
@@ -48,6 +50,11 @@ final class WalletViewController: UIViewController {
present(alert, animated: true)
}
+ @objc
+ private func goBack() {
+ Router.goBack()
+ }
+
private func showSessionProposal(_ proposal: Proposal) {
let proposalViewController = ProposalViewController(proposal: proposal)
proposalViewController.delegate = self
@@ -222,7 +229,7 @@ extension WalletViewController: ProposalViewControllerDelegate {
func didRejectSession() {
let proposal = currentProposal!
currentProposal = nil
- reject(proposalId: proposal.id, reason: .disapprovedChains)
+ reject(proposalId: proposal.id, reason: .userRejectedChains)
}
}
diff --git a/Example/IntegrationTests/Auth/AuthTests.swift b/Example/IntegrationTests/Auth/AuthTests.swift
new file mode 100644
index 000000000..731d74b14
--- /dev/null
+++ b/Example/IntegrationTests/Auth/AuthTests.swift
@@ -0,0 +1,123 @@
+import Foundation
+import XCTest
+import WalletConnectUtils
+@testable import WalletConnectKMS
+import WalletConnectRelay
+import Combine
+@testable import Auth
+
+final class AuthTests: XCTestCase {
+ var app: AuthClient!
+ var wallet: AuthClient!
+ let prvKey = Data(hex: "462c1dad6832d7d96ccf87bd6a686a4110e114aaaebd5512e552c0e3a87b480f")
+ private var publishers = [AnyCancellable]()
+
+ override func setUp() {
+ app = makeClient(prefix: "👻 App")
+ let walletAccount = Account(chainIdentifier: "eip155:1", address: "0x724d0D2DaD3fbB0C168f947B87Fa5DBe36F1A8bf")!
+ wallet = makeClient(prefix: "🤑 Wallet", account: walletAccount)
+
+ let expectation = expectation(description: "Wait Clients Connected")
+ expectation.expectedFulfillmentCount = 2
+
+ app.socketConnectionStatusPublisher.sink { status in
+ if status == .connected {
+ expectation.fulfill()
+ }
+ }.store(in: &publishers)
+
+ wallet.socketConnectionStatusPublisher.sink { status in
+ if status == .connected {
+ expectation.fulfill()
+ }
+ }.store(in: &publishers)
+
+ wait(for: [expectation], timeout: 5)
+ }
+
+ func makeClient(prefix: String, account: Account? = nil) -> AuthClient {
+ let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug)
+ let relayHost = "relay.walletconnect.com"
+ let projectId = "8ba9ee138960775e5231b70cc5ef1c3a"
+ let keychain = KeychainStorageMock()
+ let relayClient = RelayClient(relayHost: relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger)
+
+ return AuthClientFactory.create(
+ metadata: AppMetadata(name: name, description: "", url: "", icons: [""]),
+ account: account,
+ logger: logger,
+ keyValueStorage: RuntimeKeyValueStorage(),
+ keychainStorage: keychain,
+ relayClient: relayClient)
+ }
+
+ func testRequest() async {
+ let requestExpectation = expectation(description: "request delivered to wallet")
+ let uri = try! await app.request(RequestParams.stub())
+ try! await wallet.pair(uri: uri)
+ wallet.authRequestPublisher.sink { _ in
+ requestExpectation.fulfill()
+ }.store(in: &publishers)
+ wait(for: [requestExpectation], timeout: 2)
+ }
+
+ func testRespondSuccess() async {
+ let responseExpectation = expectation(description: "successful response delivered")
+ let uri = try! await app.request(RequestParams.stub())
+ try! await wallet.pair(uri: uri)
+ wallet.authRequestPublisher.sink { [unowned self] (id, message) in
+ Task(priority: .high) {
+ let signature = try! MessageSigner().sign(message: message, privateKey: prvKey)
+ let cacaoSignature = CacaoSignature(t: "eip191", s: signature)
+ try! await wallet.respond(requestId: id, signature: cacaoSignature)
+ }
+ }
+ .store(in: &publishers)
+ app.authResponsePublisher.sink { (id, result) in
+ guard case .success = result else { XCTFail(); return }
+ responseExpectation.fulfill()
+ }
+ .store(in: &publishers)
+ wait(for: [responseExpectation], timeout: 5)
+ }
+
+ func testUserRespondError() async {
+ let responseExpectation = expectation(description: "error response delivered")
+ let uri = try! await app.request(RequestParams.stub())
+ try! await wallet.pair(uri: uri)
+ wallet.authRequestPublisher.sink { [unowned self] (id, message) in
+ Task(priority: .high) {
+ try! await wallet.reject(requestId: id)
+ }
+ }
+ .store(in: &publishers)
+ app.authResponsePublisher.sink { (id, result) in
+ guard case .failure(let error) = result else { XCTFail(); return }
+ XCTAssertEqual(error, .userRejeted)
+ responseExpectation.fulfill()
+ }
+ .store(in: &publishers)
+ wait(for: [responseExpectation], timeout: 5)
+ }
+
+ func testRespondSignatureVerificationFailed() async {
+ let responseExpectation = expectation(description: "invalid signature response delivered")
+ let uri = try! await app.request(RequestParams.stub())
+ try! await wallet.pair(uri: uri)
+ wallet.authRequestPublisher.sink { [unowned self] (id, message) in
+ Task(priority: .high) {
+ let invalidSignature = "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b"
+ let cacaoSignature = CacaoSignature(t: "eip191", s: invalidSignature)
+ try! await wallet.respond(requestId: id, signature: cacaoSignature)
+ }
+ }
+ .store(in: &publishers)
+ app.authResponsePublisher.sink { (id, result) in
+ guard case .failure(let error) = result else { XCTFail(); return }
+ XCTAssertEqual(error, .signatureVerificationFailed)
+ responseExpectation.fulfill()
+ }
+ .store(in: &publishers)
+ wait(for: [responseExpectation], timeout: 2)
+ }
+}
diff --git a/Example/IntegrationTests/Chat/ChatTests.swift b/Example/IntegrationTests/Chat/ChatTests.swift
index 812b34e05..aa1a6271f 100644
--- a/Example/IntegrationTests/Chat/ChatTests.swift
+++ b/Example/IntegrationTests/Chat/ChatTests.swift
@@ -66,7 +66,7 @@ final class ChatTests: XCTestCase {
let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")!
let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")!
- Task(priority: .background) {
+ Task(priority: .high) {
let pubKey = try! await invitee.register(account: inviteeAccount)
try! await inviter.invite(publicKey: pubKey, peerAccount: inviteeAccount, openingMessage: "opening message", account: inviterAccount)
@@ -94,7 +94,7 @@ final class ChatTests: XCTestCase {
let inviteeAccount = Account(chainIdentifier: "eip155:1", address: "0x3627523167367216556273151")!
let inviterAccount = Account(chainIdentifier: "eip155:1", address: "0x36275231673672234423f")!
- Task(priority: .background) {
+ Task(priority: .high) {
let pubKey = try! await invitee.register(account: inviteeAccount)
try! await inviter.invite(publicKey: pubKey, peerAccount: inviteeAccount, openingMessage: "opening message", account: inviterAccount)
}
diff --git a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift
index 44f329ac4..a634f648b 100644
--- a/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift
+++ b/Example/IntegrationTests/Relay/RelayClientEndToEndTests.swift
@@ -67,14 +67,16 @@ final class RelayClientEndToEndTests: XCTestCase {
expectationA.assertForOverFulfill = false
expectationB.assertForOverFulfill = false
- relayA.onMessage = { topic, payload in
+ relayA.messagePublisher.sink { topic, payload in
(subscriptionATopic, subscriptionAPayload) = (topic, payload)
expectationA.fulfill()
- }
- relayB.onMessage = { topic, payload in
+ }.store(in: &publishers)
+
+ relayB.messagePublisher.sink { topic, payload in
(subscriptionBTopic, subscriptionBPayload) = (topic, payload)
expectationB.fulfill()
- }
+ }.store(in: &publishers)
+
relayA.socketConnectionStatusPublisher.sink { _ in
relayA.publish(topic: randomTopic, payload: payloadA, tag: 0, onNetworkAcknowledge: { error in
XCTAssertNil(error)
diff --git a/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift b/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift
index b048db4f8..71ac99ab3 100644
--- a/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift
+++ b/Example/IntegrationTests/Sign/Helpers/ClientDelegate.swift
@@ -1,10 +1,8 @@
import Foundation
@testable import WalletConnectSign
+import Combine
-class ClientDelegate: SignClientDelegate {
- func didChangeSocketConnectionStatus(_ status: SocketConnectionStatus) {
- onConnected?()
- }
+class ClientDelegate {
var client: SignClient
var onSessionSettled: ((Session) -> Void)?
@@ -15,43 +13,55 @@ class ClientDelegate: SignClientDelegate {
var onSessionRejected: ((Session.Proposal, Reason) -> Void)?
var onSessionDelete: (() -> Void)?
var onSessionUpdateNamespaces: ((String, [String: SessionNamespace]) -> Void)?
- var onSessionUpdateEvents: ((String, Set) -> Void)?
var onSessionExtend: ((String, Date) -> Void)?
var onEventReceived: ((Session.Event, String) -> Void)?
- var onPairingUpdate: ((Pairing) -> Void)?
- internal init(client: SignClient) {
+ private var publishers = Set()
+
+ init(client: SignClient) {
self.client = client
- client.delegate = self
+ setupSubscriptions()
}
- func didReject(proposal: Session.Proposal, reason: Reason) {
- onSessionRejected?(proposal, reason)
- }
- func didSettle(session: Session) {
- onSessionSettled?(session)
- }
- func didReceive(sessionProposal: Session.Proposal) {
- onSessionProposal?(sessionProposal)
- }
- func didReceive(sessionRequest: Request) {
- onSessionRequest?(sessionRequest)
- }
- func didDelete(sessionTopic: String, reason: Reason) {
- onSessionDelete?()
- }
- func didUpdate(sessionTopic: String, namespaces: [String: SessionNamespace]) {
- onSessionUpdateNamespaces?(sessionTopic, namespaces)
- }
- func didExtend(sessionTopic: String, to date: Date) {
- onSessionExtend?(sessionTopic, date)
- }
- func didReceive(event: Session.Event, sessionTopic: String, chainId: Blockchain?) {
- onEventReceived?(event, sessionTopic)
- }
- func didReceive(sessionResponse: Response) {
- onSessionResponse?(sessionResponse)
- }
+ private func setupSubscriptions() {
+ client.sessionSettlePublisher.sink { session in
+ self.onSessionSettled?(session)
+ }.store(in: &publishers)
+
+ client.socketConnectionStatusPublisher.sink { _ in
+ self.onConnected?()
+ }.store(in: &publishers)
+
+ client.sessionProposalPublisher.sink { proposal in
+ self.onSessionProposal?(proposal)
+ }.store(in: &publishers)
+
+ client.sessionRequestPublisher.sink { request in
+ self.onSessionRequest?(request)
+ }.store(in: &publishers)
+
+ client.sessionResponsePublisher.sink { response in
+ self.onSessionResponse?(response)
+ }.store(in: &publishers)
- func didDisconnect() {}
+ client.sessionRejectionPublisher.sink { (proposal, reason) in
+ self.onSessionRejected?(proposal, reason)
+ }.store(in: &publishers)
+
+ client.sessionDeletePublisher.sink { _ in
+ self.onSessionDelete?()
+ }.store(in: &publishers)
+
+ client.sessionUpdatePublisher.sink { (topic, namespaces) in
+ self.onSessionUpdateNamespaces?(topic, namespaces)
+ }.store(in: &publishers)
+
+ client.sessionEventPublisher.sink { (event, topic, _) in
+ self.onEventReceived?(event, topic)
+ }.store(in: &publishers)
+
+ client.sessionExtendPublisher.sink { (topic, date) in
+ self.onSessionExtend?(topic, date)
+ }.store(in: &publishers)
+ }
}
diff --git a/Example/IntegrationTests/Sign/SignClientTests.swift b/Example/IntegrationTests/Sign/SignClientTests.swift
index 4e53da7ae..cd79047f4 100644
--- a/Example/IntegrationTests/Sign/SignClientTests.swift
+++ b/Example/IntegrationTests/Sign/SignClientTests.swift
@@ -101,7 +101,7 @@ final class SignClientTests: XCTestCase {
wallet.onSessionProposal = { [unowned self] proposal in
Task(priority: .high) {
do {
- try await wallet.client.reject(proposalId: proposal.id, reason: .disapprovedChains) // TODO: Review reason
+ try await wallet.client.reject(proposalId: proposal.id, reason: .userRejectedChains) // TODO: Review reason
store.rejectedProposal = proposal
} catch { XCTFail("\(error)") }
}
diff --git a/Example/IntegrationTests/Stubs/RequestParams.swift b/Example/IntegrationTests/Stubs/RequestParams.swift
new file mode 100644
index 000000000..89cb11ae9
--- /dev/null
+++ b/Example/IntegrationTests/Stubs/RequestParams.swift
@@ -0,0 +1,25 @@
+
+import Foundation
+@testable import Auth
+
+extension RequestParams {
+ static func stub(domain: String = "service.invalid",
+ chainId: String = "1",
+ nonce: String = "32891756",
+ aud: String = "https://service.invalid/login",
+ nbf: String? = nil,
+ exp: String? = nil,
+ statement: String? = "I accept the ServiceOrg Terms of Service: https://service.invalid/tos",
+ requestId: String? = nil,
+ resources: [String]? = ["ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/", "https://example.com/my-web2-claim.json"]) -> RequestParams {
+ return RequestParams(domain: domain,
+ chainId: chainId,
+ nonce: nonce,
+ aud: aud,
+ nbf: nbf,
+ exp: exp,
+ statement: statement,
+ requestId: requestId,
+ resources: resources)
+ }
+}
diff --git a/Package.swift b/Package.swift
index a94e15017..22c22aac8 100644
--- a/Package.swift
+++ b/Package.swift
@@ -17,10 +17,15 @@ let package = Package(
name: "WalletConnectChat",
targets: ["Chat"]),
.library(
- name: "WalletConnectPairing",
- targets: ["WalletConnectPairing"])
+ name: "WalletConnectAuth",
+ targets: ["Auth"]),
+ .library(
+ name: "WalletConnectRouter",
+ targets: ["WalletConnectRouter"])
+ ],
+ dependencies: [
+ .package(url: "https://github.com/flypaper0/Web3.swift", .branch("feature/eip-155"))
],
- dependencies: [],
targets: [
.target(
name: "WalletConnectSign",
@@ -32,7 +37,13 @@ let package = Package(
path: "Sources/Chat"),
.target(
name: "Auth",
- dependencies: ["WalletConnectRelay", "WalletConnectUtils", "WalletConnectKMS", "WalletConnectPairing"],
+ dependencies: [
+ "WalletConnectRelay",
+ "WalletConnectUtils",
+ "WalletConnectKMS",
+ "WalletConnectPairing",
+ .product(name: "Web3", package: "Web3.swift")
+ ],
path: "Sources/Auth"),
.target(
name: "WalletConnectRelay",
@@ -54,6 +65,9 @@ let package = Package(
.target(
name: "Commons",
dependencies: []),
+ .target(
+ name: "WalletConnectRouter",
+ dependencies: []),
.testTarget(
name: "WalletConnectSignTests",
dependencies: ["WalletConnectSign", "TestingUtils"]),
diff --git a/Sources/Auth/Auth.swift b/Sources/Auth/Auth.swift
new file mode 100644
index 000000000..ab10c1f39
--- /dev/null
+++ b/Sources/Auth/Auth.swift
@@ -0,0 +1,26 @@
+import Foundation
+import WalletConnectRelay
+import Combine
+
+public class Auth {
+
+ public static var instance: AuthClient = {
+ guard let config = Auth.config else {
+ fatalError("Error - you must call Auth.configure(_:) before accessing the shared instance.")
+ }
+ return AuthClientFactory.create(
+ metadata: config.metadata,
+ account: config.account,
+ relayClient: Relay.instance)
+ }()
+
+ private static var config: Config?
+
+ private init() { }
+
+ static public func configure(metadata: AppMetadata, account: Account?) {
+ Auth.config = Auth.Config(
+ metadata: metadata,
+ account: account)
+ }
+}
diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift
index a9c751104..158e54a2f 100644
--- a/Sources/Auth/AuthClient.swift
+++ b/Sources/Auth/AuthClient.swift
@@ -1,31 +1,123 @@
import Foundation
+import Combine
+import WalletConnectUtils
+import WalletConnectPairing
+import WalletConnectRelay
-class AuthClient {
+
+public class AuthClient {
enum Errors: Error {
case malformedPairingURI
+ case unknownWalletAddress
+ case noPairingMatchingTopic
+ }
+ private var authRequestPublisherSubject = PassthroughSubject<(id: RPCID, message: String), Never>()
+ public var authRequestPublisher: AnyPublisher<(id: RPCID, message: String), Never> {
+ authRequestPublisherSubject.eraseToAnyPublisher()
+ }
+
+ private var authResponsePublisherSubject = PassthroughSubject<(id: RPCID, result: Result), Never>()
+ public var authResponsePublisher: AnyPublisher<(id: RPCID, result: Result), Never> {
+ authResponsePublisherSubject.eraseToAnyPublisher()
}
+ public let socketConnectionStatusPublisher: AnyPublisher
+
private let appPairService: AppPairService
- private let appRequestService: AuthRequestService
+ private let appRequestService: AppRequestService
+ private let appRespondSubscriber: AppRespondSubscriber
private let walletPairService: WalletPairService
+ private let walletRequestSubscriber: WalletRequestSubscriber
+ private let walletRespondService: WalletRespondService
+ private let cleanupService: CleanupService
+ private let pairingStorage: WCPairingStorage
+ private let pendingRequestsProvider: PendingRequestsProvider
+ public let logger: ConsoleLogging
+
+ private var account: Account?
- init(appPairService: AppPairService, appRequestService: AuthRequestService, walletPairService: WalletPairService) {
+ init(appPairService: AppPairService,
+ appRequestService: AppRequestService,
+ appRespondSubscriber: AppRespondSubscriber,
+ walletPairService: WalletPairService,
+ walletRequestSubscriber: WalletRequestSubscriber,
+ walletRespondService: WalletRespondService,
+ account: Account?,
+ pendingRequestsProvider: PendingRequestsProvider,
+ cleanupService: CleanupService,
+ logger: ConsoleLogging,
+ pairingStorage: WCPairingStorage,
+ socketConnectionStatusPublisher: AnyPublisher
+) {
self.appPairService = appPairService
self.appRequestService = appRequestService
self.walletPairService = walletPairService
+ self.walletRequestSubscriber = walletRequestSubscriber
+ self.walletRespondService = walletRespondService
+ self.appRespondSubscriber = appRespondSubscriber
+ self.account = account
+ self.pendingRequestsProvider = pendingRequestsProvider
+ self.cleanupService = cleanupService
+ self.logger = logger
+ self.pairingStorage = pairingStorage
+ self.socketConnectionStatusPublisher = socketConnectionStatusPublisher
+
+ setUpPublishers()
}
- func request(params: RequestParams) async throws -> String {
+ public func pair(uri: String) async throws {
+ guard let pairingURI = WalletConnectURI(string: uri) else {
+ throw Errors.malformedPairingURI
+ }
+ try await walletPairService.pair(pairingURI)
+ }
+
+ public func request(_ params: RequestParams) async throws -> String {
+ logger.debug("Requesting Authentication")
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
+ public func request(_ params: RequestParams, topic: String) async throws {
+ logger.debug("Requesting Authentication on existing pairing")
+ guard pairingStorage.hasPairing(forTopic: topic) else {
+ throw Errors.noPairingMatchingTopic
+ }
+ try await appRequestService.request(params: params, topic: topic)
+ }
+
+ public func respond(requestId: RPCID, signature: CacaoSignature) async throws {
+ guard let account = account else { throw Errors.unknownWalletAddress }
+ try await walletRespondService.respond(requestId: requestId, signature: signature, account: account)
+ }
+
+ public func reject(requestId: RPCID) async throws {
+ try await walletRespondService.respondError(requestId: requestId)
+ }
+
+ public func getPendingRequests() throws -> [AuthRequest] {
+ guard let account = account else { throw Errors.unknownWalletAddress }
+ return try pendingRequestsProvider.getPendingRequests(account: account)
+ }
+
+#if DEBUG
+ /// Delete all stored data such as: pairings, keys
+ ///
+ /// - Note: Doesn't unsubscribe from topics
+ public func cleanup() throws {
+ try cleanupService.cleanup()
+ }
+#endif
+
+ private func setUpPublishers() {
+ appRespondSubscriber.onResponse = { [unowned self] (id, result) in
+ authResponsePublisherSubject.send((id, result))
+ }
+
+ walletRequestSubscriber.onRequest = { [unowned self] (id, message) in
+ authRequestPublisherSubject.send((id, message))
}
- try await walletPairService.pair(pairingURI)
}
}
diff --git a/Sources/Auth/AuthClientFactory.swift b/Sources/Auth/AuthClientFactory.swift
new file mode 100644
index 000000000..6be615528
--- /dev/null
+++ b/Sources/Auth/AuthClientFactory.swift
@@ -0,0 +1,46 @@
+import Foundation
+import WalletConnectRelay
+import WalletConnectUtils
+import WalletConnectKMS
+import WalletConnectPairing
+
+public struct AuthClientFactory {
+
+ public static func create(metadata: AppMetadata, account: Account?, relayClient: RelayClient) -> AuthClient {
+ let logger = ConsoleLogger(loggingLevel: .off)
+ let keyValueStorage = UserDefaults.standard
+ let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk")
+ return AuthClientFactory.create(metadata: metadata, account: account, logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient)
+ }
+
+ static func create(metadata: AppMetadata, account: Account?, logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) -> AuthClient {
+ let historyStorage = CodableStore(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.jsonRpcHistory.rawValue)
+ let pairingStore = PairingStorage(storage: SequenceStore(store: .init(defaults: keyValueStorage, identifier: StorageDomainIdentifiers.pairings.rawValue)))
+ let kms = KeyManagementService(keychain: keychainStorage)
+ let serializer = Serializer(kms: kms)
+ let history = RPCHistory(keyValueStore: historyStorage)
+ let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serializer, logger: logger, rpcHistory: history)
+ let messageFormatter = SIWEMessageFormatter()
+ let appPairService = AppPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore)
+ let appRequestService = AppRequestService(networkingInteractor: networkingInteractor, kms: kms, appMetadata: metadata, logger: logger)
+ let messageSigner = MessageSigner(signer: Signer())
+ let appRespondSubscriber = AppRespondSubscriber(networkingInteractor: networkingInteractor, logger: logger, rpcHistory: history, signatureVerifier: messageSigner, messageFormatter: messageFormatter)
+ let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore)
+ let walletRequestSubscriber = WalletRequestSubscriber(networkingInteractor: networkingInteractor, logger: logger, kms: kms, messageFormatter: messageFormatter, address: account?.address)
+ let walletRespondService = WalletRespondService(networkingInteractor: networkingInteractor, logger: logger, kms: kms, rpcHistory: history)
+ let pendingRequestsProvider = PendingRequestsProvider(rpcHistory: history)
+ let cleanupService = CleanupService(pairingStore: pairingStore, kms: kms)
+
+ return AuthClient(appPairService: appPairService,
+ appRequestService: appRequestService,
+ appRespondSubscriber: appRespondSubscriber,
+ walletPairService: walletPairService,
+ walletRequestSubscriber: walletRequestSubscriber,
+ walletRespondService: walletRespondService,
+ account: account,
+ pendingRequestsProvider: pendingRequestsProvider,
+ cleanupService: cleanupService,
+ logger: logger,
+ pairingStorage: pairingStore, socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher)
+ }
+}
diff --git a/Sources/Auth/AuthConfig.swift b/Sources/Auth/AuthConfig.swift
new file mode 100644
index 000000000..b364ec507
--- /dev/null
+++ b/Sources/Auth/AuthConfig.swift
@@ -0,0 +1,8 @@
+import Foundation
+
+extension Auth {
+ struct Config {
+ let metadata: AppMetadata
+ let account: Account?
+ }
+}
diff --git a/Sources/Auth/Extensions/Data+Keccak256.swift b/Sources/Auth/Extensions/Data+Keccak256.swift
new file mode 100644
index 000000000..1bfbc1b8b
--- /dev/null
+++ b/Sources/Auth/Extensions/Data+Keccak256.swift
@@ -0,0 +1,9 @@
+import Foundation
+import CryptoSwift
+
+extension Data {
+
+ var keccak256: Data {
+ return sha3(.keccak256)
+ }
+}
diff --git a/Sources/Auth/Services/App/AuthRequestService.swift b/Sources/Auth/Services/App/AppRequestService.swift
similarity index 79%
rename from Sources/Auth/Services/App/AuthRequestService.swift
rename to Sources/Auth/Services/App/AppRequestService.swift
index b2b50844d..b3186e77d 100644
--- a/Sources/Auth/Services/App/AuthRequestService.swift
+++ b/Sources/Auth/Services/App/AppRequestService.swift
@@ -3,17 +3,20 @@ import WalletConnectUtils
import WalletConnectKMS
import JSONRPC
-actor AuthRequestService {
+actor AppRequestService {
private let networkingInteractor: NetworkInteracting
private let appMetadata: AppMetadata
private let kms: KeyManagementService
+ private let logger: ConsoleLogging
init(networkingInteractor: NetworkInteracting,
kms: KeyManagementService,
- appMetadata: AppMetadata) {
+ appMetadata: AppMetadata,
+ logger: ConsoleLogging) {
self.networkingInteractor = networkingInteractor
self.kms = kms
self.appMetadata = appMetadata
+ self.logger = logger
}
func request(params: RequestParams, topic: String) async throws {
@@ -24,6 +27,8 @@ actor AuthRequestService {
let payload = AuthPayload(requestParams: params, iat: issueAt)
let params = AuthRequestParams(requester: requester, payloadParams: payload)
let request = RPCRequest(method: "wc_authRequest", params: params)
+ try kms.setPublicKey(publicKey: pubKey, for: responseTopic)
+ logger.debug("AppRequestService: Subscribibg for response topic: \(responseTopic)")
try await networkingInteractor.requestNetworkAck(request, topic: topic, tag: AuthRequestParams.tag)
try await networkingInteractor.subscribe(topic: responseTopic)
}
diff --git a/Sources/Auth/Services/App/AppRespondSubscriber.swift b/Sources/Auth/Services/App/AppRespondSubscriber.swift
new file mode 100644
index 000000000..a73015b4c
--- /dev/null
+++ b/Sources/Auth/Services/App/AppRespondSubscriber.swift
@@ -0,0 +1,65 @@
+import Combine
+import Foundation
+import WalletConnectUtils
+import JSONRPC
+
+class AppRespondSubscriber {
+ private let networkingInteractor: NetworkInteracting
+ private let logger: ConsoleLogging
+ private let rpcHistory: RPCHistory
+ private let signatureVerifier: MessageSignatureVerifying
+ private let messageFormatter: SIWEMessageFormatting
+ private var publishers = [AnyCancellable]()
+
+ var onResponse: ((_ id: RPCID, _ result: Result) -> Void)?
+
+ init(networkingInteractor: NetworkInteracting,
+ logger: ConsoleLogging,
+ rpcHistory: RPCHistory,
+ signatureVerifier: MessageSignatureVerifying,
+ messageFormatter: SIWEMessageFormatting) {
+ self.networkingInteractor = networkingInteractor
+ self.logger = logger
+ self.rpcHistory = rpcHistory
+ self.signatureVerifier = signatureVerifier
+ self.messageFormatter = messageFormatter
+ subscribeForResponse()
+ }
+
+ private func subscribeForResponse() {
+ networkingInteractor.responsePublisher.sink { [unowned self] subscriptionPayload in
+ let response = subscriptionPayload.response
+ guard
+ let requestId = response.id,
+ let request = rpcHistory.get(recordId: requestId)?.request,
+ let requestParams = request.params, request.method == "wc_authRequest"
+ else { return }
+
+ networkingInteractor.unsubscribe(topic: subscriptionPayload.topic)
+
+ if let errorResponse = response.error,
+ let error = AuthError(code: errorResponse.code) {
+ onResponse?(requestId, .failure(error))
+ return
+ }
+
+ guard
+ let cacao = try? response.result?.get(Cacao.self),
+ let address = try? DIDPKH(iss: cacao.payload.iss).account.address,
+ let message = try? messageFormatter.formatMessage(from: cacao.payload)
+ else { self.onResponse?(requestId, .failure(.malformedResponseParams)); return }
+
+ guard let requestPayload = try? requestParams.get(AuthRequestParams.self)
+ else { self.onResponse?(requestId, .failure(.malformedRequestParams)); return }
+
+ guard messageFormatter.formatMessage(from: requestPayload.payloadParams, address: address) == message
+ else { self.onResponse?(requestId, .failure(.messageCompromised)); return }
+
+ guard let _ = try? signatureVerifier.verify(signature: cacao.signature.s, message: message, address: address)
+ else { self.onResponse?(requestId, .failure(.signatureVerificationFailed)); return }
+
+ onResponse?(requestId, .success(cacao))
+
+ }.store(in: &publishers)
+ }
+}
diff --git a/Sources/Auth/Services/App/AuthRespondSubscriber.swift b/Sources/Auth/Services/App/AuthRespondSubscriber.swift
deleted file mode 100644
index 5fcd7355d..000000000
--- a/Sources/Auth/Services/App/AuthRespondSubscriber.swift
+++ /dev/null
@@ -1,39 +0,0 @@
-import Combine
-import Foundation
-import WalletConnectUtils
-import JSONRPC
-
-class AuthRespondSubscriber {
- private let networkingInteractor: NetworkInteracting
- private let logger: ConsoleLogging
- private let rpcHistory: RPCHistory
- private var publishers = [AnyCancellable]()
- var onResponse: ((_ id: RPCID, _ cacao: Cacao) -> Void)?
-
- init(networkingInteractor: NetworkInteracting,
- logger: ConsoleLogging,
- rpcHistory: RPCHistory) {
- self.networkingInteractor = networkingInteractor
- self.logger = logger
- self.rpcHistory = rpcHistory
- subscribeForResponse()
- }
-
- private func subscribeForResponse() {
- networkingInteractor.responsePublisher.sink { [unowned self] subscriptionPayload in
- guard let request = rpcHistory.get(recordId: subscriptionPayload.response.id!)?.request,
- request.method == "wc_authRequest" else { return }
- networkingInteractor.unsubscribe(topic: subscriptionPayload.topic)
- guard let cacao = try? subscriptionPayload.response.result?.get(Cacao.self) else {
- logger.debug("Malformed auth response params")
- return
- }
- do {
- try CacaoSignatureVerifier().verifySignature(cacao)
- onResponse?(subscriptionPayload.response.id!, cacao)
- } catch {
- logger.debug("Received response with invalid signature")
- }
- }.store(in: &publishers)
- }
-}
diff --git a/Sources/Auth/Services/Common/CacaoFormatter.swift b/Sources/Auth/Services/Common/CacaoFormatter.swift
index 43ddb9058..b8331b577 100644
--- a/Sources/Auth/Services/Common/CacaoFormatter.swift
+++ b/Sources/Auth/Services/Common/CacaoFormatter.swift
@@ -2,11 +2,13 @@ import Foundation
import WalletConnectUtils
protocol CacaoFormatting {
- func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ issuer: Account) -> Cacao
+ func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> Cacao
}
class CacaoFormatter: CacaoFormatting {
- func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ issuer: Account) -> Cacao {
- fatalError("not implemented")
+ func format(_ request: AuthRequestParams, _ signature: CacaoSignature, _ didpkh: DIDPKH) -> Cacao {
+ let header = CacaoHeader(t: "eip4361")
+ let payload = CacaoPayload(params: request.payloadParams, didpkh: didpkh)
+ return Cacao(header: header, payload: payload, signature: signature)
}
}
diff --git a/Sources/Auth/Services/Common/CacaoSignatureVerifier.swift b/Sources/Auth/Services/Common/CacaoSignatureVerifier.swift
deleted file mode 100644
index 64f70488b..000000000
--- a/Sources/Auth/Services/Common/CacaoSignatureVerifier.swift
+++ /dev/null
@@ -1,15 +0,0 @@
-import Foundation
-
-protocol CacaoSignatureVerifying {
- func verifySignature(_ cacao: Cacao) throws
-}
-
-class CacaoSignatureVerifier: CacaoSignatureVerifying {
- enum Errors: Error {
- case signatureInvalid
- }
-
- func verifySignature(_ cacao: Cacao) throws {
- fatalError("not implemented")
- }
-}
diff --git a/Sources/Auth/Services/Common/CleanupService.swift b/Sources/Auth/Services/Common/CleanupService.swift
new file mode 100644
index 000000000..6a5a01334
--- /dev/null
+++ b/Sources/Auth/Services/Common/CleanupService.swift
@@ -0,0 +1,20 @@
+import Foundation
+import WalletConnectKMS
+import WalletConnectUtils
+import WalletConnectPairing
+
+final class CleanupService {
+
+ private let pairingStore: WCPairingStorage
+ private let kms: KeyManagementServiceProtocol
+
+ init(pairingStore: WCPairingStorage, kms: KeyManagementServiceProtocol) {
+ self.pairingStore = pairingStore
+ self.kms = kms
+ }
+
+ func cleanup() throws {
+ pairingStore.deleteAll()
+ try kms.deleteAll()
+ }
+}
diff --git a/Sources/Auth/Services/Common/NetworkingInteractor.swift b/Sources/Auth/Services/Common/NetworkingInteractor.swift
index ffdc61603..24419b295 100644
--- a/Sources/Auth/Services/Common/NetworkingInteractor.swift
+++ b/Sources/Auth/Services/Common/NetworkingInteractor.swift
@@ -13,6 +13,7 @@ protocol NetworkInteracting {
func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws
func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws
func respond(topic: String, response: RPCResponse, tag: Int, envelopeType: Envelope.EnvelopeType) async throws
+ func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws
}
extension NetworkInteracting {
@@ -22,18 +23,22 @@ extension NetworkInteracting {
}
class NetworkingInteractor: NetworkInteracting {
+ private var publishers = Set()
private let relayClient: RelayClient
private let serializer: Serializing
private let rpcHistory: RPCHistory
private let logger: ConsoleLogging
+
+ private let requestPublisherSubject = PassthroughSubject()
var requestPublisher: AnyPublisher {
requestPublisherSubject.eraseToAnyPublisher()
}
- private let requestPublisherSubject = PassthroughSubject()
+
+ private let responsePublisherSubject = PassthroughSubject()
var responsePublisher: AnyPublisher {
responsePublisherSubject.eraseToAnyPublisher()
}
- private let responsePublisherSubject = PassthroughSubject()
+
var socketConnectionStatusPublisher: AnyPublisher
init(relayClient: RelayClient,
@@ -45,9 +50,10 @@ class NetworkingInteractor: NetworkInteracting {
self.rpcHistory = rpcHistory
self.logger = logger
self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher
- relayClient.onMessage = { [unowned self] topic, message in
+ relayClient.messagePublisher.sink { [unowned self] (topic, message) in
manageSubscription(topic, message)
}
+ .store(in: &publishers)
}
func subscribe(topic: String) async throws {
@@ -55,7 +61,13 @@ class NetworkingInteractor: NetworkInteracting {
}
func unsubscribe(topic: String) {
- fatalError("not implemented")
+ relayClient.unsubscribe(topic: topic) { [unowned self] error in
+ if let error = error {
+ logger.error(error)
+ } else {
+ rpcHistory.deleteAll(forTopic: topic)
+ }
+ }
}
func request(_ request: RPCRequest, topic: String, tag: Int, envelopeType: Envelope.EnvelopeType) async throws {
@@ -91,11 +103,18 @@ class NetworkingInteractor: NetworkInteracting {
try await relayClient.publish(topic: topic, payload: message, tag: tag)
}
+ func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws {
+ let error = JSONRPCError(code: reason.code, message: reason.message)
+ let response = RPCResponse(id: requestId, error: error)
+ let message = try! serializer.serialize(topic: topic, encodable: response, envelopeType: envelopeType)
+ try await relayClient.publish(topic: topic, payload: message, tag: tag)
+ }
+
private func manageSubscription(_ topic: String, _ encodedEnvelope: String) {
if let deserializedJsonRpcRequest: RPCRequest = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
handleRequest(topic: topic, request: deserializedJsonRpcRequest)
- } else if let deserializedJsonRpcResponse: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
- handleResponse(response: deserializedJsonRpcResponse)
+ } else if let response: RPCResponse = serializer.tryDeserialize(topic: topic, encodedEnvelope: encodedEnvelope) {
+ handleResponse(response: response)
} else {
logger.debug("Networking Interactor - Received unknown object type from networking relay")
}
diff --git a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift
index 1861e2808..64f6a6ced 100644
--- a/Sources/Auth/Services/Common/SIWEMessageFormatter.swift
+++ b/Sources/Auth/Services/Common/SIWEMessageFormatter.swift
@@ -3,22 +3,44 @@ import WalletConnectUtils
protocol SIWEMessageFormatting {
func formatMessage(from authPayload: AuthPayload, address: String) -> String
+ func formatMessage(from payload: CacaoPayload) throws -> String
}
struct SIWEMessageFormatter: SIWEMessageFormatting {
- func formatMessage(from authPayload: AuthPayload, address: String) -> String {
- SIWEMessage(domain: authPayload.domain,
- uri: authPayload.aud,
+ func formatMessage(from payload: AuthPayload, address: String) -> String {
+ let message = SIWEMessage(domain: payload.domain,
+ uri: payload.aud,
address: address,
- version: authPayload.version,
- nonce: authPayload.nonce,
- chainId: authPayload.chainId,
- iat: authPayload.iat,
- nbf: authPayload.nbf,
- exp: authPayload.exp,
- statement: authPayload.statement,
- requestId: authPayload.requestId,
- resources: authPayload.resources).formatted
+ version: payload.version,
+ nonce: payload.nonce,
+ chainId: payload.chainId,
+ iat: payload.iat,
+ nbf: payload.nbf,
+ exp: payload.exp,
+ statement: payload.statement,
+ requestId: payload.requestId,
+ resources: payload.resources
+ )
+ return message.formatted
+ }
+
+ func formatMessage(from payload: CacaoPayload) throws -> String {
+ let address = try DIDPKH(iss: payload.iss).account.address
+ let message = SIWEMessage(
+ domain: payload.domain,
+ uri: payload.aud,
+ address: address,
+ version: payload.version,
+ nonce: payload.nonce,
+ chainId: "1",
+ iat: payload.iat,
+ nbf: payload.nbf,
+ exp: payload.exp,
+ statement: payload.statement,
+ requestId: payload.requestId,
+ resources: payload.resources
+ )
+ return message.formatted
}
}
diff --git a/Sources/Auth/Services/Signer/DIDPKH.swift b/Sources/Auth/Services/Signer/DIDPKH.swift
new file mode 100644
index 000000000..fad1a3431
--- /dev/null
+++ b/Sources/Auth/Services/Signer/DIDPKH.swift
@@ -0,0 +1,33 @@
+import Foundation
+import WalletConnectUtils
+
+struct DIDPKH {
+ static let didPrefix: String = "did:pkh"
+
+ enum Errors: Error {
+ case invalidDIDPKH
+ case invalidAccount
+ }
+
+ let account: Account
+ let iss: String
+
+ init(iss: String) throws {
+ guard iss.starts(with: DIDPKH.didPrefix)
+ else { throw Errors.invalidDIDPKH }
+
+ guard let string = iss.components(separatedBy: DIDPKH.didPrefix + ":").last
+ else { throw Errors.invalidDIDPKH }
+
+ guard let account = Account(string)
+ else { throw Errors.invalidAccount }
+
+ self.iss = iss
+ self.account = account
+ }
+
+ init(account: Account) {
+ self.iss = "\(DIDPKH.didPrefix):\(account.absoluteString)"
+ self.account = account
+ }
+}
diff --git a/Sources/Auth/Services/Signer/MessageSigner.swift b/Sources/Auth/Services/Signer/MessageSigner.swift
new file mode 100644
index 000000000..d07972a5c
--- /dev/null
+++ b/Sources/Auth/Services/Signer/MessageSigner.swift
@@ -0,0 +1,36 @@
+import Foundation
+
+protocol MessageSignatureVerifying {
+ func verify(signature: String, message: String, address: String) throws
+}
+
+protocol MessageSigning {
+ func sign(message: String, privateKey: Data) throws -> String
+}
+
+public struct MessageSigner: MessageSignatureVerifying, MessageSigning {
+
+ enum Errors: Error {
+ case signatureValidationFailed
+ case utf8EncodingFailed
+ }
+
+ private let signer: Signer
+
+ public init(signer: Signer = Signer()) {
+ self.signer = signer
+ }
+
+ public func sign(message: String, privateKey: Data) throws -> String {
+ guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed }
+ let signature = try signer.sign(message: messageData, with: privateKey)
+ return signature.toHexString()
+ }
+
+ public func verify(signature: String, message: String, address: String) throws {
+ guard let messageData = message.data(using: .utf8) else { throw Errors.utf8EncodingFailed }
+ let signatureData = Data(hex: signature)
+ guard try signer.isValid(signature: signatureData, message: messageData, address: address)
+ else { throw Errors.signatureValidationFailed }
+ }
+}
diff --git a/Sources/Auth/Services/Signer/Signer.swift b/Sources/Auth/Services/Signer/Signer.swift
new file mode 100644
index 000000000..3d5903296
--- /dev/null
+++ b/Sources/Auth/Services/Signer/Signer.swift
@@ -0,0 +1,44 @@
+import Foundation
+import Web3
+
+public struct Signer {
+
+ typealias Signature = (v: UInt, r: [UInt8], s: [UInt8])
+
+ public init() {}
+
+ func sign(message: Data, with key: Data) throws -> Data {
+ let prefixed = prefixed(message: message)
+ let privateKey = try EthereumPrivateKey(privateKey: key.bytes)
+ let signature = try privateKey.sign(message: prefixed.bytes)
+ return serialized(signature: signature)
+ }
+
+ func isValid(signature: Data, message: Data, address: String) throws -> Bool {
+ let sig = decompose(signature: signature)
+ let prefixed = prefixed(message: message)
+ let publicKey = try EthereumPublicKey(
+ message: prefixed.bytes,
+ v: EthereumQuantity(quantity: BigUInt(sig.v)),
+ r: EthereumQuantity(sig.r),
+ s: EthereumQuantity(sig.s)
+ )
+ return publicKey.address.hex(eip55: false) == address.lowercased()
+ }
+
+ private func decompose(signature: Data) -> Signature {
+ let v = signature.bytes[signature.count-1]
+ let r = signature.bytes[0..<32]
+ let s = signature.bytes[32..<64]
+ return (UInt(v), [UInt8](r), [UInt8](s))
+ }
+
+ private func serialized(signature: Signature) -> Data {
+ return Data(signature.r + signature.s + [UInt8(signature.v)])
+ }
+
+ private func prefixed(message: Data) -> Data {
+ return "\u{19}Ethereum Signed Message:\n\(message.count)"
+ .data(using: .utf8)! + message
+ }
+}
diff --git a/Sources/Auth/Services/Wallet/AuthRequestSubscriber.swift b/Sources/Auth/Services/Wallet/AuthRequestSubscriber.swift
deleted file mode 100644
index e4acc5b04..000000000
--- a/Sources/Auth/Services/Wallet/AuthRequestSubscriber.swift
+++ /dev/null
@@ -1,42 +0,0 @@
-import Combine
-import Foundation
-import WalletConnectUtils
-import JSONRPC
-
-class AuthRequestSubscriber {
- private let networkingInteractor: NetworkInteracting
- private let logger: ConsoleLogging
- private let address: String
- private var publishers = [AnyCancellable]()
- private let messageFormatter: SIWEMessageFormatting
- var onRequest: ((_ id: RPCID, _ message: String)->Void)?
-
- init(networkingInteractor: NetworkInteracting,
- logger: ConsoleLogging,
- messageFormatter: SIWEMessageFormatting,
- address: String) {
- self.networkingInteractor = networkingInteractor
- self.logger = logger
- self.address = address
- 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.payloadParams, address: address)
- 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
deleted file mode 100644
index 006459d37..000000000
--- a/Sources/Auth/Services/Wallet/AuthRespondService.swift
+++ /dev/null
@@ -1,41 +0,0 @@
-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/PendingRequestsProvider.swift b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift
new file mode 100644
index 000000000..16fe22128
--- /dev/null
+++ b/Sources/Auth/Services/Wallet/PendingRequestsProvider.swift
@@ -0,0 +1,22 @@
+import Foundation
+import JSONRPC
+import WalletConnectUtils
+
+class PendingRequestsProvider {
+ private let rpcHistory: RPCHistory
+
+ init(rpcHistory: RPCHistory) {
+ self.rpcHistory = rpcHistory
+ }
+
+ public func getPendingRequests(account: Account) throws -> [AuthRequest] {
+ let pendingRequests: [AuthRequest] = rpcHistory.getPending()
+ .filter {$0.request.method == "wc_authRequest"}
+ .compactMap {
+ guard let params = try? $0.request.params?.get(AuthRequestParams.self) else {return nil}
+ let message = SIWEMessageFormatter().formatMessage(from: params.payloadParams, address: account.address)
+ return AuthRequest(id: $0.request.id!, message: message)
+ }
+ return pendingRequests
+ }
+}
diff --git a/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift
new file mode 100644
index 000000000..6d04629ca
--- /dev/null
+++ b/Sources/Auth/Services/Wallet/WalletRequestSubscriber.swift
@@ -0,0 +1,59 @@
+import Combine
+import Foundation
+import WalletConnectUtils
+import JSONRPC
+import WalletConnectKMS
+
+class WalletRequestSubscriber {
+ private let networkingInteractor: NetworkInteracting
+ private let logger: ConsoleLogging
+ private let kms: KeyManagementServiceProtocol
+ private let address: String?
+ private var publishers = [AnyCancellable]()
+ private let messageFormatter: SIWEMessageFormatting
+ var onRequest: ((_ id: RPCID, _ message: String) -> Void)?
+
+ init(networkingInteractor: NetworkInteracting,
+ logger: ConsoleLogging,
+ kms: KeyManagementServiceProtocol,
+ messageFormatter: SIWEMessageFormatting,
+ address: String?) {
+ self.networkingInteractor = networkingInteractor
+ self.logger = logger
+ self.kms = kms
+ self.address = address
+ self.messageFormatter = messageFormatter
+ subscribeForRequest()
+ }
+
+ private func subscribeForRequest() {
+ guard let address = address else { return }
+
+ networkingInteractor.requestPublisher.sink { [unowned self] payload in
+
+ logger.debug("WalletRequestSubscriber: Received request")
+
+ guard let requestId = payload.request.id, payload.request.method == "wc_authRequest"
+ else { return }
+
+ guard let authRequestParams = try? payload.request.params?.get(AuthRequestParams.self)
+ else { return respondError(.malformedRequestParams, topic: payload.topic, requestId: requestId) }
+
+ let message = messageFormatter.formatMessage(from: authRequestParams.payloadParams, address: address)
+
+ onRequest?(requestId, message)
+ }.store(in: &publishers)
+ }
+
+ private func respondError(_ error: AuthError, topic: String, requestId: RPCID) {
+ guard let pubKey = kms.getAgreementSecret(for: topic)?.publicKey
+ else { return logger.error("Agreement key for topic \(topic) not found") }
+
+ let tag = AuthResponseParams.tag
+ let envelopeType = Envelope.EnvelopeType.type1(pubKey: pubKey.rawRepresentation)
+
+ Task(priority: .high) {
+ try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType)
+ }
+ }
+}
diff --git a/Sources/Auth/Services/Wallet/WalletRespondService.swift b/Sources/Auth/Services/Wallet/WalletRespondService.swift
new file mode 100644
index 000000000..07e01871c
--- /dev/null
+++ b/Sources/Auth/Services/Wallet/WalletRespondService.swift
@@ -0,0 +1,67 @@
+import Foundation
+import WalletConnectKMS
+import JSONRPC
+import WalletConnectUtils
+
+actor WalletRespondService {
+ 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(requestId: RPCID, signature: CacaoSignature, account: Account) async throws {
+ let authRequestParams = try getAuthRequestParams(requestId: requestId)
+ let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams)
+
+ try kms.setAgreementSecret(keys, topic: topic)
+
+ let didpkh = DIDPKH(account: account)
+ let cacao = CacaoFormatter().format(authRequestParams, signature, didpkh)
+ let response = RPCResponse(id: requestId, result: cacao)
+ try await networkingInteractor.respond(topic: topic, response: response, tag: AuthResponseParams.tag, envelopeType: .type1(pubKey: keys.publicKey.rawRepresentation))
+ }
+
+ func respondError(requestId: RPCID) async throws {
+ let authRequestParams = try getAuthRequestParams(requestId: requestId)
+ let (topic, keys) = try generateAgreementKeys(requestParams: authRequestParams)
+
+ try kms.setAgreementSecret(keys, topic: topic)
+
+ let tag = AuthResponseParams.tag
+ let error = AuthError.userRejeted
+ let envelopeType = Envelope.EnvelopeType.type1(pubKey: keys.publicKey.rawRepresentation)
+ try await networkingInteractor.respondError(topic: topic, requestId: requestId, tag: tag, reason: error, envelopeType: envelopeType)
+ }
+
+ private func getAuthRequestParams(requestId: RPCID) throws -> AuthRequestParams {
+ guard let request = rpcHistory.get(recordId: requestId)?.request
+ else { throw Errors.recordForIdNotFound }
+
+ guard let authRequestParams = try request.params?.get(AuthRequestParams.self)
+ else { throw Errors.malformedAuthRequestParams }
+
+ return authRequestParams
+ }
+
+ private func generateAgreementKeys(requestParams: AuthRequestParams) throws -> (topic: String, keys: AgreementKeys) {
+ let peerPubKey = try AgreementPublicKey(hex: requestParams.requester.publicKey)
+ let topic = peerPubKey.rawRepresentation.sha256().toHexString()
+ let selfPubKey = try kms.createX25519KeyPair()
+ let keys = try kms.performKeyAgreement(selfPublicKey: selfPubKey, peerPublicKey: peerPubKey.hexRepresentation)
+ return (topic, keys)
+ }
+}
diff --git a/Sources/Auth/StorageDomainIdentifiers.swift b/Sources/Auth/StorageDomainIdentifiers.swift
new file mode 100644
index 000000000..ed7156c67
--- /dev/null
+++ b/Sources/Auth/StorageDomainIdentifiers.swift
@@ -0,0 +1,6 @@
+import Foundation
+
+enum StorageDomainIdentifiers: String {
+ case jsonRpcHistory = "com.walletconnect.sdk.wc_jsonRpcHistoryRecord"
+ case pairings = "com.walletconnect.sdk.pairingSequences"
+}
diff --git a/Sources/Auth/Types/Aliases/Account.swift b/Sources/Auth/Types/Aliases/Account.swift
new file mode 100644
index 000000000..3ab41d381
--- /dev/null
+++ b/Sources/Auth/Types/Aliases/Account.swift
@@ -0,0 +1,4 @@
+import WalletConnectUtils
+import Foundation
+
+public typealias Account = WalletConnectUtils.Account
diff --git a/Sources/Auth/Types/AppMetadata.swift b/Sources/Auth/Types/Aliases/AppMetadata.swift
similarity index 100%
rename from Sources/Auth/Types/AppMetadata.swift
rename to Sources/Auth/Types/Aliases/AppMetadata.swift
diff --git a/Sources/Auth/Types/Aliases/RPCID.swift b/Sources/Auth/Types/Aliases/RPCID.swift
new file mode 100644
index 000000000..51cb31cdd
--- /dev/null
+++ b/Sources/Auth/Types/Aliases/RPCID.swift
@@ -0,0 +1,4 @@
+import Foundation
+import JSONRPC
+
+public typealias RPCID = JSONRPC.RPCID
diff --git a/Sources/Auth/Types/WalletConnectURI.swift b/Sources/Auth/Types/Aliases/WalletConnectURI.swift
similarity index 100%
rename from Sources/Auth/Types/WalletConnectURI.swift
rename to Sources/Auth/Types/Aliases/WalletConnectURI.swift
diff --git a/Sources/Auth/Types/Cacao/CacaoPayload.swift b/Sources/Auth/Types/Cacao/CacaoPayload.swift
index c339bd18d..1a71050c7 100644
--- a/Sources/Auth/Types/Cacao/CacaoPayload.swift
+++ b/Sources/Auth/Types/Cacao/CacaoPayload.swift
@@ -4,12 +4,26 @@ struct CacaoPayload: Codable, Equatable {
let iss: String
let domain: String
let aud: String
- let version: String
+ let version: Int
let nonce: String
let iat: String
- let nbf: String
- let exp: String
- let statement: String
- let requestId: String
- let resources: String
+ let nbf: String?
+ let exp: String?
+ let statement: String?
+ let requestId: String?
+ let resources: [String]?
+
+ init(params: AuthPayload, didpkh: DIDPKH) {
+ self.iss = didpkh.iss
+ self.domain = params.domain
+ self.aud = params.aud
+ self.version = 1
+ self.nonce = params.nonce
+ self.iat = params.iat
+ self.nbf = params.nbf
+ self.exp = params.exp
+ self.statement = params.statement
+ self.requestId = params.requestId
+ self.resources = params.resources
+ }
}
diff --git a/Sources/Auth/Types/Cacao/CacaoSignature.swift b/Sources/Auth/Types/Cacao/CacaoSignature.swift
index 97c04c142..b34ee1a40 100644
--- a/Sources/Auth/Types/Cacao/CacaoSignature.swift
+++ b/Sources/Auth/Types/Cacao/CacaoSignature.swift
@@ -1,7 +1,13 @@
import Foundation
-struct CacaoSignature: Codable, Equatable {
+public struct CacaoSignature: Codable, Equatable {
let t: String
let s: String
- let m: String
+ let m: String?
+
+ public init(t: String, s: String, m: String? = nil) {
+ self.t = t
+ self.s = s
+ self.m = m
+ }
}
diff --git a/Sources/Auth/Types/Errors/AuthError.swift b/Sources/Auth/Types/Errors/AuthError.swift
new file mode 100644
index 000000000..f5b52b21a
--- /dev/null
+++ b/Sources/Auth/Types/Errors/AuthError.swift
@@ -0,0 +1,59 @@
+import Foundation
+
+public enum AuthError: Codable, Equatable, Error {
+ case userRejeted
+ case malformedResponseParams
+ case malformedRequestParams
+ case messageCompromised
+ case signatureVerificationFailed
+}
+
+extension AuthError: Reason {
+
+ init?(code: Int) {
+ switch code {
+ case Self.userRejeted.code:
+ self = .userRejeted
+ case Self.malformedResponseParams.code:
+ self = .malformedResponseParams
+ case Self.malformedRequestParams.code:
+ self = .malformedRequestParams
+ case Self.messageCompromised.code:
+ self = .messageCompromised
+ case Self.signatureVerificationFailed.code:
+ self = .signatureVerificationFailed
+ default:
+ return nil
+ }
+ }
+
+ public var code: Int {
+ switch self {
+ case .userRejeted:
+ return 14001
+ case .malformedResponseParams:
+ return 12001
+ case .malformedRequestParams:
+ return 12002
+ case .messageCompromised:
+ return 12003
+ case .signatureVerificationFailed:
+ return 12004
+ }
+ }
+
+ public var message: String {
+ switch self {
+ case .userRejeted:
+ return "Auth request rejected by user"
+ case .malformedResponseParams:
+ return "Response params malformed"
+ case .malformedRequestParams:
+ return "Request params malformed"
+ case .messageCompromised:
+ return "Original message compromised"
+ case .signatureVerificationFailed:
+ return "Message verification failed"
+ }
+ }
+}
diff --git a/Sources/Auth/Types/Errors/Reason.swift b/Sources/Auth/Types/Errors/Reason.swift
new file mode 100644
index 000000000..bbf491fdd
--- /dev/null
+++ b/Sources/Auth/Types/Errors/Reason.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+protocol Reason {
+ var code: Int { get }
+ var message: String { get }
+}
+
diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift
index c39ba3489..46feee75f 100644
--- a/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift
+++ b/Sources/Auth/Types/ProtocolRPCParams/AuthRequestParams.swift
@@ -1,6 +1,7 @@
import Foundation
import WalletConnectUtils
+/// wc_authRequest RPC method request param
struct AuthRequestParams: Codable, Equatable {
let requester: Requester
let payloadParams: AuthPayload
diff --git a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift
index a0d64b152..2dab1477b 100644
--- a/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift
+++ b/Sources/Auth/Types/ProtocolRPCParams/AuthResponseParams.swift
@@ -1,6 +1,7 @@
import Foundation
import WalletConnectUtils
+/// wc_authRequest RPC method respond param
struct AuthResponseParams: Codable, Equatable {
let header: CacaoHeader
let payload: CacaoPayload
diff --git a/Sources/Auth/Types/Public/AuthRequest.swift b/Sources/Auth/Types/Public/AuthRequest.swift
new file mode 100644
index 000000000..f431ccb4d
--- /dev/null
+++ b/Sources/Auth/Types/Public/AuthRequest.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+public struct AuthRequest: Equatable, Codable {
+ public let id: RPCID
+ /// EIP-4361: Sign-In with Ethereum message
+ public let message: String
+}
diff --git a/Sources/Auth/Types/RequestParams.swift b/Sources/Auth/Types/RequestParams.swift
index 24e7afab9..a3f456add 100644
--- a/Sources/Auth/Types/RequestParams.swift
+++ b/Sources/Auth/Types/RequestParams.swift
@@ -1,6 +1,6 @@
import Foundation
-struct RequestParams {
+public struct RequestParams {
let domain: String
let chainId: String
let nonce: String
diff --git a/Sources/Auth/Types/RespondParams.swift b/Sources/Auth/Types/RespondParams.swift
index fcd9b3293..f7c4a4af8 100644
--- a/Sources/Auth/Types/RespondParams.swift
+++ b/Sources/Auth/Types/RespondParams.swift
@@ -1,7 +1,11 @@
import Foundation
-struct RespondParams {
- let id: Int64
- let topic: String
+public struct RespondParams: Equatable {
+ let id: RPCID
let signature: CacaoSignature
+
+ public init(id: RPCID, signature: CacaoSignature) {
+ self.id = id
+ self.signature = signature
+ }
}
diff --git a/Sources/Chat/NetworkingInteractor.swift b/Sources/Chat/NetworkingInteractor.swift
index 946a79dbd..c8bf6176c 100644
--- a/Sources/Chat/NetworkingInteractor.swift
+++ b/Sources/Chat/NetworkingInteractor.swift
@@ -39,6 +39,8 @@ class NetworkingInteractor: NetworkInteracting {
private let responsePublisherSubject = PassthroughSubject()
var socketConnectionStatusPublisher: AnyPublisher
+ private var publishers = Set()
+
init(relayClient: RelayClient,
serializer: Serializing,
logger: ConsoleLogging,
@@ -49,9 +51,10 @@ class NetworkingInteractor: NetworkInteracting {
self.jsonRpcHistory = jsonRpcHistory
self.logger = logger
self.socketConnectionStatusPublisher = relayClient.socketConnectionStatusPublisher
- relayClient.onMessage = { [unowned self] topic, message in
+
+ relayClient.messagePublisher.sink { [unowned self] (topic, message) in
manageSubscription(topic, message)
- }
+ }.store(in: &publishers)
}
func request(_ request: JSONRPCRequest, topic: String, envelopeType: Envelope.EnvelopeType) async throws {
diff --git a/Sources/JSONRPC/RPCResponse.swift b/Sources/JSONRPC/RPCResponse.swift
index e0e60f06f..ad6ba9ca6 100644
--- a/Sources/JSONRPC/RPCResponse.swift
+++ b/Sources/JSONRPC/RPCResponse.swift
@@ -47,6 +47,10 @@ public struct RPCResponse: Equatable {
self.init(id: id, outcome: .success(AnyCodable(result)))
}
+ public init(id: RPCID?, error: JSONRPCError) {
+ self.init(id: id, outcome: .failure(error))
+ }
+
public init(id: Int64, error: JSONRPCError) {
self.init(id: RPCID(id), outcome: .failure(error))
}
diff --git a/Sources/WalletConnectRelay/Relay.swift b/Sources/WalletConnectRelay/Relay.swift
new file mode 100644
index 000000000..f6f823cf5
--- /dev/null
+++ b/Sources/WalletConnectRelay/Relay.swift
@@ -0,0 +1,34 @@
+import Foundation
+
+public class Relay {
+
+ public static var instance: RelayClient = {
+ guard let config = Relay.config else {
+ fatalError("Error - you must call Relay.configure(_:) before accessing the shared instance.")
+ }
+ return RelayClient(
+ relayHost: config.relayHost,
+ projectId: config.projectId,
+ socketFactory: config.socketFactory,
+ socketConnectionType: config.socketConnectionType
+ )
+ }()
+
+ private static var config: Config?
+
+ private init() { }
+
+ static public func configure(
+ relayHost: String = "relay.walletconnect.com",
+ projectId: String,
+ socketFactory: WebSocketFactory,
+ socketConnectionType: SocketConnectionType = .automatic
+ ) {
+ Relay.config = Relay.Config(
+ relayHost: relayHost,
+ projectId: projectId,
+ socketFactory: socketFactory,
+ socketConnectionType: socketConnectionType
+ )
+ }
+}
diff --git a/Sources/WalletConnectRelay/RelayClient.swift b/Sources/WalletConnectRelay/RelayClient.swift
index 6be813de9..0c18c01d7 100644
--- a/Sources/WalletConnectRelay/RelayClient.swift
+++ b/Sources/WalletConnectRelay/RelayClient.swift
@@ -17,14 +17,18 @@ public final class RelayClient {
static let historyIdentifier = "com.walletconnect.sdk.relayer_client.subscription_json_rpc_record"
- public var onMessage: ((String, String) -> Void)?
-
let defaultTtl = 6*Time.hour
var subscriptions: [String: String] = [:]
+ public var messagePublisher: AnyPublisher<(topic: String, message: String), Never> {
+ messagePublisherSubject.eraseToAnyPublisher()
+ }
+
public var socketConnectionStatusPublisher: AnyPublisher {
socketConnectionStatusPublisherSubject.eraseToAnyPublisher()
}
+
+ private let messagePublisherSubject = PassthroughSubject<(topic: String, message: String), Never>()
private let socketConnectionStatusPublisherSubject = PassthroughSubject()
private let subscriptionResponsePublisherSubject = PassthroughSubject<(RPCID?, String), Never>()
@@ -42,6 +46,8 @@ public final class RelayClient {
private let concurrentQueue = DispatchQueue(label: "com.walletconnect.sdk.relay_client", attributes: .concurrent)
+ // MARK: - Initialization
+
init(
dispatcher: Dispatching,
logger: ConsoleLogging,
@@ -227,7 +233,7 @@ public final class RelayClient {
do {
try rpcHistory.set(request, forTopic: params.data.topic, emmitedBy: .remote)
try acknowledgeRequest(request)
- onMessage?(params.data.topic, params.data.message)
+ messagePublisherSubject.send((params.data.topic, params.data.message))
} catch {
logger.error("[RelayClient] RPC History 'set()' error: \(error)")
}
diff --git a/Sources/WalletConnectRelay/RelayConfig.swift b/Sources/WalletConnectRelay/RelayConfig.swift
new file mode 100644
index 000000000..b716ed9ca
--- /dev/null
+++ b/Sources/WalletConnectRelay/RelayConfig.swift
@@ -0,0 +1,10 @@
+import Foundation
+
+extension Relay {
+ struct Config {
+ let relayHost: String
+ let projectId: String
+ let socketFactory: WebSocketFactory
+ let socketConnectionType: SocketConnectionType
+ }
+}
diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift
index 20eab6208..5a5cec5f6 100644
--- a/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift
+++ b/Sources/WalletConnectRelay/SocketConnectionHandler/AutomaticSocketConnectionHandler.swift
@@ -36,6 +36,7 @@ class AutomaticSocketConnectionHandler: SocketConnectionHandler {
}
appStateObserver.onWillEnterForeground = { [unowned self] in
+ backgroundTaskRegistrar.invalidate()
socket.connect()
}
}
diff --git a/Sources/WalletConnectRelay/SocketConnectionHandler/BackgroundTaskRegistering.swift b/Sources/WalletConnectRelay/SocketConnectionHandler/BackgroundTaskRegistering.swift
index 836283339..7b08c94a6 100644
--- a/Sources/WalletConnectRelay/SocketConnectionHandler/BackgroundTaskRegistering.swift
+++ b/Sources/WalletConnectRelay/SocketConnectionHandler/BackgroundTaskRegistering.swift
@@ -5,6 +5,7 @@ import UIKit
protocol BackgroundTaskRegistering {
func register(name: String, completion: @escaping () -> Void)
+ func invalidate()
}
class BackgroundTaskRegistrar: BackgroundTaskRegistering {
@@ -20,6 +21,15 @@ class BackgroundTaskRegistrar: BackgroundTaskRegistering {
backgroundTaskID = .invalid
completion()
}
+#endif
+ }
+
+ func invalidate() {
+#if os(iOS)
+ if backgroundTaskID != .invalid {
+ UIApplication.shared.endBackgroundTask(backgroundTaskID)
+ backgroundTaskID = .invalid
+ }
#endif
}
}
diff --git a/Sources/WalletConnectRouter/Router.m b/Sources/WalletConnectRouter/Router.m
new file mode 100644
index 000000000..9010aa7e6
--- /dev/null
+++ b/Sources/WalletConnectRouter/Router.m
@@ -0,0 +1,26 @@
+#import
+#import "Router.h"
+
+@import UIKit;
+@import ObjectiveC.runtime;
+
+@interface UISystemNavigationAction : NSObject
+@property(nonatomic, readonly, nonnull) NSArray* destinations;
+-(BOOL)sendResponseForDestination:(NSUInteger)destination;
+@end
+
+@implementation Router
+
++ (void)goBack {
+ Ivar sysNavIvar = class_getInstanceVariable(UIApplication.class, "_systemNavigationAction");
+ UIApplication* app = UIApplication.sharedApplication;
+ UISystemNavigationAction* action = object_getIvar(app, sysNavIvar);
+ if (!action) {
+ return;
+ }
+ NSUInteger destination = action.destinations.firstObject.unsignedIntegerValue;
+ [action sendResponseForDestination:destination];
+}
+
+@end
+
diff --git a/Sources/WalletConnectRouter/include/Router.h b/Sources/WalletConnectRouter/include/Router.h
new file mode 100644
index 000000000..222fbe4cc
--- /dev/null
+++ b/Sources/WalletConnectRouter/include/Router.h
@@ -0,0 +1,5 @@
+#import
+
+@interface Router: NSObject
++ (void)goBack;
+@end
diff --git a/Sources/WalletConnectRouter/include/module.modulemap b/Sources/WalletConnectRouter/include/module.modulemap
new file mode 100644
index 000000000..036065d03
--- /dev/null
+++ b/Sources/WalletConnectRouter/include/module.modulemap
@@ -0,0 +1,3 @@
+module WalletConnectRouter {
+ header "Router.h"
+}
diff --git a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
index 66c08959d..229e2e698 100644
--- a/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
+++ b/Sources/WalletConnectSign/Engine/Common/ApproveEngine.swift
@@ -280,7 +280,7 @@ private extension ApproveEngine {
func handleSessionProposeRequest(payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) throws {
logger.debug("Received Session Proposal")
- do { try Namespace.validate(proposal.requiredNamespaces) } catch { throw Errors.respondError(payload: payload, reason: .invalidUpdateNamespaceRequest) }
+ do { try Namespace.validate(proposal.requiredNamespaces) } catch { throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest) }
proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey)
onSessionProposal?(proposal.publicRepresentation())
}
@@ -290,7 +290,7 @@ private extension ApproveEngine {
logger.debug("Did receive session settle request")
guard let proposedNamespaces = settlingProposal?.requiredNamespaces
- else { throw Errors.respondError(payload: payload, reason: .invalidUpdateNamespaceRequest) }
+ else { throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest) }
settlingProposal = nil
diff --git a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift
index 5b619f8f6..c535d7ce3 100644
--- a/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift
+++ b/Sources/WalletConnectSign/Engine/Common/SessionEngine.swift
@@ -153,7 +153,7 @@ private extension SessionEngine {
func onSessionDelete(_ payload: WCRequestSubscriptionPayload, deleteParams: SessionType.DeleteParams) throws {
let topic = payload.topic
guard sessionStore.hasSession(forTopic: topic) else {
- throw Errors.respondError(payload: payload, reason: .noContextWithTopic(context: .session, topic: topic))
+ throw Errors.respondError(payload: payload, reason: .noSessionForTopic)
}
sessionStore.delete(topic: topic)
networkingInteractor.unsubscribe(topic: topic)
@@ -172,11 +172,11 @@ private extension SessionEngine {
chainId: payloadParams.chainId)
guard let session = sessionStore.getSession(forTopic: topic) else {
- throw Errors.respondError(payload: payload, reason: .noContextWithTopic(context: .session, topic: topic))
+ throw Errors.respondError(payload: payload, reason: .noSessionForTopic)
}
let chain = request.chainId
guard session.hasNamespace(for: chain) else {
- throw Errors.respondError(payload: payload, reason: .unauthorizedTargetChain(chain.absoluteString))
+ throw Errors.respondError(payload: payload, reason: .unauthorizedChain)
}
guard session.hasPermission(forMethod: request.method, onChain: chain) else {
throw Errors.respondError(payload: payload, reason: .unauthorizedMethod(request.method))
@@ -192,7 +192,7 @@ private extension SessionEngine {
let event = eventParams.event
let topic = payload.topic
guard let session = sessionStore.getSession(forTopic: topic) else {
- throw Errors.respondError(payload: payload, reason: .noContextWithTopic(context: .session, topic: payload.topic))
+ throw Errors.respondError(payload: payload, reason: .noSessionForTopic)
}
guard
session.peerIsController,
diff --git a/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift b/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift
index 363eb5d88..0e4e22f66 100644
--- a/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift
+++ b/Sources/WalletConnectSign/Engine/NonController/NonControllerSessionStateMachine.swift
@@ -62,18 +62,18 @@ final class NonControllerSessionStateMachine {
do {
try Namespace.validate(updateParams.namespaces)
} catch {
- throw Errors.respondError(payload: payload, reason: .invalidUpdateNamespaceRequest)
+ throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest)
}
guard var session = sessionStore.getSession(forTopic: payload.topic) else {
- throw Errors.respondError(payload: payload, reason: .noContextWithTopic(context: .session, topic: payload.topic))
+ throw Errors.respondError(payload: payload, reason: .noSessionForTopic)
}
guard session.peerIsController else {
- throw Errors.respondError(payload: payload, reason: .unauthorizedUpdateNamespacesRequest)
+ throw Errors.respondError(payload: payload, reason: .unauthorizedUpdateRequest)
}
do {
try session.updateNamespaces(updateParams.namespaces, timestamp: payload.timestamp)
} catch {
- throw Errors.respondError(payload: payload, reason: .invalidUpdateNamespaceRequest)
+ throw Errors.respondError(payload: payload, reason: .invalidUpdateRequest)
}
sessionStore.setSession(session)
networkingInteractor.respondSuccess(for: payload)
@@ -83,15 +83,15 @@ final class NonControllerSessionStateMachine {
private func onSessionUpdateExpiry(_ payload: WCRequestSubscriptionPayload, updateExpiryParams: SessionType.UpdateExpiryParams) throws {
let topic = payload.topic
guard var session = sessionStore.getSession(forTopic: topic) else {
- throw Errors.respondError(payload: payload, reason: .noContextWithTopic(context: .session, topic: topic))
+ throw Errors.respondError(payload: payload, reason: .noSessionForTopic)
}
guard session.peerIsController else {
- throw Errors.respondError(payload: payload, reason: .unauthorizedUpdateExpiryRequest)
+ throw Errors.respondError(payload: payload, reason: .unauthorizedExtendRequest)
}
do {
try session.updateExpiry(to: updateExpiryParams.expiry)
} catch {
- throw Errors.respondError(payload: payload, reason: .invalidUpdateExpiryRequest)
+ throw Errors.respondError(payload: payload, reason: .invalidExtendRequest)
}
sessionStore.setSession(session)
networkingInteractor.respondSuccess(for: payload)
diff --git a/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift b/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift
index 05d7e8ad9..3389771b3 100644
--- a/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift
+++ b/Sources/WalletConnectSign/NetworkInteractor/NetworkInteractor.swift
@@ -29,7 +29,7 @@ extension NetworkInteracting {
class NetworkInteractor: NetworkInteracting {
- private var publishers = [AnyCancellable]()
+ private var publishers = Set()
private var relayClient: NetworkRelaying
private let serializer: Serializing
@@ -183,9 +183,10 @@ class NetworkInteractor: NetworkInteracting {
}
}.store(in: &publishers)
- relayClient.onMessage = { [unowned self] topic, message in
- manageSubscription(topic, message)
+ relayClient.messagePublisher.sink { [weak self] (topic, message) in
+ self?.manageSubscription(topic, message)
}
+ .store(in: &publishers)
}
private func manageSubscription(_ topic: String, _ encodedEnvelope: String) {
diff --git a/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift b/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift
index 2ea62420d..5574cf826 100644
--- a/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift
+++ b/Sources/WalletConnectSign/NetworkInteractor/NetworkRelaying.swift
@@ -5,7 +5,7 @@ import Combine
extension RelayClient: NetworkRelaying {}
protocol NetworkRelaying {
- var onMessage: ((_ topic: String, _ message: String) -> Void)? {get set}
+ var messagePublisher: AnyPublisher<(topic: String, message: String), Never> { get }
var socketConnectionStatusPublisher: AnyPublisher { get }
func connect() throws
func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws
diff --git a/Sources/WalletConnectSign/RejectionReason.swift b/Sources/WalletConnectSign/RejectionReason.swift
index cee66c716..338cffe4a 100644
--- a/Sources/WalletConnectSign/RejectionReason.swift
+++ b/Sources/WalletConnectSign/RejectionReason.swift
@@ -2,20 +2,23 @@ import Foundation
/// https://github.com/ChainAgnostic/CAIPs/blob/master/CAIPs/caip-25.md
public enum RejectionReason {
- case disapprovedChains
- case disapprovedMethods
- case disapprovedEventTypes
+ case userRejected
+ case userRejectedChains
+ case userRejectedMethods
+ case userRejectedEvents
}
internal extension RejectionReason {
func internalRepresentation() -> ReasonCode {
switch self {
- case .disapprovedChains:
- return ReasonCode.disapprovedChains
- case .disapprovedMethods:
- return ReasonCode.disapprovedMethods
- case .disapprovedEventTypes:
- return ReasonCode.disapprovedEventTypes
+ case .userRejected:
+ return ReasonCode.userRejected
+ case .userRejectedChains:
+ return ReasonCode.userRejectedChains
+ case .userRejectedMethods:
+ return ReasonCode.userRejectedMethods
+ case .userRejectedEvents:
+ return ReasonCode.userRejectedEvents
}
}
}
diff --git a/Sources/WalletConnectSign/Sign/Sign.swift b/Sources/WalletConnectSign/Sign/Sign.swift
index f00ae09be..75a384af7 100644
--- a/Sources/WalletConnectSign/Sign/Sign.swift
+++ b/Sources/WalletConnectSign/Sign/Sign.swift
@@ -7,272 +7,22 @@ public typealias Account = WalletConnectUtils.Account
public typealias Blockchain = WalletConnectUtils.Blockchain
public class Sign {
- public static let instance = Sign()
- private static var config: Config?
- private let client: SignClient
- private let relayClient: RelayClient
-
- private init() {
- guard let config = Sign.config else {
- fatalError("Error - you must call configure(_:) before accessing the shared instance.")
+ public static var instance: SignClient = {
+ guard let metadata = Sign.metadata else {
+ fatalError("Error - you must call Sign.configure(_:) before accessing the shared instance.")
}
- relayClient = RelayClient(
- relayHost: "relay.walletconnect.com",
- projectId: config.projectId,
- socketFactory: config.socketFactory,
- socketConnectionType: config.socketConnectionType
- )
- client = SignClientFactory.create(
- metadata: config.metadata,
- relayClient: relayClient
- )
- client.delegate = self
- }
-
- static public func configure(
- metadata: AppMetadata,
- projectId: String,
- socketFactory: WebSocketFactory,
- socketConnectionType: SocketConnectionType = .automatic
- ) {
- Sign.config = Sign.Config(
+ return SignClientFactory.create(
metadata: metadata,
- projectId: projectId,
- socketFactory: socketFactory,
- socketConnectionType: socketConnectionType
+ relayClient: Relay.instance
)
- }
-
- var sessionProposalPublisherSubject = PassthroughSubject()
- public var sessionProposalPublisher: AnyPublisher {
- sessionProposalPublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionRequestPublisherSubject = PassthroughSubject()
- public var sessionRequestPublisher: AnyPublisher {
- sessionRequestPublisherSubject.eraseToAnyPublisher()
- }
-
- var socketConnectionStatusPublisherSubject = PassthroughSubject()
- public var socketConnectionStatusPublisher: AnyPublisher {
- socketConnectionStatusPublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionSettlePublisherSubject = PassthroughSubject()
- public var sessionSettlePublisher: AnyPublisher {
- sessionSettlePublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionDeletePublisherSubject = PassthroughSubject<(String, Reason), Never>()
- public var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> {
- sessionDeletePublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionResponsePublisherSubject = PassthroughSubject()
- public var sessionResponsePublisher: AnyPublisher {
- sessionResponsePublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionRejectionPublisherSubject = PassthroughSubject<(Session.Proposal, Reason), Never>()
- public var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> {
- sessionRejectionPublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionUpdatePublisherSubject = PassthroughSubject<(sessionTopic: String, namespaces: [String: SessionNamespace]), Never>()
- public var sessionUpdatePublisher: AnyPublisher<(sessionTopic: String, namespaces: [String: SessionNamespace]), Never> {
- sessionUpdatePublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionEventPublisherSubject = PassthroughSubject<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never>()
- public var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> {
- sessionEventPublisherSubject.eraseToAnyPublisher()
- }
-
- var sessionExtendPublisherSubject = PassthroughSubject<(sessionTopic: String, date: Date), Never>()
- public var sessionExtendPublisher: AnyPublisher<(sessionTopic: String, date: Date), Never> {
- sessionExtendPublisherSubject.eraseToAnyPublisher()
- }
-}
-
-extension Sign: SignClientDelegate {
-
- public func didReceive(sessionProposal: Session.Proposal) {
- sessionProposalPublisherSubject.send(sessionProposal)
- }
-
- public func didReceive(sessionRequest: Request) {
- sessionRequestPublisherSubject.send(sessionRequest)
- }
-
- public func didReceive(sessionResponse: Response) {
- sessionResponsePublisherSubject.send(sessionResponse)
- }
-
- public func didDelete(sessionTopic: String, reason: Reason) {
- sessionDeletePublisherSubject.send((sessionTopic, reason))
- }
-
- public func didUpdate(sessionTopic: String, namespaces: [String: SessionNamespace]) {
- sessionUpdatePublisherSubject.send((sessionTopic, namespaces))
- }
-
- public func didExtend(sessionTopic: String, to date: Date) {
- sessionExtendPublisherSubject.send((sessionTopic, date))
- }
-
- public func didSettle(session: Session) {
- sessionSettlePublisherSubject.send(session)
- }
-
- public func didReceive(event: Session.Event, sessionTopic: String, chainId: Blockchain?) {
- sessionEventPublisherSubject.send((event, sessionTopic, chainId))
- }
+ }()
- public func didReject(proposal: Session.Proposal, reason: Reason) {
- sessionRejectionPublisherSubject.send((proposal, reason))
- }
+ private static var metadata: AppMetadata?
- public func didChangeSocketConnectionStatus(_ status: SocketConnectionStatus) {
- socketConnectionStatusPublisherSubject.send(status)
- }
-}
-
-extension Sign {
-
- /// For the Proposer to propose a session to a responder.
- /// Function will create pending pairing sequence or propose a session on existing pairing. When responder client approves pairing, session is be proposed automatically by your client.
- /// - Parameter sessionPermissions: The session permissions the responder will be requested for.
- /// - Parameter topic: Optional parameter - use it if you already have an established pairing with peer client.
- /// - Returns: Pairing URI that should be shared with responder out of bound. Common way is to present it as a QR code. Pairing URI will be nil if you are going to establish a session on existing Pairing and `topic` function parameter was provided.
- public func connect(requiredNamespaces: [String: ProposalNamespace], topic: String? = nil) async throws -> String? {
- try await client.connect(requiredNamespaces: requiredNamespaces, topic: topic)
- }
-
- /// For responder to receive a session proposal from a proposer
- /// Responder should call this function in order to accept peer's pairing proposal and be able to subscribe for future session proposals.
- /// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp.
- ///
- /// Should Error:
- /// - When URI is invalid format or missing params
- /// - When topic is already in use
- public func pair(uri: String) async throws {
- try await client.pair(uri: uri)
- }
-
- /// For the responder to approve a session proposal.
- /// - Parameters:
- /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate function: `didReceive(sessionProposal: Session.Proposal)`
- /// - accounts: A Set of accounts that the dapp will be allowed to request methods executions on.
- /// - methods: A Set of methods that the dapp will be allowed to request.
- /// - events: A Set of events
- public func approve(proposalId: String, namespaces: [String: SessionNamespace]) async throws {
- try await client.approve(proposalId: proposalId, namespaces: namespaces)
- }
-
- /// For the responder to reject a session proposal.
- /// - Parameters:
- /// - proposalId: Session Proposal Public key received from peer client in a WalletConnect delegate.
- /// - reason: Reason why the session proposal was rejected. Conforms to CAIP25.
- public func reject(proposalId: String, reason: RejectionReason) async throws {
- try await client.reject(proposalId: proposalId, reason: reason)
- }
-
- /// For the responder to update session methods
- /// - Parameters:
- /// - topic: Topic of the session that is intended to be updated.
- /// - methods: Sets of methods that will replace existing ones.
- public func update(topic: String, namespaces: [String: SessionNamespace]) async throws {
- try await client.update(topic: topic, namespaces: namespaces)
- }
-
- /// For controller to update expiry of a session
- /// - Parameters:
- /// - topic: Topic of the Session, it can be a pairing or a session topic.
- /// - ttl: Time in seconds that a target session is expected to be extended for. Must be greater than current time to expire and than 7 days
- public func extend(topic: String) async throws {
- try await client.extend(topic: topic)
- }
-
- /// For the proposer to send JSON-RPC requests to responding peer.
- /// - Parameters:
- /// - params: Parameters defining request and related session
- public func request(params: Request) async throws {
- try await client.request(params: params)
- }
-
- /// For the responder to respond on pending peer's session JSON-RPC Request
- /// - Parameters:
- /// - topic: Topic of the session for which the request was received.
- /// - response: Your JSON RPC response or an error.
- public func respond(topic: String, response: JsonRpcResult) async throws {
- try await client.respond(topic: topic, response: response)
- }
-
- /// Ping method allows to check if client's peer is online and is subscribing for your sequence topic
- ///
- /// Should Error:
- /// - When the session topic is not found
- /// - When the response is neither result or error
- /// - When the peer fails to respond within timeout
- ///
- /// - Parameters:
- /// - topic: Topic of the sequence, it can be a pairing or a session topic.
- /// - completion: Result will be success on response or error on timeout. -- TODO: timeout
- public func ping(topic: String, completion: @escaping ((Result) -> Void)) {
- client.ping(topic: topic, completion: completion)
- }
-
- /// - Parameters:
- /// - topic: Session topic
- /// - params: Event Parameters
- /// - completion: calls a handler upon completion
- public func emit(topic: String, event: Session.Event, chainId: Blockchain) async throws {
- try await client.emit(topic: topic, event: event, chainId: chainId)
- }
-
- /// - Parameters:
- /// - topic: Session topic that you want to delete
- public func disconnect(topic: String) async throws {
- try await client.disconnect(topic: topic)
- }
-
- /// - Returns: All sessions
- public func getSessions() -> [Session] {
- client.getSessions()
- }
-
- /// - Returns: All settled pairings that are active
- public func getSettledPairings() -> [Pairing] {
- client.getSettledPairings()
- }
-
- /// - Returns: Pending requests received with wc_sessionRequest
- /// - Parameter topic: topic representing session for which you want to get pending requests. If nil, you will receive pending requests for all active sessions.
- public func getPendingRequests(topic: String? = nil) -> [Request] {
- client.getPendingRequests(topic: topic)
- }
-
- /// - Parameter id: id of a wc_sessionRequest jsonrpc request
- /// - Returns: json rpc record object for given id or nil if record for give id does not exits
- public func getSessionRequestRecord(id: Int64) -> WalletConnectUtils.JsonRpcRecord? {
- client.getSessionRequestRecord(id: id)
- }
-
- public func connect() throws {
- try relayClient.connect()
- }
-
- public func disconnect(closeCode: URLSessionWebSocketTask.CloseCode) throws {
- try relayClient.disconnect(closeCode: closeCode)
- }
+ private init() { }
-#if DEBUG
- /// Delete all stored data sach as: pairings, sessions, keys
- ///
- /// - Note: Doesn't unsubscribe from topics
- public func cleanup() throws {
- try client.cleanup()
+ static public func configure(metadata: AppMetadata) {
+ Sign.metadata = metadata
}
-#endif
}
diff --git a/Sources/WalletConnectSign/Sign/SignClient.swift b/Sources/WalletConnectSign/Sign/SignClient.swift
index 1dc88cae2..d8fc4d288 100644
--- a/Sources/WalletConnectSign/Sign/SignClient.swift
+++ b/Sources/WalletConnectSign/Sign/SignClient.swift
@@ -3,9 +3,6 @@ import WalletConnectRelay
import WalletConnectUtils
import WalletConnectKMS
import Combine
-#if os(iOS)
-import UIKit
-#endif
/// An Object that expose public API to provide interactions with WalletConnect SDK
///
@@ -15,14 +12,83 @@ import UIKit
/// let metadata = AppMetadata(name: String?, description: String?, url: String?, icons: [String]?)
/// let client = SignClient(metadata: AppMetadata, projectId: String, relayHost: String)
/// ```
-///
-/// - Parameters:
-/// - 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?
+ /// Tells the delegate that session proposal has been received.
+ ///
+ /// Function is executed on responder client only
+ public var sessionProposalPublisher: AnyPublisher {
+ sessionProposalPublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that session payload request has been received
+ ///
+ /// In most cases that function is supposed to be called on wallet client.
+ /// - Parameters:
+ /// - sessionRequest: Object containing request received from peer client.
+ public var sessionRequestPublisher: AnyPublisher {
+ sessionRequestPublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that client has connected WebSocket
+ public var socketConnectionStatusPublisher: AnyPublisher {
+ socketConnectionStatusPublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that the client has settled a session.
+ ///
+ /// Function is executed on proposer and responder client when both communicating peers have successfully established a session.
+ public var sessionSettlePublisher: AnyPublisher {
+ sessionSettlePublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that the peer client has terminated the session.
+ ///
+ /// Function can be executed on any type of the client.
+ public var sessionDeletePublisher: AnyPublisher<(String, Reason), Never> {
+ sessionDeletePublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that session payload response has been received
+ ///
+ /// In most cases that function is supposed to be called on dApp client.
+ /// - Parameters:
+ /// - sessionResponse: Object containing response received from peer client.
+ public var sessionResponsePublisher: AnyPublisher {
+ sessionResponsePublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that peer client has rejected a session proposal.
+ ///
+ /// Function will be executed on proposer client only.
+ public var sessionRejectionPublisher: AnyPublisher<(Session.Proposal, Reason), Never> {
+ sessionRejectionPublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that methods has been updated in session
+ ///
+ /// Function is executed on controller and non-controller client when both communicating peers have successfully updated methods requested by the controller client.
+ public var sessionUpdatePublisher: AnyPublisher<(sessionTopic: String, namespaces: [String: SessionNamespace]), Never> {
+ sessionUpdatePublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that event has been received.
+ public var sessionEventPublisher: AnyPublisher<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never> {
+ sessionEventPublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// Tells the delegate that session expiry has been updated
+ ///
+ /// Function will be executed on controller and non-controller clients.
+ public var sessionExtendPublisher: AnyPublisher<(sessionTopic: String, date: Date), Never> {
+ sessionExtendPublisherSubject.eraseToAnyPublisher()
+ }
+
+ /// An object for logging messages
public let logger: ConsoleLogging
+
+ // MARK: - Private properties
+
private let relayClient: RelayClient
private let pairingEngine: PairingEngine
private let pairEngine: PairEngine
@@ -32,7 +98,19 @@ public final class SignClient {
private let controllerSessionStateMachine: ControllerSessionStateMachine
private let history: JsonRpcHistory
private let cleanupService: CleanupService
- private var publishers = [AnyCancellable]()
+
+ private let sessionProposalPublisherSubject = PassthroughSubject()
+ private let sessionRequestPublisherSubject = PassthroughSubject()
+ private let socketConnectionStatusPublisherSubject = PassthroughSubject()
+ private let sessionSettlePublisherSubject = PassthroughSubject()
+ private let sessionDeletePublisherSubject = PassthroughSubject<(String, Reason), Never>()
+ private let sessionResponsePublisherSubject = PassthroughSubject()
+ private let sessionRejectionPublisherSubject = PassthroughSubject<(Session.Proposal, Reason), Never>()
+ private let sessionUpdatePublisherSubject = PassthroughSubject<(sessionTopic: String, namespaces: [String: SessionNamespace]), Never>()
+ private let sessionEventPublisherSubject = PassthroughSubject<(event: Session.Event, sessionTopic: String, chainId: Blockchain?), Never>()
+ private let sessionExtendPublisherSubject = PassthroughSubject<(sessionTopic: String, date: Date), Never>()
+
+ private var publishers = Set()
// MARK: - Initialization
@@ -241,48 +319,48 @@ public final class SignClient {
private func setUpEnginesCallbacks() {
approveEngine.onSessionProposal = { [unowned self] proposal in
- delegate?.didReceive(sessionProposal: proposal)
+ sessionProposalPublisherSubject.send(proposal)
}
approveEngine.onSessionRejected = { [unowned self] proposal, reason in
- delegate?.didReject(proposal: proposal, reason: reason.publicRepresentation())
+ sessionRejectionPublisherSubject.send((proposal, reason.publicRepresentation()))
}
approveEngine.onSessionSettle = { [unowned self] settledSession in
- delegate?.didSettle(session: settledSession)
+ sessionSettlePublisherSubject.send(settledSession)
}
sessionEngine.onSessionRequest = { [unowned self] sessionRequest in
- delegate?.didReceive(sessionRequest: sessionRequest)
+ sessionRequestPublisherSubject.send(sessionRequest)
}
sessionEngine.onSessionDelete = { [unowned self] topic, reason in
- delegate?.didDelete(sessionTopic: topic, reason: reason.publicRepresentation())
+ sessionDeletePublisherSubject.send((topic, reason.publicRepresentation()))
}
controllerSessionStateMachine.onNamespacesUpdate = { [unowned self] topic, namespaces in
- delegate?.didUpdate(sessionTopic: topic, namespaces: namespaces)
+ sessionUpdatePublisherSubject.send((topic, namespaces))
}
controllerSessionStateMachine.onExtend = { [unowned self] topic, date in
- delegate?.didExtend(sessionTopic: topic, to: date)
+ sessionExtendPublisherSubject.send((topic, date))
}
nonControllerSessionStateMachine.onNamespacesUpdate = { [unowned self] topic, namespaces in
- delegate?.didUpdate(sessionTopic: topic, namespaces: namespaces)
+ sessionUpdatePublisherSubject.send((topic, namespaces))
}
nonControllerSessionStateMachine.onExtend = { [unowned self] topic, date in
- delegate?.didExtend(sessionTopic: topic, to: date)
+ sessionExtendPublisherSubject.send((topic, date))
}
sessionEngine.onEventReceived = { [unowned self] topic, event, chainId in
- delegate?.didReceive(event: event, sessionTopic: topic, chainId: chainId)
+ sessionEventPublisherSubject.send((event, topic, chainId))
}
sessionEngine.onSessionResponse = { [unowned self] response in
- delegate?.didReceive(sessionResponse: response)
+ sessionResponsePublisherSubject.send(response)
}
}
private func setUpConnectionObserving() {
relayClient.socketConnectionStatusPublisher.sink { [weak self] status in
- self?.delegate?.didChangeSocketConnectionStatus(status)
+ self?.socketConnectionStatusPublisherSubject.send(status)
}.store(in: &publishers)
}
#if DEBUG
- /// Delete all stored data sach as: pairings, sessions, keys
+ /// Delete all stored data such as: pairings, sessions, keys
///
/// - Note: Doesn't unsubscribe from topics
public func cleanup() throws {
diff --git a/Sources/WalletConnectSign/Sign/SignClientDelegate.swift b/Sources/WalletConnectSign/Sign/SignClientDelegate.swift
deleted file mode 100644
index 2a3823552..000000000
--- a/Sources/WalletConnectSign/Sign/SignClientDelegate.swift
+++ /dev/null
@@ -1,67 +0,0 @@
-import Foundation
-import WalletConnectUtils
-import WalletConnectRelay
-
-/// A protocol that defines methods that SignClient instance call on it's delegate to handle sequences level events
-public protocol SignClientDelegate: AnyObject {
-
- /// Tells the delegate that session proposal has been received.
- ///
- /// Function is executed on responder client only
- func didReceive(sessionProposal: Session.Proposal)
-
- /// Tells the delegate that session payload request has been received
- ///
- /// In most cases that function is supposed to be called on wallet client.
- /// - Parameters:
- /// - sessionRequest: Object containing request received from peer client.
- func didReceive(sessionRequest: Request)
-
- /// Tells the delegate that session payload response has been received
- ///
- /// In most cases that function is supposed to be called on dApp client.
- /// - Parameters:
- /// - sessionResponse: Object containing response received from peer client.
- func didReceive(sessionResponse: Response)
-
- /// Tells the delegate that the peer client has terminated the session.
- ///
- /// Function can be executed on any type of the client.
- func didDelete(sessionTopic: String, reason: Reason)
-
- /// Tells the delegate that methods has been updated in session
- ///
- /// Function is executed on controller and non-controller client when both communicating peers have successfully updated methods requested by the controller client.
- func didUpdate(sessionTopic: String, namespaces: [String: SessionNamespace])
-
- /// Tells the delegate that session expiry has been updated
- ///
- /// Function will be executed on controller and non-controller clients.
- func didExtend(sessionTopic: String, to date: Date)
-
- /// Tells the delegate that the client has settled a session.
- ///
- /// Function is executed on proposer and responder client when both communicating peers have successfully established a session.
- func didSettle(session: Session)
-
- /// Tells the delegate that event has been received.
- func didReceive(event: Session.Event, sessionTopic: String, chainId: Blockchain?)
-
- /// Tells the delegate that peer client has rejected a session proposal.
- ///
- /// Function will be executed on proposer client only.
- func didReject(proposal: Session.Proposal, reason: Reason)
-
- /// Tells the delegate that client has connected WebSocket
- func didChangeSocketConnectionStatus(_ status: SocketConnectionStatus)
-}
-
-public extension SignClientDelegate {
- func didReceive(event: Session.Event, sessionTopic: String, chainId: Blockchain?) {}
- func didReject(proposal: Session.Proposal, reason: Reason) {}
- func didReceive(sessionRequest: Request) {}
- func didReceive(sessionProposal: Session.Proposal) {}
- func didReceive(sessionResponse: Response) {}
- func didExtend(sessionTopic: String, to date: Date) {}
- func didDisconnect() {}
-}
diff --git a/Sources/WalletConnectSign/Sign/SignConfig.swift b/Sources/WalletConnectSign/Sign/SignConfig.swift
deleted file mode 100644
index 4ba4edeaa..000000000
--- a/Sources/WalletConnectSign/Sign/SignConfig.swift
+++ /dev/null
@@ -1,23 +0,0 @@
-import Foundation
-import WalletConnectRelay
-
-public extension Sign {
- struct Config {
- let metadata: AppMetadata
- let projectId: String
- let socketFactory: WebSocketFactory
- let socketConnectionType: SocketConnectionType
-
- public init(
- metadata: AppMetadata,
- projectId: String,
- socketFactory: WebSocketFactory,
- socketConnectionType: SocketConnectionType = .automatic
- ) {
- self.metadata = metadata
- self.projectId = projectId
- self.socketFactory = socketFactory
- self.socketConnectionType = socketConnectionType
- }
- }
-}
diff --git a/Sources/WalletConnectSign/Types/ReasonCode.swift b/Sources/WalletConnectSign/Types/ReasonCode.swift
index 496818936..cdc4f8db5 100644
--- a/Sources/WalletConnectSign/Types/ReasonCode.swift
+++ b/Sources/WalletConnectSign/Types/ReasonCode.swift
@@ -1,33 +1,33 @@
-enum ReasonCode {
+enum ReasonCode: Codable, Equatable {
- enum Context: String {
+ enum Context: String, Codable {
case pairing = "pairing"
case session = "session"
}
- // 0 (Generic)
- case generic(message: String)
+ // 1000 - (Internal)
+ case invalidMethod
+ case invalidEvent
+ case invalidUpdateRequest
+ case invalidExtendRequest
+ case noSessionForTopic
- // 1000 (Internal)
- case missingOrInvalid(String)
- case invalidUpdateAccountsRequest
- case invalidUpdateNamespaceRequest
- case invalidUpdateExpiryRequest
- case noContextWithTopic(context: Context, topic: String)
-
- // 3000 (Unauthorized)
- case unauthorizedTargetChain(String)
+ // 3000 - (Unauthorized)
case unauthorizedMethod(String)
case unauthorizedEvent(String)
- case unauthorizedUpdateAccountRequest
- case unauthorizedUpdateNamespacesRequest
- case unauthorizedUpdateExpiryRequest
- case unauthorizedMatchingController(isController: Bool)
-
- // 5000
- case disapprovedChains
- case disapprovedMethods
- case disapprovedEventTypes
+ case unauthorizedUpdateRequest
+ case unauthorizedExtendRequest
+ case unauthorizedChain
+
+ // 4001 - (EIP-1193)
+ case userRejectedRequest
+
+ // 5000 - (REJECTED (CAIP-25))
+ case userRejected
+ case userRejectedChains
+ case userRejectedMethods
+ case userRejectedEvents
+
case unsupportedChains
case unsupportedMethods
case unsupportedEvents
@@ -39,25 +39,23 @@ enum ReasonCode {
var code: Int {
switch self {
- case .generic: return 0
- case .missingOrInvalid: return 1000
-
- case .invalidUpdateAccountsRequest: return 1003
- case .invalidUpdateNamespaceRequest: return 1004
- case .invalidUpdateExpiryRequest: return 1005
- case .noContextWithTopic: return 1301
+ case .invalidMethod: return 1001
+ case .invalidEvent: return 1002
+ case .invalidUpdateRequest: return 1003
+ case .invalidExtendRequest: return 1004
- case .unauthorizedTargetChain: return 3000
case .unauthorizedMethod: return 3001
case .unauthorizedEvent: return 3002
+ case .unauthorizedUpdateRequest: return 3003
+ case .unauthorizedExtendRequest: return 3004
+ case .unauthorizedChain: return 3005
+
+ case .userRejectedRequest: return 4001
- case .unauthorizedUpdateAccountRequest: return 3003
- case .unauthorizedUpdateNamespacesRequest: return 3004
- case .unauthorizedUpdateExpiryRequest: return 3005
- case .unauthorizedMatchingController: return 3100
- case .disapprovedChains: return 5000
- case .disapprovedMethods: return 5001
- case .disapprovedEventTypes: return 5002
+ case .userRejected: return 5000
+ case .userRejectedChains: return 5001
+ case .userRejectedMethods: return 5002
+ case .userRejectedEvents: return 5003
case .unsupportedChains: return 5100
case .unsupportedMethods: return 5101
@@ -66,43 +64,47 @@ enum ReasonCode {
case .unsupportedNamespaceKey: return 5104
case .userDisconnected: return 6000
+
+ case .noSessionForTopic: return 7001
}
}
var message: String {
switch self {
- case .generic(let message):
- return message
- case .missingOrInvalid(let name):
- return "Missing or invalid \(name)"
- case .invalidUpdateAccountsRequest:
- return "Invalid update accounts request"
- case .invalidUpdateNamespaceRequest:
+ case .invalidMethod:
+ return "Invalid Method"
+ case .invalidEvent:
+ return "Invalid Event"
+ case .invalidUpdateRequest:
return "Invalid update namespace request"
- case .invalidUpdateExpiryRequest:
+ case .invalidExtendRequest:
return "Invalid update expiry request"
- case .noContextWithTopic(let context, let topic):
- return "No matching \(context) with topic: \(topic)"
- case .unauthorizedTargetChain(let chainId):
- return "Unauthorized target chain id requested: \(chainId)"
+ case .noSessionForTopic:
+ return "No matching session matching topic"
+
case .unauthorizedMethod(let method):
return "Unauthorized JSON-RPC method requested: \(method)"
case .unauthorizedEvent(let type):
return "Unauthorized event type requested: \(type)"
- case .unauthorizedUpdateAccountRequest:
- return "Unauthorized update accounts request"
- case .unauthorizedUpdateNamespacesRequest:
- return "Unauthorized update namespaces request"
- case .unauthorizedUpdateExpiryRequest:
- return "Unauthorized update expiry request"
- case .unauthorizedMatchingController(let isController):
- return "Unauthorized: peer is also \(isController ? "" : "non-")controller"
- case .disapprovedChains:
+ case .unauthorizedUpdateRequest:
+ return "Unauthorized update request"
+ case .unauthorizedExtendRequest:
+ return "Unauthorized extend request"
+ case .unauthorizedChain:
+ return "Unauthorized target chain id requested"
+
+ case .userRejectedRequest:
+ return "User rejected request"
+
+ case .userRejected:
+ return "User rejected"
+ case .userRejectedChains:
return "User disapproved requested chains"
- case .disapprovedMethods:
+ case .userRejectedMethods:
return "User disapproved requested json-rpc methods"
- case .disapprovedEventTypes:
+ case .userRejectedEvents:
return "User disapproved requested event types"
+
case .unsupportedChains:
return "Unsupported or empty chains for namespace"
case .unsupportedMethods:
diff --git a/Sources/WalletConnectUtils/RPCHistory.swift b/Sources/WalletConnectUtils/RPCHistory.swift
index dafb70d36..ae0b0df71 100644
--- a/Sources/WalletConnectUtils/RPCHistory.swift
+++ b/Sources/WalletConnectUtils/RPCHistory.swift
@@ -64,4 +64,8 @@ public final class RPCHistory {
}
}
}
+
+ public func getPending() -> [Record] {
+ storage.getAll().filter {$0.response == nil}
+ }
}
diff --git a/Sources/WalletConnectUtils/WalletConnectURI.swift b/Sources/WalletConnectUtils/WalletConnectURI.swift
index d149f7f70..18a0a6a66 100644
--- a/Sources/WalletConnectUtils/WalletConnectURI.swift
+++ b/Sources/WalletConnectUtils/WalletConnectURI.swift
@@ -2,42 +2,60 @@ import Foundation
public struct WalletConnectURI: Equatable {
+ public enum TargetAPI: String, CaseIterable {
+ case sign
+ case chat
+ case auth
+ }
+
public let topic: String
public let version: String
public let symKey: String
public let relay: RelayProtocolOptions
- public init(topic: String, symKey: String, relay: RelayProtocolOptions) {
+ public var api: TargetAPI {
+ apiType ?? .sign
+ }
+
+ public var absoluteString: String {
+ if let api = apiType {
+ return "wc:\(api.rawValue)-\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)"
+ }
+ return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)"
+ }
+
+ private let apiType: TargetAPI?
+
+ public init(topic: String, symKey: String, relay: RelayProtocolOptions, api: TargetAPI? = nil) {
self.version = "2"
self.topic = topic
self.symKey = symKey
self.relay = relay
+ self.apiType = api
}
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 {
+ guard let components = Self.parseURIComponents(from: string) 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 }
+ guard
+ let userString = components.user,
+ let version = components.host,
+ let symKey = query?["symKey"],
+ let relayProtocol = query?["relay-protocol"]
+ else {
+ return nil
+ }
+ let uriUser = Self.parseURIUser(from: userString)
let relayData = query?["relay-data"]
+
self.version = version
- self.topic = topic
+ self.topic = uriUser.topic
self.symKey = symKey
self.relay = RelayProtocolOptions(protocol: relayProtocol, data: relayData)
- }
-
- public var absoluteString: String {
- return "wc:\(topic)@\(version)?symKey=\(symKey)&\(relayQuery)"
+ self.apiType = uriUser.api
}
private var relayQuery: String {
@@ -47,4 +65,21 @@ public struct WalletConnectURI: Equatable {
}
return query
}
+
+ private static func parseURIComponents(from string: String) -> URLComponents? {
+ guard string.hasPrefix("wc:") else {
+ return nil
+ }
+ let urlString = !string.hasPrefix("wc://") ? string.replacingOccurrences(of: "wc:", with: "wc://") : string
+ return URLComponents(string: urlString)
+ }
+
+ private static func parseURIUser(from string: String) -> (topic: String, api: TargetAPI?) {
+ let splits = string.split(separator: "-")
+ if splits.count == 2, let apiFromPrefix = TargetAPI(rawValue: String(splits[0])) {
+ return (String(splits[1]), apiFromPrefix)
+ } else {
+ return (string, nil)
+ }
+ }
}
diff --git a/Tests/AuthTests/AuthRequstSubscriberTests.swift b/Tests/AuthTests/AuthRequstSubscriberTests.swift
index 990a9cb4f..fc7c6ebbb 100644
--- a/Tests/AuthTests/AuthRequstSubscriberTests.swift
+++ b/Tests/AuthTests/AuthRequstSubscriberTests.swift
@@ -8,16 +8,17 @@ import JSONRPC
class AuthRequstSubscriberTests: XCTestCase {
var networkingInteractor: NetworkingInteractorMock!
- var sut: AuthRequestSubscriber!
+ var sut: WalletRequestSubscriber!
var messageFormatter: SIWEMessageFormatterMock!
let defaultTimeout: TimeInterval = 0.01
override func setUp() {
networkingInteractor = NetworkingInteractorMock()
messageFormatter = SIWEMessageFormatterMock()
- sut = AuthRequestSubscriber(networkingInteractor: networkingInteractor,
- logger: ConsoleLoggerMock(),
- messageFormatter: messageFormatter, address: "")
+ sut = WalletRequestSubscriber(networkingInteractor: networkingInteractor,
+ logger: ConsoleLoggerMock(),
+ kms: KeyManagementServiceMock(),
+ messageFormatter: messageFormatter, address: "")
}
func testSubscribeRequest() {
diff --git a/Tests/AuthTests/CacaoSignerTests.swift b/Tests/AuthTests/CacaoSignerTests.swift
new file mode 100644
index 000000000..a1615a361
--- /dev/null
+++ b/Tests/AuthTests/CacaoSignerTests.swift
@@ -0,0 +1,40 @@
+import Foundation
+import XCTest
+@testable import Auth
+import TestingUtils
+
+class CacaoSignerTest: XCTestCase {
+
+ let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a")
+
+ let message: String =
+ """
+ service.invalid wants you to sign in with your Ethereum account:
+ 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
+
+ I accept the ServiceOrg Terms of Service: https://service.invalid/tos
+
+ URI: https://service.invalid/login
+ Version: 1
+ Chain ID: 1
+ Nonce: 32891756
+ Issued At: 2021-09-30T16:25:24Z
+ Resources:
+ - ipfs://bafybeiemxf5abjwjbikoz4mc3a3dla6ual3jsgpdr4cjr3oz3evfyavhwq/
+ - https://example.com/my-web2-claim.json
+ """
+
+ let signature = "438effc459956b57fcd9f3dac6c675f9cee88abf21acab7305e8e32aa0303a883b06dcbd956279a7a2ca21ffa882ff55cc22e8ab8ec0f3fe90ab45f306938cfa1b"
+
+ func testCacaoSign() throws {
+ let signer = MessageSigner(signer: Signer())
+
+ XCTAssertEqual(try signer.sign(message: message, privateKey: privateKey), signature)
+ }
+
+ func testCacaoVerify() throws {
+ let signer = MessageSigner(signer: Signer())
+
+ try signer.verify(signature: signature, message: message, address: "0x15bca56b6e2728aec2532df9d436bd1600e86688")
+ }
+}
diff --git a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift
index 838b47ea7..a499ea4af 100644
--- a/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift
+++ b/Tests/AuthTests/Mocks/NetworkingInteractorMock.swift
@@ -32,6 +32,10 @@ struct NetworkingInteractorMock: NetworkInteracting {
}
+ func respondError(topic: String, requestId: RPCID, tag: Int, reason: Reason, envelopeType: Envelope.EnvelopeType) async throws {
+
+ }
+
func requestNetworkAck(_ request: RPCRequest, topic: String, tag: Int) async throws {
}
diff --git a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift
index bce7c4824..086ebba3c 100644
--- a/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift
+++ b/Tests/AuthTests/Mocks/SIWEMessageFormatterMock.swift
@@ -3,7 +3,12 @@ import Foundation
class SIWEMessageFormatterMock: SIWEMessageFormatting {
var formattedMessage: String!
+
func formatMessage(from authPayload: AuthPayload, address: String) -> String {
return formattedMessage
}
+
+ func formatMessage(from payload: CacaoPayload) throws -> String {
+ return formattedMessage
+ }
}
diff --git a/Tests/AuthTests/SignerTests.swift b/Tests/AuthTests/SignerTests.swift
new file mode 100644
index 000000000..5f546bd08
--- /dev/null
+++ b/Tests/AuthTests/SignerTests.swift
@@ -0,0 +1,65 @@
+import Foundation
+import XCTest
+@testable import Auth
+import secp256k1
+import Web3
+import WalletConnectUtils
+
+class SignerTest: XCTestCase {
+
+ private let signer = Signer()
+
+ private let message = "Message".data(using: .utf8)!
+ private let privateKey = Data(hex: "305c6cde3846927892cd32762f6120539f3ec74c9e3a16b9b798b1e85351ae2a")
+ private let signature = Data(hex: "66121e60cccc01fbf7fcba694a1e08ac5db35fb4ec6c045bedba7860445b95c021cad2c595f0bf68ff896964c7c02bb2f3a3e9540e8e4595c98b737ce264cc541b")
+ private var address = "0x15bca56b6e2728aec2532df9d436bd1600e86688"
+
+ func testValidSignature() throws {
+ let result = try signer.sign(message: message, with: privateKey)
+
+ XCTAssertEqual(signature.toHexString(), result.toHexString())
+ XCTAssertTrue(try signer.isValid(signature: result, message: message, address: address))
+ }
+
+ func testEtherscanSignature() throws {
+ let addressFromEtherscan = "0x6721591d424c18b7173d55895efa1839aa57d9c2"
+ let message = "[Etherscan.io 12/08/2022 09:26:23] I, hereby verify that I am the owner/creator of the address [0x7e77dcb127f99ece88230a64db8d595f31f1b068]"
+ let signedMessageFromEtherscan = message.data(using: .utf8)!
+ let signatureHashFromEtherscan = Data(hex: "60eb9cfe362210f1b4855f4865eafc378bd442c406de22354cc9f643fb84cb265b7f6d9d10b13199e450558c328814a9038884d9993d9feb79b727366736853d1b")
+ XCTAssertTrue(try signer.isValid(
+ signature: signatureHashFromEtherscan,
+ message: signedMessageFromEtherscan,
+ address: addressFromEtherscan
+ ))
+ }
+
+ func testInvalidMessage() throws {
+ let message = "Message One".data(using: .utf8)!
+
+ XCTAssertFalse(try signer.isValid(signature: signature, message: message, address: address))
+ }
+
+ func testInvalidPubKey() throws {
+ let address = "0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07"
+
+ XCTAssertFalse(try signer.isValid(signature: signature, message: message, address: address))
+ }
+
+ func testInvalidSignature() throws {
+ let signature = Data(hex: "86deb09d045608f2753ef12f46e8da5fc2559e3a9162e580df3e62c875df7c3f64433462a59bc4ff38ce52412bff10527f4b99cc078f63ef2bb4a6f7427080aa01")
+
+ XCTAssertFalse(try signer.isValid(signature: signature, message: message, address: address))
+ }
+
+ func testSignerAddressFromIss() throws {
+ let iss = "did:pkh:eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07"
+
+ XCTAssertEqual(try DIDPKH(iss: iss).account, Account("eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07")!)
+ }
+
+ func testSignerAddressFromAccount() throws {
+ let account = Account("eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07")!
+
+ XCTAssertEqual(DIDPKH(account: account).iss, "did:pkh:eip155:1:0xBAc675C310721717Cd4A37F6cbeA1F081b1C2a07")
+ }
+}
diff --git a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift
index d13eefefb..d8f895242 100644
--- a/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift
+++ b/Tests/RelayerTests/Mocks/BackgroundTaskRegistrarMock.swift
@@ -3,7 +3,12 @@ import Foundation
class BackgroundTaskRegistrarMock: BackgroundTaskRegistering {
var completion: (() -> Void)?
+
func register(name: String, completion: @escaping () -> Void) {
self.completion = completion
}
+
+ func invalidate() {
+
+ }
}
diff --git a/Tests/RelayerTests/RelayClientTests.swift b/Tests/RelayerTests/RelayClientTests.swift
index d45bb61fe..f9c9384c2 100644
--- a/Tests/RelayerTests/RelayClientTests.swift
+++ b/Tests/RelayerTests/RelayClientTests.swift
@@ -10,6 +10,8 @@ final class RelayClientTests: XCTestCase {
var sut: RelayClient!
var dispatcher: DispatcherMock!
+ var publishers = Set()
+
override func setUp() {
dispatcher = DispatcherMock()
let logger = ConsoleLogger()
@@ -29,11 +31,12 @@ final class RelayClientTests: XCTestCase {
let subscription = Subscription(id: subscriptionId, topic: topic, message: message)
let request = subscription.asRPCRequest()
- sut.onMessage = { subscriptionTopic, subscriptionMessage in
+ sut.messagePublisher.sink { (subscriptionTopic, subscriptionMessage) in
XCTAssertEqual(subscriptionMessage, message)
XCTAssertEqual(subscriptionTopic, topic)
expectation.fulfill()
- }
+ }.store(in: &publishers)
+
dispatcher.onMessage?(try! request.asJSONEncodedString())
waitForExpectations(timeout: 0.001, handler: nil)
}
@@ -79,9 +82,11 @@ final class RelayClientTests: XCTestCase {
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
+
+ sut.messagePublisher.sink { (_, _) in
expectation.fulfill()
- }
+ }.store(in: &publishers)
+
dispatcher.onMessage?(try! request.asJSONEncodedString())
dispatcher.onMessage?(try! request.asJSONEncodedString())
waitForExpectations(timeout: 0.1, handler: nil)
diff --git a/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift b/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift
index 98f57f0ef..eac4dacb5 100644
--- a/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift
+++ b/Tests/WalletConnectSignTests/Mocks/MockedRelayClient.swift
@@ -4,25 +4,31 @@ import Foundation
@testable import WalletConnectSign
class MockedRelayClient: NetworkRelaying {
- func subscribe(topic: String) async throws {}
+
+ var messagePublisherSubject = PassthroughSubject<(topic: String, message: String), Never>()
+ var messagePublisher: AnyPublisher<(topic: String, message: String), Never> {
+ messagePublisherSubject.eraseToAnyPublisher()
+ }
var socketConnectionStatusPublisherSubject = PassthroughSubject()
var socketConnectionStatusPublisher: AnyPublisher {
socketConnectionStatusPublisherSubject.eraseToAnyPublisher()
}
+ var error: Error?
+ var prompt = false
+
func publish(topic: String, payload: String, tag: Int, prompt: Bool) async throws {
self.prompt = prompt
}
- 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)) {
self.prompt = prompt
onNetworkAcknowledge(error)
}
+ func subscribe(topic: String) async throws {}
+
func subscribe(topic: String, completion: @escaping (Error?) -> Void) {
}
diff --git a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift
index 5e254eb4c..47937bd42 100644
--- a/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift
+++ b/Tests/WalletConnectSignTests/NonControllerSessionStateMachineTests.swift
@@ -52,7 +52,7 @@ class NonControllerSessionStateMachineTests: XCTestCase {
networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateNamespaces(topic: ""))
usleep(100)
XCTAssertFalse(networkingInteractor.didRespondSuccess)
- XCTAssertEqual(networkingInteractor.lastErrorCode, 1301)
+ XCTAssertEqual(networkingInteractor.lastErrorCode, 7001)
}
func testUpdateMethodPeerErrorUnauthorized() {
@@ -61,7 +61,7 @@ class NonControllerSessionStateMachineTests: XCTestCase {
networkingInteractor.wcRequestPublisherSubject.send(WCRequestSubscriptionPayload.stubUpdateNamespaces(topic: session.topic))
usleep(100)
XCTAssertFalse(networkingInteractor.didRespondSuccess)
- XCTAssertEqual(networkingInteractor.lastErrorCode, 3004)
+ XCTAssertEqual(networkingInteractor.lastErrorCode, 3003)
}
// MARK: - Update Expiry
diff --git a/Tests/WalletConnectSignTests/WCPairingTests.swift b/Tests/WalletConnectSignTests/WCPairingTests.swift
index 6826add6d..ca6872e24 100644
--- a/Tests/WalletConnectSignTests/WCPairingTests.swift
+++ b/Tests/WalletConnectSignTests/WCPairingTests.swift
@@ -1,5 +1,5 @@
import XCTest
-import WalletConnectPairing
+@testable import WalletConnectPairing
@testable import WalletConnectSign
final class WCPairingTests: XCTestCase {
@@ -35,18 +35,67 @@ final class WCPairingTests: XCTestCase {
XCTAssertEqual(pairing.expiryDate, inactiveExpiry)
}
- func testUpdateExpiry() {
+ func testUpdateExpiryForTopic() {
var pairing = WCPairing(topic: "")
let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive)
try? pairing.updateExpiry()
XCTAssertEqual(pairing.expiryDate, activeExpiry)
}
+
+ func testUpdateExpiryForUri() {
+ var pairing = WCPairing(uri: WalletConnectURI.stub())
+ let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive)
+ try? pairing.updateExpiry()
+ XCTAssertEqual(pairing.expiryDate, activeExpiry)
+ }
- func testActivate() {
+ func testActivateTopic() {
var pairing = WCPairing(topic: "")
let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive)
+ XCTAssertFalse(pairing.active)
+ pairing.activate()
+ XCTAssertTrue(pairing.active)
+ XCTAssertEqual(pairing.expiryDate, activeExpiry)
+ }
+
+ func testActivateURI() {
+ var pairing = WCPairing(uri: WalletConnectURI.stub())
+ let activeExpiry = referenceDate.advanced(by: WCPairing.timeToLiveActive)
+ XCTAssertFalse(pairing.active)
pairing.activate()
XCTAssertTrue(pairing.active)
XCTAssertEqual(pairing.expiryDate, activeExpiry)
}
+
+ func testUpdateExpiryWhenValueIsGreaterThanMax() {
+ var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: referenceDate)
+ XCTAssertThrowsError(try pairing.updateExpiry(40 * .day)) { error in
+ XCTAssertEqual(error as! WCPairing.Errors, WCPairing.Errors.invalidUpdateExpiryValue)
+ }
+ }
+
+ func testUpdateExpiryWhenNewExpiryDateIsLessThanExpiryDate() {
+ let expiryDate = referenceDate.advanced(by: 40 * .day)
+ var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: expiryDate)
+ XCTAssertThrowsError(try pairing.updateExpiry(10 * .minute)) { error in
+ XCTAssertEqual(error as! WCPairing.Errors, WCPairing.Errors.invalidUpdateExpiryValue)
+ }
+ }
+
+ func testActivateWhenCanUpdateExpiry() {
+ var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: referenceDate)
+ XCTAssertFalse(pairing.active)
+ pairing.activate()
+ XCTAssertTrue(pairing.active)
+ XCTAssertEqual(referenceDate.advanced(by: 30 * .day), pairing.expiryDate)
+ }
+
+ func testActivateWhenUpdateExpiryIsInvalid() {
+ let expiryDate = referenceDate.advanced(by: 40 * .day)
+ var pairing = WCPairing(topic: "", relay: .stub(), peerMetadata: .stub(), expiryDate: expiryDate)
+ XCTAssertFalse(pairing.active)
+ pairing.activate()
+ XCTAssertTrue(pairing.active)
+ XCTAssertEqual(expiryDate, pairing.expiryDate)
+ }
}
diff --git a/Tests/WalletConnectSignTests/WCRelayTests.swift b/Tests/WalletConnectSignTests/WCRelayTests.swift
index 500967141..a42fbd328 100644
--- a/Tests/WalletConnectSignTests/WCRelayTests.swift
+++ b/Tests/WalletConnectSignTests/WCRelayTests.swift
@@ -33,7 +33,7 @@ class NetworkingInteractorTests: XCTestCase {
requestExpectation.fulfill()
}.store(in: &publishers)
serializer.deserialized = request
- relayClient.onMessage?(topic, testPayload)
+ relayClient.messagePublisherSubject.send((topic, testPayload))
waitForExpectations(timeout: 1.001, handler: nil)
}
diff --git a/Tests/WalletConnectSignTests/WalletConnectURITests.swift b/Tests/WalletConnectSignTests/WalletConnectURITests.swift
deleted file mode 100644
index e1e218a4b..000000000
--- a/Tests/WalletConnectSignTests/WalletConnectURITests.swift
+++ /dev/null
@@ -1,60 +0,0 @@
-import XCTest
-@testable import WalletConnectSign
-
-private let stubTopic = "8097df5f14871126866252c1b7479a14aefb980188fc35ec97d130d24bd887c8"
-private let stubSymKey = "587d5484ce2a2a6ee3ba1962fdd7e8588e06200c46823bd18fbd67def96ad303"
-private let stubProtocol = "irn"
-
-private let stubURI = "wc:7f6e504bfad60b485450578e05678ed3e8e8c4751d3c6160be17160d63ec90f9@2?symKey=587d5484ce2a2a6ee3ba1962fdd7e8588e06200c46823bd18fbd67def96ad303&relay-protocol=irn"
-
-final class WalletConnectURITests: XCTestCase {
-
- func testInitURIToString() {
- let inputURI = WalletConnectURI(
- topic: "8097df5f14871126866252c1b7479a14aefb980188fc35ec97d130d24bd887c8",
- symKey: "19c5ecc857963976fabb98ed6a3e0a6ab6b0d65c018b6e25fbdcd3a164def868",
- relay: RelayProtocolOptions(protocol: "irn", data: nil))
- let uriString = inputURI.absoluteString
- let outputURI = WalletConnectURI(string: uriString)
- XCTAssertEqual(inputURI, outputURI)
- }
-
- func testInitStringToURI() {
- let inputURIString = stubURI
- let uri = WalletConnectURI(string: inputURIString)
- let outputURIString = uri?.absoluteString
- XCTAssertEqual(inputURIString, outputURIString)
- }
-
- func testInitStringToURIAlternate() {
- let expectedString = stubURI
- let inputURIString = expectedString.replacingOccurrences(of: "wc:", with: "wc://")
- let uri = WalletConnectURI(string: inputURIString)
- let outputURIString = uri?.absoluteString
- XCTAssertEqual(expectedString, outputURIString)
- }
-
- func testInitFailsBadScheme() {
- let inputURIString = stubURI.replacingOccurrences(of: "wc:", with: "")
- let uri = WalletConnectURI(string: inputURIString)
- XCTAssertNil(uri)
- }
-
- func testInitFailsMalformedURL() {
- let inputURIString = "wc://<"
- let uri = WalletConnectURI(string: inputURIString)
- XCTAssertNil(uri)
- }
-
- func testInitFailsNoSymKeyParam() {
- let inputURIString = stubURI.replacingOccurrences(of: "symKey=\(stubSymKey)", with: "")
- let uri = WalletConnectURI(string: inputURIString)
- XCTAssertNil(uri)
- }
-
- func testInitFailsNoRelayParam() {
- let inputURIString = stubURI.replacingOccurrences(of: "&relay-protocol=\(stubProtocol)", with: "")
- let uri = WalletConnectURI(string: inputURIString)
- XCTAssertNil(uri)
- }
-}
diff --git a/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift b/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift
new file mode 100644
index 000000000..fc2f8c170
--- /dev/null
+++ b/Tests/WalletConnectUtilsTests/WalletConnectURITests.swift
@@ -0,0 +1,91 @@
+import XCTest
+@testable import WalletConnectUtils
+
+private func stubURI(api: WalletConnectURI.TargetAPI? = nil) -> (uri: WalletConnectURI, string: String) {
+ let topic = Data.randomBytes(count: 32).toHexString()
+ let symKey = Data.randomBytes(count: 32).toHexString()
+ let protocolName = "irn"
+ let uriBase = api == nil ? "wc:" : "wc:\(api!.rawValue)-"
+ let uriString = "\(uriBase)\(topic)@2?symKey=\(symKey)&relay-protocol=\(protocolName)"
+ let uri = WalletConnectURI(
+ topic: topic,
+ symKey: symKey,
+ relay: RelayProtocolOptions(protocol: protocolName, data: nil),
+ api: api)
+ return (uri, uriString)
+}
+
+final class WalletConnectURITests: XCTestCase {
+
+ // MARK: - Init URI with string
+
+ func testInitURIToString() {
+ let input = stubURI()
+ let uriString = input.uri.absoluteString
+ let outputURI = WalletConnectURI(string: uriString)
+ XCTAssertEqual(input.uri, outputURI)
+ XCTAssertEqual(input.string, outputURI?.absoluteString)
+ }
+
+ func testInitStringToURI() {
+ let inputURIString = stubURI().string
+ let uri = WalletConnectURI(string: inputURIString)
+ let outputURIString = uri?.absoluteString
+ XCTAssertEqual(inputURIString, outputURIString)
+ }
+
+ func testInitStringToURIAlternate() {
+ let expectedString = stubURI().string
+ let inputURIString = expectedString.replacingOccurrences(of: "wc:", with: "wc://")
+ let uri = WalletConnectURI(string: inputURIString)
+ let outputURIString = uri?.absoluteString
+ XCTAssertEqual(expectedString, outputURIString)
+ }
+
+ // MARK: - Init URI with prefix API identifier
+
+ func testInitFromPrefixedURIString() {
+ WalletConnectURI.TargetAPI.allCases.forEach { api in
+ let uriString = stubURI(api: api).string
+ let uri = WalletConnectURI(string: uriString)
+ XCTAssertEqual(uri?.api, api)
+ XCTAssertEqual(uri?.absoluteString, uriString)
+ }
+ }
+
+ func testAbsentPrefixFallbackToSign() {
+ let input = stubURI()
+ let uriFromParams = input.uri
+ let uriFromString = WalletConnectURI(string: input.string)
+ XCTAssertEqual(uriFromParams.api, .sign)
+ XCTAssertEqual(uriFromString?.api, .sign)
+ }
+
+ // MARK: - Init URI failure cases
+
+ func testInitFailsBadScheme() {
+ let inputURIString = stubURI().string.replacingOccurrences(of: "wc:", with: "")
+ let uri = WalletConnectURI(string: inputURIString)
+ XCTAssertNil(uri)
+ }
+
+ func testInitFailsMalformedURL() {
+ let inputURIString = "wc://<"
+ let uri = WalletConnectURI(string: inputURIString)
+ XCTAssertNil(uri)
+ }
+
+ func testInitFailsNoSymKeyParam() {
+ let input = stubURI()
+ let inputURIString = input.string.replacingOccurrences(of: "symKey=\(input.uri.symKey)", with: "")
+ let uri = WalletConnectURI(string: inputURIString)
+ XCTAssertNil(uri)
+ }
+
+ func testInitFailsNoRelayParam() {
+ let input = stubURI()
+ let inputURIString = input.string.replacingOccurrences(of: "&relay-protocol=\(input.uri.relay.protocol)", with: "")
+ let uri = WalletConnectURI(string: inputURIString)
+ XCTAssertNil(uri)
+ }
+}