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:
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)