Implements WASM smart contract compilation, optimization, and ABI generation across JavaScript/TypeScript, Python, Go, Rust, Flutter/Dart, Java, Kotlin, Swift, C, C++, C#/.NET, and Ruby. Features: - Optimization levels: None, Basic, Size, Aggressive - WASM section stripping with customizable options - Contract validation against VM requirements - ABI extraction and generation - Contract metadata handling - Gas estimation for deployment - Security analysis and recommendations - Size breakdown analysis Sub-clients: Contracts, ABI, Analysis, Validation
590 lines
20 KiB
Swift
590 lines
20 KiB
Swift
/**
|
|
* Synor Compiler SDK for Swift
|
|
*
|
|
* Smart contract compilation, optimization, and ABI generation.
|
|
*/
|
|
|
|
import Foundation
|
|
|
|
// MARK: - Enumerations
|
|
|
|
public enum OptimizationLevel: String, Codable {
|
|
case none
|
|
case basic
|
|
case size
|
|
case aggressive
|
|
}
|
|
|
|
// MARK: - Configuration
|
|
|
|
public struct StripOptions: Codable {
|
|
public var stripDebug: Bool = true
|
|
public var stripProducers: Bool = true
|
|
public var stripNames: Bool = true
|
|
public var stripCustom: Bool = true
|
|
public var preserveSections: [String] = []
|
|
public var stripUnused: Bool = true
|
|
|
|
public init() {}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case stripDebug = "strip_debug"
|
|
case stripProducers = "strip_producers"
|
|
case stripNames = "strip_names"
|
|
case stripCustom = "strip_custom"
|
|
case preserveSections = "preserve_sections"
|
|
case stripUnused = "strip_unused"
|
|
}
|
|
}
|
|
|
|
public struct CompilerConfig {
|
|
public let apiKey: String
|
|
public var endpoint: String = "https://compiler.synor.io/v1"
|
|
public var timeout: TimeInterval = 60
|
|
public var retries: Int = 3
|
|
public var debug: Bool = false
|
|
public var optimizationLevel: OptimizationLevel = .size
|
|
public var stripOptions: StripOptions?
|
|
public var maxContractSize: Int = 256 * 1024
|
|
public var useWasmOpt: Bool = true
|
|
public var validate: Bool = true
|
|
public var extractMetadata: Bool = true
|
|
public var generateAbi: Bool = true
|
|
|
|
public init(apiKey: String) {
|
|
self.apiKey = apiKey
|
|
}
|
|
}
|
|
|
|
// MARK: - Types
|
|
|
|
public struct ValidationError: Codable {
|
|
public let code: String
|
|
public let message: String
|
|
public let location: String?
|
|
}
|
|
|
|
public struct ValidationResult: Codable {
|
|
public let valid: Bool
|
|
public let errors: [ValidationError]
|
|
public let warnings: [String]
|
|
public let exportCount: Int
|
|
public let importCount: Int
|
|
public let functionCount: Int
|
|
public let memoryPages: [Int?]
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case valid, errors, warnings
|
|
case exportCount = "export_count"
|
|
case importCount = "import_count"
|
|
case functionCount = "function_count"
|
|
case memoryPages = "memory_pages"
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
valid = try container.decode(Bool.self, forKey: .valid)
|
|
errors = try container.decodeIfPresent([ValidationError].self, forKey: .errors) ?? []
|
|
warnings = try container.decodeIfPresent([String].self, forKey: .warnings) ?? []
|
|
exportCount = try container.decodeIfPresent(Int.self, forKey: .exportCount) ?? 0
|
|
importCount = try container.decodeIfPresent(Int.self, forKey: .importCount) ?? 0
|
|
functionCount = try container.decodeIfPresent(Int.self, forKey: .functionCount) ?? 0
|
|
memoryPages = try container.decodeIfPresent([Int?].self, forKey: .memoryPages) ?? [0, nil]
|
|
}
|
|
}
|
|
|
|
public struct ContractMetadata: Codable {
|
|
public let name: String?
|
|
public let version: String?
|
|
public let authors: [String]
|
|
public let description: String?
|
|
public let license: String?
|
|
public let repository: String?
|
|
public let buildTimestamp: Int64?
|
|
public let rustVersion: String?
|
|
public let sdkVersion: String?
|
|
public let custom: [String: String]
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case name, version, authors, description, license, repository, custom
|
|
case buildTimestamp = "build_timestamp"
|
|
case rustVersion = "rust_version"
|
|
case sdkVersion = "sdk_version"
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
name = try container.decodeIfPresent(String.self, forKey: .name)
|
|
version = try container.decodeIfPresent(String.self, forKey: .version)
|
|
authors = try container.decodeIfPresent([String].self, forKey: .authors) ?? []
|
|
description = try container.decodeIfPresent(String.self, forKey: .description)
|
|
license = try container.decodeIfPresent(String.self, forKey: .license)
|
|
repository = try container.decodeIfPresent(String.self, forKey: .repository)
|
|
buildTimestamp = try container.decodeIfPresent(Int64.self, forKey: .buildTimestamp)
|
|
rustVersion = try container.decodeIfPresent(String.self, forKey: .rustVersion)
|
|
sdkVersion = try container.decodeIfPresent(String.self, forKey: .sdkVersion)
|
|
custom = try container.decodeIfPresent([String: String].self, forKey: .custom) ?? [:]
|
|
}
|
|
}
|
|
|
|
public struct TypeInfo: Codable {
|
|
public let kind: String
|
|
public let size: Int?
|
|
public let element: TypeInfo?
|
|
public let inner: TypeInfo?
|
|
public let elements: [TypeInfo]?
|
|
public let name: String?
|
|
|
|
public var typeName: String {
|
|
switch kind {
|
|
case "u8", "u16", "u32", "u64", "u128", "i8", "i16", "i32", "i64", "i128",
|
|
"bool", "string", "bytes", "address", "hash256":
|
|
return kind
|
|
case "fixedBytes":
|
|
return "bytes\(size ?? 0)"
|
|
case "array":
|
|
return "\(element?.typeName ?? "")[]"
|
|
case "fixedArray":
|
|
return "\(element?.typeName ?? "")[\(size ?? 0)]"
|
|
case "option":
|
|
return "\(inner?.typeName ?? "")?"
|
|
case "tuple":
|
|
return "(\(elements?.map { $0.typeName }.joined(separator: ", ") ?? ""))"
|
|
case "struct":
|
|
return name ?? "struct"
|
|
default:
|
|
return name ?? "unknown"
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct ParamAbi: Codable {
|
|
public let name: String
|
|
public let type: TypeInfo
|
|
public let indexed: Bool
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case name, type, indexed
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
name = try container.decode(String.self, forKey: .name)
|
|
type = try container.decode(TypeInfo.self, forKey: .type)
|
|
indexed = try container.decodeIfPresent(Bool.self, forKey: .indexed) ?? false
|
|
}
|
|
}
|
|
|
|
public struct FunctionAbi: Codable {
|
|
public let name: String
|
|
public let selector: String
|
|
public let inputs: [ParamAbi]
|
|
public let outputs: [ParamAbi]
|
|
public let view: Bool
|
|
public let payable: Bool
|
|
public let doc: String?
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
name = try container.decode(String.self, forKey: .name)
|
|
selector = try container.decode(String.self, forKey: .selector)
|
|
inputs = try container.decodeIfPresent([ParamAbi].self, forKey: .inputs) ?? []
|
|
outputs = try container.decodeIfPresent([ParamAbi].self, forKey: .outputs) ?? []
|
|
view = try container.decodeIfPresent(Bool.self, forKey: .view) ?? false
|
|
payable = try container.decodeIfPresent(Bool.self, forKey: .payable) ?? false
|
|
doc = try container.decodeIfPresent(String.self, forKey: .doc)
|
|
}
|
|
}
|
|
|
|
public struct EventAbi: Codable {
|
|
public let name: String
|
|
public let topic: String
|
|
public let params: [ParamAbi]
|
|
public let doc: String?
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
name = try container.decode(String.self, forKey: .name)
|
|
topic = try container.decode(String.self, forKey: .topic)
|
|
params = try container.decodeIfPresent([ParamAbi].self, forKey: .params) ?? []
|
|
doc = try container.decodeIfPresent(String.self, forKey: .doc)
|
|
}
|
|
}
|
|
|
|
public struct ErrorAbi: Codable {
|
|
public let name: String
|
|
public let selector: String
|
|
public let params: [ParamAbi]
|
|
public let doc: String?
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
name = try container.decode(String.self, forKey: .name)
|
|
selector = try container.decode(String.self, forKey: .selector)
|
|
params = try container.decodeIfPresent([ParamAbi].self, forKey: .params) ?? []
|
|
doc = try container.decodeIfPresent(String.self, forKey: .doc)
|
|
}
|
|
}
|
|
|
|
public struct ContractAbi: Codable {
|
|
public let name: String
|
|
public let version: String
|
|
public let functions: [FunctionAbi]
|
|
public let events: [EventAbi]
|
|
public let errors: [ErrorAbi]
|
|
|
|
public func findFunction(name: String) -> FunctionAbi? {
|
|
functions.first { $0.name == name }
|
|
}
|
|
|
|
public func findBySelector(_ selector: String) -> FunctionAbi? {
|
|
functions.first { $0.selector == selector }
|
|
}
|
|
}
|
|
|
|
public struct CompilationResult: Codable {
|
|
public let contractId: String
|
|
public let code: String
|
|
public let codeHash: String
|
|
public let originalSize: Int
|
|
public let optimizedSize: Int
|
|
public let sizeReduction: Double
|
|
public let metadata: ContractMetadata?
|
|
public let abi: ContractAbi?
|
|
public let estimatedDeployGas: Int64
|
|
public let validation: ValidationResult?
|
|
public let warnings: [String]
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case contractId = "contract_id"
|
|
case code
|
|
case codeHash = "code_hash"
|
|
case originalSize = "original_size"
|
|
case optimizedSize = "optimized_size"
|
|
case sizeReduction = "size_reduction"
|
|
case metadata, abi
|
|
case estimatedDeployGas = "estimated_deploy_gas"
|
|
case validation, warnings
|
|
}
|
|
|
|
public var sizeStats: String {
|
|
String(format: "Original: %d bytes, Optimized: %d bytes, Reduction: %.1f%%",
|
|
originalSize, optimizedSize, sizeReduction)
|
|
}
|
|
|
|
public func decodeCode() -> Data? {
|
|
Data(base64Encoded: code)
|
|
}
|
|
}
|
|
|
|
public struct SizeBreakdown: Codable {
|
|
public let code: Int
|
|
public let data: Int
|
|
public let types: Int
|
|
public let functions: Int
|
|
public let memory: Int
|
|
public let table: Int
|
|
public let exports: Int
|
|
public let imports: Int
|
|
public let custom: Int
|
|
public let total: Int
|
|
}
|
|
|
|
public struct FunctionAnalysis: Codable {
|
|
public let name: String
|
|
public let size: Int
|
|
public let instructionCount: Int
|
|
public let localCount: Int
|
|
public let exported: Bool
|
|
public let estimatedGas: Int64
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case name, size, exported
|
|
case instructionCount = "instruction_count"
|
|
case localCount = "local_count"
|
|
case estimatedGas = "estimated_gas"
|
|
}
|
|
}
|
|
|
|
public struct ImportAnalysis: Codable {
|
|
public let module: String
|
|
public let name: String
|
|
public let kind: String
|
|
public let signature: String?
|
|
}
|
|
|
|
public struct SecurityIssue: Codable {
|
|
public let severity: String
|
|
public let type: String
|
|
public let description: String
|
|
public let location: String?
|
|
}
|
|
|
|
public struct SecurityAnalysis: Codable {
|
|
public let score: Int
|
|
public let issues: [SecurityIssue]
|
|
public let recommendations: [String]
|
|
}
|
|
|
|
public struct GasAnalysis: Codable {
|
|
public let deploymentGas: Int64
|
|
public let functionGas: [String: Int64]
|
|
public let memoryInitGas: Int64
|
|
public let dataSectionGas: Int64
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case deploymentGas = "deployment_gas"
|
|
case functionGas = "function_gas"
|
|
case memoryInitGas = "memory_init_gas"
|
|
case dataSectionGas = "data_section_gas"
|
|
}
|
|
}
|
|
|
|
public struct ContractAnalysis: Codable {
|
|
public let sizeBreakdown: SizeBreakdown
|
|
public let functions: [FunctionAnalysis]
|
|
public let imports: [ImportAnalysis]
|
|
public let security: SecurityAnalysis?
|
|
public let gasAnalysis: GasAnalysis?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case sizeBreakdown = "size_breakdown"
|
|
case functions, imports, security
|
|
case gasAnalysis = "gas_analysis"
|
|
}
|
|
}
|
|
|
|
public struct CompilerError: Error {
|
|
public let message: String
|
|
public let code: String?
|
|
public let httpStatus: Int?
|
|
|
|
public init(_ message: String, code: String? = nil, httpStatus: Int? = nil) {
|
|
self.message = message
|
|
self.code = code
|
|
self.httpStatus = httpStatus
|
|
}
|
|
}
|
|
|
|
// MARK: - Client
|
|
|
|
public actor SynorCompiler {
|
|
private let config: CompilerConfig
|
|
private let session: URLSession
|
|
private let decoder: JSONDecoder
|
|
private let encoder: JSONEncoder
|
|
private var closed = false
|
|
|
|
public let contracts: ContractsClient
|
|
public let abi: AbiClient
|
|
public let analysis: AnalysisClient
|
|
public let validation: ValidationClient
|
|
|
|
public init(config: CompilerConfig) {
|
|
self.config = config
|
|
let configuration = URLSessionConfiguration.default
|
|
configuration.timeoutIntervalForRequest = config.timeout
|
|
self.session = URLSession(configuration: configuration)
|
|
self.decoder = JSONDecoder()
|
|
self.encoder = JSONEncoder()
|
|
self.encoder.keyEncodingStrategy = .convertToSnakeCase
|
|
|
|
self.contracts = ContractsClient()
|
|
self.abi = AbiClient()
|
|
self.analysis = AnalysisClient()
|
|
self.validation = ValidationClient()
|
|
|
|
Task { await self.contracts.setCompiler(self) }
|
|
Task { await self.abi.setCompiler(self) }
|
|
Task { await self.analysis.setCompiler(self) }
|
|
Task { await self.validation.setCompiler(self) }
|
|
}
|
|
|
|
public var defaultOptimizationLevel: OptimizationLevel { config.optimizationLevel }
|
|
|
|
public func healthCheck() async -> Bool {
|
|
do {
|
|
let result: [String: String] = try await get("/health")
|
|
return result["status"] == "healthy"
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func getInfo() async throws -> [String: Any] {
|
|
try await get("/info")
|
|
}
|
|
|
|
public func close() {
|
|
closed = true
|
|
}
|
|
|
|
public var isClosed: Bool { closed }
|
|
|
|
public func compile(
|
|
wasm: Data,
|
|
optimizationLevel: OptimizationLevel? = nil,
|
|
stripOptions: StripOptions? = nil,
|
|
useWasmOpt: Bool? = nil,
|
|
validate: Bool? = nil,
|
|
extractMetadata: Bool? = nil,
|
|
generateAbi: Bool? = nil
|
|
) async throws -> CompilationResult {
|
|
let wasmBase64 = wasm.base64EncodedString()
|
|
var body: [String: Any] = [
|
|
"wasm": wasmBase64,
|
|
"optimization_level": (optimizationLevel ?? config.optimizationLevel).rawValue,
|
|
"use_wasm_opt": useWasmOpt ?? config.useWasmOpt,
|
|
"validate": validate ?? config.validate,
|
|
"extract_metadata": extractMetadata ?? config.extractMetadata,
|
|
"generate_abi": generateAbi ?? config.generateAbi
|
|
]
|
|
if let options = stripOptions {
|
|
body["strip_options"] = try encoder.encode(options)
|
|
}
|
|
return try await post("/compile", body: body)
|
|
}
|
|
|
|
func get<T: Decodable>(_ path: String) async throws -> T {
|
|
guard !closed else {
|
|
throw CompilerError("Client has been closed", code: "CLIENT_CLOSED")
|
|
}
|
|
|
|
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
|
request.httpMethod = "GET"
|
|
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
return try handleResponse(data: data, response: response)
|
|
}
|
|
|
|
func post<T: Decodable>(_ path: String, body: [String: Any]) async throws -> T {
|
|
guard !closed else {
|
|
throw CompilerError("Client has been closed", code: "CLIENT_CLOSED")
|
|
}
|
|
|
|
var request = URLRequest(url: URL(string: config.endpoint + path)!)
|
|
request.httpMethod = "POST"
|
|
request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
request.setValue("swift/0.1.0", forHTTPHeaderField: "X-SDK-Version")
|
|
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
return try handleResponse(data: data, response: response)
|
|
}
|
|
|
|
private func handleResponse<T: Decodable>(data: Data, response: URLResponse) throws -> T {
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw CompilerError("Invalid response")
|
|
}
|
|
|
|
if httpResponse.statusCode >= 400 {
|
|
if let error = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
throw CompilerError(
|
|
error["message"] as? String ?? "HTTP \(httpResponse.statusCode)",
|
|
code: error["code"] as? String,
|
|
httpStatus: httpResponse.statusCode
|
|
)
|
|
}
|
|
throw CompilerError("HTTP \(httpResponse.statusCode)", httpStatus: httpResponse.statusCode)
|
|
}
|
|
|
|
return try decoder.decode(T.self, from: data)
|
|
}
|
|
}
|
|
|
|
// MARK: - Sub-Clients
|
|
|
|
public actor ContractsClient {
|
|
private var compiler: SynorCompiler?
|
|
|
|
func setCompiler(_ compiler: SynorCompiler) { self.compiler = compiler }
|
|
|
|
public func compile(_ wasm: Data) async throws -> CompilationResult {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
return try await compiler.compile(wasm: wasm)
|
|
}
|
|
|
|
public func compileDev(_ wasm: Data) async throws -> CompilationResult {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
return try await compiler.compile(wasm: wasm, optimizationLevel: .none, useWasmOpt: false, validate: true)
|
|
}
|
|
|
|
public func compileProduction(_ wasm: Data) async throws -> CompilationResult {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
return try await compiler.compile(wasm: wasm, optimizationLevel: .aggressive, useWasmOpt: true, validate: true, extractMetadata: true, generateAbi: true)
|
|
}
|
|
|
|
public func get(_ contractId: String) async throws -> CompilationResult {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
return try await compiler.get("/contracts/\(contractId)")
|
|
}
|
|
}
|
|
|
|
public actor AbiClient {
|
|
private var compiler: SynorCompiler?
|
|
|
|
func setCompiler(_ compiler: SynorCompiler) { self.compiler = compiler }
|
|
|
|
public func extract(_ wasm: Data) async throws -> ContractAbi {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
let wasmBase64 = wasm.base64EncodedString()
|
|
return try await compiler.post("/abi/extract", body: ["wasm": wasmBase64])
|
|
}
|
|
|
|
public func get(_ contractId: String) async throws -> ContractAbi {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
return try await compiler.get("/contracts/\(contractId)/abi")
|
|
}
|
|
}
|
|
|
|
public actor AnalysisClient {
|
|
private var compiler: SynorCompiler?
|
|
|
|
func setCompiler(_ compiler: SynorCompiler) { self.compiler = compiler }
|
|
|
|
public func analyze(_ wasm: Data) async throws -> ContractAnalysis {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
let wasmBase64 = wasm.base64EncodedString()
|
|
return try await compiler.post("/analysis", body: ["wasm": wasmBase64])
|
|
}
|
|
|
|
public func get(_ contractId: String) async throws -> ContractAnalysis {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
return try await compiler.get("/contracts/\(contractId)/analysis")
|
|
}
|
|
|
|
public func extractMetadata(_ wasm: Data) async throws -> ContractMetadata {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
let wasmBase64 = wasm.base64EncodedString()
|
|
return try await compiler.post("/analysis/metadata", body: ["wasm": wasmBase64])
|
|
}
|
|
}
|
|
|
|
public actor ValidationClient {
|
|
private var compiler: SynorCompiler?
|
|
|
|
func setCompiler(_ compiler: SynorCompiler) { self.compiler = compiler }
|
|
|
|
public func validate(_ wasm: Data) async throws -> ValidationResult {
|
|
guard let compiler = compiler else { throw CompilerError("Compiler not set") }
|
|
let wasmBase64 = wasm.base64EncodedString()
|
|
return try await compiler.post("/validate", body: ["wasm": wasmBase64])
|
|
}
|
|
|
|
public func isValid(_ wasm: Data) async throws -> Bool {
|
|
try await validate(wasm).valid
|
|
}
|
|
|
|
public func getErrors(_ wasm: Data) async throws -> [String] {
|
|
try await validate(wasm).errors.map { $0.message }
|
|
}
|
|
}
|
|
|
|
// MARK: - Constants
|
|
|
|
public let MAX_CONTRACT_SIZE = 256 * 1024
|
|
public let MAX_MEMORY_PAGES = 16
|