Skip to content

Commit

Permalink
Merge pull request #3 from loopedresolve/main
Browse files Browse the repository at this point in the history
Refactor chain specification implementation and update tests
  • Loading branch information
loopedresolve authored Jul 26, 2024
2 parents c258c31 + 524e423 commit 840f39f
Show file tree
Hide file tree
Showing 12 changed files with 91 additions and 156 deletions.
20 changes: 11 additions & 9 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let package = Package(
targets: ["SmoldotSwift"]),
],
dependencies: [
.package(url: "https://github.com/finsig/json-rpc2", from: "0.1.0")
.package(url: "https://github.com/finsig/json-rpc2", from: "0.1.1")
],
targets: [
.target(
Expand All @@ -22,13 +22,8 @@ let package = Package(
"CSmoldot",
.product(name: "JSONRPC2", package: "json-rpc2"),
],
path: "Sources/SmoldotSwift",
resources: [
.process("Resources/polkadot.json"),
.process("Resources/kusama.json"),
.process("Resources/rococo.json"),
.process("Resources/westend.json")]
),
path: "Sources/SmoldotSwift"
),
.target(
name: "CSmoldot",
dependencies: ["smoldot"],
Expand All @@ -46,6 +41,13 @@ let package = Package(

.testTarget(
name: "SmoldotSwiftTests",
dependencies: ["SmoldotSwift"]),
dependencies: ["SmoldotSwift"],
resources: [
.process("Resources/polkadot.json"),
.process("Resources/kusama.json"),
.process("Resources/rococo.json"),
.process("Resources/westend.json")]
),

]
)
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@ Add the package declaration to your project's manifest dependencies array:

## Usage

Initialize a Chain from a specification. A Chain Specification is a JSON Object that describes a Polkadot-based blockchain network. Chain Specifications for Polkadot, Kusama, Rococo, and Westend are provided.
A Chain Specification file must be provided to initialize a chain. A Chain Specification is a JSON Object that describes a Polkadot-based blockchain network.

