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}