phat_js 0.3.0

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

extern crate alloc;

use alloc::string::String;
use alloc::vec::Vec;
use ink::env::call;
use ink::primitives::Hash;
use scale::{Decode, Encode};

pub use pink::chain_extension::{JsCode, JsValue};

#[derive(Debug, Encode, Decode, Eq, PartialEq)]
#[cfg_attr(feature = "std", derive(scale_info::TypeInfo))]
pub enum GenericValue<S, B> {
    String(S),
    Bytes(B),
    Undefined,
}
pub type RefValue<'a> = GenericValue<&'a str, &'a [u8]>;
pub type Value = GenericValue<String, Vec<u8>>;
pub type Output = Value;

impl From<Value> for JsValue {
    fn from(value: Value) -> Self {
        match value {
            Value::Undefined => Self::Undefined,
            Value::String(v) => Self::String(v),
            Value::Bytes(v) => Self::Bytes(v),
        }
    }
}

pub fn default_delegate() -> Result<Hash, String> {
    let system = pink::system::SystemRef::instance();
    let delegate = system
        .get_driver("JsDelegate2".into())
        .ok_or("No JS driver found")?;
    Ok(delegate.convert_to())
}

/// Evaluate a script with the default delegate contract code
pub fn eval(script: &str, args: &[String]) -> Result<Output, String> {
    eval_with(default_delegate()?, script, args)
}

/// Evaluate a compiled bytecode with the default delegate contract code
pub fn eval_bytecode(code: &[u8], args: &[String]) -> Result<Output, String> {
    eval_bytecode_with(default_delegate()?, code, args)
}

/// Evaluate multiple scripts with the default delegate contract code
pub fn eval_all(codes: &[RefValue], args: &[String]) -> Result<Output, String> {
    eval_all_with(default_delegate()?, codes, args)
}

/// Evaluate a script with given delegate contract code
pub fn eval_with(delegate: Hash, script: &str, args: &[String]) -> Result<Output, String> {
    call::build_call::<pink::PinkEnvironment>()
        .call_type(call::DelegateCall::new(delegate))
        .exec_input(
            call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("eval")))
                .push_arg(script)
                .push_arg(args),
        )
        .returns::<Result<Output, String>>()
        .invoke()
}

/// Evaluate a compiled script with given delegate contract code
pub fn eval_bytecode_with(
    delegate: Hash,
    script: &[u8],
    args: &[String],
) -> Result<Output, String> {
    call::build_call::<pink::PinkEnvironment>()
        .call_type(call::DelegateCall::new(delegate))
        .exec_input(
            call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("eval_bytecode")))
                .push_arg(script)
                .push_arg(args),
        )
        .returns::<Result<Output, String>>()
        .invoke()
}

/// Evaluate multiple scripts with given delegate
pub fn eval_all_with(
    delegate: Hash,
    scripts: &[RefValue],
    args: &[String],
) -> Result<Output, String> {
    call::build_call::<pink::PinkEnvironment>()
        .call_type(call::DelegateCall::new(delegate))
        .exec_input(
            call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("eval_all")))
                .push_arg(scripts)
                .push_arg(args),
        )
        .returns::<Result<Output, String>>()
        .invoke()
}

/// Evaluate async JavaScript with SideVM QuickJS.
///
/// This function is similar to [`eval`], but it uses SideVM QuickJS to evaluate the script.
/// This function would polyfill `pink.SCALE`, `pink.hash`, `pink.deriveSecret` which are not
/// available in SideVM QuickJS.
///
/// # Parameters
///
/// * `code`: A JavaScript code that can be either a source code or bytecode.
/// * `args`: A vector of strings that contain arguments passed to the JavaScript code.
///
/// # Returns
///
/// * a `JsValue` object which represents the evaluated result of the JavaScript code.
///
/// # Examples
///
/// ```ignore
/// let js_code = phat_js::JsCode::Source("setTimeout(() => { scriptOutput = '42'; }, 100);".into());
/// let res = phat_js::eval_async_js(js_code, Vec::new());
/// assert_eq!(res, JsValue::String("42".into()));
/// ```
pub fn eval_async_code(code: JsCode, args: Vec<String>) -> JsValue {
    let code_bytes = match &code {
        JsCode::Source(source) => source.as_bytes(),
        JsCode::Bytecode(bytecode) => bytecode.as_slice(),
    };
    let mut code_hash = Default::default();
    ink::env::hash_bytes::<ink::env::hash::Blake2x256>(code_bytes, &mut code_hash);
    let polyfill = polyfill_script(pink::vrf(&code_hash));
    let codes = alloc::vec![JsCode::Source(polyfill), code];
    pink::ext().js_eval(codes, args)
}

/// Evaluate async JavaScript with SideVM QuickJS.
///
/// Same as [`eval_async_code`], but takes a string as the JavaScript code.
pub fn eval_async_js(src: &str, args: &[String]) -> JsValue {
    eval_async_code(JsCode::Source(src.into()), args.to_vec())
}

fn polyfill_script(seed: impl AsRef<[u8]>) -> String {
    let seed = hex_fmt::HexFmt(seed);
    alloc::format!(
        r#"
        (function(g) {{
            const seed = "{seed}";
            const {{ SCALE, hash, hexEncode }} = Sidevm;
            g.pink = g.Pink = {{
                SCALE,
                hash,
                deriveSecret(salt) {{
                    return hash('blake2b512', seed + hexEncode(salt));
                }},
                vrf(salt) {{
                    return hash('blake2b512', 'vrf:' + seed + hexEncode(salt));
                }}
            }};
        }})(globalThis);
    "#
    )
}

/// Compile a script with the default delegate contract
pub fn compile(script: &str) -> Result<Vec<u8>, String> {
    compile_with(default_delegate()?, script)
}

/// Compile a script with given delegate contract
pub fn compile_with(delegate: Hash, script: &str) -> Result<Vec<u8>, String> {
    call::build_call::<pink::PinkEnvironment>()
        .call_type(call::DelegateCall::new(delegate))
        .exec_input(
            call::ExecutionInput::new(call::Selector::new(ink::selector_bytes!("compile")))
                .push_arg(script),
        )
        .returns::<Result<Vec<u8>, String>>()
        .invoke()
}

pub trait ConvertTo<To> {
    fn convert_to(&self) -> To;
}

impl<F, T> ConvertTo<T> for F
where
    F: AsRef<[u8; 32]>,
    T: From<[u8; 32]>,
{
    fn convert_to(&self) -> T {
        (*self.as_ref()).into()
    }
}