Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ let package = Package(
],
dependencies: [
.package(name: "BigInt", url: "https://github.com/attaswift/BigInt.git", from: "5.2.1"),
.package(name: "Starscream", url: "https://github.com/daltoniam/Starscream", from: "4.0.8")
],
targets: [
.target(
name: "Flow",
dependencies: ["BigInt"],
dependencies: ["BigInt", "Starscream"],
path: "Sources",
resources: [
.copy("Cadence/CommonCadence"),
Expand Down
1 change: 0 additions & 1 deletion Sources/Cadence/File.swift

This file was deleted.

32 changes: 32 additions & 0 deletions Sources/Extension/Publisher+Async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import Combine
import Foundation

@available(iOS, deprecated: 15.0, message: "AsyncCompatibilityKit is only useful when targeting iOS versions earlier than 15")
public extension Publisher {
Expand Down Expand Up @@ -62,3 +63,34 @@ public extension Publisher where Failure == Never {
}
}
}

struct TimeoutError: LocalizedError {
var errorDescription: String? {
return "Publisher timed out"
}
}

public func awaitPublisher<T: Publisher>(_ publisher: T, timeout: TimeInterval = 20) async throws -> T.Output {
try await withCheckedThrowingContinuation { continuation in
var cancellable: AnyCancellable?
let timeoutTask = _Concurrency.Task.detached {
try await _Concurrency.Task.sleep(nanoseconds: UInt64(timeout) * 1_000_000_000)
cancellable?.cancel()
continuation.resume(throwing: TimeoutError())
}

cancellable = publisher.first()
.sink(
receiveCompletion: { completion in
timeoutTask.cancel()
if case .failure(let error) = completion {
continuation.resume(throwing: error)
}
},
receiveValue: { value in
timeoutTask.cancel()
continuation.resume(returning: value)
}
)
}
}
11 changes: 11 additions & 0 deletions Sources/Models/FlowChainId.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ public extension Flow {
return .gRPC(.init(node: "access.mainnet.nodes.onflow.org", port: 9000))
}
}

public var defaultWebSocketNode: Flow.Transport? {
switch self {
case .mainnet:
return .websocket(URL(string: "wss://rest-mainnet.onflow.org/v1/ws")!)
case .testnet:
return .websocket(URL(string: "wss://rest-testnet.onflow.org/v1/ws")!)
default:
return nil
}
}

// TODO: Support Custom Node encode & decode
public func encode(to encoder: Encoder) throws {
Expand Down
5 changes: 5 additions & 0 deletions Sources/Network/FlowTransport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@ public extension Flow {
enum Transport: Equatable, Hashable {
case HTTP(_ url: URL)
case gRPC(_ endpoint: Endpoint)
case websocket(_ url: URL)

public var url: URL? {
switch self {
case let .HTTP(url):
return url
case .gRPC:
return nil
case let .websocket(url):
return url
}
}

Expand All @@ -37,6 +40,8 @@ public extension Flow {
return nil
case let .gRPC(endpoint):
return endpoint
case .websocket(_):
return nil
}
}

Expand Down
40 changes: 40 additions & 0 deletions Sources/Network/HTTP/AnyDecodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// File.swift
// Flow
//
// Created by Hao Fu on 24/4/2025.
//

import Foundation

public struct AnyDecodable: Decodable {
public let value: Any

public init(_ value: Any?) {
self.value = value ?? ()
}

public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()

if container.decodeNil() {
self.value = ()
} else if let bool = try? container.decode(Bool.self) {
self.value = bool
} else if let int = try? container.decode(Int.self) {
self.value = int
} else if let uint = try? container.decode(UInt.self) {
self.value = uint
} else if let double = try? container.decode(Double.self) {
self.value = double
} else if let string = try? container.decode(String.self) {
self.value = string
} else if let array = try? container.decode([AnyDecodable].self) {
self.value = array.map { $0.value }
} else if let dictionary = try? container.decode([String: AnyDecodable].self) {
self.value = dictionary.mapValues { $0.value }
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "AnyCodable value cannot be decoded")
}
}
}
124 changes: 124 additions & 0 deletions Sources/Network/Websocket/FlowPublisher.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Foundation
import Combine

public extension Flow {
/// Represents different types of events that can be published
enum PublisherEvent {
case transactionStatus(id: Flow.ID, status: Flow.TransactionResult)
case accountUpdate(address: Flow.Address)
case connectionStatus(isConnected: Bool)
case walletResponse(approved: Bool, data: [String: Any])
case block(id: Flow.ID, height: String, timestamp: Date)
case error(Error)
}

/// Central publisher manager for Flow events
class Publisher {
public static let shared = Publisher()

// Main publisher for all events
private let eventSubject = PassthroughSubject<PublisherEvent, Never>()

// Specific publishers for different event types
public var transactionPublisher: AnyPublisher<(Flow.ID, Flow.TransactionResult), Never> {
eventSubject
.compactMap { event in
if case .transactionStatus(let id, let status) = event {
return (id, status)
}
return nil
}
.eraseToAnyPublisher()
}

public var accountPublisher: AnyPublisher<Flow.Address, Never> {
eventSubject
.compactMap { event in
if case .accountUpdate(let address) = event {
return address
}
return nil
}
.eraseToAnyPublisher()
}

public var blockPublisher: AnyPublisher<Flow.WSBlockHeader, Never> {
eventSubject
.compactMap { event in
if case let .block(id, height, timestamp) = event {
return WSBlockHeader(blockId: id, height: height, timestamp: timestamp)
}
return nil
}
.eraseToAnyPublisher()
}

public var connectionPublisher: AnyPublisher<Bool, Never> {
eventSubject
.compactMap { event in
if case .connectionStatus(let isConnected) = event {
return isConnected
}
return nil
}
.eraseToAnyPublisher()
}

public var walletResponsePublisher: AnyPublisher<(Bool, [String: Any]), Never> {
eventSubject
.compactMap { event in
if case .walletResponse(let approved, let data) = event {
return (approved, data)
}
return nil
}
.eraseToAnyPublisher()
}

public var errorPublisher: AnyPublisher<Error, Never> {
eventSubject
.compactMap { event in
if case .error(let error) = event {
return error
}
return nil
}
.eraseToAnyPublisher()
}

private init() {}

// Method to publish events
public func publish(_ event: PublisherEvent) {
eventSubject.send(event)
}

// Convenience methods for publishing specific events
public func publishTransactionStatus(id: Flow.ID, status: Flow.TransactionResult) {
publish(.transactionStatus(id: id, status: status))
}

public func publishAccountUpdate(address: Flow.Address) {
publish(.accountUpdate(address: address))
}

public func publishConnectionStatus(isConnected: Bool) {
publish(.connectionStatus(isConnected: isConnected))
}

public func publishWalletResponse(approved: Bool, data: [String: Any]) {
publish(.walletResponse(approved: approved, data: data))
}

public func publishError(_ error: Error) {
publish(.error(error))
}
}
}

// Extension to Flow for easy access to publisher
public extension Flow {
var publisher: Publisher {
return Publisher.shared
}
}
65 changes: 65 additions & 0 deletions Sources/Network/Websocket/Models/WSRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// File.swift
// Flow
//
// Created by Hao Fu on 6/5/2025.
//

import Foundation

extension Flow {
public struct WSBlockHeader: Codable {
/// The identification of block
public let blockId: ID

/// The height of block
public let height: String

/// The time when the block is created
public let timestamp: Date
}

public struct WSTransactionResponse: Codable {
public let transactionResult: Flow.TransactionResult
}
}

// MARK: - Supporting Types

extension Flow.Websocket {
enum WebSocketError: Error {
case serverError(SocketError)
}

struct EmptyArguments: Codable {}

struct EventArguments: Codable {
let type: String?
let contractID: String?
let address: String?
}

public struct AccountArguments: Codable {
var startBlockId: String? = nil
var startBlockHeight: String? = nil
var heartbeatInterval: String? = nil
var eventTypes: [AccountEventType]? = nil
var accountAddresses: [String]? = nil
}

struct SendTransactionArguments: Codable {
let transaction: Flow.Transaction
}
}

public enum AccountEventType: String, Codable {
case accountCreated = "flow.AccountCreated"
case accountKeyAdded = "flow.AccountKeyAdded"
case accountKeyRemoved = "flow.AccountKeyRemoved"
case accountContractAdded = "flow.AccountContractAdded"
case accountContractUpdated = "flow.AccountContractUpdated"
case accountContractRemoved = "flow.AccountContractRemoved"
case inboxValuePublished = "flow.InboxValuePublished"
case inboxValueUnpublished = "flow.InboxValueUnpublished"
case inboxValueClaimed = "flow.InboxValueClaimed"
}
44 changes: 44 additions & 0 deletions Sources/Network/Websocket/WebSocketRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// File.swift
// Flow
//
// Created by Hao Fu on 30/4/2025.
//

import Foundation

extension Flow.Websocket {

Check warning on line 11 in Sources/Network/Websocket/WebSocketRequest.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
public enum BlockStatus: String, Codable {
case finalized
case sealed
}

Check warning on line 16 in Sources/Network/Websocket/WebSocketRequest.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
struct TransactionStatusRequest: Encodable {
let txId: String

Check warning on line 19 in Sources/Network/Websocket/WebSocketRequest.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
enum CodingKeys: String, CodingKey {
case txId = "tx_id"
}
}

Check warning on line 24 in Sources/Network/Websocket/WebSocketRequest.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
struct BlockDigestArguments: Encodable {
let blockStatus: BlockStatus
let startBlockHeight: String?
let startBlockId: String?
}

Check warning on line 30 in Sources/Network/Websocket/WebSocketRequest.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
public struct AccountStatusResponse: Codable {
public let blockId: String
public let height: String
public let accountEvents: [String: [AccountStatusEvent]]
}

Check warning on line 36 in Sources/Network/Websocket/WebSocketRequest.swift

View workflow job for this annotation

GitHub Actions / lint

Trailing Whitespace Violation: Lines should not have trailing whitespace (trailing_whitespace)
public struct AccountStatusEvent: Codable {
public let type: String
public let transactionId: String
public let transactionIndex: String
public let eventIndex: String
public let payload: String
}
}
Loading
Loading