diff --git a/service/pool/Logs.mo b/service/pool/Logs.mo index 10e46732..284c3f7a 100644 --- a/service/pool/Logs.mo +++ b/service/pool/Logs.mo @@ -1,4 +1,86 @@ +import Map "mo:base/RBTree"; +import {compare} "mo:base/Text"; +import {toArray} "mo:base/Iter"; +import {now = timeNow} "mo:base/Time"; +import {toText} "mo:base/Int"; +import {get} "mo:base/Option"; + module { + public type Origin = { origin: Text; tags: [Text] }; + public type SharedStatsByOrigin = (Map.Tree, Map.Tree, Map.Tree); + public class StatsByOrigin() { + var canisters = Map.RBTree(compare); + var installs = Map.RBTree(compare); + var tags = Map.RBTree(compare); + public func share() : SharedStatsByOrigin = (canisters.share(), installs.share(), tags.share()); + public func unshare(x : SharedStatsByOrigin) { + canisters.unshare(x.0); + installs.unshare(x.1); + tags.unshare(x.2); + }; + func addTags(list: [Text]) { + for (tag in list.vals()) { + switch (tags.get(tag)) { + case null { tags.put(tag, 1) }; + case (?n) { tags.put(tag, n + 1) }; + }; + }; + }; + public func addCanister(origin: Origin) { + switch (canisters.get(origin.origin)) { + case null { canisters.put(origin.origin, 1) }; + case (?n) { canisters.put(origin.origin, n + 1) }; + }; + // Not storing tags for create canister to avoid duplicate counting of tags + // addTags(origin.tags); + }; + public func addInstall(origin: Origin) { + switch (installs.get(origin.origin)) { + case null { installs.put(origin.origin, 1) }; + case (?n) { installs.put(origin.origin, n + 1) }; + }; + // Only record tags for canister install + addTags(origin.tags); + }; + public func dump() : ([(Text, Nat)], [(Text, Nat)], [(Text, Nat)]) { + (toArray<(Text, Nat)>(canisters.entries()), + toArray<(Text, Nat)>(installs.entries()), + toArray<(Text, Nat)>(tags.entries()) + ) + }; + public func metrics() : Text { + var result = ""; + let now = timeNow() / 1_000_000; + for ((origin, count) in canisters.entries()) { + let name = "canisters_" # origin; + let desc = "Number of canisters requested from " # origin; + result := result # encode_single_value("counter", name, count, desc, now); + }; + for ((origin, count) in installs.entries()) { + let name = "installs_" # origin; + let desc = "Number of Wasm installed from " # origin; + result := result # encode_single_value("counter", name, count, desc, now); + }; + let profiling = get(tags.get("profiling"), 0); + let asset = get(tags.get("asset"), 0); + let install = get(tags.get("install"), 0); + let reinstall = get(tags.get("reinstall"), 0); + let upgrade = get(tags.get("upgrade"), 0); + result := result + # encode_single_value("counter", "profiling", profiling, "Number of Wasm profiled", now) + # encode_single_value("counter", "asset", asset, "Number of asset Wasm canister installed", now) + # encode_single_value("counter", "install", install, "Number of Wasm with install mode", now) + # encode_single_value("counter", "reinstall", reinstall, "Number of Wasm with reinstall mode", now) + # encode_single_value("counter", "upgrade", upgrade, "Number of Wasm with upgrad mode", now); + result; + }; + }; + public func encode_single_value(kind: Text, name: Text, number: Int, desc: Text, time: Int) : Text { + "# HELP " # name # " " # desc # "\n" # + "# TYPE " # name # " " # kind # "\n" # + name # " " # toText(number) # " " # toText(time) # "\n" + }; + public type Stats = { num_of_canisters: Nat; num_of_installs: Nat; diff --git a/service/pool/Main.mo b/service/pool/Main.mo index b5eca8cb..f3cf0a56 100644 --- a/service/pool/Main.mo +++ b/service/pool/Main.mo @@ -6,6 +6,7 @@ import Option "mo:base/Option"; import Nat "mo:base/Nat"; import Text "mo:base/Text"; import Array "mo:base/Array"; +import Buffer "mo:base/Buffer"; import List "mo:base/List"; import Deque "mo:base/Deque"; import Result "mo:base/Result"; @@ -23,6 +24,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { let params = Option.get(opt_params, Types.defaultParams); var pool = Types.CanisterPool(params.max_num_canisters, params.canister_time_to_live, params.max_family_tree_size); let nonceCache = PoW.NonceCache(params.nonce_time_to_live); + var statsByOrigin = Logs.StatsByOrigin(); stable let controller = creator.caller; stable var stats = Logs.defaultStats; @@ -31,6 +33,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { stable var stableChildren : [(Principal, [Principal])] = []; stable var stableTimers : [Types.CanisterInfo] = []; stable var previousParam : ?Types.InitParams = null; + stable var stableStatsByOrigin : Logs.SharedStatsByOrigin = (#leaf, #leaf, #leaf); system func preupgrade() { let (tree, metadata, children, timers) = pool.share(); @@ -39,6 +42,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { stableChildren := children; stableTimers := timers; previousParam := ?params; + stableStatsByOrigin := statsByOrigin.share(); }; system func postupgrade() { @@ -50,15 +54,17 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { pool.unshare(stablePool, stableMetadata, stableChildren); for (info in stableTimers.vals()) { updateTimer(info); - } + }; + statsByOrigin.unshare(stableStatsByOrigin); }; public query func getInitParams() : async Types.InitParams { params; }; - public query func getStats() : async Logs.Stats { - stats; + public query func getStats() : async (Logs.Stats, [(Text, Nat)], [(Text, Nat)], [(Text, Nat)]) { + let (canister, install, tags) = statsByOrigin.dump(); + (stats, canister, install, tags); }; public query func balance() : async Nat { @@ -70,7 +76,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { ignore Cycles.accept amount; }; - private func getExpiredCanisterInfo() : async Types.CanisterInfo { + private func getExpiredCanisterInfo(origin : Logs.Origin) : async Types.CanisterInfo { switch (pool.getExpiredCanisterId()) { case (#newId) { Cycles.add(params.cycles_per_canister); @@ -79,6 +85,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { let info = { id = cid.canister_id; timestamp = now }; pool.add info; stats := Logs.updateStats(stats, #getId(params.cycles_per_canister)); + statsByOrigin.addCanister(origin); info; }; case (#reuse info) { @@ -101,6 +108,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { case _ {}; }; stats := Logs.updateStats(stats, #getId topUpCycles); + statsByOrigin.addCanister(origin); info; }; case (#outOfCapacity time) { @@ -111,7 +119,10 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { }; }; - public shared ({ caller }) func getCanisterId(nonce : PoW.Nonce) : async Types.CanisterInfo { + public shared ({ caller }) func getCanisterId(nonce : PoW.Nonce, origin : Logs.Origin) : async Types.CanisterInfo { + if (origin.origin == "") { + throw Error.reject "Please specify an origin"; + }; if (caller != controller and not nonceCache.checkProofOfWork(nonce)) { stats := Logs.updateStats(stats, #mismatch); throw Error.reject "Proof of work check failed"; @@ -122,10 +133,14 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { throw Error.reject "Nonce already used"; }; nonceCache.add nonce; - await getExpiredCanisterInfo(); + await getExpiredCanisterInfo(origin); }; - public shared ({ caller }) func installCode(info : Types.CanisterInfo, args : Types.InstallArgs, profiling : Bool, is_whitelisted : Bool) : async Types.CanisterInfo { + type InstallConfig = { profiling: Bool; is_whitelisted: Bool; origin: Logs.Origin }; + public shared ({ caller }) func installCode(info : Types.CanisterInfo, args : Types.InstallArgs, install_config : InstallConfig) : async Types.CanisterInfo { + if (install_config.origin.origin == "") { + throw Error.reject "Please specify an origin"; + }; if (info.timestamp == 0) { stats := Logs.updateStats(stats, #mismatch); throw Error.reject "Cannot install removed canister"; @@ -135,14 +150,14 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { throw Error.reject "Cannot find canister"; } else { let config = { - profiling; + profiling = install_config.profiling; remove_cycles_add = true; limit_stable_memory_page = ?(16384 : Nat32); // Limit to 1G of stable memory backend_canister_id = ?Principal.fromActor(this); }; - let wasm = if (caller == controller and is_whitelisted) { + let wasm = if (caller == controller and install_config.is_whitelisted) { args.wasm_module; - } else if (is_whitelisted) { + } else if (install_config.is_whitelisted) { await Wasm.is_whitelisted(args.wasm_module); } else { await Wasm.transform(args.wasm_module, config); @@ -155,7 +170,23 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { }; await IC.install_code newArgs; stats := Logs.updateStats(stats, #install); - switch (pool.refresh(info, profiling)) { + + // Build tags from install arguments + let tags = Buffer.fromArray(install_config.origin.tags); + if (install_config.profiling) { + tags.add("profiling"); + }; + if (install_config.is_whitelisted) { + tags.add("asset"); + }; + switch (args.mode) { + case (#install) { tags.add("install") }; + case (#upgrade) { tags.add("upgrade") }; + case (#reinstall) { tags.add("reinstall") }; + }; + let origin = { origin = install_config.origin.origin; tags = Buffer.toArray(tags) }; + statsByOrigin.addInstall(origin); + switch (pool.refresh(info, install_config.profiling)) { case (?newInfo) { updateTimer(newInfo); newInfo; @@ -241,12 +272,13 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { throw Error.reject "Only called by controller"; }; stats := Logs.defaultStats; + statsByOrigin := Logs.StatsByOrigin(); }; // Metrics public query func http_request(req : Metrics.HttpRequest) : async Metrics.HttpResponse { if (req.url == "/metrics") { - let body = Metrics.metrics stats; + let body = Metrics.metrics(stats, statsByOrigin); { status_code = 200; headers = [("Content-Type", "text/plain; version=0.0.4"), ("Content-Length", Nat.toText(body.size()))]; @@ -294,7 +326,7 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { if (not pool.findId caller) { throw Error.reject "Only a canister managed by the Motoko Playground can call create_canister"; }; - let info = await getExpiredCanisterInfo(); + let info = await getExpiredCanisterInfo({origin="spawned"; tags=[]}); let result = pool.setChild(caller, info.id); if (not result) { throw Error.reject("In the Motoko Playground, each top level canister can only spawn " # Nat.toText(params.max_family_tree_size) # " descendants including itself"); @@ -319,7 +351,8 @@ shared (creator) actor class Self(opt_params : ?Types.InitParams) = this { switch (sanitizeInputs(caller, canister_id)) { case (#ok info) { let args = { arg; wasm_module; mode; canister_id }; - ignore await installCode(info, args, pool.profiling caller, false); // inherit the profiling of the parent + let config = { profiling = pool.profiling caller; is_whitelisted = false; origin = {origin = "spawned"; tags = [] } }; + ignore await installCode(info, args, config); // inherit the profiling of the parent }; case (#err makeMsg) throw Error.reject(makeMsg "install_code"); }; diff --git a/service/pool/Metrics.mo b/service/pool/Metrics.mo index 4165615f..1642982f 100644 --- a/service/pool/Metrics.mo +++ b/service/pool/Metrics.mo @@ -1,6 +1,5 @@ import Text "mo:base/Text"; import Time "mo:base/Time"; -import Int "mo:base/Int"; import Logs "./Logs"; module { @@ -15,12 +14,8 @@ module { headers: [(Text, Text)]; body: Blob; }; - func encode_single_value(kind: Text, name: Text, number: Int, desc: Text, time: Int) : Text { - "# HELP " # name # " " # desc # "\n" # - "# TYPE " # name # " " # kind # "\n" # - name # " " # Int.toText(number) # " " # Int.toText(time) # "\n" - }; - public func metrics(stats: Logs.Stats) : Blob { + let encode_single_value = Logs.encode_single_value; + public func metrics(stats: Logs.Stats, origin: Logs.StatsByOrigin) : Blob { let now = Time.now() / 1_000_000; var result = ""; result := result # encode_single_value("counter", "canister_count", stats.num_of_canisters, "Number of canisters deployed", now); @@ -29,6 +24,7 @@ module { result := result # encode_single_value("counter", "out_of_capacity", stats.error_out_of_capacity, "Number of out of capacity requests", now); result := result # encode_single_value("counter", "total_wait_time", stats.error_total_wait_time, "Number of seconds waiting for out of capacity requests", now); result := result # encode_single_value("counter", "mismatch", stats.error_mismatch, "Number of mismatch requests including wrong nounce and timestamp", now); + result := result # origin.metrics(); Text.encodeUtf8(result) }; } diff --git a/service/pool/tests/actor_class/test.sh b/service/pool/tests/actor_class/test.sh index c6f1a59b..d8daa7e0 100644 --- a/service/pool/tests/actor_class/test.sh +++ b/service/pool/tests/actor_class/test.sh @@ -7,9 +7,10 @@ let deleter = file(".dfx/local/canisters/Deleter/Deleter.wasm"); let S = install(wasm, null, opt 100_000_000_000_000); let nonce = record { timestamp = 1 : int; nonce = 1 : nat }; -let c1 = call S.getCanisterId(nonce); +let origin = record { origin = "test"; tags = vec {} }; +let c1 = call S.getCanisterId(nonce, origin); let args = record { arg = blob ""; wasm_module = parent; mode = variant { install }; canister_id = c1.id }; -call S.installCode(c1, args, false, false); +call S.installCode(c1, args, record { profiling = false; is_whitelisted = false; origin = origin }); let c1 = c1.id; @@ -48,26 +49,26 @@ let init = opt record { let S = install(wasm, init, opt 100_000_000_000_000); let nonce = record { timestamp = 1 : int; nonce = 1 : nat }; -let c1 = call S.getCanisterId(nonce); +let c1 = call S.getCanisterId(nonce, origin); let args = record { arg = blob ""; wasm_module = parent; mode = variant { install }; canister_id = c1.id }; -call S.installCode(c1, args, false, false); +call S.installCode(c1, args, record { profiling = false; is_whitelisted = false; origin = origin }); let c1 = c1.id; fail call c1.makeChild(0); -call S.getCanisterId(nonce); -call S.getCanisterId(nonce); +call S.getCanisterId(nonce, origin); +call S.getCanisterId(nonce, origin); // Security check let S = install(wasm, null, opt 100_000_000_000_000); let nonce = record { timestamp = 1 : int; nonce = 1 : nat }; -let c1 = call S.getCanisterId(nonce); +let c1 = call S.getCanisterId(nonce, origin); let args = record { arg = blob ""; wasm_module = parent; mode = variant { install }; canister_id = c1.id }; -call S.installCode(c1, args, false, false); +call S.installCode(c1, args, record { profiling = false; is_whitelisted = false; origin = origin }); -let c2 = call S.getCanisterId(nonce); +let c2 = call S.getCanisterId(nonce, origin); let args = record { arg = blob ""; wasm_module = deleter; mode = variant { install }; canister_id = c2.id }; -call S.installCode(c2, args, false, false); +call S.installCode(c2, args, record { profiling = false; is_whitelisted = false; origin = origin }); let c1 = c1.id; let c2 = c2.id; diff --git a/service/pool/tests/canisterPool.test.sh b/service/pool/tests/canisterPool.test.sh index 2dbe7481..7033aaa8 100644 --- a/service/pool/tests/canisterPool.test.sh +++ b/service/pool/tests/canisterPool.test.sh @@ -3,6 +3,7 @@ load "prelude.sh"; let wasm = file("../../../.dfx/local/canisters/backend/backend.wasm"); let empty_wasm = blob "\00asm\01\00\00\00"; +let origin = record { origin = "test"; tags = vec {"tag"} }; let init = opt record { cycles_per_canister = 105_000_000_000 : nat; @@ -13,8 +14,8 @@ let init = opt record { }; let S = install(wasm, init, null); let nonce = record { timestamp = 1 : int; nonce = 1 : nat }; -let CID = call S.getCanisterId(nonce); -call S.installCode(CID, record { arg = blob ""; wasm_module = empty_wasm; mode = variant { install }; canister_id = CID.id }, false, false); +let CID = call S.getCanisterId(nonce, origin); +call S.installCode(CID, record { arg = blob ""; wasm_module = empty_wasm; mode = variant { install }; canister_id = CID.id }, record { profiling = false; is_whitelisted = false; origin = origin }); metadata(CID.id, "module_hash"); // Immediately expire @@ -27,13 +28,13 @@ let init = opt record { }; let S = install(wasm, init, null); -let c1 = call S.getCanisterId(nonce); +let c1 = call S.getCanisterId(nonce, origin); c1; -let c2 = call S.getCanisterId(nonce); +let c2 = call S.getCanisterId(nonce, origin); c2; -let c3 = call S.getCanisterId(nonce); +let c3 = call S.getCanisterId(nonce, origin); c3; -let c4 = call S.getCanisterId(nonce); +let c4 = call S.getCanisterId(nonce, origin); c4; assert c1.id != c2.id; assert c1.id == c3.id; @@ -48,14 +49,14 @@ let init = opt record { max_family_tree_size = 5 : nat; }; reinstall(S, wasm, init); -let c3 = call S.getCanisterId(nonce); +let c3 = call S.getCanisterId(nonce, origin); c3; -let c4 = call S.getCanisterId(nonce); +let c4 = call S.getCanisterId(nonce, origin); c4; -fail call S.getCanisterId(nonce); +fail call S.getCanisterId(nonce, origin); assert _ ~= "No available canister id"; call S.removeCode(c4); -call S.getCanisterId(nonce); +call S.getCanisterId(nonce, origin); assert _.id == c4.id; assert _.timestamp != c4.timestamp; @@ -68,7 +69,7 @@ let init = opt record { max_family_tree_size = 5 : nat; }; let S = install(wasm, init, opt 100_000_000_000); -fail call S.getCanisterId(nonce); +fail call S.getCanisterId(nonce, origin); assert _ ~= "105_000_000_000 cycles"; call ic.provisional_top_up_canister( record { @@ -76,7 +77,7 @@ call ic.provisional_top_up_canister( amount = 100_000_000_000_000; }, ); -call S.getCanisterId(nonce); +call S.getCanisterId(nonce, origin); // Enough time has passed that the timer has removed the canister code fail metadata(CID.id, "module_hash"); diff --git a/service/pool/tests/nonce.test.sh b/service/pool/tests/nonce.test.sh index e327e162..09f61f73 100644 --- a/service/pool/tests/nonce.test.sh +++ b/service/pool/tests/nonce.test.sh @@ -2,6 +2,7 @@ load "prelude.sh"; let wasm = file("../../../.dfx/local/canisters/backend/backend.wasm"); +let origin = record { origin = "test"; tags = vec {} }; identity alice; let init = opt record { @@ -13,11 +14,11 @@ let init = opt record { }; let S = install(wasm, init, null); -call S.getCanisterId(record { timestamp = 4780472194_000_000_000; nonce = 1 }); -fail call S.getCanisterId(record { timestamp = 4780472194_000_000_000; nonce = 1 }); +call S.getCanisterId(record { timestamp = 4780472194_000_000_000; nonce = 1 }, origin); +fail call S.getCanisterId(record { timestamp = 4780472194_000_000_000; nonce = 1 }, origin); assert _ ~= "Nonce already used"; -call S.getCanisterId(record { timestamp = 4780472194_000_000_001; nonce = 1 }); +call S.getCanisterId(record { timestamp = 4780472194_000_000_001; nonce = 1 }, origin); identity bob; -fail call S.getCanisterId(record { timestamp = 4780472194_000_000_002; nonce = 1 }); +fail call S.getCanisterId(record { timestamp = 4780472194_000_000_002; nonce = 1 }, origin); assert _ ~= "Proof of work check failed"; diff --git a/service/pool/tests/upgrade.test.sh b/service/pool/tests/upgrade.test.sh index 63044291..175e70c9 100644 --- a/service/pool/tests/upgrade.test.sh +++ b/service/pool/tests/upgrade.test.sh @@ -2,6 +2,7 @@ load "prelude.sh"; let wasm = file("../../../.dfx/local/canisters/backend/backend.wasm"); +let origin = record { origin = "test"; tags = vec {"tag"} }; let init = opt record { cycles_per_canister = 105_000_000_000 : nat; @@ -13,15 +14,15 @@ let init = opt record { let S = install(wasm, init, null); let nonce = record { timestamp = 1 : int; nonce = 1 : nat }; -let c1 = call S.getCanisterId(nonce); +let c1 = call S.getCanisterId(nonce, origin); c1; -let c2 = call S.getCanisterId(nonce); +let c2 = call S.getCanisterId(nonce, origin); c2; upgrade(S, wasm, init); -let c3 = call S.getCanisterId(nonce); +let c3 = call S.getCanisterId(nonce, origin); c3; -let c4 = call S.getCanisterId(nonce); +let c4 = call S.getCanisterId(nonce, origin); c4; assert c1.id != c2.id; assert c1.id == c3.id; @@ -35,12 +36,16 @@ let init = opt record { canister_time_to_live = 3600_000_000_000 : nat; max_family_tree_size = 5 : nat; }; +let stats = call S.getStats(); upgrade(S, wasm, init); -let c5 = call S.getCanisterId(nonce); +// stats are preserved after upgrade +call S.getStats(); +assert _ == stats; +let c5 = call S.getCanisterId(nonce, origin); c5; assert c5.id != c1.id; assert c5.id != c2.id; -fail call S.getCanisterId(nonce); +fail call S.getCanisterId(nonce, origin); assert _ ~= "No available canister id"; // Cannot reduce pool @@ -54,5 +59,5 @@ let init = opt record { fail upgrade(S, wasm, init); assert _ ~= "Cannot reduce canisterPool for upgrade"; // still old canister, new TTL does not apply -fail call S.getCanisterId(nonce); +fail call S.getCanisterId(nonce, origin); assert _ ~= "No available canister id"; diff --git a/service/wasm-utils/Cargo.lock b/service/wasm-utils/Cargo.lock index 8e97ea43..0e40da88 100644 --- a/service/wasm-utils/Cargo.lock +++ b/service/wasm-utils/Cargo.lock @@ -60,9 +60,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "candid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "762aa04e3a889d47a1773b74ee3b13438a9bc895954fff79ebf7f308c3744a6c" +checksum = "88f6eec0ae850e006ef0fe306f362884d370624094ec55a6a26de18b251774be" dependencies = [ "anyhow", "binread", @@ -87,14 +87,14 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810b3bd60244f282090652ffc7c30a9d23892e72dfe443e46ee55569044f7dd5" +checksum = "158403ea38fab5904ae47a5d67eb7047650a91681407f5ccbcbcabc4f4ffb489" dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -329,9 +329,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "memchr" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "num-bigint" @@ -382,7 +382,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -483,7 +483,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -540,9 +540,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -560,22 +560,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] diff --git a/service/wasm-utils/Cargo.toml b/service/wasm-utils/Cargo.toml index 6d5c2301..a07ea747 100644 --- a/service/wasm-utils/Cargo.toml +++ b/service/wasm-utils/Cargo.toml @@ -12,7 +12,7 @@ hex = "0.4.3" ic-cdk = "0.10.0" serde = "1.0" serde_bytes = "0.11" -candid = "0.9.1" +candid = "0.9.6" ic-wasm = { version = "0.4.0", default-features = false } sha2 = "0.10.6" diff --git a/src/App.tsx b/src/App.tsx index 7134e636..b51061ce 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -18,6 +18,7 @@ import { getActorAliases, getDeployedCanisters, getShareableProject, + WorkplaceReducerAction, } from "./contexts/WorkplaceState"; import { ProjectModal } from "./components/ProjectModal"; import { DeployModal, DeploySetter } from "./components/DeployModal"; @@ -70,13 +71,21 @@ const hasUrlParams = !!( urlParams.get("post") ); async function fetchFromUrlParams( - dispatch: (WorkplaceReducerAction) => void + dispatch: (action: WorkplaceReducerAction) => void ): Promise | undefined> { const git = urlParams.get("git"); const tag = urlParams.get("tag"); const editorKey = urlParams.get("post"); if (editorKey) { - return setupEditorIntegration(editorKey, dispatch, worker); + const result = await setupEditorIntegration(editorKey, dispatch, worker); + if (result) { + const { origin, files } = result; + await dispatch({ + type: "setOrigin", + payload: { origin: `playground:post:${origin}`, tags: [] }, + }); + return files; + } } if (git) { const repo = { @@ -84,6 +93,10 @@ async function fetchFromUrlParams( branch: urlParams.get("branch") || "main", dir: urlParams.get("dir") || "", }; + await dispatch({ + type: "setOrigin", + payload: { origin: "playground:git", tags: [`git:${git}`] }, + }); return await worker.fetchGithub(repo); } if (tag) { @@ -132,6 +145,10 @@ async function fetchFromUrlParams( }); } } + await dispatch({ + type: "setOrigin", + payload: { origin: "playground:tag", tags: [`tag:${tag}`] }, + }); return files; } } @@ -290,6 +307,7 @@ export function App() { candid={candidCode} initTypes={initTypes} logger={logger} + origin={workplaceState.origin} /> { try { logger.log(`Deploying code...`); @@ -119,14 +121,15 @@ export async function deploy( throw new Error(`Cannot ${mode} for new canister`); } logger.log(`Requesting a new canister id...`); - canisterInfo = await createCanister(worker, logger); + canisterInfo = await createCanister(worker, logger, origin); updatedState = await install( canisterInfo, wasm, args, "install", profiling, - logger + logger, + origin ); } else { if (mode !== "reinstall" && mode !== "upgrade") { @@ -138,7 +141,8 @@ export async function deploy( args, mode, profiling, - logger + logger, + origin ); } //updatedState.candid = candid_source; @@ -152,11 +156,14 @@ export async function deploy( async function createCanister( worker, - logger: ILoggingStore + logger: ILoggingStore, + origin: Origin ): Promise { const timestamp = BigInt(Date.now()) * BigInt(1_000_000); const nonce = await worker.pow(timestamp); - const info = await backend.getCanisterId(nonce); + // remove tags for create canister to avoid duplicate counting + const no_tags = { origin: origin.origin, tags: [] }; + const info = await backend.getCanisterId(nonce, no_tags); logger.log(`Got canister id ${info.id}`); return { id: info.id, @@ -175,7 +182,8 @@ async function install( args: Uint8Array, mode: string, profiling: boolean, - logger: ILoggingStore + logger: ILoggingStore, + origin: Origin ): Promise { if (!canisterInfo) { throw new Error("no canister id"); @@ -187,11 +195,15 @@ async function install( mode: { [mode]: null }, canister_id: canisterId, }; + const installConfig = { + profiling, + is_whitelisted: false, + origin, + }; const new_info = await backend.installCode( canisterInfo, installArgs, - profiling, - false + installConfig ); canisterInfo = new_info; logger.log(`Code installed at canister id ${canisterInfo.id}`); diff --git a/src/components/CanisterModal.tsx b/src/components/CanisterModal.tsx index 62a12f3f..c5d0e85b 100644 --- a/src/components/CanisterModal.tsx +++ b/src/components/CanisterModal.tsx @@ -108,6 +108,10 @@ export function CanisterModal({ isOpen, close, deploySetter }) { await deploySetter.setInitTypes(init); await deploySetter.setCandidCode(candid); await deploySetter.setShowDeployModal(true); + await dispatch({ + type: "setOrigin", + payload: { origin: "playground:wasm", tags: [] }, + }); } async function addCanister() { if (error || !canisterName || !canisterId || !candid) { diff --git a/src/components/DeployModal.tsx b/src/components/DeployModal.tsx index 2b84d388..8ba8ea5b 100644 --- a/src/components/DeployModal.tsx +++ b/src/components/DeployModal.tsx @@ -100,6 +100,7 @@ interface DeployModalProps { candid: string; initTypes: Array; logger: ILoggingStore; + origin: string | undefined; } const MAX_CANISTERS = 3; @@ -116,6 +117,7 @@ export function DeployModal({ candid, initTypes, logger, + origin, }: DeployModalProps) { const [canisterName, setCanisterName] = useState(""); const [inputs, setInputs] = useState([]); @@ -244,7 +246,8 @@ export function DeployModal({ mode, compileResult.wasm, profiling, - logger + logger, + origin ); await isDeploy(false); if (info) { diff --git a/src/components/ImportGithub.tsx b/src/components/ImportGithub.tsx index 07eb119f..c0f5e4fe 100644 --- a/src/components/ImportGithub.tsx +++ b/src/components/ImportGithub.tsx @@ -2,7 +2,10 @@ import styled from "styled-components"; import { useState, useContext } from "react"; import { Button } from "./shared/Button"; import { PackageInfo } from "../workers/file"; -import { WorkerContext } from "../contexts/WorkplaceState"; +import { + WorkerContext, + WorkplaceDispatchContext, +} from "../contexts/WorkplaceState"; import { Field } from "./shared/Field"; const Container = styled.div` @@ -33,12 +36,17 @@ export function ImportGitHub({ importCode, close, isPackageModal = false }) { const [error, setError] = useState(""); const [name, setName] = useState(""); const worker = useContext(WorkerContext); + const dispatch = useContext(WorkplaceDispatchContext); async function fetchCode() { const files = await worker.fetchGithub({ repo, branch, dir }); if (files) { setError(""); importCode(files); close(); + await dispatch({ + type: "setOrigin", + payload: { origin: "playground:git", tags: [`git:${repo}`] }, + }); } else { setError(`Cannot find repo or the directory contains no ".mo" files.`); } diff --git a/src/components/ProjectModal.tsx b/src/components/ProjectModal.tsx index 219b4b1d..2eb8ee8e 100644 --- a/src/components/ProjectModal.tsx +++ b/src/components/ProjectModal.tsx @@ -9,7 +9,10 @@ import { Tab, Tabs } from "./shared/Tabs"; import { ImportGitHub } from "./ImportGithub"; import { fetchExample, exampleProjects, ExampleProject } from "../examples"; -import { WorkerContext } from "../contexts/WorkplaceState"; +import { + WorkerContext, + WorkplaceDispatchContext, +} from "../contexts/WorkplaceState"; import iconCaretRight from "../assets/images/icon-caret-right.svg"; const ModalContainer = styled.div` @@ -63,16 +66,25 @@ export function ProjectModal({ isFirstOpen, }: ProjectModalProps) { const worker = useContext(WorkerContext); + const dispatch = useContext(WorkplaceDispatchContext); async function handleSelectProjectAndClose(project: ExampleProject) { const files = await fetchExample(worker, project); if (files) { await importCode(files); close(); } + await dispatch({ + type: "setOrigin", + payload: { origin: "playground", tags: [`example:${project.name}`] }, + }); } async function emptyProject() { await importCode({ "Main.mo": "" }); close(); + await dispatch({ + type: "setOrigin", + payload: { origin: "playground", tags: ["file:new"] }, + }); } const welcomeText = ( @@ -123,12 +135,12 @@ export function ProjectModal({ New Motoko project - {Object.entries(exampleProjects).map(([name, project]) => ( + {exampleProjects.map((project) => ( handleSelectProjectAndClose(project)} > - {name} + {project.name} ))} diff --git a/src/contexts/WorkplaceState.ts b/src/contexts/WorkplaceState.ts index ea5281eb..d669f15f 100644 --- a/src/contexts/WorkplaceState.ts +++ b/src/contexts/WorkplaceState.ts @@ -2,12 +2,17 @@ import * as React from "react"; import { CanisterInfo } from "../build"; import { PackageInfo } from "../workers/file"; +export interface Origin { + origin: string; + tags: Array; +} export interface WorkplaceState { files: Record; selectedFile: string | null; canisters: Record; selectedCanister: string | null; packages: Record; + origin: Origin | undefined; } export function getActorAliases( canisters: Record @@ -121,9 +126,13 @@ export type WorkplaceReducerAction = payload: { /** path of file that should be updated. Should correspond to a property in state.files */ canister: CanisterInfo; - do_not_select?: bool; + do_not_select?: boolean; /** new contents of file */ }; + } + | { + type: "setOrigin"; + payload: Origin; }; function selectFirstFile(files: Record): string | null { @@ -157,6 +166,7 @@ export const workplaceReducer = { canisters, selectedCanister: null, packages: {}, + origin: undefined, }; }, /** Return updated state based on an action */ @@ -221,6 +231,17 @@ export const workplaceReducer = { }, }; } + case "setOrigin": { + let refer = + document.referrer || (window.opener && "(opener)") || undefined; + if (refer) { + action.payload.tags.push(`ref:${refer}`); + } + return { + ...state, + origin: action.payload, + }; + } default: // this should never be reached. If there is a type error here, add a 'case' // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/declarations/backend/backend.did b/src/declarations/backend/backend.did index 11ad3ca8..1ed04c4c 100644 --- a/src/declarations/backend/backend.did +++ b/src/declarations/backend/backend.did @@ -44,16 +44,25 @@ type Self = (record {canister_id: canister_id;}); delete_canister: (record {canister_id: canister_id;}) -> (); dump: () -> (vec CanisterInfo) query; - getCanisterId: (Nonce) -> (CanisterInfo); + getCanisterId: (Nonce, Origin) -> (CanisterInfo); getInitParams: () -> (InitParams) query; - getStats: () -> (Stats) query; + getStats: () -> (Stats, vec record { + text; + nat; + }, vec record { + text; + nat; + }, vec record { + text; + nat; + }) query; getSubtree: (CanisterInfo) -> (vec record { principal; vec CanisterInfo; }) query; http_request: (HttpRequest) -> (HttpResponse) query; - installCode: (CanisterInfo, InstallArgs, bool, bool) -> (CanisterInfo); + installCode: (CanisterInfo, InstallArgs, InstallConfig) -> (CanisterInfo); install_code: (record { arg: blob; @@ -77,11 +86,22 @@ type Self = }) -> (); wallet_receive: () -> (); }; +type Origin = + record { + origin: text; + tags: vec text; + }; type Nonce = record { nonce: nat; timestamp: int; }; +type InstallConfig = + record { + is_whitelisted: bool; + origin: Origin; + profiling: bool; + }; type InstallArgs = record { arg: blob; diff --git a/src/declarations/backend/backend.did.d.ts b/src/declarations/backend/backend.did.d.ts index 5187a964..5ed64dce 100644 --- a/src/declarations/backend/backend.did.d.ts +++ b/src/declarations/backend/backend.did.d.ts @@ -29,10 +29,19 @@ export interface InstallArgs { mode: { reinstall: null } | { upgrade: null } | { install: null }; canister_id: Principal; } +export interface InstallConfig { + origin: Origin; + profiling: boolean; + is_whitelisted: boolean; +} export interface Nonce { nonce: bigint; timestamp: bigint; } +export interface Origin { + origin: string; + tags: Array; +} export interface Self { GCCanisters: ActorMethod<[], undefined>; balance: ActorMethod<[], bigint>; @@ -56,16 +65,24 @@ export interface Self { >; delete_canister: ActorMethod<[{ canister_id: canister_id }], undefined>; dump: ActorMethod<[], Array>; - getCanisterId: ActorMethod<[Nonce], CanisterInfo>; + getCanisterId: ActorMethod<[Nonce, Origin], CanisterInfo>; getInitParams: ActorMethod<[], InitParams>; - getStats: ActorMethod<[], Stats>; + getStats: ActorMethod< + [], + [ + Stats, + Array<[string, bigint]>, + Array<[string, bigint]>, + Array<[string, bigint]> + ] + >; getSubtree: ActorMethod< [CanisterInfo], Array<[Principal, Array]> >; http_request: ActorMethod<[HttpRequest], HttpResponse>; installCode: ActorMethod< - [CanisterInfo, InstallArgs, boolean, boolean], + [CanisterInfo, InstallArgs, InstallConfig], CanisterInfo >; install_code: ActorMethod< diff --git a/src/declarations/backend/backend.did.js b/src/declarations/backend/backend.did.js index 0009c2c3..667cbf24 100644 --- a/src/declarations/backend/backend.did.js +++ b/src/declarations/backend/backend.did.js @@ -24,6 +24,10 @@ export const idlFactory = ({ IDL }) => { compute_allocation: IDL.Opt(IDL.Nat), }); const Nonce = IDL.Record({ nonce: IDL.Nat, timestamp: IDL.Int }); + const Origin = IDL.Record({ + origin: IDL.Text, + tags: IDL.Vec(IDL.Text), + }); const Stats = IDL.Record({ num_of_installs: IDL.Nat, num_of_canisters: IDL.Nat, @@ -53,6 +57,11 @@ export const idlFactory = ({ IDL }) => { }), canister_id: IDL.Principal, }); + const InstallConfig = IDL.Record({ + origin: Origin, + profiling: IDL.Bool, + is_whitelisted: IDL.Bool, + }); const wasm_module = IDL.Vec(IDL.Nat8); const Self = IDL.Service({ GCCanisters: IDL.Func([], [], ["oneway"]), @@ -90,9 +99,18 @@ export const idlFactory = ({ IDL }) => { [] ), dump: IDL.Func([], [IDL.Vec(CanisterInfo)], ["query"]), - getCanisterId: IDL.Func([Nonce], [CanisterInfo], []), + getCanisterId: IDL.Func([Nonce, Origin], [CanisterInfo], []), getInitParams: IDL.Func([], [InitParams], ["query"]), - getStats: IDL.Func([], [Stats], ["query"]), + getStats: IDL.Func( + [], + [ + Stats, + IDL.Vec(IDL.Tuple(IDL.Text, IDL.Nat)), + IDL.Vec(IDL.Tuple(IDL.Text, IDL.Nat)), + IDL.Vec(IDL.Tuple(IDL.Text, IDL.Nat)), + ], + ["query"] + ), getSubtree: IDL.Func( [CanisterInfo], [IDL.Vec(IDL.Tuple(IDL.Principal, IDL.Vec(CanisterInfo)))], @@ -100,7 +118,7 @@ export const idlFactory = ({ IDL }) => { ), http_request: IDL.Func([HttpRequest], [HttpResponse], ["query"]), installCode: IDL.Func( - [CanisterInfo, InstallArgs, IDL.Bool, IDL.Bool], + [CanisterInfo, InstallArgs, InstallConfig], [CanisterInfo], [] ), diff --git a/src/declarations/react_app/react_app.did b/src/declarations/react_app/react_app.did index 508bda65..c4f659eb 100644 --- a/src/declarations/react_app/react_app.did +++ b/src/declarations/react_app/react_app.did @@ -109,6 +109,18 @@ type SetAssetPropertiesArguments = record { is_aliased: opt opt bool; }; +type ConfigurationResponse = record { + max_batches: opt nat64; + max_chunks: opt nat64; + max_bytes: opt nat64; +}; + +type ConfigureArguments = record { + max_batches: opt opt nat64; + max_chunks: opt opt nat64; + max_bytes: opt opt nat64; +}; + type Permission = variant { Commit; ManagePermissions; @@ -221,8 +233,12 @@ service: { is_aliased: opt bool; } ) query; set_asset_properties: (SetAssetPropertiesArguments) -> (); + get_configuration: () -> (ConfigurationResponse); + configure: (ConfigureArguments) -> (); + validate_grant_permission: (GrantPermission) -> (ValidationResult); validate_revoke_permission: (RevokePermission) -> (ValidationResult); validate_take_ownership: () -> (ValidationResult); validate_commit_proposed_batch: (CommitProposedBatchArguments) -> (ValidationResult); + validate_configure: (ConfigureArguments) -> (ValidationResult); } diff --git a/src/declarations/react_app/react_app.did.d.ts b/src/declarations/react_app/react_app.did.d.ts index f818b563..a1d26d51 100644 --- a/src/declarations/react_app/react_app.did.d.ts +++ b/src/declarations/react_app/react_app.did.d.ts @@ -25,6 +25,16 @@ export interface ComputeEvidenceArguments { batch_id: BatchId; max_iterations: [] | [number]; } +export interface ConfigurationResponse { + max_batches: [] | [bigint]; + max_bytes: [] | [bigint]; + max_chunks: [] | [bigint]; +} +export interface ConfigureArguments { + max_batches: [] | [[] | [bigint]]; + max_bytes: [] | [[] | [bigint]]; + max_chunks: [] | [[] | [bigint]]; +} export interface CreateAssetArguments { key: Key; content_type: string; @@ -118,6 +128,7 @@ export interface _SERVICE { [ComputeEvidenceArguments], [] | [Uint8Array | number[]] >; + configure: ActorMethod<[ConfigureArguments], undefined>; create_asset: ActorMethod<[CreateAssetArguments], undefined>; create_batch: ActorMethod<[{}], { batch_id: BatchId }>; create_chunk: ActorMethod< @@ -157,6 +168,7 @@ export interface _SERVICE { ], { content: Uint8Array | number[] } >; + get_configuration: ActorMethod<[], ConfigurationResponse>; grant_permission: ActorMethod<[GrantPermission], undefined>; http_request: ActorMethod<[HttpRequest], HttpResponse>; http_request_streaming_callback: ActorMethod< @@ -200,6 +212,7 @@ export interface _SERVICE { [CommitProposedBatchArguments], ValidationResult >; + validate_configure: ActorMethod<[ConfigureArguments], ValidationResult>; validate_grant_permission: ActorMethod<[GrantPermission], ValidationResult>; validate_revoke_permission: ActorMethod<[RevokePermission], ValidationResult>; validate_take_ownership: ActorMethod<[], ValidationResult>; diff --git a/src/declarations/react_app/react_app.did.js b/src/declarations/react_app/react_app.did.js index f285d95d..0bad0835 100644 --- a/src/declarations/react_app/react_app.did.js +++ b/src/declarations/react_app/react_app.did.js @@ -50,7 +50,17 @@ export const idlFactory = ({ IDL }) => { batch_id: BatchId, max_iterations: IDL.Opt(IDL.Nat16), }); + const ConfigureArguments = IDL.Record({ + max_batches: IDL.Opt(IDL.Opt(IDL.Nat64)), + max_bytes: IDL.Opt(IDL.Opt(IDL.Nat64)), + max_chunks: IDL.Opt(IDL.Opt(IDL.Nat64)), + }); const DeleteBatchArguments = IDL.Record({ batch_id: BatchId }); + const ConfigurationResponse = IDL.Record({ + max_batches: IDL.Opt(IDL.Nat64), + max_bytes: IDL.Opt(IDL.Nat64), + max_chunks: IDL.Opt(IDL.Nat64), + }); const Permission = IDL.Variant({ Prepare: IDL.Null, ManagePermissions: IDL.Null, @@ -121,6 +131,7 @@ export const idlFactory = ({ IDL }) => { [IDL.Opt(IDL.Vec(IDL.Nat8))], [] ), + configure: IDL.Func([ConfigureArguments], [], []), create_asset: IDL.Func([CreateAssetArguments], [], []), create_batch: IDL.Func( [IDL.Record({})], @@ -172,6 +183,7 @@ export const idlFactory = ({ IDL }) => { [IDL.Record({ content: IDL.Vec(IDL.Nat8) })], ["query"] ), + get_configuration: IDL.Func([], [ConfigurationResponse], []), grant_permission: IDL.Func([GrantPermission], [], []), http_request: IDL.Func([HttpRequest], [HttpResponse], ["query"]), http_request_streaming_callback: IDL.Func( @@ -229,6 +241,7 @@ export const idlFactory = ({ IDL }) => { [ValidationResult], [] ), + validate_configure: IDL.Func([ConfigureArguments], [ValidationResult], []), validate_grant_permission: IDL.Func( [GrantPermission], [ValidationResult], diff --git a/src/examples.ts b/src/examples.ts index 41105d30..cd68bc4d 100644 --- a/src/examples.ts +++ b/src/examples.ts @@ -1,6 +1,7 @@ import { RepoInfo } from "./workers/file"; export interface ExampleProject { + name: string; repo: RepoInfo; readme?: string; } @@ -12,52 +13,63 @@ const example = { const readmeURL = "https://raw.githubusercontent.com/dfinity/examples/master/motoko"; -export const exampleProjects: Record = { - "Hello, world": { +export const exampleProjects: ExampleProject[] = [ + { + name: "Hello, world", repo: { dir: "motoko/echo/src", ...example }, readme: `${readmeURL}/echo/README.md`, }, - Counter: { + { + name: "Counter", repo: { dir: "motoko/counter/src", ...example }, readme: `${readmeURL}/counter/README.md`, }, - Calculator: { + { + name: "Calculator", repo: { dir: "motoko/calc/src", ...example }, readme: `${readmeURL}/calc/README.md`, }, - "Who am I?": { + { + name: "Who am I?", repo: { dir: "motoko/whoami/src", ...example }, readme: `${readmeURL}/whoami/README.md`, }, - "Phone Book": { + { + name: "Phone Book", repo: { dir: "motoko/phone-book/src/phone-book", ...example }, readme: `${readmeURL}/phone-book/README.md`, }, - "Super Heroes": { + { + name: "Super Heroes", repo: { dir: "motoko/superheroes/src/superheroes", ...example }, readme: `${readmeURL}/superheroes/README.md`, }, - "Random Maze": { + { + name: "Random Maze", repo: { dir: "motoko/random_maze/src/random_maze", ...example }, readme: `${readmeURL}/random_maze/README.md`, }, - "Game of Life": { + { + name: "Game of Life", repo: { dir: "motoko/life", ...example }, readme: `${readmeURL}/life/README.md`, }, - "Publisher and Subscriber": { + { + name: "Publisher and Subscriber", repo: { dir: "motoko/pub-sub/src", ...example }, readme: `${readmeURL}/pub-sub/README.md`, }, - "Actor Classes": { + { + name: "Actor Classes", repo: { dir: "motoko/classes/src", ...example }, readme: `${readmeURL}/classes/README.md`, }, - "Basic DAO": { + { + name: "Basic DAO", repo: { dir: "motoko/basic_dao/src", ...example }, readme: `${readmeURL}/basic_dao/README.md`, }, -}; +]; export async function fetchExample( worker, diff --git a/src/integrations/allowedOrigins.js b/src/integrations/allowedOrigins.ts similarity index 90% rename from src/integrations/allowedOrigins.js rename to src/integrations/allowedOrigins.ts index 2fd5c20f..f8193b03 100644 --- a/src/integrations/allowedOrigins.js +++ b/src/integrations/allowedOrigins.ts @@ -2,7 +2,7 @@ // please submit a PR including the URL prefix for your application. // Read more: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns -const ALLOWED_ORIGINS = [ +const ALLOWED_ORIGINS: (string | RegExp)[] = [ /^https?:\/\/(localhost|127\.0\.0\.1)(:[0-9]+)?$/, // Localhost "https://blocks-editor.github.io", // Blocks (visual Motoko smart contract editor) ]; diff --git a/src/integrations/editorIntegration.ts b/src/integrations/editorIntegration.ts index f43ada0f..7c0799fe 100644 --- a/src/integrations/editorIntegration.ts +++ b/src/integrations/editorIntegration.ts @@ -18,6 +18,11 @@ type EditorIntegrationResponse = { acknowledge: number; }; +export interface EditorIntegrationResult { + origin: string; + files: Record; +} + export const INTEGRATION_HOOKS: Partial = {}; // Cached return value to ensure at most one initialization @@ -35,7 +40,7 @@ export async function setupEditorIntegration( editorKey: string, dispatch: (WorkplaceReducerAction) => void, worker // MocWorker -): Promise | undefined> { +): Promise { if (previousResult) { return previousResult; } @@ -111,7 +116,10 @@ export async function setupEditorIntegration( // Load a default empty project previousResult = { - "Main.mo": "", + origin, + files: { + "Main.mo": "", + }, }; return previousResult; }