Skip to content

Commit

Permalink
Merge pull request #511 from WalletConnect/pairing-api
Browse files Browse the repository at this point in the history
[Pairing] Pairing API
  • Loading branch information
llbartekll authored Sep 27, 2022
2 parents 5307b48 + 81c4fd7 commit d8e6a47
Show file tree
Hide file tree
Showing 23 changed files with 418 additions and 10 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,6 @@ jobs:
- 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: |
Expand All @@ -43,6 +37,12 @@ jobs:
restore-keys: |
${{ runner.os }}-spm-
- 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: ./.github/actions/ci
with:
type: ${{ matrix.test-type }}
Expand Down
12 changes: 12 additions & 0 deletions Example/ExampleApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
84CE644E279ED2FF00142511 /* SelectChainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE644D279ED2FF00142511 /* SelectChainView.swift */; };
84CE6452279ED42B00142511 /* ConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE6451279ED42B00142511 /* ConnectView.swift */; };
84CE645527A29D4D00142511 /* ResponseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CE645427A29D4C00142511 /* ResponseViewController.swift */; };
84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CEC64528D89D6B00D081A8 /* PairingTests.swift */; };
84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D2A66528A4F51E0088AE09 /* AuthTests.swift */; };
84DDB4ED28ABB663003D66ED /* WalletConnectAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 84DDB4EC28ABB663003D66ED /* WalletConnectAuth */; };
84F568C2279582D200D0A289 /* Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F568C1279582D200D0A289 /* Signer.swift */; };
Expand Down Expand Up @@ -235,6 +236,7 @@
84CE6451279ED42B00142511 /* ConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectView.swift; sourceTree = "<group>"; };
84CE6453279FFE1100142511 /* Wallet.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Wallet.entitlements; sourceTree = "<group>"; };
84CE645427A29D4C00142511 /* ResponseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseViewController.swift; sourceTree = "<group>"; };
84CEC64528D89D6B00D081A8 /* PairingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingTests.swift; sourceTree = "<group>"; };
84D2A66528A4F51E0088AE09 /* AuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTests.swift; sourceTree = "<group>"; };
84F568C1279582D200D0A289 /* Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Signer.swift; sourceTree = "<group>"; };
84F568C32795832A00D0A289 /* EthereumTransaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EthereumTransaction.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -581,6 +583,14 @@
path = Connect;
sourceTree = "<group>";
};
84CEC64728D8A98900D081A8 /* Pairing */ = {
isa = PBXGroup;
children = (
84CEC64528D89D6B00D081A8 /* PairingTests.swift */,
);
path = Pairing;
sourceTree = "<group>";
};
84D2A66728A4F5260088AE09 /* Auth */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -1019,6 +1029,7 @@
A5E03DEE286464DB00888481 /* IntegrationTests */ = {
isa = PBXGroup;
children = (
84CEC64728D8A98900D081A8 /* Pairing */,
A5E03E0B28646AA500888481 /* Relay */,
A5E03E0A28646A8A00888481 /* Stubs */,
A5E03E0928646A8100888481 /* Sign */,
Expand Down Expand Up @@ -1455,6 +1466,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
84CEC64628D89D6B00D081A8 /* PairingTests.swift in Sources */,
767DC83528997F8E00080FA9 /* EthSendTransaction.swift in Sources */,
A5E03E03286466F400888481 /* ChatTests.swift in Sources */,
84D2A66628A4F51E0088AE09 /* AuthTests.swift in Sources */,
Expand Down
56 changes: 56 additions & 0 deletions Example/IntegrationTests/Pairing/PairingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Foundation
import XCTest
import WalletConnectUtils
@testable import WalletConnectKMS
import WalletConnectRelay
import Combine
import WalletConnectNetworking
@testable import WalletConnectPairing


final class PairingTests: XCTestCase {
var appPairingClient: PairingClient!
var walletPairingClient: PairingClient!

var appPushClient: PushClient!
var walletPushClient: PushClient!

var pairingStorage: PairingStorage!

private var publishers = [AnyCancellable]()

override func setUp() {
(appPairingClient, appPushClient) = makeClients(prefix: "🤖 App")
(walletPairingClient, walletPushClient) = makeClients(prefix: "🐶 Wallet")
}

func makeClients(prefix: String) -> (PairingClient, PushClient) {
let keychain = KeychainStorageMock()
let logger = ConsoleLogger(suffix: prefix, loggingLevel: .debug)
let projectId = "3ca2919724fbfa5456a25194e369a8b4"
let relayClient = RelayClient(relayHost: URLConfig.relayHost, projectId: projectId, keychainStorage: keychain, socketFactory: SocketFactory(), logger: logger)

let pairingClient = PairingClientFactory.create(logger: logger, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, relayClient: relayClient)

let pushClient = PushClientFactory.create(logger: logger, keyValueStorage: RuntimeKeyValueStorage(), keychainStorage: keychain, relayClient: relayClient, pairingClient: pairingClient)
return (pairingClient, pushClient)

}

func testProposePushOnPairing() async throws {
let exp = expectation(description: "testProposePushOnPairing")

walletPushClient.proposalPublisher.sink { _ in
exp.fulfill()
}.store(in: &publishers)

let uri = try await appPairingClient.create()

try await walletPairingClient.pair(uri: uri)

try await appPushClient.propose(topic: uri.topic)

wait(for: [exp], timeout: 2)
}
}

1 change: 1 addition & 0 deletions Sources/WalletConnectNetworking/NetworkInteracting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WalletConnectRelay

public protocol NetworkInteracting {
var socketConnectionStatusPublisher: AnyPublisher<SocketConnectionStatus, Never> { get }
var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> { get }
func subscribe(topic: String) async throws
func unsubscribe(topic: String)
func request(_ request: RPCRequest, topic: String, protocolMethod: ProtocolMethod, envelopeType: Envelope.EnvelopeType) async throws
Expand Down
6 changes: 3 additions & 3 deletions Sources/WalletConnectNetworking/NetworkInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public class NetworkingInteractor: NetworkInteracting {
private let requestPublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest), Never>()
private let responsePublisherSubject = PassthroughSubject<(topic: String, request: RPCRequest, response: RPCResponse), Never>()

private var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> {
public var requestPublisher: AnyPublisher<(topic: String, request: RPCRequest), Never> {
requestPublisherSubject.eraseToAnyPublisher()
}

Expand Down Expand Up @@ -56,13 +56,13 @@ public class NetworkingInteractor: NetworkInteracting {
}
}

public func requestSubscription<Request: Codable>(on request: ProtocolMethod) -> AnyPublisher<RequestSubscriptionPayload<Request>, Never> {
public func requestSubscription<RequestParams: Codable>(on request: ProtocolMethod) -> AnyPublisher<RequestSubscriptionPayload<RequestParams>, Never> {
return requestPublisher
.filter { rpcRequest in
return rpcRequest.request.method == request.method
}
.compactMap { topic, rpcRequest in
guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(Request.self) else { return nil }
guard let id = rpcRequest.id, let request = try? rpcRequest.params?.get(RequestParams.self) else { return nil }
return RequestSubscriptionPayload(id: id, topic: topic, request: request)
}
.eraseToAnyPublisher()
Expand Down
48 changes: 48 additions & 0 deletions Sources/WalletConnectPairing/PairingClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Foundation
import WalletConnectUtils
import WalletConnectRelay
import WalletConnectNetworking
import Combine

public class PairingClient: PairingRegisterer {
private let walletPairService: WalletPairService
private let appPairService: AppPairService
public let socketConnectionStatusPublisher: AnyPublisher<SocketConnectionStatus, Never>
private let logger: ConsoleLogging
private let networkingInteractor: NetworkInteracting
private let pairingRequestsSubscriber: PairingRequestsSubscriber

init(appPairService: AppPairService,
networkingInteractor: NetworkInteracting,
logger: ConsoleLogging,
walletPairService: WalletPairService,
pairingRequestsSubscriber: PairingRequestsSubscriber,
socketConnectionStatusPublisher: AnyPublisher<SocketConnectionStatus, Never>
) {
self.appPairService = appPairService
self.walletPairService = walletPairService
self.networkingInteractor = networkingInteractor
self.socketConnectionStatusPublisher = socketConnectionStatusPublisher
self.logger = logger
self.pairingRequestsSubscriber = pairingRequestsSubscriber
}
/// For wallet to establish a pairing
/// Wallet should call this function in order to accept peer's pairing proposal and be able to subscribe for future requests.
/// - Parameter uri: Pairing URI that is commonly presented as a QR code by a dapp or delivered with universal linking.
///
/// Throws Error:
/// - When URI is invalid format or missing params
/// - When topic is already in use
public func pair(uri: WalletConnectURI) async throws {
try await walletPairService.pair(uri)
}

public func create() async throws -> WalletConnectURI {
return try await appPairService.create()
}

public func register(method: ProtocolMethod) {
pairingRequestsSubscriber.subscribeForRequest(method)
}
}

36 changes: 36 additions & 0 deletions Sources/WalletConnectPairing/PairingClientFactory.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import WalletConnectRelay
import WalletConnectUtils
import WalletConnectKMS
import WalletConnectNetworking

public struct PairingClientFactory {

public static func create(relayClient: RelayClient) -> PairingClient {
let logger = ConsoleLogger(loggingLevel: .off)
let keyValueStorage = UserDefaults.standard
let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk")
return PairingClientFactory.create(logger: logger, keyValueStorage: keyValueStorage, keychainStorage: keychainStorage, relayClient: relayClient)
}

static func create(logger: ConsoleLogging, keyValueStorage: KeyValueStorage, keychainStorage: KeychainStorageProtocol, relayClient: RelayClient) -> PairingClient {
let kms = KeyManagementService(keychain: keychainStorage)
let serializer = Serializer(kms: kms)
let history = RPCHistoryFactory.createForNetwork(keyValueStorage: keyValueStorage)
let networkingInteractor = NetworkingInteractor(relayClient: relayClient, serializer: serializer, logger: logger, rpcHistory: history)
let pairingStore = PairingStorage(storage: SequenceStore<WCPairing>(store: .init(defaults: keyValueStorage, identifier: "")))
let appPairService = AppPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore)
let walletPairService = WalletPairService(networkingInteractor: networkingInteractor, kms: kms, pairingStorage: pairingStore)
let pairingRequestsSubscriber = PairingRequestsSubscriber(networkingInteractor: networkingInteractor, pairingStorage: pairingStore, logger: logger)

return PairingClient(
appPairService: appPairService,
networkingInteractor: networkingInteractor,
logger: logger,
walletPairService: walletPairService,
pairingRequestsSubscriber: pairingRequestsSubscriber,
socketConnectionStatusPublisher: relayClient.socketConnectionStatusPublisher
)
}
}

6 changes: 6 additions & 0 deletions Sources/WalletConnectPairing/PairingRegisterer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation
import WalletConnectNetworking

public protocol PairingRegisterer {
func register(method: ProtocolMethod)
}
27 changes: 27 additions & 0 deletions Sources/WalletConnectPairing/PairingRequester.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation
import Combine
import JSONRPC
import WalletConnectUtils
import WalletConnectKMS
import WalletConnectNetworking


public class PushProposer {
private let networkingInteractor: NetworkInteracting
private let kms: KeyManagementServiceProtocol
private let logger: ConsoleLogging

init(networkingInteractor: NetworkInteracting,
kms: KeyManagementServiceProtocol,
logger: ConsoleLogging) {
self.networkingInteractor = networkingInteractor
self.kms = kms
self.logger = logger
}

func request(topic: String, params: AnyCodable) async throws {
let protocolMethod = PushProposeProtocolMethod()
let request = RPCRequest(method: protocolMethod.method, params: params)
try await networkingInteractor.requestNetworkAck(request, topic: topic, protocolMethod: protocolMethod)
}
}
36 changes: 36 additions & 0 deletions Sources/WalletConnectPairing/PairingRequestsSubscriber.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import Foundation
import Combine
import WalletConnectUtils
import WalletConnectNetworking

public class PairingRequestsSubscriber {
private let networkingInteractor: NetworkInteracting
private let pairingStorage: PairingStorage
private var publishers = Set<AnyCancellable>()

init(networkingInteractor: NetworkInteracting, pairingStorage: PairingStorage, logger: ConsoleLogging) {
self.networkingInteractor = networkingInteractor
self.pairingStorage = pairingStorage
}

func subscribeForRequest(_ protocolMethod: ProtocolMethod) {
networkingInteractor.requestPublisher
// Pairing requests only
.filter { [unowned self] payload in
return pairingStorage.hasPairing(forTopic: payload.topic)
}
// Wrong method
.filter { payload in
return payload.request.method != protocolMethod.method
}
// Respond error
.sink { [unowned self] topic, request in
Task(priority: .high) {
// TODO - spec tag
try await networkingInteractor.respondError(topic: topic, requestId: request.id!, protocolMethod: protocolMethod, reason: PairError.methodUnsupported)
}

}.store(in: &publishers)
}

}
59 changes: 59 additions & 0 deletions Sources/WalletConnectPairing/Push/PushClient.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Foundation
import JSONRPC
import Combine
import WalletConnectKMS
import WalletConnectUtils
import WalletConnectNetworking

public class PushClient {

private var publishers = Set<AnyCancellable>()

let requestPublisherSubject = PassthroughSubject<(topic: String, params: PushRequestParams), Never>()

var proposalPublisher: AnyPublisher<(topic: String, params: PushRequestParams), Never> {
requestPublisherSubject.eraseToAnyPublisher()
}

public let logger: ConsoleLogging

private let pushProposer: PushProposer
private let networkInteractor: NetworkInteracting
private let pairingRegisterer: PairingRegisterer

init(networkInteractor: NetworkInteracting,
logger: ConsoleLogging,
kms: KeyManagementServiceProtocol,
pushProposer: PushProposer,
pairingRegisterer: PairingRegisterer) {
self.networkInteractor = networkInteractor
self.logger = logger
self.pushProposer = pushProposer
self.pairingRegisterer = pairingRegisterer

setupPairingSubscriptions()
}

public func propose(topic: String) async throws {
try await pushProposer.request(topic: topic, params: AnyCodable(PushRequestParams()))
}
}

private extension PushClient {

func setupPairingSubscriptions() {
let protocolMethod = PushProposeProtocolMethod()

pairingRegisterer.register(method: protocolMethod)

networkInteractor.responseErrorSubscription(on: protocolMethod)
.sink { [unowned self] (payload: ResponseSubscriptionErrorPayload<PushRequestParams>) in
logger.error(payload.error.localizedDescription)
}.store(in: &publishers)

networkInteractor.requestSubscription(on: protocolMethod)
.sink { [unowned self] (payload: RequestSubscriptionPayload<PushRequestParams>) in
requestPublisherSubject.send((payload.topic, payload.request))
}.store(in: &publishers)
}
}
Loading

0 comments on commit d8e6a47

Please sign in to comment.