Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Approve engine refactor #260

Merged
merged 13 commits into from
Jun 8, 2022
6 changes: 5 additions & 1 deletion Example/ExampleApp/Wallet/WalletViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,11 @@ extension WalletViewController: ProposalViewControllerDelegate {
print("did reject session")
let proposal = currentProposal!
currentProposal = nil
Sign.instance.reject(proposal: proposal, reason: .disapprovedChains)
do {
try Sign.instance.reject(proposalId: proposal.id, reason: .disapprovedChains)
} catch {
print("Session rejection error: \(error.localizedDescription)")
}
}
}

Expand Down
180 changes: 30 additions & 150 deletions Sources/WalletConnectSign/Engine/Common/PairingEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,44 +3,32 @@ import Combine
import WalletConnectUtils
import WalletConnectKMS


final class PairingEngine {
var onSessionProposal: ((Session.Proposal)->())?
var onProposeResponse: ((String, SessionProposal)->())?
var onSessionRejected: ((Session.Proposal, SessionType.Reason)->())?

private let proposalPayloadsStore: CodableStore<WCRequestSubscriptionPayload>

private let networkingInteractor: NetworkInteracting
private let kms: KeyManagementServiceProtocol
private let pairingStore: WCPairingStorage
private let sessionToPairingTopic: CodableStore<String>
private var metadata: AppMetadata
private var publishers = [AnyCancellable]()
private let logger: ConsoleLogging
private let topicInitializer: () -> String
init(networkingInteractor: NetworkInteracting,
kms: KeyManagementServiceProtocol,
pairingStore: WCPairingStorage,
sessionToPairingTopic: CodableStore<String>,
metadata: AppMetadata,
logger: ConsoleLogging,
topicGenerator: @escaping () -> String = String.generateTopic,
proposalPayloadsStore: CodableStore<WCRequestSubscriptionPayload> = CodableStore<WCRequestSubscriptionPayload>(defaults: RuntimeKeyValueStorage(), identifier: StorageDomainIdentifiers.proposals.rawValue)) {

init(
networkingInteractor: NetworkInteracting,
kms: KeyManagementServiceProtocol,
pairingStore: WCPairingStorage,
metadata: AppMetadata,
logger: ConsoleLogging,
topicGenerator: @escaping () -> String = String.generateTopic
) {
self.networkingInteractor = networkingInteractor
self.kms = kms
self.metadata = metadata
self.pairingStore = pairingStore
self.logger = logger
self.topicInitializer = topicGenerator
self.sessionToPairingTopic = sessionToPairingTopic
self.proposalPayloadsStore = proposalPayloadsStore
setUpWCRequestHandling()
setupNetworkingSubscriptions()
setupExpirationHandling()
restoreSubscriptions()
networkingInteractor.onPairingResponse = { [weak self] in
self?.handleResponse($0)
}
}

func hasPairing(for topic: String) -> Bool {
Expand Down Expand Up @@ -106,147 +94,39 @@ final class PairingEngine {
}
}
}

func reject(proposal: SessionProposal, reason: ReasonCode) {
guard let payload = try? proposalPayloadsStore.get(key: proposal.proposer.publicKey) else {
return
}
proposalPayloadsStore.delete(forKey: proposal.proposer.publicKey)
networkingInteractor.respondError(for: payload, reason: reason)
// todo - delete pairing if inactive
}

func approveProposal(proposerPubKey: String, validating sessionNamespaces: [String: SessionNamespace]) -> (String, SessionProposal)? {
guard let payload = try? proposalPayloadsStore.get(key: proposerPubKey),
case .sessionPropose(let proposal) = payload.wcRequest.params else {
//TODO - throws
return nil
}
let proposedNamespaces = proposal.requiredNamespaces
proposalPayloadsStore.delete(forKey: proposerPubKey)

do {
try Namespace.validate(sessionNamespaces)
try Namespace.validateApproved(sessionNamespaces, against: proposedNamespaces)
let selfPublicKey = try! kms.createX25519KeyPair()
let agreementKey = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposal.proposer.publicKey)
//todo - extend pairing
let sessionTopic = agreementKey.derivedTopic()

try! kms.setAgreementSecret(agreementKey, topic: sessionTopic)
guard let relay = proposal.relays.first else {return nil}
let proposeResponse = SessionType.ProposeResponse(relay: relay, responderPublicKey: selfPublicKey.hexRepresentation)
let response = JSONRPCResponse<AnyCodable>(id: payload.wcRequest.id, result: AnyCodable(proposeResponse))
logger.debug("Responding session propose")
networkingInteractor.respond(topic: payload.topic, response: .response(response)) { _ in }
return (sessionTopic, proposal)
} catch {
networkingInteractor.respondError(for: payload, reason: .missingOrInvalid("agreement keys"))
return nil
}
}
}

