damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit a04a4012929a986697b9f43a0bfb086e7f339e7f
parent 640fbf23eab2d5522b2676743db606046cff8ad7
Author: William Casarin <jb55@jb55.com>
Date:   Tue,  4 Jul 2023 11:42:16 -0700

nscript: load script view

This allows you to open and run scripts for testing purposes, but only
from external links such as nostr:nscript...

Diffstat:
Mdamus-c/wasm.c | 16++++------------
Mdamus-c/wasm.h | 10++++++++++
Mdamus.xcodeproj/project.pbxproj | 12++++++++++++
Mdamus/ContentView.swift | 15++++++++++++++-
Mdamus/Nostr/NostrLink.swift | 3+++
Mdamus/Util/Bech32Object.swift | 3+++
Mdamus/Util/Router.swift | 10+++++++++-
Adamus/Views/NostrScript/LoadScript.swift | 163+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MdamusTests/NostrScriptTests.swift | 8++++----
Mnostrscript/NostrScript.swift | 46+++++++++++++++++++++++++++++++++++++++-------
10 files changed, 261 insertions(+), 25 deletions(-)

diff --git a/damus-c/wasm.c b/damus-c/wasm.c @@ -753,15 +753,6 @@ static char *instr_name(enum instr_tag tag) return unk; } -static INLINE int was_section_parsed(struct module *module, - enum section_tag section) -{ - if (section == section_custom) - return module->custom_sections > 0; - - return module->parsed & (1 << section); -} - static INLINE int was_name_section_parsed(struct module *module, enum name_subsection_tag subsection) { @@ -1322,7 +1313,7 @@ static int parse_valtype(struct wasm_parser *p, enum valtype *valtype) } if (unlikely(!is_valtype((unsigned char)*valtype))) { - cursor_print_around(&p->cur, 10); + //cursor_print_around(&p->cur, 10); p->cur.p = start; return parse_err(p, "0x%02x is not a valid valtype tag", *valtype); } @@ -1684,7 +1675,7 @@ static int parse_reftype(struct wasm_parser *p, enum reftype *reftype) } if (!is_valid_reftype(tag)) { - cursor_print_around(&p->cur, 10); + //cursor_print_around(&p->cur, 10); parse_err(p, "invalid reftype: 0x%02x", tag); return 0; } @@ -2176,6 +2167,7 @@ static int parse_const_expr(struct expr_parser *p, struct expr *expr) } if (unlikely(!is_const_instr(tag))) { + //cursor_print_around(p->code, 20); return note_error(p->errs, p->code, "invalid const expr instruction: '%s'", instr_name(tag)); @@ -2551,7 +2543,7 @@ static int parse_wdata(struct wasm_parser *p, struct wdata *data) } if (tag > 2) { - cursor_print_around(&p->cur, 10); + //cursor_print_around(&p->cur, 10); return parse_err(p, "invalid datasegment tag: 0x%x", tag); } diff --git a/damus-c/wasm.h b/damus-c/wasm.h @@ -837,4 +837,14 @@ static INLINE struct callframe *top_callframes(struct cursor *cur, int top) return (struct callframe*)cursor_topn(cur, sizeof(struct callframe), top); } +static INLINE int was_section_parsed(struct module *module, + enum section_tag section) +{ + if (section == section_custom) + return module->custom_sections > 0; + + return module->parsed & (1 << section); +} + + #endif /* PROTOVERSE_WASM_H */ diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -46,6 +46,7 @@ 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; 4C190F202A535FC200027FD5 /* CustomizeZapModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */; }; 4C190F222A53950D00027FD5 /* bool_setting.wasm in Resources */ = {isa = PBXBuildFile; fileRef = 4C190F212A53950D00027FD5 /* bool_setting.wasm */; }; + 4C190F252A547D2000027FD5 /* LoadScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C190F242A547D2000027FD5 /* LoadScript.swift */; }; 4C198DEF29F88C6B004C165C /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */; }; 4C198DF029F88C6B004C165C /* Readme.md in Resources */ = {isa = PBXBuildFile; fileRef = 4C198DEC29F88C6B004C165C /* Readme.md */; }; 4C198DF129F88C6B004C165C /* License.txt in Resources */ = {isa = PBXBuildFile; fileRef = 4C198DED29F88C6B004C165C /* License.txt */; }; @@ -470,6 +471,7 @@ 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; 4C190F1F2A535FC200027FD5 /* CustomizeZapModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomizeZapModel.swift; sourceTree = "<group>"; }; 4C190F212A53950D00027FD5 /* bool_setting.wasm */ = {isa = PBXFileReference; lastKnownFileType = file; name = bool_setting.wasm; path = nostrscript/bool_setting.wasm; sourceTree = SOURCE_ROOT; }; + 4C190F242A547D2000027FD5 /* LoadScript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadScript.swift; sourceTree = "<group>"; }; 4C198DEB29F88C6B004C165C /* BlurHashEncode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BlurHashEncode.swift; sourceTree = "<group>"; }; 4C198DEC29F88C6B004C165C /* Readme.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = Readme.md; sourceTree = "<group>"; }; 4C198DED29F88C6B004C165C /* License.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = License.txt; sourceTree = "<group>"; }; @@ -974,6 +976,14 @@ path = Zaps; sourceTree = "<group>"; }; + 4C190F232A547D1700027FD5 /* NostrScript */ = { + isa = PBXGroup; + children = ( + 4C190F242A547D2000027FD5 /* LoadScript.swift */, + ); + path = NostrScript; + sourceTree = "<group>"; + }; 4C198DEA29F88C6B004C165C /* BlurHash */ = { isa = PBXGroup; children = ( @@ -1048,6 +1058,7 @@ 4C75EFA227FA576C0006080F /* Views */ = { isa = PBXGroup; children = ( + 4C190F232A547D1700027FD5 /* NostrScript */, 4C7D09692A0AEA0400943473 /* CodeScanner */, 4C7D095A2A098C5C00943473 /* Wallet */, 4C8D1A6D29F31E4100ACDF75 /* Buttons */, @@ -1753,6 +1764,7 @@ 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */, 4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */, 7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */, + 4C190F252A547D2000027FD5 /* LoadScript.swift in Sources */, 4CE8794C2995B59E00F758CC /* RelayMetadatas.swift in Sources */, 4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */, 4CDA128A29E9D10C0006FA5A /* SignalView.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -224,6 +224,12 @@ struct ContentView: View { navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet)) } + func open_script(_ script: [UInt8]) { + print("pushing script nav") + let model = ScriptModel(data: script, state: .not_loaded) + navigationCoordinator.push(route: Route.Script(script: model)) + } + func open_profile(id: String) { let profile_model = ProfileModel(pubkey: id, damus: damus_state!) let followers = FollowersModel(damus_state: damus_state!, target: id) @@ -331,7 +337,9 @@ struct ContentView: View { case .filter(let filt): self.open_search(filt: filt) case .profile(let id): self.open_profile(id: id) case .event(let ev): self.open_event(ev: ev) - case .wallet_connect(let nwc): self.open_wallet(nwc: nwc)} + case .wallet_connect(let nwc): self.open_wallet(nwc: nwc) + case .script(let data): self.open_script(data) + } } } .onReceive(handle_notify(.compose)) { notif in @@ -946,6 +954,7 @@ enum OpenResult { case filter(NostrFilter) case event(NostrEvent) case wallet_connect(WalletConnectURL) + case script([UInt8]) } func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> Void) { @@ -973,5 +982,9 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> result(.filter(filt)) break // TODO: handle filter searches? + case .script(let script): + result(.script(script)) + break } } + diff --git a/damus/Nostr/NostrLink.swift b/damus/Nostr/NostrLink.swift @@ -11,6 +11,7 @@ import Foundation enum NostrLink: Equatable { case ref(ReferencedId) case filter(NostrFilter) + case script([UInt8]) } func encode_pubkey_uri(_ ref: ReferencedId) -> String { @@ -105,6 +106,8 @@ func decode_nostr_bech32_uri(_ s: String) -> NostrLink? { return .ref(ReferencedId(ref_id: pubkey, relay_id: nil, key: "p")) case .note(let id): return .ref(ReferencedId(ref_id: id, relay_id: nil, key: "e")) + case .nscript(let data): + return .script(data) } } diff --git a/damus/Util/Bech32Object.swift b/damus/Util/Bech32Object.swift @@ -12,6 +12,7 @@ enum Bech32Object { case nsec(String) case npub(String) case note(String) + case nscript([UInt8]) static func parse(_ str: String) -> Bech32Object? { guard let decoded = try? bech32_decode(str) else { @@ -24,6 +25,8 @@ enum Bech32Object { return .nsec(hex_encode(decoded.data)) } else if decoded.hrp == "note" { return .note(hex_encode(decoded.data)) + } else if decoded.hrp == "nscript" { + return .nscript(decoded.data.bytes) } return nil diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift @@ -16,6 +16,7 @@ enum Route: Hashable { case Following(following: FollowingModel) case MuteList(users: [String]) case RelayConfig + case Script(script: ScriptModel) case Bookmarks case Config case EditMetadata @@ -105,6 +106,8 @@ enum Route: Hashable { WalletScannerView(result: walletScanResult) case .FollowersYouKnow(let friendedFollowers, let followers): FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers) + case .Script(let load_model): + LoadScript(pool: damusState.pool, model: load_model) } } @@ -172,8 +175,10 @@ enum Route: Hashable { return true case (.FollowersYouKnow(_, _), .FollowersYouKnow(_, _)): return true + case (.Script(_), .Script(_)): + return true default: - return false + return true } } @@ -259,6 +264,9 @@ enum Route: Hashable { hasher.combine("followersYouKnow") hasher.combine(friendedFollowers) hasher.combine(followers.sub_id) + case .Script(let model): + hasher.combine("script") + hasher.combine(model.data.count) } } } diff --git a/damus/Views/NostrScript/LoadScript.swift b/damus/Views/NostrScript/LoadScript.swift @@ -0,0 +1,163 @@ +// +// LoadScript.swift +// damus +// +// Created by William Casarin on 2023-07-04. +// + +import SwiftUI + +struct ScriptLoaded { + let script: NostrScript + let state: LoadedState +} + +enum LoadedState { + case loaded + case running + case ran(NostrScriptRunResult) +} + +enum LoadScriptState { + case not_loaded + case loading + case loaded(ScriptLoaded) + case failed(NostrScriptLoadErr) + + static func loaded(script: NostrScript) -> LoadScriptState { + return .loaded(ScriptLoaded(script: script, state: .loaded)) + } +} + +class ScriptModel: ObservableObject { + var data: [UInt8] + @Published var state: LoadScriptState + + init(data: [UInt8], state: LoadScriptState) { + self.data = data + self.state = state + } + + func run() async { + guard case .loaded(let script) = state else { + return + } + self.state = .loaded(.init(script: script.script, state: .running)) + + let t = Task.detached { + return script.script.run() + } + + let res = await t.value + self.state = .loaded(.init(script: script.script, state: .ran(res))) + } + + @MainActor + func load(pool: RelayPool) async { + guard case .not_loaded = state else { + return + } + self.state = .loading + let script = NostrScript(pool: pool, data: self.data) + let t = Task.detached { + print("loading script") + return script.load() + } + + let load_err = await t.value + + let t2 = Task { @MainActor in + if let load_err { + self.state = .failed(load_err) + return + } + + self.state = .loaded(script: script) + } + + await t2.value + } +} + +struct LoadScript: View { + let pool: RelayPool + + @ObservedObject var model: ScriptModel + + func ScriptView(_ script: ScriptLoaded) -> some View { + ScrollView { + VStack { + let imports = script.script.imports() + + (Text(verbatim: "\(imports.count)") + + Text(" Imports")) + .font(.title) + + ForEach(imports.indices, id: \.self) { ind in + Text(imports[ind]) + } + + switch script.state { + case .loaded: + BigButton("Run") { + Task { + await model.run() + } + } + case .running: + Text("Running...") + case .ran(let result): + switch result { + case .runtime_err(let errs): + Text("Runtime error") + .font(.title2) + ForEach(errs.indices, id: \.self) { ind in + Text(verbatim: errs[ind]) + } + case .suspend: + Text("Ran to suspension.") + case .finished(let code): + Text("Executed successfuly, returned with code \(code)") + } + } + } + } + } + + var body: some View { + Group { + switch self.model.state { + case .not_loaded: + ProgressView() + .progressViewStyle(.circular) + case .loading: + ProgressView() + .progressViewStyle(.circular) + case .loaded(let loaded): + ScriptView(loaded) + case .failed(let load_err): + VStack(spacing: 20) { + Text("NostrScript Error") + .font(.title) + switch load_err { + case .parse: + Text("Failed to parse") + case .module_init: + Text("Failed to initialize") + } + } + } + } + .task { + await model.load(pool: self.pool) + } + .navigationTitle("NostrScript") + } +} + + +/* + #Preview { + LoadScript() + } + */ diff --git a/damusTests/NostrScriptTests.swift b/damusTests/NostrScriptTests.swift @@ -38,13 +38,13 @@ final class NostrScriptTests: XCTestCase { func test_bool_set() throws { var data = try load_bool_set_test_wasm().bytes let pool = RelayPool() - let script = NostrScript(pool: pool) + let script = NostrScript(pool: pool, data: data) let pk = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245" UserSettingsStore.pubkey = pk let key = pk_setting_key(pk, key: "nozaps") UserDefaults.standard.set(true, forKey: key) - let load_err = script.load(wasm: &data) + let load_err = script.load() XCTAssertNil(load_err) let res = script.run() @@ -62,9 +62,9 @@ final class NostrScriptTests: XCTestCase { func test_nostrscript() throws { var data = try loadTestWasm().bytes let pool = RelayPool() - let script = NostrScript(pool: pool) + let script = NostrScript(pool: pool, data: data) - let load_err = script.load(wasm: &data) + let load_err = script.load() XCTAssertNil(load_err) let res = script.run() diff --git a/nostrscript/NostrScript.swift b/nostrscript/NostrScript.swift @@ -41,21 +41,29 @@ enum NostrScriptLoadResult { case loaded(wasm_interp) } +enum NostrScriptError: Error { + case not_loaded +} + class NostrScript { private var interp: wasm_interp private var parser: wasm_parser var waiting_on: NScriptWaiting? + var loaded: Bool + var data: [UInt8] private(set) var runstate: NostrScriptRunResult? private(set) var pool: RelayPool private(set) var event: NostrResponse? - init(pool: RelayPool) { + init(pool: RelayPool, data: [UInt8]) { self.interp = wasm_interp() self.parser = wasm_parser() self.pool = pool self.event = nil self.runstate = nil + self.loaded = false + self.data = data } deinit { @@ -80,15 +88,37 @@ class NostrScript { } } - func test(_ str: String) { - print("hello from \(str)") + func imports() -> [String] { + guard self.loaded, + was_section_parsed(interp.module, section_import) > 0, + let module = maybe_pointee(interp.module) + else { + return [] + } + + var imports = [String]() + + var i = 0 + while i < module.import_section.num_imports { + let imp = module.import_section.imports[i] + + imports.append(String(cString: imp.name)) + + i += 1 + } + + return imports } - func load(wasm: inout [UInt8]) -> NostrScriptLoadErr? { - switch nscript_load(&parser, &interp, &wasm, UInt(wasm.count)) { + func load() -> NostrScriptLoadErr? { + guard !loaded else { + return nil + } + switch nscript_load(&parser, &interp, &self.data, UInt(data.count)) { case NSCRIPT_LOADED: print("load num_exports \(interp.module.pointee.export_section.num_exports)") interp.context = Unmanaged.passUnretained(self).toOpaque() + self.loaded = true return nil case NSCRIPT_INIT_ERR: return .module_init @@ -292,7 +322,9 @@ public func nscript_set_bool(interp: UnsafeMutablePointer<wasm_interp>?, setting } let key = pk_setting_key(UserSettingsStore.pubkey ?? "", key: setting) - UserDefaults.standard.set(val > 0 ? true : false, forKey: key) + let b = val > 0 ? true : false + print("nscript setting bool setting \(setting) to \(b)") + UserDefaults.standard.set(b, forKey: key) stack_push_i32(interp, 1); return 1; @@ -316,7 +348,7 @@ public func nscript_pool_send_to(interp: UnsafeMutablePointer<wasm_interp>?, pre } func nscript_pool_send(script: NostrScript, req req_str: String) -> Int32 { - script.test("pool_send: '\(req_str)'") + //script.test("pool_send: '\(req_str)'") DispatchQueue.main.sync { script.pool.send_raw(.custom(req_str), skip_ephemeral: false)