diff --git a/UnstoppableWallet/TonConnectAPI/.gitignore b/UnstoppableWallet/TonConnectAPI/.gitignore new file mode 100644 index 0000000000..3b29812086 --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/UnstoppableWallet/TonConnectAPI/Generation/Package.swift b/UnstoppableWallet/TonConnectAPI/Generation/Package.swift new file mode 100644 index 0000000000..9e16fe7893 --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/Generation/Package.swift @@ -0,0 +1,15 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TonAPI", + platforms: [ + .macOS(.v12), .iOS(.v13) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-generator", .upToNextMinor(from: "0.3.0")), + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + ] +) diff --git a/UnstoppableWallet/TonConnectAPI/Generation/generate_api.sh b/UnstoppableWallet/TonConnectAPI/Generation/generate_api.sh new file mode 100644 index 0000000000..39931db21c --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/Generation/generate_api.sh @@ -0,0 +1,5 @@ +#!/bin/bash +swift run swift-openapi-generator generate \ + --mode types --mode client \ + --output-directory ../Sources/TonConnectAPI\ + ./openapi.yml \ No newline at end of file diff --git a/UnstoppableWallet/TonConnectAPI/Generation/openapi.yml b/UnstoppableWallet/TonConnectAPI/Generation/openapi.yml new file mode 100644 index 0000000000..44decf6157 --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/Generation/openapi.yml @@ -0,0 +1,93 @@ +openapi: 3.0.2 +info: + title: TonConnect + version: 1.0.0 +servers: + - url: "https://bridge.tonapi.io/bridge" +paths: + /events: + get: + operationId: events + parameters: + - $ref: '#/components/parameters/clientIdParameter' + - name: last_event_id + in: query + required: false + schema: + type: string + responses: + "200": + description: OK + content: + text/event-stream: + schema: + type: string + format: binary + /message: + post: + operationId: message + parameters: + - name: client_id + in: query + required: true + schema: + type: string + - name: to + in: query + required: true + schema: + type: string + - name: ttl + in: query + required: true + schema: + type: integer + format: int64 + requestBody: + required: true + content: + text/plain: + schema: + type: string + contentEncoding: base64 + responses: + '200': + $ref: '#/components/responses/Response' + 'default': + $ref: '#/components/responses/Response' +components: + parameters: + clientIdParameter: + in: query + name: client_id + required: true + explode: false + schema: + type: array + items: + type: string + toParameter: + in: query + name: to + required: true + explode: false + schema: + type: array + items: + type: string + responses: + Response: + description: OK + content: + application/json: + schema: + type: object + required: + - message + - statusCode + properties: + message: + type: string + statusCode: + type: integer + format: int64 \ No newline at end of file diff --git a/UnstoppableWallet/TonConnectAPI/Package.swift b/UnstoppableWallet/TonConnectAPI/Package.swift new file mode 100644 index 0000000000..315fe29e01 --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.7 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "TonConnectAPI", + platforms: [ + .macOS(.v12), .iOS(.v13) + ], + products: [ + .library(name: "TonConnectAPI", targets: ["TonConnectAPI"]) + ], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.3.0")), + ], + targets: [ + .target(name: "TonConnectAPI", + dependencies: [ + .product( + name: "OpenAPIRuntime", + package: "swift-openapi-runtime" + ) + ], + path: "Sources", + sources: ["TonConnectAPI"] + ) + ] +) diff --git a/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift b/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift new file mode 100644 index 0000000000..986555708e --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Client.swift @@ -0,0 +1,166 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +import HTTPTypes +public struct Client: APIProtocol { + /// The underlying HTTP client. + private let client: UniversalClient + /// Creates a new client. + /// - Parameters: + /// - serverURL: The server URL that the client connects to. Any server + /// URLs defined in the OpenAPI document are available as static methods + /// on the ``Servers`` type. + /// - configuration: A set of configuration values for the client. + /// - transport: A transport that performs HTTP operations. + /// - middlewares: A list of middlewares to call before the transport. + public init( + serverURL: Foundation.URL, + configuration: Configuration = .init(), + transport: any ClientTransport, + middlewares: [any ClientMiddleware] = [] + ) { + self.client = .init( + serverURL: serverURL, + configuration: configuration, + transport: transport, + middlewares: middlewares + ) + } + private var converter: Converter { client.converter } + /// - Remark: HTTP `GET /events`. + /// - Remark: Generated from `#/paths//events/get(events)`. + public func events(_ input: Operations.events.Input) async throws -> Operations.events.Output { + try await client.send( + input: input, + forOperation: Operations.events.id, + serializer: { input in + let path = try converter.renderedPath(template: "/events", parameters: []) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .get) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsURI( + in: &request, + style: .form, + explode: true, + name: "client_id", + value: input.query.client_id + ) + try converter.setQueryItemAsURI( + in: &request, + style: .form, + explode: true, + name: "last_event_id", + value: input.query.last_event_id + ) + converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) + return (request, nil) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.events.Output.Ok.Body + if try contentType == nil + || converter.isMatchingContentType(received: contentType, expectedRaw: "text/event-stream") + { + body = try converter.getResponseBodyAsBinary( + OpenAPIRuntime.HTTPBody.self, + from: responseBody, + transforming: { value in .text_event_hyphen_stream(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .ok(.init(body: body)) + default: return .undocumented(statusCode: response.status.code, .init()) + } + } + ) + } + /// - Remark: HTTP `POST /message`. + /// - Remark: Generated from `#/paths//message/post(message)`. + public func message(_ input: Operations.message.Input) async throws -> Operations.message.Output { + try await client.send( + input: input, + forOperation: Operations.message.id, + serializer: { input in + let path = try converter.renderedPath(template: "/message", parameters: []) + var request: HTTPTypes.HTTPRequest = .init(soar_path: path, method: .post) + suppressMutabilityWarning(&request) + try converter.setQueryItemAsURI( + in: &request, + style: .form, + explode: true, + name: "client_id", + value: input.query.client_id + ) + try converter.setQueryItemAsURI( + in: &request, + style: .form, + explode: true, + name: "to", + value: input.query.to + ) + try converter.setQueryItemAsURI( + in: &request, + style: .form, + explode: true, + name: "ttl", + value: input.query.ttl + ) + converter.setAcceptHeader(in: &request.headerFields, contentTypes: input.headers.accept) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .plainText(value): + body = try converter.setRequiredRequestBodyAsBinary( + value, + headerFields: &request.headerFields, + contentType: "text/plain" + ) + } + return (request, body) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Components.Responses.Response.Body + if try contentType == nil + || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") + { + body = try await converter.getResponseBodyAsJSON( + Components.Responses.Response.Body.jsonPayload.self, + from: responseBody, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .ok(.init(body: body)) + default: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Components.Responses.Response.Body + if try contentType == nil + || converter.isMatchingContentType(received: contentType, expectedRaw: "application/json") + { + body = try await converter.getResponseBodyAsJSON( + Components.Responses.Response.Body.jsonPayload.self, + from: responseBody, + transforming: { value in .json(value) } + ) + } else { + throw converter.makeUnexpectedContentTypeError(contentType: contentType) + } + return .`default`(statusCode: response.status.code, .init(body: body)) + } + } + ) + } +} diff --git a/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Types.swift b/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Types.swift new file mode 100644 index 0000000000..e230c90f9d --- /dev/null +++ b/UnstoppableWallet/TonConnectAPI/Sources/TonConnectAPI/Types.swift @@ -0,0 +1,339 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +/// A type that performs HTTP operations defined by the OpenAPI document. +public protocol APIProtocol: Sendable { + /// - Remark: HTTP `GET /events`. + /// - Remark: Generated from `#/paths//events/get(events)`. + func events(_ input: Operations.events.Input) async throws -> Operations.events.Output + /// - Remark: HTTP `POST /message`. + /// - Remark: Generated from `#/paths//message/post(message)`. + func message(_ input: Operations.message.Input) async throws -> Operations.message.Output +} +/// Convenience overloads for operation inputs. +extension APIProtocol { + /// - Remark: HTTP `GET /events`. + /// - Remark: Generated from `#/paths//events/get(events)`. + public func events(query: Operations.events.Input.Query, headers: Operations.events.Input.Headers = .init()) + async throws -> Operations.events.Output + { try await events(Operations.events.Input(query: query, headers: headers)) } + /// - Remark: HTTP `POST /message`. + /// - Remark: Generated from `#/paths//message/post(message)`. + public func message( + query: Operations.message.Input.Query, + headers: Operations.message.Input.Headers = .init(), + body: Operations.message.Input.Body + ) async throws -> Operations.message.Output { + try await message(Operations.message.Input(query: query, headers: headers, body: body)) + } +} +/// Server URLs defined in the OpenAPI document. +public enum Servers { + public static func server1() throws -> Foundation.URL { + try Foundation.URL(validatingOpenAPIServerURL: "https://bridge.tonapi.io/bridge") + } +} +/// Types generated from the components section of the OpenAPI document. +public enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + public enum Schemas {} + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + public enum Parameters { + /// - Remark: Generated from `#/components/parameters/clientIdParameter`. + public typealias clientIdParameter = [Swift.String] + /// - Remark: Generated from `#/components/parameters/toParameter`. + public typealias toParameter = [Swift.String] + } + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + public enum RequestBodies {} + /// Types generated from the `#/components/responses` section of the OpenAPI document. + public enum Responses { + public struct Response: Sendable, Hashable { + /// - Remark: Generated from `#/components/responses/Response/content`. + @frozen public enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/components/responses/Response/content/json`. + public struct jsonPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/responses/Response/content/json/message`. + public var message: Swift.String + /// - Remark: Generated from `#/components/responses/Response/content/json/statusCode`. + public var statusCode: Swift.Int64 + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - message: + /// - statusCode: + public init(message: Swift.String, statusCode: Swift.Int64) { + self.message = message + self.statusCode = statusCode + } + public enum CodingKeys: String, CodingKey { + case message + case statusCode + } + } + /// - Remark: Generated from `#/components/responses/Response/content/application\/json`. + case json(Components.Responses.Response.Body.jsonPayload) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + public var json: Components.Responses.Response.Body.jsonPayload { + get throws { + switch self { + case let .json(body): return body + } + } + } + } + /// Received HTTP response body + public var body: Components.Responses.Response.Body + /// Creates a new `Response`. + /// + /// - Parameters: + /// - body: Received HTTP response body + public init(body: Components.Responses.Response.Body) { self.body = body } + } + } + /// Types generated from the `#/components/headers` section of the OpenAPI document. + public enum Headers {} +} +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +public enum Operations { + /// - Remark: HTTP `GET /events`. + /// - Remark: Generated from `#/paths//events/get(events)`. + public enum events { + public static let id: Swift.String = "events" + public struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/events/GET/query`. + public struct Query: Sendable, Hashable { + /// - Remark: Generated from `#/paths/events/GET/query/client_id`. + public var client_id: Components.Parameters.clientIdParameter + /// - Remark: Generated from `#/paths/events/GET/query/last_event_id`. + public var last_event_id: Swift.String? + /// Creates a new `Query`. + /// + /// - Parameters: + /// - client_id: + /// - last_event_id: + public init(client_id: Components.Parameters.clientIdParameter, last_event_id: Swift.String? = nil) { + self.client_id = client_id + self.last_event_id = last_event_id + } + } + public var query: Operations.events.Input.Query + /// - Remark: Generated from `#/paths/events/GET/header`. + public struct Headers: Sendable, Hashable { + public var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType] = + .defaultValues() + ) { self.accept = accept } + } + public var headers: Operations.events.Input.Headers + /// Creates a new `Input`. + /// + /// - Parameters: + /// - query: + /// - headers: + public init(query: Operations.events.Input.Query, headers: Operations.events.Input.Headers = .init()) { + self.query = query + self.headers = headers + } + } + @frozen public enum Output: Sendable, Hashable { + public struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/events/GET/responses/200/content`. + @frozen public enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/events/GET/responses/200/content/text\/event-stream`. + case text_event_hyphen_stream(OpenAPIRuntime.HTTPBody) + /// The associated value of the enum case if `self` is `.text_event_hyphen_stream`. + /// + /// - Throws: An error if `self` is not `.text_event_hyphen_stream`. + /// - SeeAlso: `.text_event_hyphen_stream`. + public var text_event_hyphen_stream: OpenAPIRuntime.HTTPBody { + get throws { + switch self { + case let .text_event_hyphen_stream(body): return body + } + } + } + } + /// Received HTTP response body + public var body: Operations.events.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + public init(body: Operations.events.Output.Ok.Body) { self.body = body } + } + /// OK + /// + /// - Remark: Generated from `#/paths//events/get(events)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.events.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + public var ok: Operations.events.Output.Ok { + get throws { + switch self { + case let .ok(response): return response + default: try throwUnexpectedResponseStatus(expectedStatus: "ok", response: self) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case text_event_hyphen_stream + case other(Swift.String) + public init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "text/event-stream": self = .text_event_hyphen_stream + default: self = .other(rawValue) + } + } + public var rawValue: Swift.String { + switch self { + case let .other(string): return string + case .text_event_hyphen_stream: return "text/event-stream" + } + } + public static var allCases: [Self] { [.text_event_hyphen_stream] } + } + } + /// - Remark: HTTP `POST /message`. + /// - Remark: Generated from `#/paths//message/post(message)`. + public enum message { + public static let id: Swift.String = "message" + public struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/message/POST/query`. + public struct Query: Sendable, Hashable { + /// - Remark: Generated from `#/paths/message/POST/query/client_id`. + public var client_id: Swift.String + /// - Remark: Generated from `#/paths/message/POST/query/to`. + public var to: Swift.String + /// - Remark: Generated from `#/paths/message/POST/query/ttl`. + public var ttl: Swift.Int64 + /// Creates a new `Query`. + /// + /// - Parameters: + /// - client_id: + /// - to: + /// - ttl: + public init(client_id: Swift.String, to: Swift.String, ttl: Swift.Int64) { + self.client_id = client_id + self.to = to + self.ttl = ttl + } + } + public var query: Operations.message.Input.Query + /// - Remark: Generated from `#/paths/message/POST/header`. + public struct Headers: Sendable, Hashable { + public var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + public init( + accept: [OpenAPIRuntime.AcceptHeaderContentType] = + .defaultValues() + ) { self.accept = accept } + } + public var headers: Operations.message.Input.Headers + /// - Remark: Generated from `#/paths/message/POST/requestBody`. + @frozen public enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/message/POST/requestBody/content/text\/plain`. + case plainText(OpenAPIRuntime.HTTPBody) + } + public var body: Operations.message.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - query: + /// - headers: + /// - body: + public init( + query: Operations.message.Input.Query, + headers: Operations.message.Input.Headers = .init(), + body: Operations.message.Input.Body + ) { + self.query = query + self.headers = headers + self.body = body + } + } + @frozen public enum Output: Sendable, Hashable { + /// OK + /// + /// - Remark: Generated from `#/paths//message/post(message)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Components.Responses.Response) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + public var ok: Components.Responses.Response { + get throws { + switch self { + case let .ok(response): return response + default: try throwUnexpectedResponseStatus(expectedStatus: "ok", response: self) + } + } + } + /// OK + /// + /// - Remark: Generated from `#/paths//message/post(message)/responses/default`. + /// + /// HTTP response code: `default`. + case `default`(statusCode: Swift.Int, Components.Responses.Response) + /// The associated value of the enum case if `self` is `.`default``. + /// + /// - Throws: An error if `self` is not `.`default``. + /// - SeeAlso: `.`default``. + public var `default`: Components.Responses.Response { + get throws { + switch self { + case let .`default`(_, response): return response + default: try throwUnexpectedResponseStatus(expectedStatus: "default", response: self) + } + } + } + } + @frozen public enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + public init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": self = .json + default: self = .other(rawValue) + } + } + public var rawValue: Swift.String { + switch self { + case let .other(string): return string + case .json: return "application/json" + } + } + public static var allCases: [Self] { [.json] } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj index d4efe14e9f..4710bb3c6e 100644 --- a/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj +++ b/UnstoppableWallet/UnstoppableWallet.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ @@ -2672,6 +2672,14 @@ D0F132A82B6B990500C7310E /* RbfDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F132A62B6B990500C7310E /* RbfDataSource.swift */; }; D0F9F5172B99857700C3190A /* FeeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9F5162B99857700C3190A /* FeeSettings.swift */; }; D0F9F5182B99857700C3190A /* FeeSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0F9F5162B99857700C3190A /* FeeSettings.swift */; }; + D30D7E5F2CAAC89700B8CAA7 /* TonConnectEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E5E2CAAC89700B8CAA7 /* TonConnectEventHandler.swift */; }; + D30D7E602CAAC89700B8CAA7 /* TonConnectEventHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E5E2CAAC89700B8CAA7 /* TonConnectEventHandler.swift */; }; + D30D7E622CAACCF300B8CAA7 /* TonConnectSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E612CAACCF300B8CAA7 /* TonConnectSendView.swift */; }; + D30D7E632CAACCF300B8CAA7 /* TonConnectSendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E612CAACCF300B8CAA7 /* TonConnectSendView.swift */; }; + D30D7E652CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E642CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift */; }; + D30D7E662CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E642CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift */; }; + D30D7E682CAACD0400B8CAA7 /* TonConnectSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E672CAACD0400B8CAA7 /* TonConnectSendHandler.swift */; }; + D30D7E692CAACD0400B8CAA7 /* TonConnectSendHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D7E672CAACD0400B8CAA7 /* TonConnectSendHandler.swift */; }; D311DA1C2BD114B00013DB8F /* MarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1B2BD114B00013DB8F /* MarketView.swift */; }; D311DA1D2BD114B00013DB8F /* MarketView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1B2BD114B00013DB8F /* MarketView.swift */; }; D311DA1F2BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D311DA1E2BD115240013DB8F /* MarketGlobalViewModel.swift */; }; @@ -2772,6 +2780,38 @@ D3604E9C28F03DC00066C366 /* FeeRateKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9B28F03DC00066C366 /* FeeRateKit */; }; D3604E9E28F03DC00066C366 /* BinanceChainKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9D28F03DC00066C366 /* BinanceChainKit */; }; D3604EA028F03DC00066C366 /* DashKit in Frameworks */ = {isa = PBXBuildFile; productRef = D3604E9F28F03DC00066C366 /* DashKit */; }; + D362879F2CA2B34E00ADAF3B /* TonConnectListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D362879E2CA2B34E00ADAF3B /* TonConnectListView.swift */; }; + D36287A02CA2B34E00ADAF3B /* TonConnectListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D362879E2CA2B34E00ADAF3B /* TonConnectListView.swift */; }; + D36287A22CA2B35A00ADAF3B /* TonConnectListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287A12CA2B35A00ADAF3B /* TonConnectListViewModel.swift */; }; + D36287A32CA2B35A00ADAF3B /* TonConnectListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287A12CA2B35A00ADAF3B /* TonConnectListViewModel.swift */; }; + D36287A62CA2B89200ADAF3B /* TonConnectParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287A52CA2B89200ADAF3B /* TonConnectParameters.swift */; }; + D36287A72CA2B89200ADAF3B /* TonConnectParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287A52CA2B89200ADAF3B /* TonConnectParameters.swift */; }; + D36287A92CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287A82CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift */; }; + D36287AA2CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287A82CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift */; }; + D36287AC2CA2BA2700ADAF3B /* TonConnectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287AB2CA2BA2700ADAF3B /* TonConnectManager.swift */; }; + D36287AD2CA2BA2700ADAF3B /* TonConnectManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287AB2CA2BA2700ADAF3B /* TonConnectManager.swift */; }; + D36287AF2CA2BC9900ADAF3B /* TonConnectManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287AE2CA2BC9900ADAF3B /* TonConnectManifest.swift */; }; + D36287B02CA2BC9900ADAF3B /* TonConnectManifest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287AE2CA2BC9900ADAF3B /* TonConnectManifest.swift */; }; + D36287B22CA2CC8000ADAF3B /* TonConnectConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287B12CA2CC8000ADAF3B /* TonConnectConnectView.swift */; }; + D36287B32CA2CC8000ADAF3B /* TonConnectConnectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287B12CA2CC8000ADAF3B /* TonConnectConnectView.swift */; }; + D36287B52CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287B42CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift */; }; + D36287B62CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287B42CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift */; }; + D36287B82CA2D17F00ADAF3B /* TonConnectConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287B72CA2D17F00ADAF3B /* TonConnectConfig.swift */; }; + D36287B92CA2D17F00ADAF3B /* TonConnectConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287B72CA2D17F00ADAF3B /* TonConnectConfig.swift */; }; + D36287BB2CA3D50F00ADAF3B /* TonConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287BA2CA3D50F00ADAF3B /* TonConnect.swift */; }; + D36287BC2CA3D50F00ADAF3B /* TonConnect.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287BA2CA3D50F00ADAF3B /* TonConnect.swift */; }; + D36287BE2CA3D7B100ADAF3B /* TonConnect+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287BD2CA3D7B100ADAF3B /* TonConnect+Encodable.swift */; }; + D36287BF2CA3D7B100ADAF3B /* TonConnect+Encodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287BD2CA3D7B100ADAF3B /* TonConnect+Encodable.swift */; }; + D36287C12CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287C02CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift */; }; + D36287C22CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287C02CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift */; }; + D36287C42CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287C32CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift */; }; + D36287C52CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287C32CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift */; }; + D36287C82CA3DF7A00ADAF3B /* TonConnectAPI in Frameworks */ = {isa = PBXBuildFile; productRef = D36287C72CA3DF7A00ADAF3B /* TonConnectAPI */; }; + D36287CA2CA3DF8600ADAF3B /* TonConnectAPI in Frameworks */ = {isa = PBXBuildFile; productRef = D36287C92CA3DF8600ADAF3B /* TonConnectAPI */; }; + D36287CC2CA3E12100ADAF3B /* TonConnectApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287CB2CA3E12100ADAF3B /* TonConnectApp.swift */; }; + D36287CD2CA3E12100ADAF3B /* TonConnectApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287CB2CA3E12100ADAF3B /* TonConnectApp.swift */; }; + D36287CF2CA408E200ADAF3B /* TonConnectStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287CE2CA408E200ADAF3B /* TonConnectStorage.swift */; }; + D36287D02CA408E200ADAF3B /* TonConnectStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36287CE2CA408E200ADAF3B /* TonConnectStorage.swift */; }; D368F56B2844F45900F79777 /* AppIconAlternate.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D368F5662844F2C400F79777 /* AppIconAlternate.xcassets */; }; D368F56C2844F45A00F79777 /* AppIconAlternate.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D368F5662844F2C400F79777 /* AppIconAlternate.xcassets */; }; D36DE0AB272FD612000BC916 /* SwapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36DE0AA272FD612000BC916 /* SwapViewController.swift */; }; @@ -4519,6 +4559,10 @@ D0F132A32B6B98F500C7310E /* RbfViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfViewModel.swift; sourceTree = ""; }; D0F132A62B6B990500C7310E /* RbfDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RbfDataSource.swift; sourceTree = ""; }; D0F9F5162B99857700C3190A /* FeeSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeSettings.swift; sourceTree = ""; }; + D30D7E5E2CAAC89700B8CAA7 /* TonConnectEventHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectEventHandler.swift; sourceTree = ""; }; + D30D7E612CAACCF300B8CAA7 /* TonConnectSendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectSendView.swift; sourceTree = ""; }; + D30D7E642CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectSendViewModel.swift; sourceTree = ""; }; + D30D7E672CAACD0400B8CAA7 /* TonConnectSendHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectSendHandler.swift; sourceTree = ""; }; D311DA1B2BD114B00013DB8F /* MarketView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketView.swift; sourceTree = ""; }; D311DA1E2BD115240013DB8F /* MarketGlobalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketGlobalViewModel.swift; sourceTree = ""; }; D311DA212BD23C230013DB8F /* MarketAdvancedSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarketAdvancedSearchView.swift; sourceTree = ""; }; @@ -4567,6 +4611,21 @@ D350DDC92AE27E4A00CF1989 /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/AppWidget.strings; sourceTree = ""; }; D350DDCA2AE2818A00CF1989 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/AppWidget.intentdefinition; sourceTree = ""; }; D35B518821942E7A00504FBA /* TermsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TermsViewController.swift; sourceTree = ""; }; + D362879E2CA2B34E00ADAF3B /* TonConnectListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectListView.swift; sourceTree = ""; }; + D36287A12CA2B35A00ADAF3B /* TonConnectListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectListViewModel.swift; sourceTree = ""; }; + D36287A52CA2B89200ADAF3B /* TonConnectParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectParameters.swift; sourceTree = ""; }; + D36287A82CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectRequestPayload.swift; sourceTree = ""; }; + D36287AB2CA2BA2700ADAF3B /* TonConnectManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectManager.swift; sourceTree = ""; }; + D36287AE2CA2BC9900ADAF3B /* TonConnectManifest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectManifest.swift; sourceTree = ""; }; + D36287B12CA2CC8000ADAF3B /* TonConnectConnectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectConnectView.swift; sourceTree = ""; }; + D36287B42CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectConnectViewModel.swift; sourceTree = ""; }; + D36287B72CA2D17F00ADAF3B /* TonConnectConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectConfig.swift; sourceTree = ""; }; + D36287BA2CA3D50F00ADAF3B /* TonConnect.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnect.swift; sourceTree = ""; }; + D36287BD2CA3D7B100ADAF3B /* TonConnect+Encodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TonConnect+Encodable.swift"; sourceTree = ""; }; + D36287C02CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectResponseBuilder.swift; sourceTree = ""; }; + D36287C32CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectSessionCrypto.swift; sourceTree = ""; }; + D36287CB2CA3E12100ADAF3B /* TonConnectApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectApp.swift; sourceTree = ""; }; + D36287CE2CA408E200ADAF3B /* TonConnectStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TonConnectStorage.swift; sourceTree = ""; }; D368F5662844F2C400F79777 /* AppIconAlternate.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = AppIconAlternate.xcassets; sourceTree = ""; }; D36DE0AA272FD612000BC916 /* SwapViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapViewController.swift; sourceTree = ""; }; D36DE0AE272FD689000BC916 /* SwapModule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwapModule.swift; sourceTree = ""; }; @@ -4697,6 +4756,7 @@ D3E323C82AE7B8E400F73914 /* KeychainAccess in Frameworks */, 500F1D1327AA87D1002AA419 /* AlignedCollectionViewFlowLayout in Frameworks */, 6BDA29AB29D6F37C003847ED /* ECashKit in Frameworks */, + D36287C82CA3DF7A00ADAF3B /* TonConnectAPI in Frameworks */, D3604E8C28F03DBF0066C366 /* BitcoinCashKit in Frameworks */, D3993DC428F429AA008720FB /* UnstoppableDomainsResolution in Frameworks */, D3604E9E28F03DC00066C366 /* BinanceChainKit in Frameworks */, @@ -4744,6 +4804,7 @@ D3AF5A8929FFD85800C1399E /* RxCocoa in Frameworks */, 500F1D1127AA87BC002AA419 /* AlignedCollectionViewFlowLayout in Frameworks */, D3604E6928F02DF30066C366 /* BitcoinCashKit in Frameworks */, + D36287CA2CA3DF8600ADAF3B /* TonConnectAPI in Frameworks */, D3993DC228F42992008720FB /* UnstoppableDomainsResolution in Frameworks */, D3604E8528F03CDC0066C366 /* BinanceChainKit in Frameworks */, D3C187BA2907CFAB00FE1900 /* Checkpoints in Frameworks */, @@ -5438,6 +5499,7 @@ 11B354AB3C3435F2D5F39EE6 /* Modules */ = { isa = PBXGroup; children = ( + D362879D2CA2B32900ADAF3B /* TonConnect */, D311DA1A2BCD28C10013DB8F /* SendNew */, 11B35344CEA8CF35257A6975 /* Launch */, 11B35597F6C8570E3C7ABFBF /* Main */, @@ -8618,6 +8680,32 @@ path = Volume; sourceTree = ""; }; + D362879D2CA2B32900ADAF3B /* TonConnect */ = { + isa = PBXGroup; + children = ( + D362879E2CA2B34E00ADAF3B /* TonConnectListView.swift */, + D36287A12CA2B35A00ADAF3B /* TonConnectListViewModel.swift */, + D36287A52CA2B89200ADAF3B /* TonConnectParameters.swift */, + D36287A82CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift */, + D36287AB2CA2BA2700ADAF3B /* TonConnectManager.swift */, + D36287AE2CA2BC9900ADAF3B /* TonConnectManifest.swift */, + D36287B12CA2CC8000ADAF3B /* TonConnectConnectView.swift */, + D36287B42CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift */, + D36287B72CA2D17F00ADAF3B /* TonConnectConfig.swift */, + D36287BA2CA3D50F00ADAF3B /* TonConnect.swift */, + D36287BD2CA3D7B100ADAF3B /* TonConnect+Encodable.swift */, + D36287C02CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift */, + D36287C32CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift */, + D36287CB2CA3E12100ADAF3B /* TonConnectApp.swift */, + D36287CE2CA408E200ADAF3B /* TonConnectStorage.swift */, + D30D7E5E2CAAC89700B8CAA7 /* TonConnectEventHandler.swift */, + D30D7E612CAACCF300B8CAA7 /* TonConnectSendView.swift */, + D30D7E642CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift */, + D30D7E672CAACD0400B8CAA7 /* TonConnectSendHandler.swift */, + ); + path = TonConnect; + sourceTree = ""; + }; D36E50882BF7656E00C361BD /* Watchlist */ = { isa = PBXGroup; children = ( @@ -8824,6 +8912,7 @@ D08C93B02B91E3B400A7D1D5 /* Hodler */, 6BBCE4A22BDA419200ABBD55 /* Web3Wallet */, 6BDDB59B2C1816B500DE9D56 /* TonKit */, + D36287C72CA3DF7A00ADAF3B /* TonConnectAPI */, ); productName = Wallet; productReference = D38405CE218317DF007D50AD /* Unstoppable D.app */; @@ -8885,6 +8974,7 @@ D08C93AE2B91E39E00A7D1D5 /* Hodler */, 6BBCE4A42BDA419B00ABBD55 /* Web3Wallet */, 6BDDB5A02C1AC73D00DE9D56 /* TonKit */, + D36287C92CA3DF8600ADAF3B /* TonConnectAPI */, ); productName = Wallet; productReference = D38406BE21831B3D007D50AD /* Unstoppable.app */; @@ -9055,6 +9145,7 @@ D08C93AD2B91E37E00A7D1D5 /* XCRemoteSwiftPackageReference "Hodler" */, 6BF66DD82BA1A73300963242 /* XCRemoteSwiftPackageReference "ObjectMapper" */, 6BDDB59F2C1AC73200DE9D56 /* XCRemoteSwiftPackageReference "TonKit" */, + D36287C62CA3DF7A00ADAF3B /* XCLocalSwiftPackageReference "TonConnectAPI" */, ); productRefGroup = D3285F4320BD158E00644076 /* Products */; projectDirPath = ""; @@ -9270,6 +9361,7 @@ 11B3556C12B91FD86A72A193 /* LitecoinAdapter.swift in Sources */, D3447DEB25E38300009928D9 /* WalletConnectManager.swift in Sources */, 11B35854532EEA0AE6AC2010 /* RateAppManager.swift in Sources */, + D36287AF2CA2BC9900ADAF3B /* TonConnectManifest.swift in Sources */, 58AAA393DF93EFFA5047589B /* FeeSlider.swift in Sources */, 58AAA1283FC7F83F62FC5961 /* FeeSliderValueView.swift in Sources */, 58AAA0C0DB5C901FE58F5F9B /* FeeSliderWrapper.swift in Sources */, @@ -9295,11 +9387,13 @@ 11B35D835774B7F4E286BF32 /* AlertRouter.swift in Sources */, 11B35996BB3F179501DC0B08 /* BottomSheetTitleView.swift in Sources */, 1A5646F57C3677EBC462673C /* AppError.swift in Sources */, + D36287B32CA2CC8000ADAF3B /* TonConnectConnectView.swift in Sources */, 1A564E0B7DB0060B9600FCB1 /* BalanceErrorViewController.swift in Sources */, 1A5643A263F288A4E83409FA /* BalanceErrorViewModel.swift in Sources */, D03328A02BF6199600BBB364 /* InfoView.swift in Sources */, 1A564504E164177DD6EECFBA /* BalanceErrorService.swift in Sources */, 1A564168590F031AA453E1D1 /* BalanceErrorModule.swift in Sources */, + D36287A22CA2B35A00ADAF3B /* TonConnectListViewModel.swift in Sources */, 11B351FB99274553725754E4 /* GuidesModule.swift in Sources */, 11B35E94A7BCB0FEE8E144A9 /* GuidesViewModel.swift in Sources */, 11B3560E158C55624C466E27 /* GuidesViewController.swift in Sources */, @@ -9568,6 +9662,7 @@ D36DE0B4272FD689000BC916 /* SwapProviderManager.swift in Sources */, 11B357118379A537844A83D0 /* EvmSyncSource.swift in Sources */, 11B354CAD4BC4FAB3889838D /* EvmSyncSourceManager.swift in Sources */, + D36287B62CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift in Sources */, 11B3573B8B8DA5C1DE332EB6 /* EvmNetworkViewController.swift in Sources */, 11B3558589D57B3EAD53919F /* EvmNetworkViewModel.swift in Sources */, 11B351F04B82B33855E2CEBB /* EvmNetworkService.swift in Sources */, @@ -9575,6 +9670,7 @@ 11B352D006136B42D2705778 /* BalanceData.swift in Sources */, D36DE0DF272FD887000BC916 /* OneInchViewModel.swift in Sources */, 6BB14F732BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */, + D30D7E692CAACD0400B8CAA7 /* TonConnectSendHandler.swift in Sources */, D09200C6293F21720091981A /* RestoreNonStandardViewModel.swift in Sources */, 11B35F20127C070137781ED5 /* AddTokenModule.swift in Sources */, 11B35D722A70E8B4776AB5A8 /* AddBep2TokenBlockchainService.swift in Sources */, @@ -9619,6 +9715,7 @@ 11B352AE20E447BA1E9E890B /* SwitchAccountViewModel.swift in Sources */, 11B355CA37285348558E98A9 /* SwitchAccountViewController.swift in Sources */, 11B35448AE945A8647EF4856 /* SwitchAccountModule.swift in Sources */, + D36287BE2CA3D7B100ADAF3B /* TonConnect+Encodable.swift in Sources */, 58AAA1AAD335F236D130FCBB /* SwapConfirmationModule.swift in Sources */, 58AAAA3F2EF03D83A5500228 /* SwapConfirmationViewController.swift in Sources */, D0A6902C2C00ACF600E59296 /* CautionDataSource.swift in Sources */, @@ -9634,6 +9731,7 @@ 58AAA15A6F863CD3940FEBCC /* CoinCardModule.swift in Sources */, 58AAADDF965D00631448EE04 /* SwapCoinCardViewModel.swift in Sources */, 58AAAA19D6C7812306A164EA /* SwapCoinCardCell.swift in Sources */, + D30D7E5F2CAAC89700B8CAA7 /* TonConnectEventHandler.swift in Sources */, 58AAAF222E553D8DCD123AB2 /* SwapAllowanceService.swift in Sources */, 58AAADE506ABABF4F25F98E1 /* SwapPendingAllowanceService.swift in Sources */, D023D2722A25CF61004F65B0 /* TronAdapter.swift in Sources */, @@ -9665,6 +9763,7 @@ 2FA5D7CDF884D2655E066C3E /* TransactionValue.swift in Sources */, 58AAA3F0AFD0D0F5FCD24DEF /* SelectorButton.swift in Sources */, D05E969B2A26278D002CCD71 /* TronApproveTransactionRecord.swift in Sources */, + D30D7E622CAACCF300B8CAA7 /* TonConnectSendView.swift in Sources */, D05E969E2A2627AF002CCD71 /* TronContractCallTransactionRecord.swift in Sources */, 6B2907262AF0CB8A006157D6 /* WalletConnectAppShowView.swift in Sources */, D00DAE462B626C2900F48E1D /* GasPrice.swift in Sources */, @@ -9725,6 +9824,7 @@ ABC9A2542EA47C2ED85C06B9 /* WalletConnectListViewController.swift in Sources */, ABC9A7A9E27CC5F93BE5018B /* WalletConnectListService.swift in Sources */, D3F9B0382BE3B5AA009FFA95 /* WalletConnectSendView.swift in Sources */, + D36287A92CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift in Sources */, ABC9A5C2E2976341520D2F6D /* WalletConnectListModule.swift in Sources */, ABC9ADCDC949C4C63D1260DE /* WalletConnectListViewModel.swift in Sources */, ABC9A85FAA53B6FB01F94171 /* WalletConnectScanQrViewModel.swift in Sources */, @@ -9754,6 +9854,8 @@ ABC9ADCAB9BB8D2144F8D3CC /* SendConfirmationViewController.swift in Sources */, 6BCD53072A161F4100993F20 /* ICloudBackupTermsService.swift in Sources */, ABC9AE3D64AF3981A68D9913 /* SendConfirmationViewModel.swift in Sources */, + D30D7E652CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift in Sources */, + D36287CF2CA408E200ADAF3B /* TonConnectStorage.swift in Sources */, 11B35DBC25EF36ABA6E13857 /* SectionsTableView.swift in Sources */, 11B35774CEE79A1FD5265FB0 /* EnabledWalletStorage.swift in Sources */, D311DA1D2BD114B00013DB8F /* MarketView.swift in Sources */, @@ -9794,6 +9896,7 @@ D3A580982BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift in Sources */, D3F9B02C2BE3A9A1009FFA95 /* MultiSwapSendView.swift in Sources */, 6B5F5E162C0DDD7500E03EB2 /* RankView.swift in Sources */, + D36287BC2CA3D50F00ADAF3B /* TonConnect.swift in Sources */, ABC9AB3DAD30AA400DEB719C /* SendBitcoinService.swift in Sources */, ABC9AD49CCD14F97CD912454 /* SendBitcoinAdapterService.swift in Sources */, ABC9AF9F8113DB5D54140E7A /* SendBitcoinViewController.swift in Sources */, @@ -9803,6 +9906,7 @@ ABC9A8D215CC5D6A70736E84 /* SendBaseService.swift in Sources */, ABC9ABD9B19AD5D97E332EBE /* SendBinanceViewController.swift in Sources */, ABC9AAEE109F50E3500DD6D4 /* SendBitcoinFactory.swift in Sources */, + D36287C12CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift in Sources */, ABC9A348E1ADBF5EE7E62B06 /* SendBinanceFactory.swift in Sources */, ABC9A29A23C043A3FD65AF1C /* SendBinanceFeeWarningViewModel.swift in Sources */, 11B35E87DDBCD81A36436A13 /* ExternalContractCallTransactionRecord.swift in Sources */, @@ -9840,6 +9944,7 @@ ABC9AA309248821942E78740 /* MarketCardCell.swift in Sources */, 11B358B0C17F5F8F7764BDBE /* CellComponent.swift in Sources */, 11B351B95F191EEA750D9955 /* BalancePrimaryValue.swift in Sources */, + D362879F2CA2B34E00ADAF3B /* TonConnectListView.swift in Sources */, 11B35C2D75133B8F13101A24 /* BalancePrimaryValueManager.swift in Sources */, 11B350F9E67A095998D9462C /* BalanceConversionManager.swift in Sources */, 11B35D11D00301F7B67B0340 /* AppIconManager.swift in Sources */, @@ -9901,12 +10006,14 @@ 11B35EB226E9B03410E6E383 /* EvmPrivateKeyViewModel.swift in Sources */, 11B35653622A466CE9E6FA71 /* EvmPrivateKeyViewController.swift in Sources */, 11B3503F5875A29E6949B13C /* EvmPrivateKeyService.swift in Sources */, + D36287CC2CA3E12100ADAF3B /* TonConnectApp.swift in Sources */, 11B35F4CF3289335EF9004F1 /* ExtendedKeyViewController.swift in Sources */, 11B3591DF0CC1D367C1241AF /* ExtendedKeyModule.swift in Sources */, D087627429815DAE00E6FFD4 /* ChooseWatchViewController.swift in Sources */, 11B35749EBFB7FE593BECE9E /* ExtendedKeyViewModel.swift in Sources */, 11B3547938D32DCE88B4A1FC /* ExtendedKeyService.swift in Sources */, 11B35B9E640DA42CE7ECE92E /* BackupMnemonicWordsCell.swift in Sources */, + D36287C52CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift in Sources */, 6BCD53032A161F4100993F20 /* ICloudBackupTermsViewModel.swift in Sources */, 11B35052C5059CDD8E4BA940 /* BackupMnemonicWordCell.swift in Sources */, 11B35DF1D8B5125CF13A1812 /* RestoreMnemonicHintView.swift in Sources */, @@ -10172,6 +10279,7 @@ ABC9A1B8D2BCB6660A12AAE2 /* ChartIndicatorsViewModel.swift in Sources */, D0118E492B7C9A5200D55CE6 /* ResendBitcoinModule.swift in Sources */, ABC9AD41E7C88963F6512905 /* ChartIndicatorsRepository.swift in Sources */, + D36287A62CA2B89200ADAF3B /* TonConnectParameters.swift in Sources */, ABC9ABD7C7746ABF50DD646F /* ChartIndicatorFactory.swift in Sources */, ABC9A54917CDA7F9EAE237C4 /* ChartIndicatorsModule.swift in Sources */, 11B3530088E70831A648EC63 /* CexDepositNetworkRaw.swift in Sources */, @@ -10211,6 +10319,7 @@ 11B35245BBF4B5F9F07676F4 /* CexWithdrawConfirmViewController.swift in Sources */, 11B35974BDD90836CF5C0DB1 /* CexDepositService.swift in Sources */, 11B35963BA1215A80E8B26D0 /* CexDepositModule.swift in Sources */, + D36287B92CA2D17F00ADAF3B /* TonConnectConfig.swift in Sources */, 11B35FB4B6E5E6B442ADE3B2 /* BinanceCexProvider.swift in Sources */, D311DA202BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */, 11B355F11DDA5EC8082C43DF /* BinanceWithdrawHandler.swift in Sources */, @@ -10513,6 +10622,7 @@ 11B35991B877DCF82D6E51B5 /* BaseUniswapMultiSwapProvider.swift in Sources */, 11B3570C71E4B47382D2DA4B /* MultiSwapTokenSelectView.swift in Sources */, 11B357EC10CF3DB905D9F19E /* MultiSwapTokenSelectViewModel.swift in Sources */, + D36287AC2CA2BA2700ADAF3B /* TonConnectManager.swift in Sources */, 11B35C2D31DE8081AC572B27 /* BaseUniswapV2MultiSwapProvider.swift in Sources */, ABC9AEAC5703842A1475E208 /* ShortcutButtonsView.swift in Sources */, ABC9ABBA9C269AC5F474CE11 /* AddressViewNew.swift in Sources */, @@ -10723,6 +10833,7 @@ 11B354B8BD1C3C036F6DE16A /* LitecoinAdapter.swift in Sources */, D3447DEA25E38300009928D9 /* WalletConnectManager.swift in Sources */, 11B35883290BAA462C0B8F9D /* RateAppManager.swift in Sources */, + D36287B02CA2BC9900ADAF3B /* TonConnectManifest.swift in Sources */, 58AAA3A5967BAB513DD4B33F /* FeeSlider.swift in Sources */, 58AAAA534E94A3276B7F7EF2 /* FeeSliderValueView.swift in Sources */, 58AAAF3B68B9F64DF20FA5AB /* FeeSliderWrapper.swift in Sources */, @@ -10748,11 +10859,13 @@ 11B35FC5D80CC1D4854DBC94 /* BottomSheetTitleView.swift in Sources */, 1A564F382602A283C370E7FB /* AppError.swift in Sources */, D36DE0F9272FD92F000BC916 /* SwapSelectProviderModule.swift in Sources */, + D36287B22CA2CC8000ADAF3B /* TonConnectConnectView.swift in Sources */, D36DE0E7272FD887000BC916 /* OneInchModule.swift in Sources */, 1A5649169A405D3288324442 /* BalanceErrorViewController.swift in Sources */, D033289F2BF6199600BBB364 /* InfoView.swift in Sources */, 1A564D3DB55C8CB8B5AED664 /* BalanceErrorViewModel.swift in Sources */, D36DE0DE272FD887000BC916 /* OneInchViewModel.swift in Sources */, + D36287A32CA2B35A00ADAF3B /* TonConnectListViewModel.swift in Sources */, 1A5649926B0064083045BEEB /* BalanceErrorService.swift in Sources */, 1A564001701A1E77AF7A651B /* BalanceErrorModule.swift in Sources */, 11B35E8E0F5E5F43E65B8A98 /* GuidesModule.swift in Sources */, @@ -11021,6 +11134,7 @@ 1A564032D3C011BCAA7D44A8 /* DeepLinkService.swift in Sources */, 11B35804545D048C0EDB8089 /* EvmSyncSource.swift in Sources */, 11B352F14D96C26D946E3877 /* EvmSyncSourceManager.swift in Sources */, + D36287B52CA2CC8600ADAF3B /* TonConnectConnectViewModel.swift in Sources */, 11B35F73153DEE805DD539CE /* EvmNetworkViewController.swift in Sources */, 11B35B507F2F843A5B3E4C7C /* EvmNetworkViewModel.swift in Sources */, 11B35B81B3C6EDC01D0B3C1F /* EvmNetworkService.swift in Sources */, @@ -11028,6 +11142,7 @@ D36DE0C0272FD864000BC916 /* UniswapService.swift in Sources */, 11B35ED22837284580055F0A /* BalanceData.swift in Sources */, 11B3534B567884E30A871F32 /* AddTokenModule.swift in Sources */, + D30D7E682CAACD0400B8CAA7 /* TonConnectSendHandler.swift in Sources */, 6BB14F722BFE550600E879B2 /* MarketEtfFetcher.swift in Sources */, 11B35758262A961566ABB87F /* AddBep2TokenBlockchainService.swift in Sources */, 58AAADEE16D9605E4FA0390A /* UniswapSettingsViewModel.swift in Sources */, @@ -11072,6 +11187,7 @@ 11B35FE8D60BFF31C3104484 /* SwitchAccountViewModel.swift in Sources */, 11B351AF3AE7DC307974DD6B /* SwitchAccountViewController.swift in Sources */, 11B358C3281DE0A34D192CF0 /* SwitchAccountModule.swift in Sources */, + D36287BF2CA3D7B100ADAF3B /* TonConnect+Encodable.swift in Sources */, 58AAA926E1D95F61CA06EFB8 /* SwapConfirmationModule.swift in Sources */, 58AAA1152EEEBC93FCC3CAAC /* SwapConfirmationViewController.swift in Sources */, 58AAAC9A4813120F3B786D18 /* SwapConfirmationAmountCell.swift in Sources */, @@ -11087,6 +11203,7 @@ 58AAA88F6D5F5509E7FAB02D /* Global.swift in Sources */, 58AAABC21BB15CAC923A13CB /* CoinCardModule.swift in Sources */, 58AAA29A7E913ABAB6592FD7 /* SwapCoinCardViewModel.swift in Sources */, + D30D7E602CAAC89700B8CAA7 /* TonConnectEventHandler.swift in Sources */, 58AAA539E1E8F4EA861A3F80 /* SwapCoinCardCell.swift in Sources */, 58AAAE3339DA7E8D7C467718 /* SwapAllowanceService.swift in Sources */, 58AAAC4F4425F8C33B604020 /* SwapPendingAllowanceService.swift in Sources */, @@ -11118,6 +11235,7 @@ 11B355A29CDAF16148F1C546 /* CoinManager.swift in Sources */, D36DE0B0272FD689000BC916 /* SwapModule.swift in Sources */, 11B3580B9C21B55ACC07B043 /* AdapterManager.swift in Sources */, + D30D7E632CAACCF300B8CAA7 /* TonConnectSendView.swift in Sources */, 11B35F25D1209C6DB33ADA55 /* AdapterFactory.swift in Sources */, 11B351909FE0FA637B5B1EC5 /* CoinValue.swift in Sources */, 2FA5DE1250DA9D85CD9BF1A3 /* TransactionValue.swift in Sources */, @@ -11178,6 +11296,7 @@ 1A5649CEE6EFCC06A10F69CF /* TraitCell.swift in Sources */, ABC9A3C3FC55AB33A9896382 /* UIViewController.swift in Sources */, 6BE8A07B2ADE2F8D0012DE7F /* Currency.swift in Sources */, + D36287AA2CA2B8BE00ADAF3B /* TonConnectRequestPayload.swift in Sources */, D3F9B0372BE3B5AA009FFA95 /* WalletConnectSendView.swift in Sources */, ABC9A9AC7890BE4AAE7DDC84 /* WalletConnectSessionManager.swift in Sources */, 2FA5D18A57B386FD3A4384BA /* Eip1559EvmFeeViewModel.swift in Sources */, @@ -11207,6 +11326,8 @@ 11B35696E9CD808522BEFCD6 /* BlockchainSettingRecordStorage.swift in Sources */, 11B3581F4D975FC21B9A25F2 /* BtcBlockchainSettingsModule.swift in Sources */, 11B359131D838F3191A8C520 /* BtcBlockchainSettingsViewModel.swift in Sources */, + D30D7E662CAACCFB00B8CAA7 /* TonConnectSendViewModel.swift in Sources */, + D36287D02CA408E200ADAF3B /* TonConnectStorage.swift in Sources */, 11B35D2C28E3116F58A543E2 /* BtcBlockchainSettingsService.swift in Sources */, 11B358AE5241256C9AAFB588 /* SyncMode_v_0_24.swift in Sources */, 11B359AA9A3F1FD68323C64E /* BlockchainSettingRecord_v_0_24.swift in Sources */, @@ -11247,6 +11368,7 @@ 11B35CCAC0A3C35C1B9BD918 /* NftActivityViewModel.swift in Sources */, 11B35AC60BE4DC210C3C2312 /* NftActivityService.swift in Sources */, 11B35DC513240DBF0C78ED92 /* NftAssetButtonCell.swift in Sources */, + D36287BB2CA3D50F00ADAF3B /* TonConnect.swift in Sources */, 11B351DDFD1A7BC393EFA6E1 /* CustomToken.swift in Sources */, D3A580972BE8AA90003953F4 /* BitcoinSendSettingsViewModel.swift in Sources */, D3F9B02B2BE3A9A1009FFA95 /* MultiSwapSendView.swift in Sources */, @@ -11256,6 +11378,7 @@ ABC9A774500F8D8D3D9E04DD /* SendBitcoinAdapterService.swift in Sources */, ABC9A08340695A0AFCE9C2F2 /* SendBitcoinViewController.swift in Sources */, ABC9A69BADD39C6E9239A2A1 /* SendViewModelOld.swift in Sources */, + D36287C22CA3D99A00ADAF3B /* TonConnectResponseBuilder.swift in Sources */, ABC9A7E28714A9A19A2160D4 /* SendModule.swift in Sources */, ABC9A5BBFC1960B1DD8F62B7 /* SendBinanceService.swift in Sources */, ABC9A621D59D9DAE28A03865 /* SendBaseService.swift in Sources */, @@ -11293,6 +11416,7 @@ ABC9AECE6AD4A9DEA41DDBD9 /* ProChartFetcher.swift in Sources */, ABC9A59B465A9C59F93DFB96 /* ChartCell.swift in Sources */, ABC9A9562DD283B6FCACBCF9 /* MarketCardTitleView.swift in Sources */, + D36287A02CA2B34E00ADAF3B /* TonConnectListView.swift in Sources */, ABC9AC1BD5C95957726F8AE8 /* MarketCardValueView.swift in Sources */, ABC9ADE7C30F3F992FD9E1CC /* Array.swift in Sources */, ABC9AE223619E13A296BED51 /* MarketCardCell.swift in Sources */, @@ -11354,12 +11478,14 @@ 11B356DC58C5312D5034D30E /* RecoveryPhraseViewModel.swift in Sources */, 11B35C3418D6E7D94CB5C2AF /* RecoveryPhraseService.swift in Sources */, 11B3585E88319E5BBBB9CD3F /* EvmPrivateKeyModule.swift in Sources */, + D36287CD2CA3E12100ADAF3B /* TonConnectApp.swift in Sources */, 11B35FF02BADC6D832836C44 /* EvmPrivateKeyViewModel.swift in Sources */, 11B353F2788F7E8F42C5E03D /* EvmPrivateKeyViewController.swift in Sources */, 11B356B6EBCCF4754C7AF94E /* EvmPrivateKeyService.swift in Sources */, 11B35258C072691B5BD7C41E /* ExtendedKeyViewController.swift in Sources */, 11B359C829669CC55E530476 /* ExtendedKeyModule.swift in Sources */, D087627329815DAE00E6FFD4 /* ChooseWatchViewController.swift in Sources */, + D36287C42CA3DC8F00ADAF3B /* TonConnectSessionCrypto.swift in Sources */, 11B35F6CD2706B10781456E8 /* ExtendedKeyViewModel.swift in Sources */, 6BCD53022A161F4100993F20 /* ICloudBackupTermsViewModel.swift in Sources */, 11B35066480B6EB9F124EBC0 /* ExtendedKeyService.swift in Sources */, @@ -11625,6 +11751,7 @@ ABC9A8D91CFED1961B618241 /* ChartIndicatorsService.swift in Sources */, ABC9A0B5A5577704AC99F47B /* ChartIndicatorsViewModel.swift in Sources */, ABC9A47D4666FA5115F98629 /* ChartIndicatorsRepository.swift in Sources */, + D36287A72CA2B89200ADAF3B /* TonConnectParameters.swift in Sources */, ABC9A4FF1E1964FB77700C4E /* ChartIndicatorFactory.swift in Sources */, D0118E482B7C9A5200D55CE6 /* ResendBitcoinModule.swift in Sources */, ABC9AA4B0A6C33CAD5F3B050 /* ChartIndicatorsModule.swift in Sources */, @@ -11664,6 +11791,7 @@ 11B35D720E414376DB0611A8 /* CexWithdrawConfirmViewModel.swift in Sources */, 11B3517B16E90C016A588C7C /* CexWithdrawConfirmViewController.swift in Sources */, 11B3590189E28D408E207E19 /* CexDepositService.swift in Sources */, + D36287B82CA2D17F00ADAF3B /* TonConnectConfig.swift in Sources */, 11B35C2EB8D6EC593915640F /* CexDepositModule.swift in Sources */, D311DA1F2BD115240013DB8F /* MarketGlobalViewModel.swift in Sources */, 11B3595BD960FE1B998ADF6F /* BinanceCexProvider.swift in Sources */, @@ -11966,6 +12094,7 @@ 11B35CA4F61231F536BE9A24 /* PancakeV2MultiSwapProvider.swift in Sources */, 11B352D47A0F5A3E4AF8F948 /* BaseUniswapV3MultiSwapProvider.swift in Sources */, 11B35FB2259B3BB16AFE5624 /* UniswapV3MultiSwapProvider.swift in Sources */, + D36287AD2CA2BA2700ADAF3B /* TonConnectManager.swift in Sources */, 11B35BD170E89520DF7D384E /* BaseUniswapMultiSwapProvider.swift in Sources */, 11B353FEF0B59658B61D77C6 /* MultiSwapTokenSelectView.swift in Sources */, 11B35E10073AE1D08E0FF272 /* MultiSwapTokenSelectViewModel.swift in Sources */, @@ -12781,6 +12910,13 @@ }; /* End XCConfigurationList section */ +/* Begin XCLocalSwiftPackageReference section */ + D36287C62CA3DF7A00ADAF3B /* XCLocalSwiftPackageReference "TonConnectAPI" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = TonConnectAPI; + }; +/* End XCLocalSwiftPackageReference section */ + /* Begin XCRemoteSwiftPackageReference section */ 500F1D0F27AA87BC002AA419 /* XCRemoteSwiftPackageReference "AlignedCollectionViewFlowLayout" */ = { isa = XCRemoteSwiftPackageReference; @@ -12835,7 +12971,7 @@ repositoryURL = "https://github.com/horizontalsystems/TonKit.Swift"; requirement = { kind = exactVersion; - version = 1.0.10; + version = 1.0.11; }; }; 6BF66DD82BA1A73300963242 /* XCRemoteSwiftPackageReference "ObjectMapper" */ = { @@ -13339,6 +13475,15 @@ package = D3604E8628F03D9E0066C366 /* XCRemoteSwiftPackageReference "DashKit" */; productName = DashKit; }; + D36287C72CA3DF7A00ADAF3B /* TonConnectAPI */ = { + isa = XCSwiftPackageProductDependency; + productName = TonConnectAPI; + }; + D36287C92CA3DF8600ADAF3B /* TonConnectAPI */ = { + isa = XCSwiftPackageProductDependency; + package = D36287C62CA3DF7A00ADAF3B /* XCLocalSwiftPackageReference "TonConnectAPI" */; + productName = TonConnectAPI; + }; D36E0C2928D084AB00B622B9 /* CollectionViewCenteredFlowLayout */ = { isa = XCSwiftPackageProductDependency; package = D36E0C2828D084AB00B622B9 /* XCRemoteSwiftPackageReference "CollectionViewCenteredFlowLayout" */; diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/Contents.json b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/Contents.json new file mode 100644 index 0000000000..282ddbea5a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ton connet@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ton connet@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/ton connet@2x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/ton connet@2x.png new file mode 100644 index 0000000000..d575353dc3 Binary files /dev/null and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/ton connet@2x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/ton connet@3x.png b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/ton connet@3x.png new file mode 100644 index 0000000000..a5bed6f370 Binary files /dev/null and b/UnstoppableWallet/UnstoppableWallet/Assets.xcassets/Icons/ton_connect_24.imageset/ton connet@3x.png differ diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/JettonAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/JettonAdapter.swift index fe62bdfb71..2ad1c2ba9b 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/JettonAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/JettonAdapter.swift @@ -115,7 +115,7 @@ extension JettonAdapter: IBalanceAdapter { extension JettonAdapter: IDepositAdapter { var receiveAddress: DepositAddress { - DepositAddress(tonKit.receiveAddress.toString(bounceable: false)) + DepositAddress(tonKit.receiveAddress.toString(testOnly: TonKitManager.isTestNet, bounceable: false)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift index 537d6a888a..7e36f7db1c 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonAdapter.swift @@ -83,7 +83,7 @@ extension TonAdapter: IBalanceAdapter { extension TonAdapter: IDepositAdapter { var receiveAddress: DepositAddress { - DepositAddress(tonKit.receiveAddress.toString(bounceable: false)) + DepositAddress(tonKit.receiveAddress.toString(testOnly: TonKitManager.isTestNet, bounceable: false)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonEventConverter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonEventConverter.swift index b203cf7e21..e6abd07589 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonEventConverter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonEventConverter.swift @@ -31,7 +31,7 @@ class TonEventConverter { } private func jettonValue(jetton: Jetton, value: BigUInt, sign: FloatingPointSign) -> TransactionValue { - let query = TokenQuery(blockchainType: .ton, tokenType: .jetton(address: jetton.address.toString(bounceable: true))) + let query = TokenQuery(blockchainType: .ton, tokenType: .jetton(address: jetton.address.toString(testOnly: TonKitManager.isTestNet, bounceable: true))) if let token = try? coinManager.token(query: query) { let value = convertAmount(amount: value, decimals: token.decimals, sign: sign) @@ -43,7 +43,7 @@ class TonEventConverter { } private func format(address: AccountAddress) -> String { - address.address.toString(bounceable: !address.isWallet) + address.address.toString(testOnly: TonKitManager.isTestNet, bounceable: !address.isWallet) } private func actionType(type: Action.`Type`) -> TonTransactionRecord.Action.`Type` { diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonTransactionAdapter.swift b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonTransactionAdapter.swift index e4a0cce7e3..1ab4071b23 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonTransactionAdapter.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Adapters/TonTransactionAdapter.swift @@ -99,7 +99,7 @@ extension TonTransactionAdapter: ITransactionsAdapter { tokenType = .native case .jetton: if let jettonAddress = tagToken.jettonAddress { - tokenType = .jetton(address: jettonAddress.toString(bounceable: true)) + tokenType = .jetton(address: jettonAddress.toString(testOnly: TonKitManager.isTestNet, bounceable: true)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Core/App.swift b/UnstoppableWallet/UnstoppableWallet/Core/App.swift index 5d18309bf1..cbf01fd3a6 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/App.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/App.swift @@ -100,6 +100,8 @@ class App { let statManager: StatManager + let tonConnectManager: TonConnectManager + let kitCleaner: KitCleaner let appManager: AppManager @@ -314,6 +316,9 @@ class App { let statStorage = StatStorage(dbPool: dbPool) statManager = StatManager(marketKit: marketKit, storage: statStorage, userDefaultsStorage: userDefaultsStorage) + let tonConnectStorage = try TonConnectStorage(dbPool: dbPool) + tonConnectManager = TonConnectManager(storage: tonConnectStorage) + kitCleaner = KitCleaner(accountManager: accountManager) appManager = AppManager( diff --git a/UnstoppableWallet/UnstoppableWallet/Core/Managers/TonKitManager.swift b/UnstoppableWallet/UnstoppableWallet/Core/Managers/TonKitManager.swift index 6445afd198..d1c26280f1 100644 --- a/UnstoppableWallet/UnstoppableWallet/Core/Managers/TonKitManager.swift +++ b/UnstoppableWallet/UnstoppableWallet/Core/Managers/TonKitManager.swift @@ -44,7 +44,7 @@ class TonKitManager { let tonKit = try TonKit.Kit.instance( address: address, - network: .mainNet, + network: Self.network, walletId: account.id, minLogLevel: .error ) @@ -159,6 +159,18 @@ extension TonKitManager { } extension TonKitManager { + static var network: TonKit.Network { + // .mainNet + .testNet + } + + static var isTestNet: Bool { + switch network { + case .mainNet: return false + case .testNet: return true + } + } + static func contract(publicKey: Data) -> WalletContract { WalletV4R2(publicKey: publicKey) } @@ -177,6 +189,6 @@ extension TonKitManager { extension Jetton { var tokenType: TokenType { - .jetton(address: address.toString(bounceable: true)) + .jetton(address: address.toString(testOnly: TonKitManager.isTestNet, bounceable: true)) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift index d8bff21190..f68b7d0705 100644 --- a/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift +++ b/UnstoppableWallet/UnstoppableWallet/Models/AccountType.swift @@ -149,6 +149,13 @@ enum AccountType { } } + var supportsTonConnect: Bool { + switch self { + case .mnemonic: return true + default: return false + } + } + var supportsNft: Bool { switch self { case .cex: return false diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/AddToken/AddJettonBlockchainService.swift b/UnstoppableWallet/UnstoppableWallet/Modules/AddToken/AddJettonBlockchainService.swift index e9da6f79ed..dc4afc2451 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/AddToken/AddJettonBlockchainService.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/AddToken/AddJettonBlockchainService.swift @@ -31,7 +31,7 @@ extension AddJettonBlockchainService: IAddTokenBlockchainService { do { let address = try TonSwift.Address.parse(reference) - reference = address.toString(bounceable: true) + reference = address.toString(testOnly: TonKitManager.isTestNet, bounceable: true) } catch {} return TokenQuery(blockchainType: blockchain.type, tokenType: .jetton(address: reference)) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift index 81c3280d2a..cfbceefda3 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Main/MainModule.swift @@ -51,7 +51,10 @@ enum MainModule { let widgetCoinHandler = WidgetCoinAppShowModule.handler(parentViewController: viewController) let sendAddressHandler = AddressAppShowModule.handler(parentViewController: viewController) let telegramUserHandler = TelegramUserHandler.handler(parentViewController: viewController) + let tonConnectHandler = TonConnectEventHandler(parentViewController: viewController) + eventHandler.append(handler: deepLinkHandler) + eventHandler.append(handler: tonConnectHandler) eventHandler.append(handler: widgetCoinHandler) eventHandler.append(handler: sendAddressHandler) eventHandler.append(handler: telegramUserHandler) diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift index 114c088a10..c1a96a72a7 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendData.swift @@ -15,4 +15,5 @@ enum SendData { case ton(token: Token, amount: Decimal, address: FriendlyAddress, memo: String?) case swap(tokenIn: Token, tokenOut: Token, amountIn: Decimal, provider: IMultiSwapProvider) case walletConnect(request: WalletConnectRequest) + case tonConnect(request: TonConnectSendTransactionRequest) } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift index 8fb966e8bf..679daf25e0 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/SendHandlerFactory.swift @@ -19,6 +19,8 @@ enum SendHandlerFactory { return MultiSwapSendHandler.instance(tokenIn: tokenIn, tokenOut: tokenOut, amountIn: amountIn, provider: provider) case let .walletConnect(request): return WalletConnectSendHandler.instance(request: request) + case let .tonConnect(request): + return try? TonConnectSendHandler.instance(request: request) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TonSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TonSendHandler.swift index cbac90b501..83cf6d93d9 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TonSendHandler.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/SendNew/TonSendHandler.swift @@ -49,7 +49,7 @@ extension TonSendHandler: ISendHandler { do { let _transferData = try adapter.transferData(recipient: address, amount: sendAmount, comment: memo) - let result = try await TonKit.Kit.emulate(transferData: _transferData, contract: contract, network: .mainNet) + let result = try await TonKit.Kit.emulate(transferData: _transferData, contract: contract, network: TonKitManager.network) let estimatedFee = TonAdapter.amount(kitAmount: result.totalFee) fee = estimatedFee @@ -93,7 +93,8 @@ extension TonSendHandler: ISendHandler { throw SendError.invalidData } - try await TonKit.Kit.send(transferData: transferData, contract: contract, secretKey: secretKey, network: .mainNet) + let boc = try await TonKit.Kit.boc(transferData: transferData, contract: contract, secretKey: secretKey, network: TonKitManager.network) + try await TonKit.Kit.send(boc: boc, contract: contract, network: TonKitManager.network) } } diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift index 3c870a1e28..8854b87209 100644 --- a/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift +++ b/UnstoppableWallet/UnstoppableWallet/Modules/Settings/Main/MainSettingsViewController.swift @@ -19,6 +19,7 @@ class MainSettingsViewController: ThemeViewController { private let manageAccountsCell = BaseSelectableThemeCell() private let walletConnectCell = BaseSelectableThemeCell() + private let tonConnectCell = BaseSelectableThemeCell() private let securityCell = BaseSelectableThemeCell() private let appearanceCell = BaseSelectableThemeCell() private let contactBookCell = BaseSelectableThemeCell() @@ -70,6 +71,9 @@ class MainSettingsViewController: ThemeViewController { walletConnectCell.set(backgroundStyle: .lawrence) syncWalletConnectCell() + tonConnectCell.set(backgroundStyle: .lawrence) + syncTonConnectCell() + securityCell.set(backgroundStyle: .lawrence, isFirst: true) syncSecurityCell() @@ -170,6 +174,16 @@ class MainSettingsViewController: ThemeViewController { ) } + private func syncTonConnectCell(text: String? = nil, highlighted: Bool = false) { + buildTitleValue( + cell: tonConnectCell, + image: UIImage(named: "ton_connect_24"), + title: "TON Connect", + value: !highlighted ? text : nil, + badge: highlighted ? text : nil + ) + } + private func syncBaseCurrency(value: String? = nil) { buildTitleValue(cell: baseCurrencyCell, image: UIImage(named: "usd_24"), title: "settings.base_currency".localized, value: value) } @@ -248,6 +262,15 @@ class MainSettingsViewController: ThemeViewController { self?.viewModel.onTapWalletConnect() } ), + 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")), @@ -538,6 +561,10 @@ class MainSettingsViewController: ThemeViewController { stat(page: .settings, event: .open(page: .contactUs)) } + + private func onTapTonConnect() { + navigationController?.pushViewController(TonConnectListView().toViewController(title: "TON Connect"), animated: true) + } } extension MainSettingsViewController: SectionsDataSource { diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect+Encodable.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect+Encodable.swift new file mode 100644 index 0000000000..53ddab18d4 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect+Encodable.swift @@ -0,0 +1,128 @@ +import Foundation +import TonSwift +import TweetNacl + +extension TonConnect.ConnectEvent { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .success(success): + try container.encode(success) + case let .error(error): + try container.encode(error) + } + } +} + +extension TonConnect.ConnectItemReply { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .tonAddress(address): + try container.encode(address) + case let .tonProof(proof): + try container.encode(proof) + } + } +} + +extension TonConnect.TonProofItemReply { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .success(success): + try container.encode(success) + case let .error(error): + try container.encode(error) + } + } +} + +extension TonConnect.TonAddressItemReply { + enum CodingKeys: String, CodingKey { + case name + case address + case network + case publicKey + case walletStateInit + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(address.toRaw(), forKey: .address) + try container.encode("\(network.rawValue)", forKey: .network) + try container.encode(publicKey.hexString, forKey: .publicKey) + + let builder = Builder() + try walletStateInit.storeTo(builder: builder) + try container.encode( + builder.endCell().toBoc().base64EncodedString(), + forKey: .walletStateInit + ) + } +} + +extension TonConnect.TonProofItemReplySuccess.Signature { + func data() -> Data { + let string = "ton-proof-item-v2/".data(using: .utf8)! + let addressWorkchain = UInt32(bigEndian: UInt32(address.workchain)) + + let addressWorkchainData = withUnsafeBytes(of: addressWorkchain) { a in + Data(a) + } + let addressHash = address.hash + let domainLength = withUnsafeBytes(of: UInt32(littleEndian: domain.lengthBytes)) { a in + Data(a) + } + let domainValue = domain.value.data(using: .utf8)! + let timestamp = withUnsafeBytes(of: UInt64(littleEndian: timestamp)) { a in + Data(a) + } + let payload = payload.data(using: .utf8)! + + return string + addressWorkchainData + addressHash + domainLength + domainValue + timestamp + payload + } +} + +extension TonConnect.TonProofItemReplySuccess.Proof { + enum CodingKeys: String, CodingKey { + case timestamp + case domain + case signature + case payload + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(timestamp, forKey: .timestamp) + try container.encode(domain, forKey: .domain) + + let signatureMessageData = signature.data() + let signatureMessage = signatureMessageData.sha256() + guard let prefixData = Data(hex: "ffff"), + let tonConnectData = "ton-connect".data(using: .utf8) + else { + return + } + let signatureData = (prefixData + tonConnectData + signatureMessage).sha256() + let signature = try TweetNacl.NaclSign.signDetached( + message: signatureData, + secretKey: privateKey.data + ) + try container.encode(signature, forKey: .signature) + try container.encode(payload, forKey: .payload) + } +} + +extension TonConnect.SendTransactionResponse: Encodable { + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .success(success): + try container.encode(success) + case let .error(error): + try container.encode(error) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift new file mode 100644 index 0000000000..1ec04ab6c0 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnect.swift @@ -0,0 +1,338 @@ +import Foundation +import TonSwift + +enum TonConnect { + enum Network: Int16, Hashable { + case mainnet = -239 + case testnet = -3 + } + + enum ConnectEvent: Encodable { + case success(ConnectEventSuccess) + case error(ConnectEventError) + } + + struct DeviceInfo: Encodable { + let platform = "iphone" + let appName = "Tonkeeper" + let appVersion = "3.4.0" + // let appName = AppConfig.appName + // let appVersion = AppConfig.appVersion + let maxProtocolVersion = 2 + let features = [ + FeatureCompatible.legacy(Feature()), + FeatureCompatible.feature(Feature()), + ] + + enum FeatureCompatible: Encodable { + case feature(Feature) + case legacy(Feature) + + func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + switch self { + case let .feature(feature): + try container.encode(feature) + case let .legacy(feature): + try container.encode(feature.name) + } + } + } + + struct Feature: Encodable { + let name = "SendTransaction" + let maxMessages = 4 + } + + init() {} + } + + struct ConnectEventSuccess: Encodable { + struct Payload: Encodable { + let items: [ConnectItemReply] + let device: DeviceInfo + } + + let event = "connect" + let id = Int(Date().timeIntervalSince1970) + let payload: Payload + } + + struct ConnectEventError: Encodable { + struct Payload: Encodable { + let code: Error + let message: String + } + + enum Error: Int, Encodable, Swift.Error { + case unknownError = 0 + case badRequest = 1 + case appManifestNotFound = 2 + case appManifestContentError = 3 + case unknownApp = 100 + case userDeclinedTheConnection = 300 + } + + let event = "connect_error" + let id = Int(Date().timeIntervalSince1970) + let payload: Payload + } + + struct DisconnectEvent: Encodable { + let event = "disconnect" + let id = Int(Date().timeIntervalSince1970) + } + + enum ConnectItemReply: Encodable { + case tonAddress(TonAddressItemReply) + case tonProof(TonProofItemReply) + } + + struct TonAddressItemReply: Encodable { + let name = "ton_addr" + let address: TonSwift.Address + let network: Network + let publicKey: TonSwift.PublicKey + let walletStateInit: TonSwift.StateInit + } + + enum TonProofItemReply: Encodable { + case success(TonProofItemReplySuccess) + case error(TonProofItemReplyError) + } + + struct TonProofItemReplySuccess: Encodable { + struct Proof: Encodable { + let timestamp: UInt64 + let domain: Domain + let signature: Signature + let payload: String + let privateKey: PrivateKey + } + + struct Signature: Encodable { + let address: TonSwift.Address + let domain: Domain + let timestamp: UInt64 + let payload: String + } + + struct Domain: Encodable { + let lengthBytes: UInt32 + let value: String + } + + let name = "ton_proof" + let proof: Proof + } + + struct TonProofItemReplyError: Encodable { + struct Error: Encodable { + let message: String? + let code: ErrorCode + } + + enum ErrorCode: Int, Encodable { + case unknownError = 0 + case methodNotSupported = 400 + } + + let name = "ton_proof" + let error: Error + } +} + +extension TonConnect.TonProofItemReplySuccess { + init(address: TonSwift.Address, + domain: String, + payload: String, + privateKey: PrivateKey) + { + let timestamp = UInt64(Date().timeIntervalSince1970) + let domain = Domain(domain: domain) + let signature = Signature( + address: address, + domain: domain, + timestamp: timestamp, + payload: payload + ) + let proof = Proof( + timestamp: timestamp, + domain: domain, + signature: signature, + payload: payload, + privateKey: privateKey + ) + + self.init(proof: proof) + } +} + +extension TonConnect.TonProofItemReplySuccess.Domain { + init(domain: String) { + let domainLength = UInt32(domain.utf8.count) + value = domain + lengthBytes = domainLength + } +} + +extension TonConnect { + enum SendTransactionResponse { + case success(SendTransactionResponseSuccess) + case error(SendTransactionResponseError) + } + + struct SendTransactionResponseSuccess: Encodable { + let result: String + let id: String + + init(result: String, id: String) { + self.result = result + self.id = id + } + } + + struct SendTransactionResponseError: Encodable { + struct Error: Encodable { + let code: ErrorCode + let message: String + + init(code: ErrorCode, message: String) { + self.code = code + self.message = message + } + } + + enum ErrorCode: Int, Encodable, Swift.Error { + case unknownError = 0 + case badRequest = 1 + case unknownApp = 10 + case userDeclinedTransaction = 300 + case methodNotSupported = 400 + } + + let id: String + let error: Error + + init(id: String, error: Error) { + self.id = id + self.error = error + } + } +} + +extension TonConnect { + struct AppRequest: Decodable { + enum Method: String, Decodable { + case sendTransaction + case disconnect + } + + let method: Method + let params: [SendTransactionParam] + let id: String + + enum CodingKeys: String, CodingKey { + case method + case params + case id + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + method = try container.decode(Method.self, forKey: .method) + id = try container.decode(String.self, forKey: .id) + let paramsArray = try container.decode([String].self, forKey: .params) + let jsonDecoder = JSONDecoder() + params = paramsArray.compactMap { + guard let data = $0.data(using: .utf8) else { return nil } + return try? jsonDecoder.decode(SendTransactionParam.self, from: data) + } + } + } +} + +extension TonConnect.Network: CellCodable { + func storeTo(builder: Builder) throws { + try builder.store(int: rawValue, bits: .rawValueLength) + } + + static func loadFrom(slice: Slice) throws -> TonConnect.Network { + return try slice.tryLoad { s in + let rawValue = try Int16(s.loadInt(bits: .rawValueLength)) + guard let network = TonConnect.Network(rawValue: rawValue) else { + throw TonSwift.TonError.custom("Invalid network code") + } + return network + } + } +} + +private extension Int { + static let rawValueLength = 16 +} + +public struct SendTransactionParam: Decodable { + let messages: [Message] + let validUntil: TimeInterval + let from: TonSwift.Address? + + enum CodingKeys: String, CodingKey { + case messages + case validUntil = "valid_until" + case from + case source + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + messages = try container.decode([Message].self, forKey: .messages) + validUntil = try container.decode(TimeInterval.self, forKey: .validUntil) + + if let fromValue = try? container.decode(String.self, forKey: .from) { + from = try TonSwift.Address.parse(fromValue) + } else { + from = try TonSwift.Address.parse(container.decode(String.self, forKey: .source)) + } + } + + public struct Message: Decodable { + let address: TonSwift.Address + let amount: Int64 + let stateInit: String? + let payload: String? + + enum CodingKeys: String, CodingKey { + case address + case amount + case stateInit + case payload + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + address = try TonSwift.Address.parse(container.decode(String.self, forKey: .address)) + amount = try Int64(container.decode(String.self, forKey: .amount)) ?? 0 + stateInit = try container.decodeIfPresent(String.self, forKey: .stateInit) + payload = try container.decodeIfPresent(String.self, forKey: .payload) + } + } +} + +public struct SendTransactionSignRequest: Decodable { + public let params: [SendTransactionParam] + + enum CodingKeys: String, CodingKey { + case params + } + + public init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + var params = [SendTransactionParam]() + while !container.isAtEnd { + let param = try container.decode(SendTransactionParam.self) + params.append(param) + } + self.params = params + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectApp.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectApp.swift new file mode 100644 index 0000000000..1d8e6b7dca --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectApp.swift @@ -0,0 +1,39 @@ +import GRDB +import TonSwift + +struct TonConnectApp: Codable, Identifiable { + let accountId: String + let clientId: String + let manifest: TonConnectManifest + let keyPair: TonSwift.KeyPair + + var id: String { + clientId + } +} + +extension TonConnectApp: FetchableRecord, PersistableRecord { + enum Columns { + static let accountId = Column(CodingKeys.accountId) + static let clientId = Column(CodingKeys.clientId) + static let manifest = Column(CodingKeys.manifest) + static let keyPair = Column(CodingKeys.keyPair) + } +} + +struct TonConnectLastEvent: Codable { + let uniqueField: String + let id: String + + init(id: String) { + uniqueField = "unique" + self.id = id + } +} + +extension TonConnectLastEvent: FetchableRecord, PersistableRecord { + enum Columns { + static let uniqueField = Column(CodingKeys.uniqueField) + static let id = Column(CodingKeys.id) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConfig.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConfig.swift new file mode 100644 index 0000000000..5d39bbe101 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConfig.swift @@ -0,0 +1,8 @@ +struct TonConnectConfig: Identifiable { + let parameters: TonConnectParameters + let manifest: TonConnectManifest + + var id: String { + parameters.clientId + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift new file mode 100644 index 0000000000..63f013d45a --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectView.swift @@ -0,0 +1,99 @@ +import Kingfisher +import SwiftUI + +struct TonConnectConnectView: View { + @StateObject private var viewModel: TonConnectConnectViewModel + @Environment(\.presentationMode) private var presentationMode + + @State private var selectAccountPresented = false + + init(config: TonConnectConfig) { + _viewModel = StateObject(wrappedValue: TonConnectConnectViewModel(config: config)) + } + + var body: some View { + ThemeNavigationView { + ThemeView { + BottomGradientWrapper { + ScrollView { + VStack(spacing: .margin24) { + HStack(spacing: .margin16) { + KFImage.url(viewModel.manifest.iconUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius16).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius16)) + .frame(width: 72, height: 72) + + Text(viewModel.manifest.name).themeHeadline1() + } + + ListSection { + ListRow { + Text("ton_connect.connect.url".localized).textSubhead2() + Spacer() + Text(viewModel.manifest.host).textSubhead1(color: .themeLeah) + } + + if let account = viewModel.account { + ClickableRow { + selectAccountPresented = true + } content: { + Text("ton_connect.connect.wallet".localized).textSubhead2() + Spacer() + HStack(spacing: .margin8) { + Text(account.name).textSubhead1(color: .themeLeah) + Image("arrow_small_down_20").themeIcon() + } + } + .alert( + isPresented: $selectAccountPresented, + title: "ton_connect.connect.wallet".localized, + viewItems: viewModel.eligibleAccounts.map { .init(text: $0.name, selected: viewModel.account == $0) }, + onTap: { index in + guard let index else { + return + } + + viewModel.account = viewModel.eligibleAccounts[index] + } + ) + } else { + ListRow { + Text("ton_connect.connect.wallet".localized).textSubhead2() + Spacer() + Text("ton_connect.connect.no_eligible_wallets".localized).textSubhead2() + } + } + } + + HighlightedTextView(text: "ton_connect.connect.warning".localized) + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } bottomContent: { + VStack(spacing: .margin16) { + Button(action: { + viewModel.connect() + }) { + Text("ton_connect.connect.connect".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(viewModel.account == nil) + + Button(action: { + presentationMode.wrappedValue.dismiss() + }) { + Text("ton_connect.connect.reject".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + } + } + .onReceive(viewModel.finishPublisher) { + presentationMode.wrappedValue.dismiss() + } + .navigationTitle("TON Connect") + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift new file mode 100644 index 0000000000..60da447207 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectConnectViewModel.swift @@ -0,0 +1,48 @@ +import Combine +import Foundation + +class TonConnectConnectViewModel: ObservableObject { + private let parameters: TonConnectParameters + let manifest: TonConnectManifest + + private let tonConnectManager = App.shared.tonConnectManager + private let accountManager = App.shared.accountManager + + let eligibleAccounts: [Account] + @Published var account: Account? + + private let finishSubject = PassthroughSubject() + + init(config: TonConnectConfig) { + parameters = config.parameters + manifest = config.manifest + + eligibleAccounts = accountManager.accounts.filter { $0.type.supportsTonConnect }.sorted { $0.name < $1.name } + + if let activeAccount = accountManager.activeAccount, eligibleAccounts.contains(activeAccount) { + account = activeAccount + } else { + account = eligibleAccounts.first + } + } +} + +extension TonConnectConnectViewModel { + var finishPublisher: AnyPublisher { + finishSubject.eraseToAnyPublisher() + } + + func connect() { + guard let account else { + return + } + + Task { + try await tonConnectManager.connect(account: account, parameters: parameters, manifest: manifest) + + await MainActor.run { + finishSubject.send() + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift new file mode 100644 index 0000000000..3ae6bbe06d --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectEventHandler.swift @@ -0,0 +1,38 @@ +import Combine +import UIKit + +class TonConnectEventHandler { + private let tonConnectManager = App.shared.tonConnectManager + private var cancellables = Set() + + private weak var parentViewController: UIViewController? + + init(parentViewController: UIViewController?) { + self.parentViewController = parentViewController + + tonConnectManager.sendTransactionRequestPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.handle(sendTransactionRequest: $0) } + .store(in: &cancellables) + } + + private func handle(sendTransactionRequest: TonConnectSendTransactionRequest) { + let view = TonConnectSendView(request: sendTransactionRequest) + parentViewController?.visibleController.present(view.toViewController(), animated: true) + } +} + +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 + } + + let config = try await tonConnectManager.loadTonConnectConfiguration(deeplink: deeplink) + + await MainActor.run { [weak self] in + let view = TonConnectConnectView(config: config) + self?.parentViewController?.visibleController.present(view.toViewController(), animated: true) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectListView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectListView.swift new file mode 100644 index 0000000000..d8cdc18fad --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectListView.swift @@ -0,0 +1,88 @@ +import Kingfisher +import SwiftUI + +struct TonConnectListView: View { + @StateObject private var viewModel = TonConnectListViewModel() + + @State private var qrScanPresented = false + @State private var connectConfig: TonConnectConfig? + @State private var tonConnectApp: TonConnectApp? + + var body: some View { + ThemeView { + BottomGradientWrapper { + if viewModel.items.isEmpty { + PlaceholderViewNew(image: Image("no_data_48"), text: "ton_connect.list.no_connected_apps".localized) + } else { + ScrollView { + VStack(spacing: .margin24) { + ForEach(viewModel.items) { item in + VStack(spacing: 0) { + ListSectionHeader(text: item.account.name) + ListSection { + ForEach(item.apps) { app in + ClickableRow(action: { + tonConnectApp = app + }) { + KFImage.url(app.manifest.iconUrl) + .resizable() + .placeholder { RoundedRectangle(cornerRadius: .cornerRadius4).fill(Color.themeSteel20) } + .clipShape(RoundedRectangle(cornerRadius: .cornerRadius4)) + .frame(width: .iconSize32, height: .iconSize32) + + VStack(spacing: 1) { + Text(app.manifest.name).themeBody() + Text(app.manifest.host).themeSubhead2() + } + + Image.disclosureIcon + } + } + } + } + } + } + .padding(EdgeInsets(top: .margin12, leading: .margin16, bottom: .margin32, trailing: .margin16)) + } + } + } bottomContent: { + Button(action: { + qrScanPresented = true + }) { + Text("ton_connect.list.new_connection".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + } + } + .onReceive(viewModel.openCreateConnectionPublisher) { config in + connectConfig = config + } + .sheet(item: $connectConfig) { config in + TonConnectConnectView(config: config) + } + .sheet(isPresented: $qrScanPresented) { + ScanQrViewNew(reportAfterDismiss: true, pasteEnabled: true) { deeplink in + viewModel.handle(deeplink: deeplink) + } + .ignoresSafeArea() + } + .bottomSheet(item: $tonConnectApp) { app in + ActionSheetView( + image: .trash, + title: "ton_connect.list.disconnect_app".localized, + items: [ + .description(text: "ton_connect.list.disconnect_app.description".localized(app.manifest.name)), + ], + buttons: [ + .init(style: .red, title: "ton_connect.list.disconnect_app.disconnect".localized) { + viewModel.disconnect(app: app) + tonConnectApp = nil + }, + ], + onDismiss: { tonConnectApp = nil } + ) + } + .navigationTitle("TON Connect") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectListViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectListViewModel.swift new file mode 100644 index 0000000000..59ca5b5eb7 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectListViewModel.swift @@ -0,0 +1,67 @@ +import Combine +import Foundation + +class TonConnectListViewModel: ObservableObject { + private let tonConnectManager = App.shared.tonConnectManager + private let accountManager = App.shared.accountManager + private var cancellables = Set() + + private let openCreateConnectionSubject = PassthroughSubject() + + @Published private(set) var items = [Item]() + + init() { + syncItems(tonTonnectApps: tonConnectManager.tonConnectApps) + + tonConnectManager.$tonConnectApps + .receive(on: DispatchQueue.main) + .sink { [weak self] in self?.syncItems(tonTonnectApps: $0) } + .store(in: &cancellables) + } + + private func syncItems(tonTonnectApps: [TonConnectApp]) { + let dictionary = Dictionary(grouping: tonTonnectApps, by: { $0.accountId }) + let accounts = dictionary.keys.compactMap { accountManager.account(id: $0) } + + items = accounts.sorted { $0.name < $1.name }.compactMap { account in + guard let apps = dictionary[account.id] else { + return nil + } + + return Item(account: account, apps: apps.sorted { $0.manifest.name < $1.manifest.name }) + } + } +} + +extension TonConnectListViewModel { + var openCreateConnectionPublisher: AnyPublisher { + openCreateConnectionSubject.eraseToAnyPublisher() + } + + func handle(deeplink: String) { + Task { [tonConnectManager, openCreateConnectionSubject] in + let config = try await tonConnectManager.loadTonConnectConfiguration(deeplink: deeplink) + + await MainActor.run { + openCreateConnectionSubject.send(config) + } + } + } + + func disconnect(app: TonConnectApp) { + Task { [tonConnectManager] in + try await tonConnectManager.disconnect(tonConnectApp: app) + } + } +} + +extension TonConnectListViewModel { + struct Item: Identifiable { + let account: Account + let apps: [TonConnectApp] + + var id: ID { + account.id + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift new file mode 100644 index 0000000000..1815026e3f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManager.swift @@ -0,0 +1,282 @@ +import Combine +import EventSource +import Foundation +import HdWalletKit +import HsExtensions +import StreamURLSessionTransport +import TonConnectAPI +import TonSwift +import TweetNacl + +class TonConnectManager { + private let apiClient: TonConnectAPI.Client + private let storage: TonConnectStorage + + @PostPublished private(set) var tonConnectApps = [TonConnectApp]() + + private var task: Task? + private let jsonDecoder = JSONDecoder() + + private let sendTransactionRequestSubject = PassthroughSubject() + + init(storage: TonConnectStorage) { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = TimeInterval(Int.max) + configuration.timeoutIntervalForResource = TimeInterval(Int.max) + + apiClient = TonConnectAPI.Client( + serverURL: (try? TonConnectAPI.Servers.server1()) ?? URL(string: "https://bridge.tonapi.io/bridge")!, + transport: StreamURLSessionTransport(urlSessionConfiguration: configuration), + middlewares: [] + ) + + self.storage = storage + + syncTonConnectApps() + } + + private func syncTonConnectApps() { + do { + tonConnectApps = try storage.tonConnectApps() + } catch { + tonConnectApps = [] + } + + start() + } + + public func start() { + task?.cancel() + + // print("Start") + + guard !tonConnectApps.isEmpty else { + return + } + + // print("Apps: \(tonConnectApps.map { $0.manifest.name })") + + let task = Task { [storage, tonConnectApps] in + let ids = tonConnectApps.map { $0.keyPair.publicKey.hexString }.joined(separator: ",") + + // print("IDS: \(ids)") + + let errorParser = EventSourceDecodableErrorParser() + let stream = try await EventSource.eventSource({ + let response = try await self.apiClient.events( + query: .init(client_id: [ids], last_event_id: storage.lastEventId()) + ) + + return try response.ok.body.text_event_hyphen_stream + }, errorParser: errorParser) + + // print("Start listening....") + + for try await events in stream { + handle(events: events) + } + + // print("Stop listening....") + + guard !Task.isCancelled else { return } + + start() + } + + self.task = task + } + + private func handle(events: [EventSource.Event]) { + // print("HANDLE: \(events)") + guard let event = events.last(where: { $0.event == "message" }), + let data = event.data?.data(using: .utf8), + let tonConnectEvent = try? jsonDecoder.decode(TonConnectEvent.self, from: data) + else { + return + } + + if let id = event.id { + try? storage.save(lastEventId: id) + } + + handleEvent(tonConnectEvent: tonConnectEvent) + } + + private func handleEvent(tonConnectEvent: TonConnectEvent) { + // print("HANDLE EVENT: \(tonConnectEvent)") + guard let app = tonConnectApps.first(where: { $0.clientId == tonConnectEvent.from }) else { + return + } + + do { + let sessionCrypto = try TonConnectSessionCrypto(privateKey: app.keyPair.privateKey) + + guard let senderPublicKey = Data(hex: app.clientId), let message = Data(base64Encoded: tonConnectEvent.message) else { + return + } + + let decryptedMessage = try sessionCrypto.decrypt(message: message, senderPublicKey: senderPublicKey) + + // print("DECRYPTED: \(String(data: decryptedMessage, encoding: .utf8) ?? "nil")") + + let request: TonConnect.AppRequest = try jsonDecoder.decode( + TonConnect.AppRequest.self, + from: decryptedMessage + ) + + // print("REQUEST: \(request)") + + switch request.method { + case .sendTransaction: + if let param = request.params.first { + sendTransactionRequestSubject.send(.init(id: request.id, param: param, app: app)) + } + case .disconnect: + try delete(tonConnectApp: app) + } + + } catch { + print("Log: Failed to handle ton connect event \(tonConnectEvent), error: \(error)") + } + } + + private func parseTonConnect(deeplink: String) throws -> TonConnectParameters { + 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) + else { + throw ServiceError.incorrectUrl + } + + return TonConnectParameters(version: version, clientId: clientId, requestPayload: requestPayload) + } + + private func loadManifest(url: URL) async throws -> TonConnectManifest { + let (data, _) = try await URLSession.shared.data(from: url) + let jsonDecoder = JSONDecoder() + return try jsonDecoder.decode(TonConnectManifest.self, from: data) + } + + private func send(message: Encodable, clientId: String, sessionCrypto: TonConnectSessionCrypto) async throws { + let encoded = try JSONEncoder().encode(message) + + guard let receiverPublicKey = Data(hex: clientId) else { + throw ServiceError.incorrectClientId + } + + let encrypted = try sessionCrypto.encrypt(message: encoded, receiverPublicKey: receiverPublicKey) + + _ = 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 + } + + private func storeConnectedApp(account: Account, sessionCrypto: TonConnectSessionCrypto, parameters: TonConnectParameters, manifest: TonConnectManifest) throws { + let tonConnectApp = TonConnectApp(accountId: account.id, clientId: parameters.clientId, manifest: manifest, keyPair: sessionCrypto.keyPair) + try storage.save(tonConnectApp: tonConnectApp) + } + + private func delete(tonConnectApp: TonConnectApp) throws { + try storage.delete(tonConnectApp: tonConnectApp) + + syncTonConnectApps() + } +} + +extension TonConnectManager { + var sendTransactionRequestPublisher: AnyPublisher { + sendTransactionRequestSubject.eraseToAnyPublisher() + } + + func loadTonConnectConfiguration(deeplink: String) async throws -> TonConnectConfig { + let parameters = try parseTonConnect(deeplink: deeplink) + + do { + let manifest = try await loadManifest(url: parameters.requestPayload.manifestUrl) + return TonConnectConfig(parameters: parameters, manifest: manifest) + } catch { + throw ServiceError.manifestLoadFailed + } + } + + func connect(account: Account, parameters: TonConnectParameters, manifest: TonConnectManifest) async throws { + let (publicKey, secretKey) = try TonKitManager.keyPair(accountType: account.type) + + let message = try TonConnectResponseBuilder.buildConnectEventSuccesResponse( + requestPayloadItems: parameters.requestPayload.items, + contract: TonKitManager.contract(publicKey: publicKey), + keyPair: KeyPair(publicKey: .init(data: publicKey), privateKey: .init(data: secretKey)), + manifest: manifest + ) + + let sessionCrypto = try TonConnectSessionCrypto() + + try await send(message: message, clientId: parameters.clientId, sessionCrypto: sessionCrypto) + try storeConnectedApp(account: account, sessionCrypto: sessionCrypto, parameters: parameters, manifest: manifest) + + syncTonConnectApps() + } + + func disconnect(tonConnectApp: TonConnectApp) async throws { + let sessionCrypto = try TonConnectSessionCrypto(privateKey: tonConnectApp.keyPair.privateKey) + try await send(message: TonConnect.DisconnectEvent(), clientId: tonConnectApp.clientId, sessionCrypto: sessionCrypto) + + try delete(tonConnectApp: tonConnectApp) + } + + func approve(request: TonConnectSendTransactionRequest, boc: String) async throws { + let message = TonConnect.SendTransactionResponse.success(.init(result: boc, id: request.id)) + let sessionCrypto = try TonConnectSessionCrypto(privateKey: request.app.keyPair.privateKey) + + try await send(message: message, clientId: request.app.clientId, sessionCrypto: sessionCrypto) + } + + func reject(request: TonConnectSendTransactionRequest) async throws { + let message = TonConnect.SendTransactionResponse.error(.init(id: request.id, error: .init(code: .userDeclinedTransaction, message: ""))) + let sessionCrypto = try TonConnectSessionCrypto(privateKey: request.app.keyPair.privateKey) + + try await send(message: message, clientId: request.app.clientId, sessionCrypto: sessionCrypto) + } +} + +extension TonConnectManager { + enum ServiceError: Error { + case incorrectUrl + case manifestLoadFailed + case incorrectClientId + } + + struct TonConnectError: Swift.Error, Decodable { + let statusCode: Int + let message: String + } + + struct TonConnectEvent: Decodable { + let from: String + let message: String + } +} + +private extension String { + static let tcScheme = "tc" + static let versionKey = "v" + static let clientIdKey = "id" + static let requestPayloadKey = "r" +} + +struct TonConnectSendTransactionRequest { + let id: String + let param: SendTransactionParam + let app: TonConnectApp +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManifest.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManifest.swift new file mode 100644 index 0000000000..4049e70d82 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectManifest.swift @@ -0,0 +1,13 @@ +import Foundation + +struct TonConnectManifest: Codable, Equatable { + let url: URL + let name: String + let iconUrl: URL? + let termsOfUseUrl: URL? + let privacyPolicyUrl: URL? + + var host: String { + url.host ?? "" + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift new file mode 100644 index 0000000000..3bdb161879 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectParameters.swift @@ -0,0 +1,17 @@ +struct TonConnectParameters { + let version: Version + let clientId: String + let requestPayload: TonConnectRequestPayload + + init(version: Version, clientId: String, requestPayload: TonConnectRequestPayload) { + self.version = version + self.clientId = clientId + self.requestPayload = requestPayload + } +} + +extension TonConnectParameters { + enum Version: String { + case v2 = "2" + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectRequestPayload.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectRequestPayload.swift new file mode 100644 index 0000000000..0c6be60ac3 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectRequestPayload.swift @@ -0,0 +1,38 @@ +import Foundation + +struct TonConnectRequestPayload: Decodable { + let manifestUrl: URL + let items: [Item] + + init(manifestUrl: URL, items: [Item]) { + self.manifestUrl = manifestUrl + self.items = items + } +} + +extension TonConnectRequestPayload { + enum Item: Decodable { + case tonAddress + case tonProof(payload: String) + case unknown + + enum CodingKeys: CodingKey { + case name + case payload + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let name = try container.decode(String.self, forKey: .name) + switch name { + case "ton_addr": + self = .tonAddress + case "ton_proof": + let payload = try container.decode(String.self, forKey: .payload) + self = .tonProof(payload: payload) + default: + self = .unknown + } + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectResponseBuilder.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectResponseBuilder.swift new file mode 100644 index 0000000000..6cb625d1be --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectResponseBuilder.swift @@ -0,0 +1,32 @@ +import Foundation +import TonSwift + +public enum TonConnectResponseBuilder { + static func buildConnectEventSuccesResponse(requestPayloadItems: [TonConnectRequestPayload.Item], contract: Contract, keyPair: KeyPair, manifest: TonConnectManifest) throws -> TonConnect.ConnectEventSuccess { + let address = try contract.address() + + let replyItems = requestPayloadItems.compactMap { item in + switch item { + case .tonAddress: + return TonConnect.ConnectItemReply.tonAddress(.init( + address: address, + network: TonConnect.Network.mainnet, + publicKey: keyPair.publicKey, + walletStateInit: contract.stateInit + ) + ) + case let .tonProof(payload): + return TonConnect.ConnectItemReply.tonProof(.success(.init( + address: address, + domain: manifest.host, + payload: payload, + privateKey: keyPair.privateKey + ))) + case .unknown: + return nil + } + } + + return TonConnect.ConnectEventSuccess(payload: .init(items: replyItems, device: .init())) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendHandler.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendHandler.swift new file mode 100644 index 0000000000..72339cdbdc --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendHandler.swift @@ -0,0 +1,273 @@ +import BigInt +import Foundation +import MarketKit +import TonKit +import TonSwift + +class TonConnectSendHandler { + private let tonConnectManager = App.shared.tonConnectManager + private let request: TonConnectSendTransactionRequest + private let transferData: TransferData + private let contract: WalletContract + private let secretKey: Data + let baseToken: Token + private let converter: TonEventConverter + + init(request: TonConnectSendTransactionRequest, transferData: TransferData, contract: WalletContract, secretKey: Data, baseToken: Token, converter: TonEventConverter) { + self.request = request + self.transferData = transferData + self.contract = contract + self.secretKey = secretKey + self.baseToken = baseToken + self.converter = converter + } +} + +extension TonConnectSendHandler: ISendHandler { + var expirationDuration: Int? { + 10 + } + + func sendData(transactionSettings _: TransactionSettings?) async throws -> ISendData { + var fee: Decimal? + var transactionError: Error? + var record: TonTransactionRecord? + + do { + let result = try await TonKit.Kit.emulate(transferData: transferData, contract: contract, network: TonKitManager.network) + + record = converter.transactionRecord(event: result.event) + fee = TonAdapter.amount(kitAmount: result.totalFee) + } catch { + transactionError = error + } + + return SendData(record: record, fee: fee, transactionError: transactionError) + } + + func send(data _: ISendData) async throws { + let boc = try await TonKit.Kit.boc(transferData: transferData, contract: contract, secretKey: secretKey, network: TonKitManager.network) + + try await TonKit.Kit.send(boc: boc, contract: contract, network: TonKitManager.network) + try await tonConnectManager.approve(request: request, boc: boc) + } +} + +extension TonConnectSendHandler { + class SendData: ISendData { + private let record: TonTransactionRecord? + private let fee: Decimal? + private let transactionError: Error? + + init(record: TonTransactionRecord?, fee: Decimal?, transactionError: Error?) { + self.record = record + self.fee = fee + self.transactionError = transactionError + } + + var feeData: FeeData? { + nil + } + + var canSend: Bool { + transactionError == nil + } + + var customSendButtonTitle: String? { + nil + } + + var rateCoins: [Coin] { + [] // todo + } + + private func caution(transactionError: Error, feeToken: Token) -> CautionNew { + let title: String + let text: String + + if let tonError = transactionError as? TonConnectSendHandler.TransactionError { + switch tonError { + case let .insufficientTonBalance(balance): + let coinValue = CoinValue(kind: .token(token: feeToken), value: balance) + let balanceString = ValueFormatter.instance.formatShort(coinValue: coinValue) + + title = "fee_settings.errors.insufficient_balance".localized + text = "fee_settings.errors.insufficient_balance.info".localized(balanceString ?? "") + } + } else { + title = "ethereum_transaction.error.title".localized + text = transactionError.convertedError.smartDescription + } + + return CautionNew(title: title, text: text, type: .error) + } + + func cautions(baseToken: Token) -> [CautionNew] { + var cautions = [CautionNew]() + + if let transactionError { + cautions.append(caution(transactionError: transactionError, feeToken: baseToken)) + } + + return cautions + } + + func sections(baseToken: Token, currency: Currency, rates: [String: Decimal]) -> [[SendField]] { + var sections = [[SendField]]() + + if let record { + for action in record.actions { + var fields: [SendField] + + switch action.type { + case let .send(value, to, _, comment): + if let token = value.token, let decimalValue = value.decimalValue { + fields = [ + .amount( + title: "send.confirmation.you_send".localized, + token: token, + coinValueType: .regular(coinValue: CoinValue(kind: .token(token: token), value: decimalValue)), + currencyValue: rates[token.coin.uid].map { CurrencyValue(currency: currency, value: $0 * decimalValue) }, + type: .outgoing + ), + .address( + title: "send.confirmation.to".localized, + value: to, + blockchainType: .ton + ), + ] + + if let comment { + fields.append(.levelValue(title: "send.confirmation.comment".localized, value: comment, level: .regular)) + } + } else { + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Send", level: .regular)] + } + case let .receive(value, from, comment): + if let token = value.token, let decimalValue = value.decimalValue { + fields = [ + .amount( + title: "send.confirmation.you_receive".localized, + token: token, + coinValueType: .regular(coinValue: CoinValue(kind: .token(token: token), value: decimalValue)), + currencyValue: rates[token.coin.uid].map { CurrencyValue(currency: currency, value: $0 * decimalValue) }, + type: .incoming + ), + .address( + title: "send.confirmation.from".localized, + value: from, + blockchainType: .ton + ), + ] + + if let comment { + fields.append(.levelValue(title: "send.confirmation.comment".localized, value: comment, level: .regular)) + } + } else { + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Receive", level: .regular)] + } + case .burn: + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Burn", level: .regular)] + case .mint: + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Mint", level: .regular)] + case .swap: + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Swap", level: .regular)] + case .contractDeploy: + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Contract- Deploy", level: .regular)] + case .contractCall: + fields = [.levelValue(title: "send.confirmation.action".localized, value: "Contract Call", level: .regular)] + case let .unsupported(type): + fields = [.levelValue(title: "send.confirmation.action".localized, value: type, level: .regular)] + } + + switch action.status { + case .failed: + fields.append(.levelValue(title: "send.confirmation.status".localized, value: "send.confirmation.status.failed".localized, level: .error)) + default: () + } + + sections.append(fields) + } + } + + sections.append(feeFields(currency: currency, feeToken: baseToken, feeTokenRate: rates[baseToken.coin.uid])) + + return sections + } + + private func feeFields(currency: Currency, feeToken: Token, feeTokenRate: Decimal?) -> [SendField] { + var viewItems = [SendField]() + + if let fee { + let coinValue = CoinValue(kind: .token(token: feeToken), value: fee) + let currencyValue = feeTokenRate.map { CurrencyValue(currency: currency, value: fee * $0) } + + viewItems.append( + .value( + title: "fee_settings.network_fee".localized, + description: .init(title: "fee_settings.network_fee".localized, description: "fee_settings.network_fee.info".localized), + coinValue: coinValue, + currencyValue: currencyValue, + formatFull: true + ) + ) + } + + return viewItems + } + } +} + +extension TonConnectSendHandler { + enum SendError: Error { + case invalidAmount + case invalidData + } + + enum TransactionError: Error { + case insufficientTonBalance(balance: Decimal) + } + + enum FactoryError: Error { + case noAccount + case noBaseToken + } +} + +extension TonConnectSendHandler { + static func instance(request: TonConnectSendTransactionRequest) throws -> TonConnectSendHandler { + guard let account = App.shared.accountManager.account(id: request.app.accountId) else { + throw FactoryError.noAccount + } + + let (publicKey, secretKey) = try TonKitManager.keyPair(accountType: account.type) + let contract = TonKitManager.contract(publicKey: publicKey) + let address = try contract.address() + + let payloads = request.param.messages.map { message in + TonKit.Kit.Payload( + value: BigInt(integerLiteral: message.amount), + recipientAddress: message.address, + stateInit: message.stateInit, + payload: message.payload + ) + } + + let transferData = try TonKit.Kit.transferData(sender: address, payloads: payloads) + + guard let baseToken = try? App.shared.coinManager.token(query: .init(blockchainType: .ton, tokenType: .native)) else { + throw FactoryError.noBaseToken + } + + let transactionSource = TransactionSource(blockchainType: .ton, meta: nil) + + return TonConnectSendHandler( + request: request, + transferData: transferData, + contract: contract, + secretKey: secretKey, + baseToken: baseToken, + converter: TonEventConverter(address: address, source: transactionSource, baseToken: baseToken, coinManager: App.shared.coinManager) + ) + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendView.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendView.swift new file mode 100644 index 0000000000..f58efccf75 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendView.swift @@ -0,0 +1,64 @@ +import ComponentKit +import MarketKit +import SwiftUI + +struct TonConnectSendView: View { + @StateObject var viewModel: TonConnectSendViewModel + @StateObject var sendViewModel: SendViewModel + + @Environment(\.presentationMode) private var presentationMode + + init(request: TonConnectSendTransactionRequest) { + _viewModel = .init(wrappedValue: TonConnectSendViewModel(request: request)) + _sendViewModel = .init(wrappedValue: SendViewModel(sendData: .tonConnect(request: request))) + } + + var body: some View { + ThemeNavigationView { + ThemeView { + BottomGradientWrapper { + SendView(viewModel: sendViewModel) + } bottomContent: { + VStack(spacing: .margin16) { + switch sendViewModel.state { + case .syncing: + EmptyView() + case .success: + Button(action: { + Task { + try await sendViewModel.send() + + await MainActor.run { + presentationMode.wrappedValue.dismiss() + } + } + }) { + Text("wallet_connect.button.confirm".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .yellow)) + .disabled(sendViewModel.sending) + case .failed: + Button(action: { + sendViewModel.sync() + }) { + Text("send.confirmation.refresh".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + } + + Button(action: { + viewModel.reject() + presentationMode.wrappedValue.dismiss() + }) { + Text("button.reject".localized) + } + .buttonStyle(PrimaryButtonStyle(style: .gray)) + .disabled(sendViewModel.sending) + } + } + } + .navigationTitle(viewModel.appName) + .navigationBarTitleDisplayMode(.inline) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendViewModel.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendViewModel.swift new file mode 100644 index 0000000000..3f6e4ac17f --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSendViewModel.swift @@ -0,0 +1,20 @@ +import Combine + +class TonConnectSendViewModel: ObservableObject { + private let tonConnectManager = App.shared.tonConnectManager + private let request: TonConnectSendTransactionRequest + + init(request: TonConnectSendTransactionRequest) { + self.request = request + } + + var appName: String { + request.app.manifest.name + } + + func reject() { + Task { [tonConnectManager, request] in + try await tonConnectManager.reject(request: request) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSessionCrypto.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSessionCrypto.swift new file mode 100644 index 0000000000..553509d0ee --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectSessionCrypto.swift @@ -0,0 +1,58 @@ +import Foundation +import TonSwift +import TweetNacl + +public struct TonConnectSessionCrypto { + public let sessionId: String + public let keyPair: KeyPair + + public init() throws { + let keyPair = try TweetNacl.NaclBox.keyPair() + self.keyPair = KeyPair(publicKey: .init(data: keyPair.publicKey), + privateKey: .init(data: keyPair.secretKey)) + sessionId = keyPair.publicKey.hexString() + } + + public init(privateKey: PrivateKey) throws { + let keyPair = try TweetNacl.NaclBox.keyPair(fromSecretKey: privateKey.data) + self.keyPair = KeyPair(publicKey: .init(data: keyPair.publicKey), + privateKey: .init(data: keyPair.secretKey)) + sessionId = keyPair.publicKey.hexString() + } + + public func encrypt(message: Data, receiverPublicKey: Data) throws -> Data { + let nonce = try createNonce() + let encrypted = try TweetNacl.NaclBox.box( + message: message, + nonce: nonce, + publicKey: receiverPublicKey, + secretKey: keyPair.privateKey.data + ) + return nonce + encrypted + } + + public func decrypt(message: Data, senderPublicKey: Data) throws -> Data { + guard message.count >= .nonceLength else { + return Data() + } + let nonce = message[0 ..< Int.nonceLength] + let internalMessage = message[Int.nonceLength ..< message.count] + let decrypted = try TweetNacl.NaclBox.open( + message: internalMessage, + nonce: nonce, + publicKey: senderPublicKey, + secretKey: keyPair.privateKey.data + ) + return decrypted + } +} + +private extension TonConnectSessionCrypto { + func createNonce() throws -> Data { + return try TweetNacl.NaclUtil.secureRandomData(count: .nonceLength) + } +} + +private extension Int { + static let nonceLength = 24 +} diff --git a/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectStorage.swift b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectStorage.swift new file mode 100644 index 0000000000..c5a1119921 --- /dev/null +++ b/UnstoppableWallet/UnstoppableWallet/Modules/TonConnect/TonConnectStorage.swift @@ -0,0 +1,66 @@ +import Foundation +import GRDB + +class TonConnectStorage { + private let dbPool: DatabasePool + + init(dbPool: DatabasePool) throws { + self.dbPool = dbPool + + try migrator.migrate(dbPool) + } + + var migrator: DatabaseMigrator { + var migrator = DatabaseMigrator() + + migrator.registerMigration("Create tonConnectApp") { db in + try db.create(table: "tonConnectApp") { t in + t.column(TonConnectApp.Columns.accountId.name, .text).notNull() + t.column(TonConnectApp.Columns.clientId.name, .text).notNull() + t.column(TonConnectApp.Columns.manifest.name, .text).notNull() + t.column(TonConnectApp.Columns.keyPair.name, .text).notNull() + + t.primaryKey([TonConnectApp.Columns.accountId.name, TonConnectApp.Columns.clientId.name], onConflict: .replace) + } + + try db.create(table: "tonConnectLastEvent") { t in + t.primaryKey(TonConnectLastEvent.Columns.uniqueField.name, .text, onConflict: .replace) + t.column(TonConnectLastEvent.Columns.id.name, .text).notNull() + } + } + + return migrator + } +} + +extension TonConnectStorage { + func tonConnectApps() throws -> [TonConnectApp] { + try dbPool.read { db in + try TonConnectApp.fetchAll(db) + } + } + + func save(tonConnectApp: TonConnectApp) throws { + _ = try dbPool.write { db in + try tonConnectApp.insert(db) + } + } + + func delete(tonConnectApp: TonConnectApp) throws { + _ = try dbPool.write { db in + try TonConnectApp.filter(TonConnectApp.Columns.accountId == tonConnectApp.accountId && TonConnectApp.Columns.clientId == tonConnectApp.clientId).deleteAll(db) + } + } + + func lastEventId() throws -> String? { + try dbPool.read { db in + try TonConnectLastEvent.fetchOne(db)?.id + } + } + + func save(lastEventId: String) throws { + _ = try dbPool.write { db in + try TonConnectLastEvent(id: lastEventId).insert(db) + } + } +} diff --git a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings index 78ccc689db..360145fe57 100644 --- a/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings +++ b/UnstoppableWallet/UnstoppableWallet/en.lproj/Localizable.strings @@ -450,8 +450,10 @@ "send.confirmation.title" = "Confirm"; "send.confirmation.you_send" = "You Send"; +"send.confirmation.you_receive" = "You Receive"; "send.confirmation.transfer" = "Transfer"; "send.confirmation.to" = "To"; +"send.confirmation.from" = "From"; "send.confirmation.own" = "Own"; "send.confirmation.contact_name" = "Contact Name"; "send.confirmation.domain" = "Domain"; @@ -465,6 +467,10 @@ "send.confirmation.replace_by_fee" = "Replace by Fee"; "send.confirmation.replaced_transactions" = "Replaced Transactions"; "send.confirmation.input" = "Input"; +"send.confirmation.comment" = "Comment"; +"send.confirmation.action" = "Action"; +"send.confirmation.status" = "Status"; +"send.confirmation.status.failed" = "Failed"; "send.confirmation.sync_failed" = "Sync Failed"; "send.confirmation.invalid_data" = "Invalid Data"; @@ -1787,6 +1793,20 @@ "wallet_connect.paired_dapps.disconnect_all" = "Delete All"; "wallet_connect.pending_requests.nonactive_footer" = "To open a request you must activate the desired wallet"; +// Ton Connect + +"ton_connect.list.new_connection" = "New Connection"; +"ton_connect.list.no_connected_apps" = "No Connected Apps"; +"ton_connect.list.disconnect_app" = "Disconnect App"; +"ton_connect.list.disconnect_app.description" = "Are you sure you want to disconnect from \"%@\"?"; +"ton_connect.list.disconnect_app.disconnect" = "Disconnect"; +"ton_connect.connect.url" = "URL"; +"ton_connect.connect.wallet" = "Wallet"; +"ton_connect.connect.no_eligible_wallets" = "No eligible wallets"; +"ton_connect.connect.warning" = "By clicking approve, you allow this app to view your public address. This is an important security step to protect your data from potential phishing risks."; +"ton_connect.connect.connect" = "Connect"; +"ton_connect.connect.reject" = "Reject"; + // App Status "app_status.title" = "App Status";