//MARK: - Private
// MARK: Private

private func setUpWCRequestHandling() {
networkingInteractor.wcRequestPublisher.sink { [unowned self] subscriptionPayload in
switch subscriptionPayload.wcRequest.params {
case .pairingPing(_):
wcPairingPing(subscriptionPayload)
case .sessionPropose(let proposeParams):
wcSessionPropose(subscriptionPayload, proposal: proposeParams)
default:
return
}
}.store(in: &publishers)
}

private func wcSessionPropose(_ payload: WCRequestSubscriptionPayload, proposal: SessionType.ProposeParams) {
logger.debug("Received Session Proposal")
do {
try Namespace.validate(proposal.requiredNamespaces)
} catch {
// TODO: respond error
return
}
try? proposalPayloadsStore.set(payload, forKey: proposal.proposer.publicKey)
onSessionProposal?(proposal.publicRepresentation())
}

private func wcPairingPing(_ payload: WCRequestSubscriptionPayload) {
networkingInteractor.respondSuccess(for: payload)
}
private extension PairingEngine {

private func restoreSubscriptions() {
func setupNetworkingSubscriptions() {
networkingInteractor.transportConnectionPublisher
.sink { [unowned self] (_) in
let topics = pairingStore.getAll()
.map{$0.topic}
topics.forEach{ topic in Task{try? await networkingInteractor.subscribe(topic: topic)}}
}.store(in: &publishers)

networkingInteractor.wcRequestPublisher
.sink { [unowned self] subscriptionPayload in
switch subscriptionPayload.wcRequest.params {
case .pairingPing(_):
wcPairingPing(subscriptionPayload)
default:
return
}
}.store(in: &publishers)
}

func wcPairingPing(_ payload: WCRequestSubscriptionPayload) {
networkingInteractor.respondSuccess(for: payload)
}

private func setupExpirationHandling() {
func setupExpirationHandling() {
pairingStore.onPairingExpiration = { [weak self] pairing in
self?.kms.deleteSymmetricKey(for: pairing.topic)
self?.networkingInteractor.unsubscribe(topic: pairing.topic)
}
}

private func handleResponse(_ response: WCResponse) {
switch response.requestParams {
case .sessionPropose(let proposal):
handleProposeResponse(pairingTopic: response.topic, proposal: proposal, result: response.result)
default:
break
}
}

private func handleProposeResponse(pairingTopic: String, proposal: SessionProposal, result: JsonRpcResult) {
guard var pairing = pairingStore.getPairing(forTopic: pairingTopic) else {
return
}
switch result {
case .response(let response):

// Activate the pairing
if !pairing.active {
pairing.activate()
} else {
try? pairing.updateExpiry()
}

pairingStore.setPairing(pairing)

let selfPublicKey = try! AgreementPublicKey(hex: proposal.proposer.publicKey)
var agreementKeys: AgreementKeys!

do {
let proposeResponse = try response.result.get(SessionType.ProposeResponse.self)
agreementKeys = try kms.performKeyAgreement(selfPublicKey: selfPublicKey, peerPublicKey: proposeResponse.responderPublicKey)
} catch {
//TODO - handle error
logger.debug(error)
return
}

let sessionTopic = agreementKeys.derivedTopic()
logger.debug("Received Session Proposal response")

try? kms.setAgreementSecret(agreementKeys, topic: sessionTopic)
try! sessionToPairingTopic.set(pairingTopic, forKey: sessionTopic)
onProposeResponse?(sessionTopic, proposal)

case .error(let error):
if !pairing.active {
kms.deleteSymmetricKey(for: pairing.topic)
networkingInteractor.unsubscribe(topic: pairing.topic)
pairingStore.delete(topic: pairingTopic)
}
logger.debug("Session Proposal has been rejected")
kms.deletePrivateKey(for: proposal.proposer.publicKey)
onSessionRejected?(proposal.publicRepresentation(), SessionType.Reason(code: error.error.code, message: error.error.message))
return
}
}
}
Loading