phat_js/lib.rs
1//! A library for interacting with the contract phat-quickjs
2//!
3//! The available JS APIs can be found [here](https://github.com/Phala-Network/phat-quickjs/blob/master/npm_package/pink-env/src/index.ts).
4//!
5//! # Script args and return value
6//!
7//! The `eval_*` functions take a script as source code and args as input. It eval the source by delegate call to the code pointed to by
8//! driver `JsDelegate2` and return the value of the js expression. The return value only lost-less supports string or Uint8Array. Ojbects
9//! of other types will be casted to string.
10//!
11//! Example:
12//! ```no_run
13//! let src = r#"
14//! (function () {
15//! return scriptArgs[0] + scriptArgs[1];
16//! })()
17//! "#;
18//! let output = phat_js::eval(src, &["foo".into(), "bar".into()]);
19//! assert_eq!(output, Ok(phat_js::Output::String("foobar".into())));
20//! ```
21//!
22//! # JS API examples
23//!
24//! ## Cross-contract call
25//!
26//! ```js
27//! // Delegate calling
28//! const delegateOutput = pink.invokeContractDelegate({
29//! codeHash:
30//! "0x0000000000000000000000000000000000000000000000000000000000000000",
31//! selector: 0xdeadbeef,
32//! input: "0x00",
33//! });
34//!
35//! // Instance calling
36//! const contractOutput = pink.invokeContract({
37//! callee: "0x0000000000000000000000000000000000000000000000000000000000000000",
38//! input: "0x00",
39//! selector: 0xdeadbeef,
40//! gasLimit: 0n,
41//! value: 0n,
42//! });
43//! ```
44//!
45//! This is the low-level API for cross-contract call.
46//! If you have the contract metadata file, there is a [script](https://github.com/Phala-Network/phat-quickjs/blob/master/scripts/meta2js.py) helps
47//! to generate the high-level API for cross-contract call. For example run the following command:
48//!
49//! ```shell
50//! python meta2js.py --keep System::version /path/to/system.contract
51//! ```
52//! would generate the following code:
53//! ```js
54//! function invokeContract(callee, selector, args, metadata, registry) {
55//! const inputCodec = pink.SCALE.codec(metadata.inputs, registry);
56//! const outputCodec = pink.SCALE.codec(metadata.output, registry);
57//! const input = inputCodec.encode(args ?? []);
58//! const output = pink.invokeContract({ callee, selector, input });
59//! return outputCodec.decode(output);
60//! }
61//! class System {
62//! constructor(address) {
63//! this.typeRegistryRaw = '#u16\n(0,0,0)\n<CouldNotReadInput::1>\n<Ok:1,Err:2>'
64//! this.typeRegistry = pink.SCALE.parseTypes(this.typeRegistryRaw);
65//! this.address = address;
66//! }
67//!
68//! system$Version() {
69//! const io = {"inputs": [], "output": 3};
70//! return invokeContract(this.address, 2278132365, [], io, this.typeRegistry);
71//! }
72//! }
73//! ```
74//!
75//! Then you can use the high-level API to call the contract:
76//! ```js
77//! const system = new System(systemAddress);
78//! const version = system.system$Version();
79//! console.log("version:", version);
80//! ```
81//!
82//! ## HTTP request
83//!
84//! HTTP request is supported in the JS environment. However, the API is sync rather than async.
85//! This is different from other JavaScript engines. For example:
86//! ```js
87//! const response = pink.httpReqeust({
88//! url: "https://httpbin.org/ip",
89//! method: "GET",
90//! returnTextBody: true,
91//! });
92//! console.log(response.body);
93//! ```
94//!
95//! ## SCALE codec
96//!
97//! Let's introduce the details of the SCALE codec API which is not documented in the above link.
98//!
99//! The SCALE codec API is mounted on the global object `pink.SCALE` which contains the following functions:
100//!
101//! - `pink.SCALE.parseTypes(types: string): TypeRegistry`
102//! - `pink.SCALE.codec(type: string | number | number[], typeRegistry?: TypeRegistry): Codec`
103//!
104//! Let's make a basice example to show how to use the SCALE codec API:
105//!
106//! ```js
107//! const types = `
108//! Hash=[u8;32]
109//! Info={hash:Hash,size:u32}
110//! `;
111//! const typeRegistry = pink.SCALE.parseTypes(types);
112//! const infoCodec = pink.SCALE.codec(`Info`, typeRegistry);
113//! const encoded = infoCodec.encode({
114//! hash: "0x1234567890123456789012345678901234567890123456789012345678901234",
115//! size: 1234,
116//! });
117//! console.log("encoded:", encoded);
118//! const decoded = infoCodec.decode(encoded);
119//! pink.inspect("decoded:", decoded);
120//! ```
121//!
122//! The above code will output:
123//! ```text
124//! JS: encoded: 18,52,86,120,144,18,52,86,120,144,18,52,86,120,144,18,52,86,120,144,18,52,86,120,144,18,52,86,120,144,18,52,210,4,0,0
125//! JS: decoded: {
126//! JS: hash: 0x1234567890123456789012345678901234567890123456789012345678901234,
127//! JS: size: 1234
128//! JS: }
129//! ```
130//!
131//! Or using the direct encode/decode api which support literal type definition as well as a typename or id, for example:
132//!
133//! ```js
134//! const data = { name: "Alice", age: 18 };
135//! const encoded = pink.SCALE.encode(data, "{ name: str, age: u8 }");
136//! const decoded = pink.SCALE.decode(encoded, "{ name: str, age: u8 }");
137//! ```
138//!
139//! ## Grammar of the type definition
140//! ### Basic grammar
141//! In the above example, we use the following type definition:
142//! ```text
143//! Hash=[u8;32]
144//! Info={hash:Hash,size:u32}
145//! ```
146//! where we define a type `Hash` which is an array of 32 bytes, and a type `Info` which is a struct containing a `Hash` and a `u32`.
147//!
148//! The grammar is defined as follows:
149//!
150//! Each entry is type definition, which is of the form `name=type`. Where name must be a valid identifier,
151//! and type is a valid type expression described below.
152//!
153//! Type expression can be one of the following:
154//!
155//! | Type Expression | Description | Example | JS type |
156//! |-------------------------|-------------------------------------------|------------------|--------------------|
157//! | `bool` | Primitive type bool | | `true`, `false` |
158//! | `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64`, `i128` | Primitive number types | | number or bigint |
159//! | `str` | Primitive type str | | string |
160//! | `[type;size]` | Array type with element type `type` and size `size`. | `[u8; 32]` | Array of elements. (Uint8Array or `0x` prefixed hex string is allowed for [u8; N]) |
161//! | `[type]` | Sequence type with element type `type`. | `[u8]` | Array of elements. (Uint8Array or `0x` prefixed hex string is allowed for [u8]) |
162//! | `(type1, type2, ...)` | Tuple type with elements of type `type1`, `type2`, ... | `(u8, str)` | Array of value for inner type. (e.g. `[42, 'foobar']`) |
163//! | `{field1:type1, field2:type2, ...}` | Struct type with fields and types. | `{age:u32, name:str}` | Object with field name as key |
164//! | `<variant1:type1, variant2:type2, ...>` | Enum type with variants and types. if the variant is a unit variant, then the type expression can be omitted.| `<Success:i32, Error:str>`, `<None,Some:u32>` | Object with variant name as key. (e.g. `{Some: 42}`)|
165//! | `@type` | Compact number types. Only unsigned number types is supported | `@u64` | number or bigint |
166//!
167//! ### Generic type support
168//!
169//! Generic parameters can be added to the type definition, for example:
170//!
171//! ```text
172//! Vec<T>=[T]
173//! ```
174//!
175//! ### Option type
176//! The Option type is not a special type, but a vanilla enum type. It is needed to be defined by the user explicitly. Same for the Result type.
177//!
178//! ```text
179//! Option<T>=<None,Some:T>
180//! Result<T,E>=<Ok:T,Err:E>
181//! ```
182//!
183//! There is one special syntax for the Option type:
184//! ```text
185//! Option<T>=<_None,_Some:T>
186//! ```
187//! If the Option type is defined in this way, then the `None` variant would be decoded as `null` instead of `{None: null}` and the `Some` variant would be decoded as the inner value directly instead of `{Some: innerValue}`.
188//! For example:
189//! ```js
190//! const encoded = pink.SCALE.encode(42, "<_None,_Some:u32>");
191//! const decoded = pink.SCALE.decode(encoded, "<_None,_Some:u32>");
192//! console.log(decoded); // 42
193//! ```
194//!
195//! ### Nested type definition
196//!
197//! Type definition can be nested, for example:
198//!
199//! ```text
200//! Block={header:{hash:[u8;32],size:u32}}
201//! ```
202//!
203//! ## Error handling
204//! Host calls would throw an exception if any error is encountered. For example, if we pass an invalid method to the API:
205//! ```js
206//! try {
207//! const response = pink.httpReqeust({
208//! url: "https://httpbin.org/ip",
209//! method: 42,
210//! returnTextBody: true,
211//! });
212//! console.log(response.body);
213//! } catch (err) {
214//! console.log("Some error ocurred:", err);
215//! }
216//! ```
217//!
218#![cfg_attr(not(feature = "std"), no_std)]
219
220extern crate alloc;
221
222use alloc::string::String;
223use alloc::vec::Vec;
224use ink::env::call;
225use ink::primitives::Hash;
226use scale::{Decode, Encode};
227
228pub use pink::chain_extension::{JsCode, JsValue};
229
230#[derive(Debug, Encode, Decode, Eq, PartialEq)]
231#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
232pub enum GenericValue<S, B> {
233 String(S),
234 Bytes(B),
235 Undefined,
236}
237pub type RefValue<'a> = GenericValue<&'a str, &'a [u8]>;
238pub type Value = GenericValue<String, Vec<u8>>;
239pub type Output = Value;
240
241impl From<Value> for JsValue {
242 fn from(value: Value) -> Self {
243 match value {
244 Value::Undefined => Self::Undefined,
245 Value::String(v) => Self::String(v),
246 Value::Bytes(v) => Self::Bytes(v),
247 }
248 }
249}
250
251pub fn default_delegate() -> Result<Hash, String> {
252 let system = pink::system::SystemRef::instance();
253 let delegate = system
254 .get_driver("JsDelegate2".into())
255 .ok_or("No JS driver found")?;
256 Ok(delegate.convert_to())
257}
258
259/// Evaluate a script with the default delegate contract code
260pub fn eval(script: &str, args: &[String]) -> Result<Output, String> {
261 eval_with(default_delegate()?, script, args)
262}
263
264/// Evaluate a compiled bytecode with the default delegate contract code
265pub fn eval_bytecode(code: &[u8], args: &[String]) -> Result<Output, String> {
266 eval_bytecode_with(default_delegate()?, code, args)
267}
268
269/// Evaluate multiple scripts with the default delegate contract code
270pub fn eval_all(codes: &[RefValue], args: &[String]) -> Result<Output, String> {
271 eval_all_with(default_delegate()?, codes, args)
272}
273
274/// Evaluate a script with given delegate contract code
275pub fn eval_with(delegate: Hash, script: &str, args: &[String]) -> Result<Output, String> {
276 call::build_call::<pink::PinkEnvironment>()
277 .call_type(call::DelegateCall::new(delegate))
278 .exec_input(
279 call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("eval")))
280 .push_arg(script)
281 .push_arg(args),
282 )
283 .returns::<Result<Output, String>>()
284 .invoke()
285}
286
287/// Evaluate a compiled script with given delegate contract code
288pub fn eval_bytecode_with(
289 delegate: Hash,
290 script: &[u8],
291 args: &[String],
292) -> Result<Output, String> {
293 call::build_call::<pink::PinkEnvironment>()
294 .call_type(call::DelegateCall::new(delegate))
295 .exec_input(
296 call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("eval_bytecode")))
297 .push_arg(script)
298 .push_arg(args),
299 )
300 .returns::<Result<Output, String>>()
301 .invoke()
302}
303
304/// Evaluate multiple scripts with given delegate
305pub fn eval_all_with(
306 delegate: Hash,
307 scripts: &[RefValue],
308 args: &[String],
309) -> Result<Output, String> {
310 call::build_call::<pink::PinkEnvironment>()
311 .call_type(call::DelegateCall::new(delegate))
312 .exec_input(
313 call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("eval_all")))
314 .push_arg(scripts)
315 .push_arg(args),
316 )
317 .returns::<Result<Output, String>>()
318 .invoke()
319}
320
321/// Evaluate async JavaScript with SideVM QuickJS.
322///
323/// This function is similar to [`eval`], but it uses SideVM QuickJS to evaluate the script.
324/// This function would polyfill `pink.SCALE`, `pink.hash`, `pink.deriveSecret` which are not
325/// available in SideVM QuickJS.
326///
327/// # Parameters
328///
329/// * `code`: A JavaScript code that can be either a source code or bytecode.
330/// * `args`: A vector of strings that contain arguments passed to the JavaScript code.
331///
332/// # Returns
333///
334/// * a `JsValue` object which represents the evaluated result of the JavaScript code.
335///
336/// # Examples
337///
338/// ```ignore
339/// let js_code = phat_js::JsCode::Source("setTimeout(() => { scriptOutput = '42'; }, 100);".into());
340/// let res = phat_js::eval_async_js(js_code, Vec::new());
341/// assert_eq!(res, JsValue::String("42".into()));
342/// ```
343pub fn eval_async_code(code: JsCode, args: Vec<String>) -> JsValue {
344 let code_bytes = match &code {
345 JsCode::Source(source) => source.as_bytes(),
346 JsCode::Bytecode(bytecode) => bytecode.as_slice(),
347 };
348 let mut code_hash = Default::default();
349 ink::env::hash_bytes::<ink::env::hash::Blake2x256>(code_bytes, &mut code_hash);
350 let polyfill = polyfill_script(pink::vrf(&code_hash));
351 let codes = alloc::vec![JsCode::Source(polyfill), code];
352 pink::ext().js_eval(codes, args)
353}
354
355/// Evaluate async JavaScript with SideVM QuickJS.
356///
357/// Same as [`eval_async_code`], but takes a string as the JavaScript code.
358pub fn eval_async_js(src: &str, args: &[String]) -> JsValue {
359 eval_async_code(JsCode::Source(src.into()), args.to_vec())
360}
361
362fn polyfill_script(seed: impl AsRef<[u8]>) -> String {
363 let seed = hex_fmt::HexFmt(seed);
364 alloc::format!(
365 r#"
366 (function(g) {{
367 const seed = "{seed}";
368 const {{ SCALE, hash, hexEncode }} = Sidevm;
369 g.pink = g.Pink = {{
370 SCALE,
371 hash,
372 deriveSecret(salt) {{
373 return hash('blake2b512', seed + hexEncode(salt));
374 }},
375 vrf(salt) {{
376 return hash('blake2b512', 'vrf:' + seed + hexEncode(salt));
377 }}
378 }};
379 }})(globalThis);
380 "#
381 )
382}
383
384/// Compile a script with the default delegate contract
385pub fn compile(script: &str) -> Result<Vec<u8>, String> {
386 compile_with(default_delegate()?, script)
387}
388
389/// Compile a script with given delegate contract
390pub fn compile_with(delegate: Hash, script: &str) -> Result<Vec<u8>, String> {
391 call::build_call::<pink::PinkEnvironment>()
392 .call_type(call::DelegateCall::new(delegate))
393 .exec_input(
394 call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("compile")))
395 .push_arg(script),
396 )
397 .returns::<Result<Vec<u8>, String>>()
398 .invoke()
399}
400
401pub trait ConvertTo<To> {
402 fn convert_to(&self) -> To;
403}
404
405impl<F, T> ConvertTo<T> for F
406where
407 F: AsRef<[u8; 32]>,
408 T: From<[u8; 32]>,
409{
410 fn convert_to(&self) -> T {
411 (*self.as_ref()).into()
412 }
413}