*Example Chain Specification JSON files for Polkadot, Kusama, Rococo, and Westend can be copied for use from [/Tests/SmoldotSwiftTests/Resources](https://github.com/loopedresolve/smoldot-swift/tree/main/Tests/SmoldotSwiftTests/Resources).*


Initialize a chain from a specification file:

```swift
var chain = Chain(specification: .polkadot)
var chain = Chain(specificationFile: {Resource file URL})
```

Add the chain to the client to connect to the network:
Expand Down
48 changes: 0 additions & 48 deletions Sources/SmoldotSwift/Chain+Resources.swift

This file was deleted.

6 changes: 5 additions & 1 deletion Sources/SmoldotSwift/Chain+Specification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,16 @@
import Foundation

extension Chain.Specification {


/// Chain name as defined in the specification.
public var name: String {
precondition(self["name"] != nil, "Chain Specification is missing required key `name`")
return self["name"] as! String
}

/// Chain identifier as defined in the specification.
var id: String {
precondition(self["id"] != nil, "Chain Specification is missing required key `id`")
return self["id"] as! String
}
}
17 changes: 16 additions & 1 deletion Sources/SmoldotSwift/Chain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,31 @@ public final class Chain: Hashable {
/// connects to, the other nodes that it initially communicates with, and the initial state that nodes
/// must agree on to produce blocks.
///
/// A typelias is used rather than defining an explicit type so that Foundation `JSONSerialization`
/// can be used to convert the JSON into a `Dictionary` type representation of the object with key
/// values of type `Any`.
///
/// - Important:
/// Niether the validity of the Chain Specification JSON nor its conformance to the ChainSpec trait
/// is handled by Swift and will produce fatal error information in the Rust environment logger.
///
public typealias Specification = JSONObject

/// Creates a Chain from the provided Chain Specification.
/// Creates a Chain from the a Chain Specification JSON object.
///
/// See ``Specification`` for more information.
///
public init(specification: Specification) {
self.specification = specification
}

/// Creates a Chain from a Chain Specification JSON file.
public convenience init(specificationFile url: URL) throws {
let data = try Data(contentsOf: url)
let specification = try JSONSerialization.jsonObject(with: data) as! Specification
self.init(specification: specification)
}

public func hash(into hasher: inout Hasher) {
hasher.combine(specification.id)
}
Expand Down
16 changes: 7 additions & 9 deletions Sources/SmoldotSwift/Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,9 @@ public final class Client {
///
public func add(chain: inout Chain) throws {
guard !chain.isValid else {
throw ClientError(message: "Chain has already been added.")
}
guard let data = try? JSONSerialization.data(withJSONObject: chain.specification) else {
throw ClientError(message: "Invalid JSON object.") /// note: checks for validity only, does not check Chain Specification JSON object correctness. intentional.
throw ClientError.chainHasAlreadyBeenAdded
}
let data = try JSONSerialization.data(withJSONObject: chain.specification)
let string = String(data: data, encoding: .utf8)
chain.id = Chain.Id( smoldot_add_chain(string) )
}
Expand All @@ -65,7 +63,7 @@ public final class Client {
///
public func remove(chain: inout Chain) throws {
guard let id = chain.id else {
throw ClientError(message: "Chain not found in client.")
throw ClientError.chainNotFound
}
smoldot_remove_chain(id)
}
Expand All @@ -79,12 +77,12 @@ public final class Client {
///
public func send(request: JSONRPC2Request, to chain: Chain) throws {
guard let id = chain.id else {
throw ClientError(message: "Chain not found in client.")
throw ClientError.chainNotFound
}
let encoder = JSONEncoder()
let data = try encoder.encode(request)
guard let string = String(data: data, encoding: .utf8) else {
throw ClientError(message: "Error encoding request.")
throw ClientError.errorEncodingRequest
}
smoldot_json_rpc_request(id, string)
}
Expand All @@ -104,7 +102,7 @@ public final class Client {
Task.detached {
while (true) {
guard let id = chain.id else {
throw ClientError(message: "Chain not found in client.")
throw ClientError.chainNotFound
}
guard let cString = smoldot_wait_next_json_rpc_response(id) else {
break
Expand All @@ -128,7 +126,7 @@ public final class Client {
///
public func response(from chain: Chain) async throws -> String? {
guard let id = chain.id else {
throw ClientError(message: "Chain not found in client.")
throw ClientError.chainNotFound
}
guard let cString = smoldot_wait_next_json_rpc_response(id) else {
return nil
Expand Down
10 changes: 4 additions & 6 deletions Sources/SmoldotSwift/ClientError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,8 @@

import Foundation

public struct ClientError: Error, CustomStringConvertible {
let message: String

public var description: String {
return "[error: \(message)]"
}
public enum ClientError: Error {
case chainHasAlreadyBeenAdded
case chainNotFound
case errorEncodingRequest
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
122 changes: 42 additions & 80 deletions Tests/SmoldotSwiftTests/SmoldotSwiftTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,114 +8,76 @@ final class SmoldotSwiftTests: XCTestCase {
var chain: Chain!

override func setUp() async throws {
chain = Chain(specification: .polkadot)
///
/// Chain specification file to use for testing. If adding a file, also explicitly declare the resource for
/// the test target in the package manifest.
///
let url = Bundle.module.url(forResource: "polkadot", withExtension: "json")!
//let url = Bundle.module.url(forResource: "kusama", withExtension: "json")!
//let url = Bundle.module.url(forResource: "rococo", withExtension: "json")!
//let url = Bundle.module.url(forResource: "westend", withExtension: "json")!

chain = try Chain(specificationFile: url)

XCTAssertFalse( chain.isValid )
}

func testAddChain() throws {
try Client.shared.add(chain: &chain)
/// Add the chain to the client
XCTAssertNoThrow( try Client.shared.add(chain: &chain) )

XCTAssertTrue( chain.isValid )
}

func testAddChainAlreadyAdded() throws {
try Client.shared.add(chain: &chain)

XCTAssertThrowsError( try Client.shared.add(chain: &chain) )
}

/*
func testAddChainRemoveChainMemoryPerformance() async throws {
self.measure(metrics: [XCTMemoryMetric()]) {
let exp = expectation(description: "Finished")
Task {
try Client.shared.add(chain: &chain)
//try await Task.sleep(nanoseconds: 1_000_000_000 * 30) // sleep
try Client.shared.remove(chain: &chain)
//try await Task.sleep(nanoseconds: 1_000_000_000 * 30) // sleep
exp.fulfill()
}
wait(for: [exp], timeout: 1_000_000_000 * 30)
/// Add the chain to the client
XCTAssertNoThrow( try Client.shared.add(chain: &chain) )

/// Add the chain to the client again
XCTAssertThrowsError( try Client.shared.add(chain: &chain) ) { error in
XCTAssertTrue( error as! ClientError == ClientError.chainHasAlreadyBeenAdded )
}
}
*/

func testRemoveChain() throws {
try Client.shared.add(chain: &chain)
try Client.shared.remove(chain: &chain)
/// Add the chain to the client
XCTAssertNoThrow( try Client.shared.add(chain: &chain) )
XCTAssertTrue( chain.isValid )

/// Remove the chain from the client
XCTAssertNoThrow( try Client.shared.remove(chain: &chain) )
XCTAssertFalse( chain.isValid )
}

func testRemoveChainNotAdded() throws {
XCTAssertThrowsError( try Client.shared.remove(chain: &chain) )
}

func testJSONRPCRequestResponse() async throws {
try Client.shared.add(chain: &chain)

let request = try JSONRPC2Request(string: "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"system_chain\",\"params\":[]}")

XCTAssertNoThrow( try Client.shared.send(request: request, to: chain) )

let responseData = try await Client.shared.response(from: chain)?.data(using: .utf8)

XCTAssertNotNil(responseData)

let response = try JSONDecoder().decode(Response.self, from: responseData!)

XCTAssertNotNil(request.identifier)

XCTAssertEqual(response.identifier, request.identifier!)

switch response.result {
case .success(let json):
XCTAssertEqual(json.description, "Polkadot")
case .failure(_):
XCTFail()
/// Try to remove the chain when it has not been added to the client.
XCTAssertThrowsError( try Client.shared.remove(chain: &chain) ) { error in
XCTAssertTrue( error as! ClientError == ClientError.chainNotFound )
}
}

func testJSONRPC2RequestInvalidJSON() async throws {

XCTAssertThrowsError(try JSONRPC2Request(string: "invalid json") )
/// Try to build a JSON-RPC2 request from a non-JSON value.
XCTAssertThrowsError( try JSONRPC2Request(string: "invalid json") ) { error in
XCTAssertTrue( (error as! JSONRPC2Error).code == JSONRPC2Error.Code.invalidRequest )
}
}

func testJSONRPC2RequestInvalidJSONRPCVersion() async throws {

XCTAssertThrowsError(try JSONRPC2Request(string: "{\"id\":1,\"jsonrpc\":\"1.0\",\"method\":\"system_chain\",\"params\":[]}") )
/// Try to build a JSON-RPC 1.0 request.
XCTAssertThrowsError( try JSONRPC2Request(string: "{\"id\":1,\"jsonrpc\":\"1.0\",\"method\":\"system_chain\",\"params\":[]}") ) { error in
XCTAssertTrue( (error as! JSONRPC2Error).code == JSONRPC2Error.Code.invalidRequest )
}
}

func testJSONRPC2RequestChainNotAdded() async throws {
let chain = Chain(specification: .kusama)

let request = try JSONRPC2Request(string: "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"system_chain\",\"params\":[]}")
/// Try to send a request to a chain without first adding it to the client.
let request = try? JSONRPC2Request(string: "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"system_chain\",\"params\":[]}")
XCTAssertNotNil(request)

XCTAssertThrowsError( try Client.shared.send(request: request, to: chain) )
}

}


fileprivate extension Chain.Specification {

static var polkadot: JSONObject {
return jsonObject(resourceName: "polkadot")
}

static var kusama: JSONObject {
return jsonObject(resourceName: "kusama")
}

private static func jsonObject(resourceName name: String) -> JSONObject {
guard let url = Bundle.module.url(forResource: name, withExtension: "json") else {
fatalError()
}
guard let data = try? Data(contentsOf: url) else {
fatalError()
XCTAssertThrowsError( try Client.shared.send(request: request!, to: chain) ) { error in
XCTAssertTrue( error as! ClientError == ClientError.chainNotFound )
}
guard let jsonObject = try? JSONSerialization.jsonObject(with: data) as? JSONObject else {
fatalError()
}
return jsonObject
}

}

0 comments on commit 840f39f

Please sign in to comment.