NostrScript.swift (10831B)
1 // 2 // NostrScript.swift 3 // damus 4 // 5 // Created by William Casarin on 2023-06-02. 6 // 7 8 import Foundation 9 10 enum NostrScriptLoadErr { 11 case parse 12 case module_init 13 } 14 15 enum NostrScriptRunResult { 16 case runtime_err([String]) 17 case suspend 18 case finished(Int) 19 20 var exited: Bool { 21 switch self { 22 case .runtime_err: 23 return true 24 case .suspend: 25 return false 26 case .finished: 27 return true 28 } 29 } 30 31 var is_suspended: Bool { 32 if case .suspend = self { 33 return true 34 } 35 return false 36 } 37 } 38 39 enum NostrScriptLoadResult { 40 case err(NostrScriptLoadErr) 41 case loaded(wasm_interp) 42 } 43 44 enum NostrScriptError: Error { 45 case not_loaded 46 } 47 48 class NostrScript { 49 private var interp: wasm_interp 50 private var parser: wasm_parser 51 var waiting_on: NScriptWaiting? 52 var loaded: Bool 53 var data: [UInt8] 54 55 private(set) var runstate: NostrScriptRunResult? 56 private(set) var pool: RelayPool 57 private(set) var event: NostrResponse? 58 59 init(pool: RelayPool, data: [UInt8]) { 60 self.interp = wasm_interp() 61 self.parser = wasm_parser() 62 self.pool = pool 63 self.event = nil 64 self.runstate = nil 65 self.loaded = false 66 self.data = data 67 } 68 69 deinit { 70 wasm_parser_free(&self.parser) 71 wasm_interp_free(&self.interp) 72 } 73 74 func is_suspended(on: NScriptWaiting) -> Bool { 75 return self.waiting_on == on 76 } 77 78 func can_resume(with: NScriptResumeWith) -> Bool { 79 guard let waiting_on else { 80 return false 81 } 82 switch waiting_on { 83 case .event(let subid): 84 switch with { 85 case .event(let resp): 86 return resp.subid == subid 87 } 88 } 89 } 90 91 func imports() -> [String] { 92 guard self.loaded, 93 was_section_parsed(interp.module, section_import) > 0, 94 let module = maybe_pointee(interp.module) 95 else { 96 return [] 97 } 98 99 var imports = [String]() 100 101 var i = 0 102 while i < module.import_section.num_imports { 103 let imp = module.import_section.imports[i] 104 105 imports.append(String(cString: imp.name)) 106 107 i += 1 108 } 109 110 return imports 111 } 112 113 func load() -> NostrScriptLoadErr? { 114 guard !loaded else { 115 return nil 116 } 117 switch nscript_load(&parser, &interp, &self.data, UInt(data.count)) { 118 case NSCRIPT_LOADED: 119 print("load num_exports \(interp.module.pointee.export_section.num_exports)") 120 interp.context = Unmanaged.passUnretained(self).toOpaque() 121 self.loaded = true 122 return nil 123 case NSCRIPT_INIT_ERR: 124 return .module_init 125 case NSCRIPT_PARSE_ERR: 126 return .parse 127 default: 128 return .parse 129 } 130 } 131 132 func resume(with: NScriptResumeWith) -> NostrScriptRunResult? { 133 guard let runstate, runstate.is_suspended, can_resume(with: with) else { 134 return nil 135 } 136 137 switch with { 138 case .event(let resp): 139 load_data(resp: resp) 140 } 141 142 let st = nscript_run(interp: &interp, resuming: true) 143 self.runstate = st 144 self.event = nil 145 return st 146 } 147 148 private func load_data(resp: NostrResponse) { 149 self.event = resp 150 } 151 152 func run() -> NostrScriptRunResult { 153 if let runstate { 154 return runstate 155 } 156 157 let st = nscript_run(interp: &interp, resuming: false) 158 self.runstate = st 159 return st 160 } 161 } 162 163 fileprivate func interp_nostrscript(interp: UnsafeMutablePointer<wasm_interp>?) -> NostrScript? { 164 guard let interp = interp?.pointee else { 165 return nil 166 } 167 168 return Unmanaged<NostrScript>.fromOpaque(interp.context).takeUnretainedValue() 169 } 170 171 fileprivate func asm_str_byteptr(cstr: UnsafePointer<UInt8>, len: Int32) -> String? { 172 let u16 = cstr.withMemoryRebound(to: UInt16.self, capacity: Int(len)) { p in p } 173 return asm_str(cstr: u16, len: len) 174 } 175 176 fileprivate func asm_str(cstr: UnsafePointer<UInt16>, len: Int32) -> String? { 177 return String(utf16CodeUnits: cstr, count: Int(len)) 178 } 179 180 enum NScriptCommand: Int { 181 case pool_send = 1 182 case add_relay = 2 183 case event_await = 3 184 case event_get_type = 4 185 case event_get_note = 5 186 case note_get_kind = 6 187 case note_get_content = 7 188 case note_get_content_length = 8 189 } 190 191 enum NScriptEventType: Int { 192 case ok = 1 193 case note = 2 194 case notice = 3 195 case eose = 4 196 case auth = 5 197 198 init(resp: NostrResponse) { 199 switch resp { 200 case .event: 201 self = .note 202 case .notice: 203 self = .notice 204 case .eose: 205 self = .eose 206 case .ok: 207 self = .ok 208 case .auth: 209 self = .auth 210 } 211 } 212 } 213 214 enum NScriptWaiting: Equatable { 215 case event(String) 216 217 var subid: String { 218 switch self { 219 case .event(let subid): 220 return subid 221 } 222 } 223 } 224 225 enum NScriptResumeWith { 226 case event(NostrResponse) 227 } 228 229 enum NScriptCmdResult { 230 case suspend(NScriptWaiting) 231 case ok 232 case fatal 233 } 234 235 @_cdecl("nscript_nostr_cmd") 236 public func nscript_nostr_cmd(interp: UnsafeMutablePointer<wasm_interp>?, cmd: Int32, value: UnsafePointer<UInt8>, len: Int32) -> Int32 { 237 guard let script = interp_nostrscript(interp: interp), 238 let cmd = NScriptCommand(rawValue: Int(cmd)) else { 239 return 0 240 } 241 242 print("nostr_cmd \(cmd)") 243 244 switch cmd { 245 case .pool_send: 246 guard let req = asm_str_byteptr(cstr: value, len: len) else { return 0 } 247 let res = nscript_pool_send(script: script, req: req) 248 stack_push_i32(interp, 0); 249 return res; 250 251 case .add_relay: 252 guard let relay = asm_str_byteptr(cstr: value, len: len) else { return 0 } 253 let ok = nscript_add_relay(script: script, relay: relay) 254 stack_push_i32(interp, ok ? 1 : 0) 255 return 1; 256 257 case .event_await: 258 guard let subid = asm_str_byteptr(cstr: value, len: len) else { return 0 } 259 nscript_event_await(script: script, subid: subid) 260 let ev_handle: Int32 = 1 261 stack_push_i32(interp, ev_handle); 262 return BUILTIN_SUSPEND 263 264 case .event_get_type: 265 guard let event = script.event else { 266 stack_push_i32(interp, 0); 267 return 1 268 } 269 270 let type = NScriptEventType(resp: event) 271 stack_push_i32(interp, Int32(type.rawValue)); 272 return 1 273 274 case .event_get_note: 275 guard let event = script.event, case .event = event 276 else { stack_push_i32(interp, 0); return 1 } 277 278 let note_handle: Int32 = 1 279 stack_push_i32(interp, note_handle) 280 return 1 281 282 case .note_get_kind: 283 guard let event = script.event, case .event(_, let note) = event 284 else { 285 stack_push_i32(interp, 0); 286 return 1 287 288 } 289 290 stack_push_i32(interp, Int32(note.kind)) 291 return 1 292 293 case .note_get_content: 294 guard let event = script.event, case .event(_, let note) = event 295 else { stack_push_i32(interp, 0); return 1 } 296 297 stack_push_i32(interp, Int32(note.kind)) 298 return 1 299 300 case .note_get_content_length: 301 guard let event = script.event, case .event(_, let note) = event 302 else { stack_push_i32(interp, 0); return 1 } 303 304 stack_push_i32(interp, Int32(note.content.utf8.count)) 305 return 1 306 } 307 308 } 309 310 func nscript_add_relay(script: NostrScript, relay: String) -> Bool { 311 guard let url = RelayURL(relay) else { return false } 312 let desc = RelayDescriptor(url: url, info: .rw, variant: .ephemeral) 313 return (try? script.pool.add_relay(desc)) != nil 314 } 315 316 317 @_cdecl("nscript_set_bool") 318 public func nscript_set_bool(interp: UnsafeMutablePointer<wasm_interp>?, setting: UnsafePointer<UInt16>, setting_len: Int32, val: Int32) -> Int32 { 319 320 guard let setting = asm_str(cstr: setting, len: setting_len), 321 UserSettingsStore.bool_options.contains(setting) 322 else { 323 stack_push_i32(interp, 0); 324 return 1; 325 } 326 327 let key = pk_setting_key(UserSettingsStore.pubkey ?? .empty, key: setting) 328 let b = val > 0 ? true : false 329 print("nscript setting bool setting \(setting) to \(b)") 330 UserDefaults.standard.set(b, forKey: key) 331 332 stack_push_i32(interp, 1); 333 return 1; 334 } 335 336 @_cdecl("nscript_pool_send_to") 337 public func nscript_pool_send_to(interp: UnsafeMutablePointer<wasm_interp>?, preq: UnsafePointer<UInt16>, req_len: Int32, to: UnsafePointer<UInt16>, to_len: Int32) -> Int32 { 338 339 guard let script = interp_nostrscript(interp: interp), 340 let req_str = asm_str(cstr: preq, len: req_len), 341 let to = asm_str(cstr: to, len: to_len), 342 let to_relay_url = RelayURL(to) 343 else { 344 return 0 345 } 346 347 DispatchQueue.main.async { 348 script.pool.send_raw(.custom(req_str), to: [to_relay_url], skip_ephemeral: false) 349 } 350 351 return 1; 352 } 353 354 func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 { 355 //script.test("pool_send: '\(req_str)'") 356 357 DispatchQueue.main.sync { 358 script.pool.send_raw(.custom(req_str), skip_ephemeral: false) 359 } 360 361 return 1; 362 } 363 364 func nscript_event_await(script: NostrScript, subid: String) { 365 script.waiting_on = .event(subid) 366 } 367 368 func nscript_get_error_backtrace(errors: inout errors) -> [String] { 369 var xs = [String]() 370 var errs = cursor() 371 var err = error() 372 373 copy_cursor(&errors.cur, &errs) 374 errs.p = errs.start; 375 376 while (errs.p < errors.cur.p) { 377 if (cursor_pull_error(&errs, &err) == 0) { 378 return xs 379 } 380 381 xs.append(String(cString: err.msg)) 382 } 383 384 return xs 385 } 386 387 func nscript_run(interp: inout wasm_interp, resuming: Bool) -> NostrScriptRunResult { 388 var res: Int32 = 0 389 var retval: Int32 = 0 390 391 if (resuming) { 392 print("resuming nostrscript"); 393 res = interp_wasm_module_resume(&interp, &retval); 394 } else { 395 res = interp_wasm_module(&interp, &retval); 396 } 397 398 if res == 0 { 399 print_callstack(&interp); 400 print_error_backtrace(&interp.errors); 401 let backtrace = nscript_get_error_backtrace(errors: &interp.errors) 402 return .runtime_err(backtrace) 403 } 404 405 if res == BUILTIN_SUSPEND { 406 return .suspend 407 } 408 409 //print_stack(&interp.stack); 410 wasm_interp_free(&interp); 411 412 return .finished(Int(retval)) 413 } 414