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

Enable and update ton connect. #6055

Merged
merged 2 commits into from
Dec 17, 2024
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
46 changes: 42 additions & 4 deletions UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,19 +134,19 @@ public struct Client: APIProtocol {
switch response.status.code {
case 200:
let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
let body: Components.Responses.Response.Body
let bodyPayload: BodyUniversalData
if try contentType == nil
|| converter.isMatchingContentType(received: contentType, expectedRaw: "application/json")
{
body = try await converter.getResponseBodyAsJSON(
Components.Responses.Response.Body.jsonPayload.self,
bodyPayload = try await converter.getResponseBodyAsJSON(
JsonPayload.self,
from: responseBody,
transforming: { value in .json(value) }
)
} else {
throw converter.makeUnexpectedContentTypeError(contentType: contentType)
}
return .ok(.init(body: body))
return .ok(.init(body: .json(try bodyPayload.json)))
default:
let contentType = converter.extractContentTypeIfPresent(in: response.headerFields)
let body: Components.Responses.Response.Body
Expand All @@ -167,3 +167,41 @@ public struct Client: APIProtocol {
)
}
}

enum BodyUniversalData: Sendable, Hashable {

case json(JsonPayload)
public var json: Components.Responses.Response.Body.jsonPayload {
get throws {
switch self {
case let .json(body):
guard let message = body.message ?? body.status else {
throw ParsingError.cantParseBody
}
return .init(message: message, statusCode: body.statusCode ?? 200)
}
}
}
}

public struct JsonPayload: Codable, Hashable, Sendable {
public var message: String?
public var status: String?
public var statusCode: Int64?

public init(status: Swift.String?, message: String?, statusCode: Int64?) {
self.status = status
self.message = message
self.statusCode = statusCode
}

public enum CodingKeys: String, CodingKey {
case status
case message
case statusCode
}
}

public enum ParsingError: Error {
case cantParseBody
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class AddressUriParser {
}

// try to parse ton deeplink
if scheme == DeepLinkManager.tonDeepLinkScheme, let tonScheme = BlockchainType.ton.uriScheme {
if scheme == DeepLinkManager.deepLinkScheme, let tonScheme = BlockchainType.ton.uriScheme {
var uri = AddressUri(scheme: tonScheme)
uri.address = components.path.stripping(prefix: "/")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import RxSwift
class DeepLinkManager {
static let deepLinkScheme = "unstoppable.money"
static let tonDeepLinkScheme = "ton"
static let tonUniversalHost = "ton-connect"
static let tonDeepLinkHost = "tc"

private let newSchemeRelay = BehaviorRelay<DeepLink?>(value: nil)
}
Expand All @@ -33,6 +35,13 @@ extension DeepLinkManager {
return true
}

if ((scheme == DeepLinkManager.deepLinkScheme && (host == Self.tonDeepLinkHost || host == Self.tonUniversalHost)) ||
(scheme == "https" && host == Self.deepLinkScheme && path == "/\(Self.tonUniversalHost)")),
let parameters = try? TonConnectManager.parseParameters(queryItems: queryItems) {
newSchemeRelay.accept(.tonConnect(parameters: parameters))
return true
}

if scheme == Self.tonDeepLinkScheme {
let parser = AddressParserFactory.parser(blockchainType: .ton, tokenType: nil)
do {
Expand Down Expand Up @@ -74,6 +83,7 @@ extension DeepLinkManager {
extension DeepLinkManager {
enum DeepLink {
case walletConnect(url: String)
case tonConnect(parameters: TonConnectParameters)
case coin(uid: String)
case transfer(addressUri: AddressUri)
case referral(telegramUserId: String, referralCode: String)
Expand Down
1 change: 1 addition & 0 deletions UnstoppableWallet/UnstoppableWallet/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechrome</string>
<string>tg</string>
<string>twitter</string>
<string>cydia</string>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ enum MainModule {
let tonConnectHandler = TonConnectEventHandler(parentViewController: viewController)

eventHandler.append(handler: deepLinkHandler)
// eventHandler.append(handler: tonConnectHandler)
eventHandler.append(handler: tonConnectHandler)
eventHandler.append(handler: widgetCoinHandler)
eventHandler.append(handler: sendAddressHandler)
eventHandler.append(handler: telegramUserHandler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,15 +262,15 @@ class MainSettingsViewController: ThemeViewController {
self?.viewModel.onTapWalletConnect()
}
),
// StaticRow(
// cell: tonConnectCell,
// id: "ton-connect",
// height: .heightCell48,
// autoDeselect: true,
// action: { [weak self] in
// self?.onTapTonConnect()
// }
// ),
StaticRow(
cell: tonConnectCell,
id: "ton-connect",
height: .heightCell48,
autoDeselect: true,
action: { [weak self] in
self?.onTapTonConnect()
}
),
tableView.universalRow48(
id: "backup-manager",
image: .local(UIImage(named: "icloud_24")),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ enum TonConnect {

struct DeviceInfo: Encodable {
let platform = "iphone"
let appName = "Tonkeeper"
let appVersion = "3.4.0"
// let appName = AppConfig.appName
// let appVersion = AppConfig.appVersion
let appName = "Unstoppable Wallet"
let appVersion = AppConfig.appVersion
let maxProtocolVersion = 2
let features = [
FeatureCompatible.legacy(Feature()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ struct TonConnectConnectView: View {

@State private var selectAccountPresented = false

init(config: TonConnectConfig) {
_viewModel = StateObject(wrappedValue: TonConnectConnectViewModel(config: config))
init(config: TonConnectConfig, returnDeepLink: String? = nil) {
_viewModel = StateObject(wrappedValue: TonConnectConnectViewModel(config: config, returnDeepLink: returnDeepLink))
}

var body: some View {
Expand Down Expand Up @@ -91,6 +91,10 @@ struct TonConnectConnectView: View {
}
.onReceive(viewModel.finishPublisher) {
presentationMode.wrappedValue.dismiss()

if let deeplink = viewModel.returnDeepLink, let url = URL(string: deeplink), UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url)
}
}
.navigationTitle("TON Connect")
.navigationBarTitleDisplayMode(.inline)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Foundation
class TonConnectConnectViewModel: ObservableObject {
private let parameters: TonConnectParameters
let manifest: TonConnectManifest
let returnDeepLink: String?

private let tonConnectManager = App.shared.tonConnectManager
private let accountManager = App.shared.accountManager
Expand All @@ -13,9 +14,10 @@ class TonConnectConnectViewModel: ObservableObject {

private let finishSubject = PassthroughSubject<Void, Never>()

init(config: TonConnectConfig) {
init(config: TonConnectConfig, returnDeepLink: String?) {
parameters = config.parameters
manifest = config.manifest
self.returnDeepLink = returnDeepLink

eligibleAccounts = accountManager.accounts.filter(\.type.supportsTonConnect).sorted { $0.name < $1.name }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,25 @@ class TonConnectEventHandler {

extension TonConnectEventHandler: IEventHandler {
func handle(source _: StatPage, event: Any, eventType _: EventHandler.EventType) async throws {
guard let deeplink = event as? String else {
throw EventHandler.HandleError.noSuitableHandler
var config: TonConnectConfig?
let returnDeepLink: String?

if case let .tonConnect(parameters) = event as? DeepLinkManager.DeepLink {
config = try await tonConnectManager.loadTonConnectConfiguration(parameters: parameters)
returnDeepLink = parameters.returnDeepLink
} else if let deeplink = event as? String {
config = try await tonConnectManager.loadTonConnectConfiguration(deeplink: deeplink)
returnDeepLink = nil
} else {
returnDeepLink = nil
}

let config = try await tonConnectManager.loadTonConnectConfiguration(deeplink: deeplink)

guard let config = config else {
throw EventHandler.HandleError.noSuitableHandler
}

await MainActor.run { [weak self] in
let view = TonConnectConnectView(config: config)
let view = TonConnectConnectView(config: config, returnDeepLink: returnDeepLink)
self?.parentViewController?.visibleController.present(view.toViewController(), animated: true)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class TonConnectManager {
configuration.timeoutIntervalForResource = TimeInterval(Int.max)

apiClient = TonConnectAPI.Client(
serverURL: (try? TonConnectAPI.Servers.server1()) ?? URL(string: "https://bridge.tonapi.io/bridge")!,
serverURL: URL(string: "https://bridge.unstoppable.money/bridge")!,
transport: StreamURLSessionTransport(urlSessionConfiguration: configuration),
middlewares: []
)
Expand Down Expand Up @@ -165,19 +165,12 @@ class TonConnectManager {
guard
let url = URL(string: deeplink),
let components = URLComponents(url: url, resolvingAgainstBaseURL: true),
components.scheme == .tcScheme,
let queryItems = components.queryItems,
let versionValue = queryItems.first(where: { $0.name == .versionKey })?.value,
let version = TonConnectParameters.Version(rawValue: versionValue),
let clientId = queryItems.first(where: { $0.name == .clientIdKey })?.value,
let requestPayloadValue = queryItems.first(where: { $0.name == .requestPayloadKey })?.value,
let requestPayloadData = requestPayloadValue.data(using: .utf8),
let requestPayload = try? JSONDecoder().decode(TonConnectRequestPayload.self, from: requestPayloadData)
components.scheme == .httpsScheme || components.scheme == .tcScheme
else {
throw ServiceError.incorrectUrl
}

return TonConnectParameters(version: version, clientId: clientId, requestPayload: requestPayload)
return try TonConnectManager.parseParameters(queryItems: components.queryItems)
}

private func loadManifest(url: URL) async throws -> TonConnectManifest {
Expand All @@ -195,11 +188,11 @@ class TonConnectManager {

let encrypted = try sessionCrypto.encrypt(message: encoded, receiverPublicKey: receiverPublicKey)

_ = try await apiClient.message(
let _ = try await apiClient.message(
query: .init(client_id: sessionCrypto.sessionId, to: clientId, ttl: 300),
body: .plainText(.init(stringLiteral: encrypted.base64EncodedString()))
)

// _ = try resp.ok.body.json
}

Expand All @@ -220,9 +213,7 @@ extension TonConnectManager {
sendTransactionRequestSubject.eraseToAnyPublisher()
}

func loadTonConnectConfiguration(deeplink: String) async throws -> TonConnectConfig {
let parameters = try parseTonConnect(deeplink: deeplink)

func loadTonConnectConfiguration(parameters: TonConnectParameters) async throws -> TonConnectConfig {
do {
let manifest = try await loadManifest(url: parameters.requestPayload.manifestUrl)
return TonConnectConfig(parameters: parameters, manifest: manifest)
Expand All @@ -231,6 +222,12 @@ extension TonConnectManager {
}
}

func loadTonConnectConfiguration(deeplink: String) async throws -> TonConnectConfig {
let parameters = try parseTonConnect(deeplink: deeplink)

return try await loadTonConnectConfiguration(parameters: parameters)
}

func connect(account: Account, parameters: TonConnectParameters, manifest: TonConnectManifest) async throws {
let (publicKey, secretKey) = try TonKitManager.keyPair(accountType: account.type)

Expand Down Expand Up @@ -271,6 +268,24 @@ extension TonConnectManager {
}
}

extension TonConnectManager {
static func parseParameters(queryItems: [URLQueryItem]?) throws -> TonConnectParameters {
guard let queryItems = queryItems,
let versionValue = queryItems.first(where: { $0.name == .versionKey })?.value,
let version = TonConnectParameters.Version(rawValue: versionValue),
let clientId = queryItems.first(where: { $0.name == .clientIdKey })?.value,
let requestPayloadValue = queryItems.first(where: { $0.name == .requestPayloadKey })?.value,
let requestPayloadData = requestPayloadValue.data(using: .utf8),
let requestPayload = try? JSONDecoder().decode(TonConnectRequestPayload.self, from: requestPayloadData)
else {
throw ServiceError.incorrectUrl
}

let returnDeepLink = queryItems.first(where: { $0.name == .returnDeepLink })?.value
return TonConnectParameters(version: version, clientId: clientId, requestPayload: requestPayload, ret: returnDeepLink)
}
}

extension TonConnectManager {
enum ServiceError: Error {
case incorrectUrl
Expand All @@ -291,9 +306,11 @@ extension TonConnectManager {

private extension String {
static let tcScheme = "tc"
static let httpsScheme = "https"
static let versionKey = "v"
static let clientIdKey = "id"
static let requestPayloadKey = "r"
static let returnDeepLink = "ret"
}

struct TonConnectSendTransactionRequest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ struct TonConnectParameters {
let version: Version
let clientId: String
let requestPayload: TonConnectRequestPayload
let returnDeepLink: String?

init(version: Version, clientId: String, requestPayload: TonConnectRequestPayload) {
init(version: Version, clientId: String, requestPayload: TonConnectRequestPayload, ret: String? = nil) {
self.version = version
self.clientId = clientId
self.requestPayload = requestPayload
self.returnDeepLink = ret
}
}

Expand Down
Loading