diff --git a/docs/developer-docs/backend/python/index.mdx b/docs/developer-docs/backend/python/index.mdx index ce5dff7fc1..98f679f917 100644 --- a/docs/developer-docs/backend/python/index.mdx +++ b/docs/developer-docs/backend/python/index.mdx @@ -20,6 +20,52 @@ Kybra allows developers to bring their existing Python skills and workflows to I Kybra is a community contributed project and is not maintained by DFINITY. Kybra is developed by [Demergent Labs](https://github.com/demergent-labs), a for-profit company that receives a grant from DFINITY for development of Kybra. ::: +## Using Kybra + +To use Kybra: + +- It is highly recommended that `dfx` commands should be run in a Python virtual environment that contains the `kybra` package. + +- The virtual environment **must** use the following dependencies and the exact versions listed: + + - `dfx v0.19.0` + + - Python `v3.10.7` + +To set up a Python virtual environment and install Kybra, use the following commands: + + + +``` +curl https://pyenv.run | bash +~/.pyenv/bin/pyenv install 3.10.7 +~/.pyenv/versions/3.10.7/bin/python -m venv venv +source venv/bin/activate +pip install kybra +``` + + + +``` +brew install pyenv +export PYENV_ROOT="$HOME/.pyenv" +[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" +eval "$(pyenv init -)" +source ~/.zprofile +pyenv install 3.10.7 +~/.pyenv/versions/3.10.7/bin/python -m venv venv +source venv/bin/activate +pip install kybra +``` + + + + +Kybra is not currently supported on Windows. + + + + ## Documentation - [Kybra Python CDK documentation](https://demergent-labs.github.io/kybra/). diff --git a/docs/developer-docs/backend/typescript/index.mdx b/docs/developer-docs/backend/typescript/index.mdx index 2143891a98..0d27bda31f 100644 --- a/docs/developer-docs/backend/typescript/index.mdx +++ b/docs/developer-docs/backend/typescript/index.mdx @@ -12,10 +12,22 @@ JavaScript is a well-known programming language used for web pages and other int TypeScript is a syntax built on top of JavaScript that enables types. -To develop TypeScript and JavaScript smart contracts on ICP, the canister development kit (CDK) known as [Azle](https://demergent-labs.github.io/azle/azle.html) can be used. +To develop TypeScript and JavaScript smart contracts on ICP, the canister development kit (CDK) known as [Azle](https://demergent-labs.github.io/azle/azle.html) can be used. Azle is designed as a fully comprehensive TypeScript and JavaScript environment for ICP canisters. It supports as many relevant environment APIs as possible. These APIs are similar to those available in other frameworks such as Node.js and in environments such as web browsers. +To use Azle, you must install the Azle `dfx` extension with the command: + +``` +npx azle install-dfx-extension +``` + +Then, you can create new Azle projects using the `npm azle new` command: + +``` +npx azle new my_project +``` + Azle allows developers to bring their existing TypeScript and JavaScript skills and workflows to ICP, such as using npm packages and VS code intellisense. :::caution diff --git a/docs/developer-docs/getting-started/default-template.mdx b/docs/developer-docs/getting-started/default-template.mdx index 74108a9caf..8c369de02d 100644 --- a/docs/developer-docs/getting-started/default-template.mdx +++ b/docs/developer-docs/getting-started/default-template.mdx @@ -88,9 +88,14 @@ hello/ ├── src # Source files directory │   ├── hello_backend | | └── index.ts -│   ├── hello_frontend -│   │   ├── index.html -│   │   └── index.ts +│ ├── hello_frontend +│ ├── assets +│ │ ├── logo.png +│ │ ├── main.css +│ │ └── sample-asset.txt +│ └── src +│ ├── index.html +│ └── index.js └── tsconfig.json ``` @@ -113,16 +118,12 @@ hello/ │ │ ├── main.py │ ├── hello_frontend │ ├── assets -| |── index.html -| |── package.json +│ │ ├── logo.png +│ │ ├── main.css +│ │ └── sample-asset.txt │ └── src -│ ├── App.js -│ ├── index.scss -│ ├── logo2.svg -│ ├── main.js -│ └── vite-env.d.ts -| |── tsconfig.json -| |── vite.config.js +│ ├── index.html +│ └── index.js └── tsconfig.json ``` @@ -241,7 +242,20 @@ By default, the `dfx.json` file will contain automatically generated configurati "node_compatibility": true } } + "hello_frontend": { + "dependencies": [ + "hello_backend" + ], + "frontend": { + "entrypoint": "src/hello_frontend/src/index.html" + }, + "source": [ + "src/hello_frontend/assets", + "dist/hello_frontend/" + ], + "type": "assets" } + }, } ``` @@ -252,23 +266,35 @@ By default, the `dfx.json` file will contain automatically generated configurati ```json title="dfx.json" { "canisters": { - "hello_backend": { - "build": "python -m kybra hello_backend src/hello_backend/src/main.py src/hello_backend/hello_backend.did", - "candid": "src/hello_backend/hello_backend.did", - "gzip": true, - "post_install": ".kybra/hello_backend/post_install.sh", - "type": "custom", - "wasm": ".kybra/hello_backend/hello_backend.wasm" + "hello_backend": { + "type": "custom", + "build": "python -m kybra hello_backend src/hello_backend/src/main.py src/hello_backend/hello_backend.did", + "candid": "src/hello_backend/hello_backend.did", + "gzip": true, + "wasm": ".kybra/hello_backend/hello_backend.wasm" + "metadata": [ + { + "name": "candid:service", + "path": "src/main.did" + }, + { + "name": "cdk:name", + "content": "kybra" + } + ], }, "hello_frontend": { "dependencies": [ "hello_backend" ], - "source": [ - "src/hello_frontend/dist" - ], - "type": "assets", - "workspace": "hello_frontend" + "frontend": { + "entrypoint": "src/hello_frontend/src/index.html" + }, + "source": [ + "src/hello_frontend/assets", + "dist/hello_frontend/" + ], + "type": "assets" } }, "defaults": { @@ -282,7 +308,6 @@ By default, the `dfx.json` file will contain automatically generated configurati } ``` - diff --git a/docs/developer-docs/getting-started/hello-world.mdx b/docs/developer-docs/getting-started/hello-world.mdx index 58861d66b0..83df19b66e 100644 --- a/docs/developer-docs/getting-started/hello-world.mdx +++ b/docs/developer-docs/getting-started/hello-world.mdx @@ -71,6 +71,10 @@ Download and install the latest version of [dfx](install/index.mdx). `dfx` is a sh -ci "$(curl -fsSL https://internetcomputer.org/install.sh)" ``` +:::info +For Kybra projects, you will need to install `dfx` version `0.19.0` or newer, and you must run all `dfx` commands in a Python virtual environment. +::: + ### Install [Node.js](https://nodejs.org/en/download) ```bash @@ -203,7 +207,7 @@ TypeScript canisters can be written using the Azle canister development kit. For npx azle install-dfx-extension ``` -The default canister code for Azle projects contains the following code: +The default canister code for Azle projects contains the following: ```typescript title="src/hello_backend/src/index.ts" import { IDL, query, update } from 'azle'; @@ -227,7 +231,43 @@ export default class { }> -Create the project directory and file structure: +Python canisters can be written using the Kybra canister development kit. + +To set up a Python virtual environment, use the following commands: + + + +``` +curl https://pyenv.run | bash +~/.pyenv/bin/pyenv install 3.10.7 +~/.pyenv/versions/3.10.7/bin/python -m venv venv +source venv/bin/activate +pip install kybra +``` + + + +``` +brew install pyenv +export PYENV_ROOT="$HOME/.pyenv" +[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" +eval "$(pyenv init -)" +source ~/.zprofile +pyenv install 3.10.7 +~/.pyenv/versions/3.10.7/bin/python -m venv venv +source venv/bin/activate +pip install kybra +``` + + + + +Refer to the [pyenv-win](https://github.com/pyenv-win/pyenv-win?tab=readme-ov-file) documentation for pyenv installation options on Windows. + + + + +The default canister code for Kybra projects contains the following: ```python title="src/hello_backend/src/main.py" from kybra import query diff --git a/docs/developer-docs/smart-contracts/advanced-features/handling-get-post-requests.mdx b/docs/developer-docs/smart-contracts/advanced-features/handling-get-post-requests.mdx index a4562b1ce3..b8d213aab5 100644 --- a/docs/developer-docs/smart-contracts/advanced-features/handling-get-post-requests.mdx +++ b/docs/developer-docs/smart-contracts/advanced-features/handling-get-post-requests.mdx @@ -221,59 +221,165 @@ Check out the [Azle documentation](https://github.com/demergent-labs/azle/tree/m ```python -from kybra import blob, Func, nat16, Opt, query, Query, Record, Tuple, Variant, Vec +from kybra import ( + Alias, + blob, + Func, + ic, + nat, + nat16, + Opt, + Query, + query, + Record, + StableBTreeMap, + Tuple, + update, + Variant, + Vec, +) -class HttpRequest(Record): - method: str - url: str - headers: Vec["Header"] - body: blob +class Token(Record): + arbitrary_data: str -class HttpResponse(Record): - status_code: nat16 - headers: Vec["Header"] +class StreamingCallbackHttpResponse(Record): body: blob - streaming_strategy: Opt["StreamingStrategy"] - upgrade: Opt[bool] + token: Opt[Token] -Header = Tuple[str, str] +Callback = Func(Query[[Token], StreamingCallbackHttpResponse]) -class StreamingStrategy(Variant): - Callback: "CallbackStrategy" +class CallbackStrategy(Record): + callback: Callback + token: Token -class CallbackStrategy(Record): - callback: "Callback" - token: "Token" +class StreamingStrategy(Variant, total=False): + Callback: CallbackStrategy -Callback = Func(Query[["Token"], "StreamingCallbackHttpResponse"]) +HeaderField = Alias[Tuple[str, str]] -class StreamingCallbackHttpResponse(Record): +class HttpResponse(Record): + status_code: nat16 + headers: Vec[HeaderField] body: blob - token: Opt["Token"] + streaming_strategy: Opt[StreamingStrategy] + upgrade: Opt[bool] -class Token(Record): - arbitrary_data: str +class HttpRequest(Record): + method: str + url: str + headers: Vec[HeaderField] + body: blob + certificate_version: opt nat16 + + +stable_storage = StableBTreeMap[str, nat]( + memory_id=0, max_key_size=15, max_value_size=1_000 +) + +stable_storage.insert("counter", 0) + + +def isGzip(x: HeaderField) -> bool: + return x[0].lower() == "accept-encoding" and "gzip" in x[1].lower() @query def http_request(req: HttpRequest) -> HttpResponse: + ic.print("Hello from http_request") + + if req["method"] == "GET": + if next(filter(isGzip, req["headers"]), None) is None: + if req["url"] == "/stream": + return { + "status_code": 200, + "headers": [("content-type", "text/plain")], + "body": "Counter".encode("utf-8"), + "streaming_strategy": { + "Callback": { + "callback": (ic.id(), "http_streaming"), + "token": {"arbitrary_data": "start"}, + } + }, + "upgrade": False, + } + return { + "status_code": 200, + "headers": [("content-type", "text/plain")], + "body": f"Counter is {stable_storage.get('counter')}\n{req['url']}".encode( + "utf-8" + ), + "streaming_strategy": None, + "upgrade": None, + } + return { + "status_code": 200, + "headers": [("content-type", "text/plain"), ("content-encoding", "gzip")], + "body": bytes( + [ + 31, + 139, + 8, + 0, + 152, + 2, + 27, + 98, + 0, + 3, + 43, + 44, + 77, + 45, + 170, + 228, + 2, + 0, + 214, + 128, + 43, + 5, + 6, + 0, + 0, + 0, + ] + ), + "streaming_strategy": None, + "upgrade": None, + } + + if req["method"] == "POST": + return { + "status_code": 204, + "headers": [], + "body": "".encode("utf-8"), + "streaming_strategy": None, + "upgrade": True, + } + return { - "status_code": 200, + "status_code": 400, "headers": [], - "body": bytes(), + "body": "Invalid request".encode("utf-8"), "streaming_strategy": None, - "upgrade": False, + "upgrade": None, } ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + Check out the [Kybra documentation](https://demergent-labs.github.io/kybra/http.html) for more info on HTTP requests. @@ -470,59 +576,161 @@ Check out the [Azle documentation](https://github.com/demergent-labs/azle/tree/m ```python -from kybra import blob, Func, nat16, Opt, query, Query, Record, Tuple, Variant, Vec +from kybra import ( + Alias, + blob, + Func, + ic, + nat, + nat16, + Opt, + Query, + query, + Record, + StableBTreeMap, + Tuple, + update, + Variant, + Vec, +) -class HttpRequest(Record): - method: str - url: str - headers: Vec["Header"] - body: blob +class Token(Record): + arbitrary_data: str -class HttpResponse(Record): - status_code: nat16 - headers: Vec["Header"] +class StreamingCallbackHttpResponse(Record): body: blob - streaming_strategy: Opt["StreamingStrategy"] - upgrade: Opt[bool] + token: Opt[Token] -Header = Tuple[str, str] +Callback = Func(Query[[Token], StreamingCallbackHttpResponse]) -class StreamingStrategy(Variant): - Callback: "CallbackStrategy" +class CallbackStrategy(Record): + callback: Callback + token: Token -class CallbackStrategy(Record): - callback: "Callback" - token: "Token" +class StreamingStrategy(Variant, total=False): + Callback: CallbackStrategy -Callback = Func(Query[["Token"], "StreamingCallbackHttpResponse"]) +HeaderField = Alias[Tuple[str, str]] -class StreamingCallbackHttpResponse(Record): +class HttpResponse(Record): + status_code: nat16 + headers: Vec[HeaderField] body: blob - token: Opt["Token"] + streaming_strategy: Opt[StreamingStrategy] + upgrade: Opt[bool] -class Token(Record): - arbitrary_data: str +class HttpRequest(Record): + method: str + url: str + headers: Vec[HeaderField] + body: blob -@query +stable_storage = StableBTreeMap[str, nat]( + memory_id=0, max_key_size=15, max_value_size=1_000 +) + +stable_storage.insert("counter", 0) + + +def isGzip(x: HeaderField) -> bool: + return x[0].lower() == "accept-encoding" and "gzip" in x[1].lower() + +@update def http_request_update(req: HttpRequest) -> HttpResponse: + ic.print("Hello from update") + global stable_storage + + if req["method"] == "POST": + counter = stable_storage.get("counter") or 0 + stable_storage.insert("counter", counter + 1) + + if next(filter(isGzip, req["headers"]), None) is None: + return { + "status_code": 201, + "headers": [("content-type", "text/plain")], + "body": f"Counter updated to {stable_storage.get('counter')}".encode( + "utf-8" + ), + "streaming_strategy": None, + "upgrade": None, + } + return { + "status_code": 201, + "headers": [("content-type", "text/plain"), ("content-encoding", "gzip")], + "body": bytes( + [ + 31, + 139, + 8, + 0, + 55, + 2, + 27, + 98, + 0, + 3, + 43, + 45, + 72, + 73, + 44, + 73, + 229, + 2, + 0, + 168, + 218, + 145, + 108, + 7, + 0, + 0, + 0, + ] + ), + "streaming_strategy": None, + "upgrade": None, + } + return { - "status_code": 200, + "status_code": 400, "headers": [], - "body": bytes(), + "body": "Invalid request".encode("utf-8"), "streaming_strategy": None, - "upgrade": False, + "upgrade": None, } + + +@query +def http_streaming(token: Token) -> StreamingCallbackHttpResponse: + ic.print("Hello from http_streaming") + if token["arbitrary_data"] == "start": + return {"body": " is ".encode("utf-8"), "token": {"arbitrary_data": "next"}} + if token["arbitrary_data"] == "next": + return { + "body": f"{stable_storage.get('counter')}".encode("utf-8"), + "token": {"arbitrary_data": "last"}, + } + if token["arbitrary_data"] == "last": + return {"body": " streaming\n".encode("utf-8"), "token": None} + ic.trap("unreachable") ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + Check out the [Kybra documentation](https://demergent-labs.github.io/kybra/http.html) for more info on HTTP requests. diff --git a/docs/developer-docs/smart-contracts/advanced-features/management-canister.mdx b/docs/developer-docs/smart-contracts/advanced-features/management-canister.mdx index 61a5ed06ef..b613d3f321 100644 --- a/docs/developer-docs/smart-contracts/advanced-features/management-canister.mdx +++ b/docs/developer-docs/smart-contracts/advanced-features/management-canister.mdx @@ -52,9 +52,10 @@ Here are some examples calling the management canister: ```motoko actor { let ic = actor "aaaaa-aa" : IC.Self; - public func startCanister(canisterId: Principal) : async () { - await ic.start_canister({canister_id = canisterId}); - }; + public func createCanister(settings: ?canister_settings) : async () { + await ic.create_canister({canister_id = canisterId}); + canister_id : canister_id; +}; }; ``` @@ -64,20 +65,7 @@ actor { ```rust #[ic_cdk_macros::update] -async fn install_code(canister_id: CanisterId, wasm_module: Vec) { - use ic_cdk::api::management_canister::{ - install_code, CanisterInstallMode, InstallCodeArgumentExtended - }; - - let arg = InstallCodeArgumentExtended { - mode: CanisterInstallMode::Install, - canister_id, - wasm_module, - arg: vec![], - sender_canister_version: None, - }; - install_code(arg).await; -} +let (canister,) : (candid::Principal,) = ic_cdk::api::call(candid::Principal::management_canister(), "create_canister", ()).await?; ``` @@ -108,19 +96,58 @@ export default class { }> ```python +from kybra import ( + Async, + CallResult, + match, + Principal, + query, + update, + Variant, +) +from kybra.canisters.management import ( + CreateCanisterResult, + management_canister, +) + +from typing import TypedDict + +class ExecuteCreateCanisterResult(Variant, total=False): + Ok: CreateCanisterResult + Err: str + +class State(TypedDict): + created_canister_id: Principal + +state: State = {"created_canister_id": Principal.from_str("aaaaa-aa")} + + @update -def get_raw_rand() -> Async[RawRandResult]: - raw_rand_call_result: CallResult[blob] = yield management_canister.raw_rand() +def execute_create_canister() -> Async[ExecuteCreateCanisterResult]: + create_canister_result_call_result: CallResult[ + CreateCanisterResult + ] = yield management_canister.create_canister({"settings": None}).with_cycles( + 50_000_000_000_000 + ) + + def handle_ok( + create_canister_result: CreateCanisterResult, + ) -> ExecuteCreateCanisterResult: + + return {"Ok": create_canister_result} return match( - raw_rand_call_result, - { - "Ok": lambda randomness: {"Ok": randomness}, - "Err": lambda err: {"Err": err}, - }, + create_canister_result_call_result, + {"Ok": handle_ok, "Err": lambda err: {"Err": err}}, ) ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + diff --git a/docs/developer-docs/smart-contracts/advanced-features/periodic-tasks.mdx b/docs/developer-docs/smart-contracts/advanced-features/periodic-tasks.mdx index 250cfb7e1e..a74422afff 100644 --- a/docs/developer-docs/smart-contracts/advanced-features/periodic-tasks.mdx +++ b/docs/developer-docs/smart-contracts/advanced-features/periodic-tasks.mdx @@ -2,11 +2,11 @@ keywords: [advanced, concept, tasks, timers, periodic tasks, periodic timers] --- -import TabItem from "@theme/TabItem"; -import useBaseUrl from "@docusaurus/useBaseUrl"; -import { AdornedTabs } from "/src/components/Tabs/AdornedTabs"; -import { AdornedTab } from "/src/components/Tabs/AdornedTab"; -import { BetaChip } from "/src/components/Chip/BetaChip"; +import TabItem from "@theme/TabItem"; +import useBaseUrl from "@docusaurus/useBaseUrl"; +import { AdornedTabs } from "/src/components/Tabs/AdornedTabs"; +import { AdornedTab } from "/src/components/Tabs/AdornedTab"; +import { BetaChip } from "/src/components/Chip/BetaChip"; import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow"; # Periodic tasks and timers @@ -60,16 +60,12 @@ For the code composability reasons, i.e. to be able to use different libraries w ```motoko -import { print } = "mo:base/Debug"; -import { recurringTimer } = "mo:base/Timer"; +system func timer(setGlobalTimer : Nat64 -> ()) : async () { + let next = Nat64.fromIntWrap(Time.now()) + 20_000_000_000; + setGlobalTimer(next); // absolute time in nanoseconds + print("Tick!"); +} -actor Alarm { - let N = 5; - private func ring() : async () { - print("Motoko Timer Ring!"); - }; - ignore recurringTimer(#seconds N, ring); -}; ``` @@ -121,23 +117,77 @@ export default class { ```python from kybra import ( + blob, Duration, ic, + query, + Record, TimerId, update, + void, ) +class StatusReport(Record): + single: bool + capture: str + + +class TimerIds(Record): + single: TimerId + capture: TimerId + + +status: StatusReport = { + "single": False, + "capture": "", +} + + @update -def set_timer(delay: Duration) -> TimerId: - return ic.set_timer(delay, timer_callback) +def clear_timer(timer_id: TimerId) -> void: + ic.clear_timer(timer_id) + ic.print(f"timer {timer_id} cancelled") + + +@update +def set_timers(delay: Duration, interval: Duration) -> TimerIds: + captured_value = "🚩" + + single_id = ic.set_timer(delay, one_time_timer_callback) + + capture_id = ic.set_timer( + delay, + lambda: update_capture_status(captured_value) + or ic.print(f"Timer captured value: {captured_value}"), + ) + + return { + "single": single_id, + "capture": capture_id, + } + +@query +def status_report() -> StatusReport: + return status + + +def one_time_timer_callback(): + status["single"] = True + ic.print("one_time_timer_callback called") -def timer_callback(): - ic.print("timer_callback") +def update_capture_status(value: str): + status["capture"] = value ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + @@ -182,13 +232,16 @@ use std::time::Duration; const N: Duration = Duration::from_secs(5); +const Y: Duration = Duration::from_secs(20); + fn ring() { ic_cdk::println!("Rust Timer Ring!"); } #[ic_cdk::init] fn init() { - let _timer_id = ic_cdk_timers::set_timer_interval(N, ring); + let _timer_id1 = ic_cdk_timers::set_timer_interval(N, ring); + let _timer_id2 = ic_cdk_timers::set_timer_interval(Y, ring); } #[ic_cdk::post_upgrade] @@ -360,29 +413,20 @@ from kybra.canisters.management import management_canister class StatusReport(Record): single: bool - inline: nat8 capture: str repeat: nat8 - single_cross_canister: blob - repeat_cross_canister: blob class TimerIds(Record): single: TimerId - inline: TimerId capture: TimerId repeat: TimerId - single_cross_canister: TimerId - repeat_cross_canister: TimerId status: StatusReport = { "single": False, - "inline": 0, "capture": "", "repeat": 0, - "single_cross_canister": bytes(), - "repeat_cross_canister": bytes(), } @@ -398,16 +442,6 @@ def set_timers(delay: Duration, interval: Duration) -> TimerIds: single_id = ic.set_timer(delay, one_time_timer_callback) - # Note: You cannot set global variables from within a lambda but you can - # call a function that sets a global variable. So we've moved the "setting" - # functionality out into helper functions while the printing is kept here in - # the lambda. - - inline_id = ic.set_timer( - delay, - lambda: update_inline_status() or ic.print("Inline timer called"), - ) - capture_id = ic.set_timer( delay, lambda: update_capture_status(captured_value) @@ -416,22 +450,14 @@ def set_timers(delay: Duration, interval: Duration) -> TimerIds: repeat_id = ic.set_timer_interval(interval, repeat_timer_callback) - single_cross_canister_id = ic.set_timer(delay, single_cross_canister_timer_callback) - - repeat_cross_canister_id = ic.set_timer_interval( - interval, repeat_cross_canister_timer_callback - ) - return { "single": single_id, - "inline": inline_id, "capture": capture_id, "repeat": repeat_id, - "single_cross_canister": single_cross_canister_id, - "repeat_cross_canister": repeat_cross_canister_id, } + @query def status_report() -> StatusReport: return status @@ -447,35 +473,15 @@ def repeat_timer_callback(): ic.print(f"Repeating timer. Call {status['repeat']}") -def update_inline_status(): - status["inline"] = 1 - - def update_capture_status(value: str): status["capture"] = value +``` +:::caution -def single_cross_canister_timer_callback() -> Async[void]: - ic.print("single_cross_canister_timer_callback") - - result: CallResult[blob] = yield management_canister.raw_rand() - - def handle_ok(ok: blob): - status["single_cross_canister"] = ok - - match(result, {"Ok": handle_ok, "Err": lambda err: ic.print(err)}) - - -def repeat_cross_canister_timer_callback() -> Async[void]: - ic.print("repeat_cross_canister_timer_callback") - - result: CallResult[blob] = yield management_canister.raw_rand() - - def handle_ok(ok: blob): - status["repeat_cross_canister"] += ok +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). - match(result, {"Ok": handle_ok, "Err": lambda err: ic.print(err)}) -``` +::: @@ -502,13 +508,13 @@ Because of those limitations, in most cases CDK timers library for [Rust](https: ## Frequently asked questions -- #### Do timers support deterministic time slicing (DTS)? +- #### Do timers support deterministic time slicing (DTS)? Yes, as the CDK timers library initiates a self canister call to execute each task, normal [update message instruction limits](/docs/current/developer-docs/smart-contracts/maintain/resource-limits) apply with DTS enabled. -- #### What happens if a timer handler awaits for a call? +- #### What happens if a timer handler awaits for a call? Normal await point rules apply: any new execution can start at the await point: a new message, another timer handler or a heartbeat. Once that new execution is finished or reached its await point, the execution of the current timer handler might be resumed. -- #### What happens if a 1s periodic timer is executing for 2s? +- #### What happens if a 1s periodic timer is executing for 2s? If there are no await points in the timer handler, the periodic timer will be rescheduled at the end of the execution. If there are await points, it's implementation-defined when the periodic timer is rescheduled. ## Tutorials and examples diff --git a/docs/developer-docs/smart-contracts/advanced-features/randomness.mdx b/docs/developer-docs/smart-contracts/advanced-features/randomness.mdx index c202438857..dc41f35c3e 100644 --- a/docs/developer-docs/smart-contracts/advanced-features/randomness.mdx +++ b/docs/developer-docs/smart-contracts/advanced-features/randomness.mdx @@ -56,6 +56,7 @@ In addition to the `raw_rand` method, Motoko offers a [Random module](/docs/curr ```rust let (randomBytes,): (Vec,) = ic_cdk::api::call(Principal.management_canister(), "raw_rand", ()).await?; ``` + }> @@ -80,24 +81,41 @@ export default class { }> ```python -from kybra import Async, blob, CallResult, match, update, Variant -from kybra.canisters.management import management_canister - - -class RandomBytesResult(Variant, total=False): +from kybra import ( + Async, + blob, + CallResult, + match, + update, + Variant, +) +from kybra.canisters.management import ( + management_canister, +) + +class RawRandResult(Variant, total=False): Ok: blob Err: str - @update -def random_bytes() -> Async[RandomBytesResult]: - call_result: CallResult[blob] = yield management_canister.raw_rand() +def get_raw_rand() -> Async[RawRandResult]: + raw_rand_call_result: CallResult[blob] = yield management_canister.raw_rand() return match( - call_result, {"Ok": lambda ok: {"Ok": ok}, "Err": lambda err: {"Err": err}} + raw_rand_call_result, + { + "Ok": lambda randomness: {"Ok": randomness}, + "Err": lambda err: {"Err": err}, + }, ) ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + diff --git a/docs/developer-docs/smart-contracts/advanced-features/serving-http-request.mdx b/docs/developer-docs/smart-contracts/advanced-features/serving-http-request.mdx deleted file mode 100644 index bd3e9573c5..0000000000 --- a/docs/developer-docs/smart-contracts/advanced-features/serving-http-request.mdx +++ /dev/null @@ -1,247 +0,0 @@ ---- -keywords: [advanced, tutorial, serving http, http requests] ---- - -import TabItem from "@theme/TabItem"; -import { AdornedTabs } from "/src/components/Tabs/AdornedTabs"; -import { AdornedTab } from "/src/components/Tabs/AdornedTab"; -import { BetaChip } from "/src/components/Chip/BetaChip"; -import { MarkdownChipRow } from "/src/components/Chip/MarkdownChipRow"; - -# Serving an HTTP request - - - -## Overview - -Canisters can serve or handle an incoming HTTP request using the -[HTTP Gateway Protocol](/docs/current/references/http-gateway-protocol-spec). - -This allows developers to host web applications and APIs from a canister. - -## How it works - -An HTTP request from a client gets intercepted by the HTTP Gateway Protocol, -which identifies the target canister and encodes the request in Candid. This -encoded request is then sent to the canister for processing. Once processed, the -canister replies with an HTTP response. Finally, the HTTP Gateway Protocol -decodes the response using Candid and sends it back to the client, completing -the communication loop. - -For detailed information on how it works, please refer to the -[HTTP Gateway Protocol specification](/docs/current/references/http-gateway-protocol-spec). - -## How to make a request - -The following example returns 'Hello, World!' in the body at the `/hello` -endpoint. - - - - -```motoko -import HashMap = "mo:base/HashMap"; -import Blob = "mo:base/Blob"; -import Text "mo:base/Text"; -import Option "mo:base/Option"; - -actor { - - public type HttpRequest = { - body: Blob; - headers: [HeaderField]; - method: Text; - url: Text; - }; - - public type ChunkId = Nat; - public type SetAssetContentArguments = { - chunk_ids: [ChunkId]; - content_encoding: Text; - key: Key; - sha256: ?Blob; - }; - public type Path = Text; - public type Key = Text; - - public type HttpResponse = { - body: Blob; - headers: [HeaderField]; - status_code: Nat16; - }; - - public type HeaderField = (Text, Text); - - private func removeQuery(str: Text): Text { - return Option.unwrap(Text.split(str, #char '?').next()); - }; - - public query func http_request(req: HttpRequest): async (HttpResponse) { - let path = removeQuery(req.url); - if(path == "/hello") { - return { - body = Text.encodeUtf8("root page :" # path); - headers = []; - status_code = 200; - }; - }; - - return { - body = Text.encodeUtf8("404 Not found :" # path); - headers = []; - status_code = 404; - }; - }; -} -``` - - - - - -```rust -type HeaderField = (String, String); - -struct HttpResponse { - status_code: u16, - headers: Vec, - body: Cow<'static, Bytes>, -} - -struct HttpRequest { - method: String, - url: String, - headers: Vec<(String, String)>, - body: ByteBuf, -} - -#[query] -fn http_request(req: HttpRequest) -> HttpResponse { - let path = req.url.path(); - if path == "/hello" { - HttpResponse { - status_code: 200, - headers: Vec::new(), - body: b"hello, world!".to_vec(), - streaming_strategy: None, - upgrade: None, - } - } else { - HttpResponse { - status_code: 404, - headers: Vec::new(), - body: b"404 Not found :".to_vec(), - streaming_strategy: None, - upgrade: None, - } - } -} -``` - - - -}> - -```typescript -import { - blob, - bool, - Canister, - Func, - nat16, - None, - Opt, - query, - Record, - text, - Tuple, - Variant, - Vec -} from 'azle'; - -type HeaderField = [text, text]; -const HeaderField = Tuple(text, text); - -const HttpResponse = Record({ - status_code: nat16, - headers: Vec(HeaderField), - body: blob, -}); - -const HttpRequest = Record({ - method: text, - url: text, - headers: Vec(HeaderField), - body: blob, -}); - -export default class{ - @query([HttpRequest], HttpResponse, (req) => { - const path = req.url.pathname; - if(path == "/hello") { - return { - status_code: 200, - headers: [], - body: Buffer.from('hello, world!') - }; - } return { - body: Text.encodeUtf8("404 Not found :"); - headers: []; - status_code:404; - } - }), -}; -``` - - - -}> - -```python -from kybra import blob, Func, nat16, Opt, query, Query, Record, Tuple, Variant, Vec - - -class HttpRequest(Record): - method: str - url: str - headers: Vec["Header"] - body: blob - -class HttpResponse(Record): - status_code: nat16 - headers: Vec["Header"] - body: blob - -Header = Tuple[str, str] - -@query -def http_request(req: HttpRequest) -> HttpResponse: - path = req.url.path - if path == "/hello": - return { - "status_code": 200, - "headers": [], - "body": Buffer.from_text("hello, world!").encode("utf-8"), - } - else: - return { - "status_code": 404, - "headers": [], - "body": b"404 Not found :", -} -``` - -To learn more about serving an HTTP request in Python, refer to -[the Kybra book reference on incoming HTTP requests](https://demergent-labs.github.io/kybra/http.html). - - - - -## Additional examples - -The -[HTTP counter project](https://github.com/dfinity/examples/tree/master/motoko/http_counter) -is an example in Motoko of a 'counter' application that uses the `http_request` -method to read the current counter value or access some pre-stored data and the -`http_request_update` method to increment the counter and retrieve the updated -value. diff --git a/docs/developer-docs/smart-contracts/advanced-features/time-and-timestamps.mdx b/docs/developer-docs/smart-contracts/advanced-features/time-and-timestamps.mdx index 62cd755ed4..65461ba9ba 100644 --- a/docs/developer-docs/smart-contracts/advanced-features/time-and-timestamps.mdx +++ b/docs/developer-docs/smart-contracts/advanced-features/time-and-timestamps.mdx @@ -71,9 +71,15 @@ from kybra import ic, nat64, query @query def time() -> nat64: -    return ic.time() +  return ic.time() ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + diff --git a/docs/developer-docs/smart-contracts/call/arguments.mdx b/docs/developer-docs/smart-contracts/call/arguments.mdx index 4d62e6728b..21a9a317d4 100644 --- a/docs/developer-docs/smart-contracts/call/arguments.mdx +++ b/docs/developer-docs/smart-contracts/call/arguments.mdx @@ -85,7 +85,6 @@ To learn more about variants in Typescript via Azle, refer to [the Azle code](ht ```python from kybra import int32, query, Vec - @query def get_numbers() -> Vec[int32]: return [1, 2, 3, 4, 5, 6, 7] diff --git a/docs/developer-docs/smart-contracts/call/overview.mdx b/docs/developer-docs/smart-contracts/call/overview.mdx index b6996e3397..b23a9a3329 100644 --- a/docs/developer-docs/smart-contracts/call/overview.mdx +++ b/docs/developer-docs/smart-contracts/call/overview.mdx @@ -102,6 +102,13 @@ def inc() -> nat64: return counter ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + + @@ -168,6 +175,13 @@ def greet(name: str) -> str: return f"Hello, {name}!" ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + + diff --git a/docs/developer-docs/smart-contracts/deploy/overview.mdx b/docs/developer-docs/smart-contracts/deploy/overview.mdx index ad8a4ab85e..4743cf22fd 100644 --- a/docs/developer-docs/smart-contracts/deploy/overview.mdx +++ b/docs/developer-docs/smart-contracts/deploy/overview.mdx @@ -172,8 +172,8 @@ Here is an example: -This example uses a global timer that gets called immediately after the canister -starts: +This example creates a periodic timer that gets called immediately after the +canister starts: ```motoko import Time "mo:base/Time"; @@ -214,169 +214,66 @@ fn init(timer_interval_secs: u64) { }> -This example creates a timer function that must be manually called once a canister starts: +This example creates a periodic timer that gets called immediately after the +canister starts: ```typescript -import { - call, - clearTimer, - IDL, - query, - setTimer, - setTimerInterval, - update -} from 'azle'; - -const StatusReport = IDL.Record({ - single: IDL.Bool, - inline: IDL.Int8, - capture: IDL.Text, - repeat: IDL.Int8, - singleCrossCanister: IDL.Vec(IDL.Nat8), - repeatCrossCanister: IDL.Vec(IDL.Nat8) -}); -type StatusReport = { - single: boolean; - inline: number; - capture: string; - repeat: number; - singleCrossCanister: Uint8Array; - repeatCrossCanister: Uint8Array; -}; - -const TimerIds = IDL.Record({ - single: IDL.Nat64, - inline: IDL.Nat64, - capture: IDL.Nat64, - repeat: IDL.Nat64, - singleCrossCanister: IDL.Nat64, - repeatCrossCanister: IDL.Nat64 -}); -type TimerIds = { - single: bigint; - inline: bigint; - capture: bigint; - repeat: bigint; - singleCrossCanister: bigint; - repeatCrossCanister: bigint; -}; - -let statusReport: StatusReport = { - single: false, - inline: 0, - capture: '', - repeat: 0, - singleCrossCanister: Uint8Array.from([]), - repeatCrossCanister: Uint8Array.from([]) -}; +import { IDL, setTimer, update } from 'azle'; export default class { - @update([IDL.Nat64]) - clearTimer(timerId: bigint): void { - clearTimer(timerId); - console.info(`timer ${timerId} cancelled`); - } - - @update([IDL.Nat64, IDL.Nat64], TimerIds) - setTimers(delay: bigint, interval: bigint): TimerIds { - const capturedValue = '🚩'; - - const singleId = setTimer(delay, oneTimeTimerCallback); - - const inlineId = setTimer(delay, () => { - statusReport.inline = 1; - console.info('Inline timer called'); - }); - - const captureId = setTimer(delay, () => { - statusReport.capture = capturedValue; - console.info(`Timer captured value ${capturedValue}`); - }); - - const repeatId = setTimerInterval(interval, () => { - statusReport.repeat++; - console.info(`Repeating timer. Call ${statusReport.repeat}`); - }); - - const singleCrossCanisterId = setTimer( - delay, - singleCrossCanisterTimerCallback - ); - - const repeatCrossCanisterId = setTimerInterval( - interval, - repeatCrossCanisterTimerCallback + @update([], IDL.Nat64) + createTimer(): bigint { + const timerId = setTimer(1_000n, () => + console.log('timer callback called') ); - return { - single: singleId, - inline: inlineId, - capture: captureId, - repeat: repeatId, - singleCrossCanister: singleCrossCanisterId, - repeatCrossCanister: repeatCrossCanisterId - }; - } - - @query([], StatusReport) - statusReport(): StatusReport { - return statusReport; + return timerId; } } - -function oneTimeTimerCallback(): void { - statusReport.single = true; - console.info('oneTimeTimerCallback called'); -} - -async function singleCrossCanisterTimerCallback(): Promise { - console.info('singleCrossCanisterTimerCallback'); - - statusReport.singleCrossCanister = await getRandomness(); -} - -async function repeatCrossCanisterTimerCallback(): Promise { - console.info('repeatCrossCanisterTimerCallback'); - - statusReport.repeatCrossCanister = Uint8Array.from([ - ...statusReport.repeatCrossCanister, - ...(await getRandomness()) - ]); -} - -async function getRandomness(): Promise { - return await call('aaaaa-aa', 'raw_rand', { - returnIdlType: IDL.Vec(IDL.Nat8) - }); -} ``` }> -This example creates a timer function that must be manually called once a +This example creates a periodic timer that gets called immediately after the canister starts: ```python from kybra import ( -    Duration, -    ic, -    TimerId, -    update, + blob, + Duration, + ic, + query, + Record, + TimerId, + update, + void, ) +class TimerIds(Record): + single: TimerId @update -def set_timer(delay: Duration) -> TimerId: -    return ic.set_timer(delay, timer_callback) +def set_timers(delay: Duration, interval: Duration) -> TimerIds: + single_id = ic.set_timer(delay, one_time_timer_callback) -def timer_callback(): -    ic.print("timer_callback") + return { + "single": single_id, + } +def one_time_timer_callback(): + ic.print("one_time_timer_callback called") ``` +:::caution + +Kybra canisters must be deployed from a Python virtual environment. [Learn more in the Kybra docs](/docs/current/developer-docs/backend/python/). + +::: + + diff --git a/submodules/internetidentity b/submodules/internetidentity index ce331167c4..f8eaa539fb 160000 --- a/submodules/internetidentity +++ b/submodules/internetidentity @@ -1 +1 @@ -Subproject commit ce331167c49b61b48ee9a2a7ebb0a25522d67f8f +Subproject commit f8eaa539fba2363d4b87ca42c82227aa74f075d3 diff --git a/submodules/motoko b/submodules/motoko index 5ac33ad976..de582b3aec 160000 --- a/submodules/motoko +++ b/submodules/motoko @@ -1 +1 @@ -Subproject commit 5ac33ad976c51d1320fdfc5389851fe106095f42 +Subproject commit de582b3aecf4d475d4603c620bdca83540d42889 diff --git a/submodules/quill b/submodules/quill index 2a951a69dd..88158f521f 160000 --- a/submodules/quill +++ b/submodules/quill @@ -1 +1 @@ -Subproject commit 2a951a69dd8ac0eb52707236a4fc3e08f6682201 +Subproject commit 88158f521f5f777bfe3ce68e5b21e1f2bc9fcadd diff --git a/submodules/response-verfication b/submodules/response-verfication index 600b3d5339..da70db9383 160000 --- a/submodules/response-verfication +++ b/submodules/response-verfication @@ -1 +1 @@ -Subproject commit 600b3d5339381bbfe3454ce870a04afa79005c3d +Subproject commit da70db93832f88ecc556ae082612aedec47d3816 diff --git a/submodules/samples b/submodules/samples index 88e574173c..152edd0025 160000 --- a/submodules/samples +++ b/submodules/samples @@ -1 +1 @@ -Subproject commit 88e574173c94ed5a2dc610ff1f4f17c3ea1e5bd1 +Subproject commit 152edd00251b46db3d962e5af5d7e3d5ce3c2350 diff --git a/submodules/sdk b/submodules/sdk index 993ae6df38..e6942eb450 160000 --- a/submodules/sdk +++ b/submodules/sdk @@ -1 +1 @@ -Subproject commit 993ae6df38caef8aae5291570b78954334d16b21 +Subproject commit e6942eb4508ebec140efbf2def0466ee8f5e1fc0