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

[Notify] Config from explorer #1185

Merged
merged 10 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Example/IntegrationTests/Push/NotifyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@ final class NotifyTests: XCTestCase {
keychainStorage: keychain,
environment: .sandbox)
let keyserverURL = URL(string: "https://keys.walletconnect.com")!
let client = NotifyClientFactory.create(keyserverURL: keyserverURL,
let client = NotifyClientFactory.create(projectId: InputConfig.projectId,
keyserverURL: keyserverURL,
logger: notifyLogger,
keyValueStorage: keyValueStorage,
keychainStorage: keychain,
Expand Down Expand Up @@ -151,7 +152,7 @@ final class NotifyTests: XCTestCase {

func testWalletCreatesAndUpdatesSubscription() async {
let expectation = expectation(description: "expects to create and update notify subscription")
let updateScope: Set<String> = ["alerts"]
let updateScope: Set<String> = ["8529aae8-cb26-4d49-922e-eb099044bebe"]
expectation.assertForOverFulfill = false

var didUpdate = false
Expand Down Expand Up @@ -183,7 +184,7 @@ final class NotifyTests: XCTestCase {
func testNotifyServerSubscribeAndNotifies() async throws {
let subscribeExpectation = expectation(description: "creates notify subscription")
let messageExpectation = expectation(description: "receives a notify message")
let notifyMessage = NotifyMessage.stub()
let notifyMessage = NotifyMessage.stub(type: "8529aae8-cb26-4d49-922e-eb099044bebe")

var didNotify = false
walletNotifyClientA.subscriptionsPublisher
Expand Down
6 changes: 3 additions & 3 deletions Example/IntegrationTests/Stubs/PushMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import Foundation
import WalletConnectNotify

extension NotifyMessage {
static func stub() -> NotifyMessage {
static func stub(type: String) -> NotifyMessage {
return NotifyMessage(
title: "swift_test",
body: "cad9a52d-9b0f-4aed-9cca-3e9568a079f9",
body: "body",
icon: "https://images.unsplash.com/photo-1581224463294-908316338239?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=250&q=80",
url: "https://web3inbox.com",
type: "private")
type: type)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ struct NotifyPreferencesView: View {
Toggle(isOn: .init(get: {
viewModel.update[title]?.enabled ?? value.enabled
}, set: { newValue in
viewModel.update[title] = ScopeValue(description: value.description, enabled: newValue)
viewModel.update[title] = ScopeValue(id: value.id, name: value.name, description: value.description, enabled: newValue)
})) {
VStack(alignment: .leading, spacing: 4) {
Text(title)
Text(value.name)
.foregroundColor(.primary)
.font(.system(size: 14, weight: .semibold))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import Foundation

public struct NotifyClientFactory {

public static func create(groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String) -> NotifyClient {
public static func create(projectId: String, groupIdentifier: String, networkInteractor: NetworkInteracting, pairingRegisterer: PairingRegisterer, pushClient: PushClient, crypto: CryptoProvider, notifyHost: String) -> NotifyClient {
let logger = ConsoleLogger(prefix: "🔔",loggingLevel: .debug)
let keyValueStorage = UserDefaults.standard
let keyserverURL = URL(string: "https://keys.walletconnect.com")!
let keychainStorage = KeychainStorage(serviceIdentifier: "com.walletconnect.sdk")
let groupKeychainService = GroupKeychainStorage(serviceIdentifier: groupIdentifier)

return NotifyClientFactory.create(
projectId: projectId,
keyserverURL: keyserverURL,
logger: logger,
keyValueStorage: keyValueStorage,
Expand All @@ -24,6 +25,7 @@ public struct NotifyClientFactory {
}

static func create(
projectId: String,
keyserverURL: URL,
logger: ConsoleLogging,
keyValueStorage: KeyValueStorage,
Expand All @@ -46,12 +48,11 @@ public struct NotifyClientFactory {
let deleteNotifySubscriptionRequester = DeleteNotifySubscriptionRequester(keyserver: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, webDidResolver: webDidResolver, kms: kms, logger: logger, notifyStorage: notifyStorage)
let resubscribeService = NotifyResubscribeService(networkInteractor: networkInteractor, notifyStorage: notifyStorage, logger: logger)

let dappsMetadataStore = CodableStore<AppMetadata>(defaults: keyValueStorage, identifier: NotifyStorageIdntifiers.dappsMetadataStore)
let notifyConfigProvider = NotifyConfigProvider()
let notifyConfigProvider = NotifyConfigProvider(projectId: projectId)

let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyConfigProvider: notifyConfigProvider, dappsMetadataStore: dappsMetadataStore)
let notifySubscribeRequester = NotifySubscribeRequester(keyserverURL: keyserverURL, networkingInteractor: networkInteractor, identityClient: identityClient, logger: logger, kms: kms, webDidResolver: webDidResolver, notifyConfigProvider: notifyConfigProvider)

let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, dappsMetadataStore: dappsMetadataStore, notifyConfigProvider: notifyConfigProvider)
let notifySubscribeResponseSubscriber = NotifySubscribeResponseSubscriber(networkingInteractor: networkInteractor, kms: kms, logger: logger, groupKeychainStorage: groupKeychainStorage, notifyStorage: notifyStorage, notifyConfigProvider: notifyConfigProvider)

let notifyUpdateRequester = NotifyUpdateRequester(keyserverURL: keyserverURL, webDidResolver: webDidResolver, identityClient: identityClient, networkingInteractor: networkInteractor, notifyConfigProvider: notifyConfigProvider, logger: logger, notifyStorage: notifyStorage)

Expand Down
35 changes: 35 additions & 0 deletions Sources/WalletConnectNotify/Client/Wallet/NotifyConfig.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation

struct NotifyConfig: Codable {
struct NotificationType: Codable {
let id: String
let name: String
let description: String
}
struct ImageUrl: Codable {
let sm: String?
let md: String?
let lg: String?
}
let id: String
let name: String
let homepage: String
let description: String
let dapp_url: String
let image_url: ImageUrl?
let notificationTypes: [NotificationType]

var appDomain: String {
return URL(string: dapp_url)?.host ?? dapp_url
}

var metadata: AppMetadata {
return AppMetadata(
name: name,
description:
description,
url: appDomain,
icons: [image_url?.sm, image_url?.md, image_url?.lg].compactMap { $0 }
)
}
}
33 changes: 33 additions & 0 deletions Sources/WalletConnectNotify/Client/Wallet/NotifyConfigAPI.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

enum NotifyConfigAPI: HTTPService {

var path: String {
return "/w3i/v1/notify-config"
}

var method: HTTPMethod {
return .get
}

var body: Data? {
return nil
}

var queryParameters: [String : String]? {
switch self {
case .notifyDApps(let projectId, let appDomain):
return ["projectId": projectId, "appDomain": appDomain]
}
}

var additionalHeaderFields: [String : String]? {
return nil
}

var scheme: String {
return "https"
}

case notifyDApps(projectId: String, appDomain: String)
}
Original file line number Diff line number Diff line change
@@ -1,29 +1,48 @@

import Foundation

actor NotifyConfigProvider {
enum Errors: Error {
case invalidUrl

private let projectId: String

private var cache: [String: NotifyConfig] = [:]

init(projectId: String) {
self.projectId = projectId
}

private var cache = [String: Set<NotificationType>]()
func resolveNotifyConfig(appDomain: String) async -> NotifyConfig {
if let config = cache[appDomain] {
return config
}

func getSubscriptionScope(appDomain: String) async throws -> Set<NotificationType> {
if let availableScope = cache[appDomain] {
return availableScope
do {
let httpClient = HTTPNetworkClient(host: "explorer-api.walletconnect.com")
let request = NotifyConfigAPI.notifyDApps(projectId: projectId, appDomain: appDomain)
let response = try await httpClient.request(NotifyConfigResponse.self, at: request)
let config = response.data
cache[appDomain] = config
return config
} catch {
return emptyConfig(appDomain: appDomain)
}
guard let notifyConfigUrl = URL(string: "https://\(appDomain)/.well-known/wc-notify-config.json") else { throw Errors.invalidUrl }
let (data, _) = try await URLSession.shared.data(from: notifyConfigUrl)
let config = try JSONDecoder().decode(NotificationConfig.self, from: data)
let availableScope = Set(config.types)
cache[appDomain] = availableScope
return availableScope
}
}

private extension NotifyConfigProvider {

struct NotifyConfigResponse: Codable {
let data: NotifyConfig
}

func getMetadata(appDomain: String) async throws -> AppMetadata {
guard let notifyConfigUrl = URL(string: "https://\(appDomain)/.well-known/wc-notify-config.json") else { throw Errors.invalidUrl }
let (data, _) = try await URLSession.shared.data(from: notifyConfigUrl)
let config = try JSONDecoder().decode(NotificationConfig.self, from: data)
return AppMetadata(name: config.name, description: config.description, url: appDomain, icons: config.icons)
func emptyConfig(appDomain: String) -> NotifyConfig {
return NotifyConfig(
id: UUID().uuidString,
name: appDomain,
homepage: "https://\(appDomain)",
description: "",
dapp_url: "https://\(appDomain)",
image_url: nil,
notificationTypes: []
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,37 @@ class NotifySubscriptionsBuilder {
var result = [NotifySubscription]()

for subscription in notifyServerSubscriptions {
let scope = try await buildScope(selectedScope: subscription.scope, appDomain: subscription.appDomain)
guard let metadata = try? await notifyConfigProvider.getMetadata(appDomain: subscription.appDomain),
let topic = try? SymmetricKey(hex: subscription.symKey).derivedTopic() else { continue }

let notifySubscription = NotifySubscription(topic: topic,
account: subscription.account,
relay: RelayProtocolOptions(protocol: "irn", data: nil),
metadata: metadata,
scope: scope,
expiry: subscription.expiry,
symKey: subscription.symKey)
result.append(notifySubscription)
let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: subscription.appDomain)

do {
let topic = try SymmetricKey(hex: subscription.symKey).derivedTopic()
let scope = try await buildScope(selectedScope: subscription.scope, availableScope: config.notificationTypes)

result.append(NotifySubscription(
topic: topic,
account: subscription.account,
relay: RelayProtocolOptions(protocol: "irn", data: nil),
metadata: config.metadata,
scope: scope,
expiry: subscription.expiry,
symKey: subscription.symKey
))
} catch {
continue
}
}

return result
}

private func buildScope(selectedScope: [String], appDomain: String) async throws -> [String: ScopeValue] {
let availableScope = try await notifyConfigProvider.getSubscriptionScope(appDomain: appDomain)
private func buildScope(selectedScope: [String], availableScope: [NotifyConfig.NotificationType]) async throws -> [String: ScopeValue] {
return availableScope.reduce(into: [:]) {
$0[$1.name] = ScopeValue(description: $1.description, enabled: selectedScope.contains($1.name))
$0[$1.id] = ScopeValue(
id: $1.id,
name: $1.name,
description: $1.description,
enabled: selectedScope.contains($1.id)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class NotifySubscribeRequester {
private let kms: KeyManagementService
private let logger: ConsoleLogging
private let webDidResolver: NotifyWebDidResolver
private let dappsMetadataStore: CodableStore<AppMetadata>
private let notifyConfigProvider: NotifyConfigProvider

init(keyserverURL: URL,
Expand All @@ -22,33 +21,29 @@ class NotifySubscribeRequester {
logger: ConsoleLogging,
kms: KeyManagementService,
webDidResolver: NotifyWebDidResolver,
notifyConfigProvider: NotifyConfigProvider,
dappsMetadataStore: CodableStore<AppMetadata>
notifyConfigProvider: NotifyConfigProvider
) {
self.keyserverURL = keyserverURL
self.identityClient = identityClient
self.networkingInteractor = networkingInteractor
self.logger = logger
self.kms = kms
self.webDidResolver = webDidResolver
self.dappsMetadataStore = dappsMetadataStore
self.notifyConfigProvider = notifyConfigProvider
}

@discardableResult func subscribe(appDomain: String, account: Account) async throws -> NotifySubscriptionPayload.Wrapper {

logger.debug("Subscribing for Notify, dappUrl: \(appDomain)")

let metadata = try await notifyConfigProvider.getMetadata(appDomain: appDomain)
let config = try await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain)

let peerPublicKey = try await webDidResolver.resolveAgreementKey(domain: metadata.url)
let peerPublicKey = try await webDidResolver.resolveAgreementKey(domain: appDomain)
let subscribeTopic = peerPublicKey.rawRepresentation.sha256().toHexString()

let keysY = try generateAgreementKeys(peerPublicKey: peerPublicKey)

let responseTopic = keysY.derivedTopic()

dappsMetadataStore.set(metadata, forKey: responseTopic)

try kms.setSymmetricKey(keysY.sharedKey, for: subscribeTopic)
try kms.setAgreementSecret(keysY, topic: responseTopic)
Expand Down Expand Up @@ -80,10 +75,15 @@ class NotifySubscribeRequester {
}

private func createJWTWrapper(dappPubKey: DIDKey, subscriptionAccount: Account, appDomain: String) async throws -> NotifySubscriptionPayload.Wrapper {
let types = try await notifyConfigProvider.getSubscriptionScope(appDomain: appDomain)
let scope = types.map{$0.name}.joined(separator: " ")
let config = await notifyConfigProvider.resolveNotifyConfig(appDomain: appDomain)
let app = DIDWeb(host: appDomain)
let jwtPayload = NotifySubscriptionPayload(dappPubKey: dappPubKey, keyserver: keyserverURL, subscriptionAccount: subscriptionAccount, app: app, scope: scope)
let jwtPayload = NotifySubscriptionPayload(
dappPubKey: dappPubKey,
keyserver: keyserverURL,
subscriptionAccount: subscriptionAccount,
app: app,
scope: config.notificationTypes.map { $0.id }.joined(separator: " ")
)
return try identityClient.signAndCreateWrapper(
payload: jwtPayload,
account: subscriptionAccount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@ class NotifySubscribeResponseSubscriber {
private let logger: ConsoleLogging
private let notifyStorage: NotifyStorage
private let groupKeychainStorage: KeychainStorageProtocol
private let dappsMetadataStore: CodableStore<AppMetadata>
private let notifyConfigProvider: NotifyConfigProvider

init(networkingInteractor: NetworkInteracting,
kms: KeyManagementServiceProtocol,
logger: ConsoleLogging,
groupKeychainStorage: KeychainStorageProtocol,
notifyStorage: NotifyStorage,
dappsMetadataStore: CodableStore<AppMetadata>,
notifyConfigProvider: NotifyConfigProvider
) {
self.networkingInteractor = networkingInteractor
self.kms = kms
self.logger = logger
self.groupKeychainStorage = groupKeychainStorage
self.notifyStorage = notifyStorage
self.dappsMetadataStore = dappsMetadataStore
self.notifyConfigProvider = notifyConfigProvider
subscribeForSubscriptionResponse()
}
Expand Down
1 change: 1 addition & 0 deletions Sources/WalletConnectNotify/Notify.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public class Notify {
}
Push.configure(pushHost: config.pushHost, environment: config.environment)
return NotifyClientFactory.create(
projectId: Networking.projectId,
groupIdentifier: config.groupIdentifier,
networkInteractor: Networking.interactor,
pairingRegisterer: Pair.registerer,
Expand Down
1 change: 0 additions & 1 deletion Sources/WalletConnectNotify/NotifyStorageIdntifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,5 @@ enum NotifyStorageIdntifiers {
static let notifySubscription = "com.walletconnect.notify.notifySubscription"

static let notifyMessagesRecords = "com.walletconnect.sdk.notifyMessagesRecords"
static let dappsMetadataStore = "com.walletconnect.sdk.dappsMetadataStore"
static let coldStartStore = "com.walletconnect.sdk.coldStartStore"
}
Loading