Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions rust/pkg/cardano_serialization_lib.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,27 @@ declare export function min_ada_required(
minimum_utxo_val: BigNum
): BigNum;

/**
* Receives a script JSON string
* and returns a NativeScript.
* Cardano Wallet and Node styles are supported.
*
* * wallet: https://input-output-hk.github.io/cardano-wallet/api/edge/#operation/listSharedWallets
* * payment_script_template is the schema
* * node: https://github.com/input-output-hk/cardano-node/blob/master/doc/reference/simple-scripts.md
*
* self_address is expected to be a Bip32PublicKey as hex-encoded bytes
* @param {string} json
* @param {string} self_address
* @param {number} schema
* @returns {NativeScript}
*/
declare export function encode_json_str_to_native_script(
json: string,
self_address: string,
schema: number
): NativeScript;

/**
*/

Expand Down Expand Up @@ -263,6 +284,15 @@ declare export var MetadataJsonSchema: {|
+DetailedSchema: 2, // 2
|};

/**
* Used to choosed the schema for a script JSON string
*/

declare export var ScriptSchema: {|
+Wallet: 0, // 0
+Node: 1, // 1
|};

/**
*/

Expand Down
261 changes: 259 additions & 2 deletions rust/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::error::{DeserializeError, DeserializeFailure};
use cbor_event::{self, de::Deserializer, se::{Serialize, Serializer}};
use hex::FromHex;
use serde_json;
use std::{collections::HashMap, io::{BufRead, Seek, Write}};
use itertools::Itertools;
use std::io::{BufRead, Seek, Write};
use std::cmp;
use std::ops::{Rem, Div, Sub};

use super::*;
use crate::error::{DeserializeError, DeserializeFailure};

// JsError can't be used by non-wasm targets so we use this macro to expose
// either a DeserializeError or a JsError error depending on if we're on a
Expand Down Expand Up @@ -1004,6 +1007,193 @@ pub fn min_ada_required(
}
}

/// Used to choosed the schema for a script JSON string
#[wasm_bindgen]
pub enum ScriptSchema {
Wallet,
Node,
}

/// Receives a script JSON string
/// and returns a NativeScript.
/// Cardano Wallet and Node styles are supported.
///
/// * wallet: https://github.com/input-output-hk/cardano-wallet/blob/master/specifications/api/swagger.yaml
/// * node: https://github.com/input-output-hk/cardano-node/blob/master/doc/reference/simple-scripts.md
///
/// self_xpub is expected to be a Bip32PublicKey as hex-encoded bytes
#[wasm_bindgen]
pub fn encode_json_str_to_native_script(
json: &str,
self_xpub: &str,
schema: ScriptSchema,
) -> Result<NativeScript, JsError> {
let value: serde_json::Value =
serde_json::from_str(&json).map_err(|e| JsError::from_str(&e.to_string()))?;

let native_script = match schema {
ScriptSchema::Wallet => encode_wallet_value_to_native_script(value, self_xpub)?,
ScriptSchema::Node => todo!(),
};

Ok(native_script)
}

fn encode_wallet_value_to_native_script(value: serde_json::Value, self_xpub: &str) -> Result<NativeScript, JsError> {
match value {
serde_json::Value::Object(map)
if map.contains_key("cosigners") && map.contains_key("template") =>
{
let mut cosigners = HashMap::new();

if let serde_json::Value::Object(cosigner_map) = map.get("cosigners").unwrap() {
for (key, value) in cosigner_map.iter() {
if let serde_json::Value::String(xpub) = value {
if xpub == "self" {
cosigners.insert(key.to_owned(), self_xpub.to_owned());
} else {
cosigners.insert(key.to_owned(), xpub.to_owned());
}
} else {
return Err(JsError::from_str("cosigner value must be a string"));
}
}
} else {
return Err(JsError::from_str("cosigners must be a map"));
}

let template = map.get("template").unwrap();

let template_native_script = encode_template_to_native_script(template, &cosigners)?;

Ok(template_native_script)
}
_ => Err(JsError::from_str(
"top level must be an object. cosigners and template keys are required",
)),
}
}

fn encode_template_to_native_script(
template: &serde_json::Value,
cosigners: &HashMap<String, String>,
) -> Result<NativeScript, JsError> {
match template {
serde_json::Value::String(cosigner) => {
if let Some(xpub) = cosigners.get(cosigner) {
let bytes =
Vec::from_hex(xpub).map_err(|e| JsError::from_str(&e.to_string()))?;

let public_key = Bip32PublicKey::from_bytes(&bytes)?;

Ok(NativeScript::new_script_pubkey(&ScriptPubkey::new(
&public_key.to_raw_key().hash(),
)))
} else {
Err(JsError::from_str(&format!("cosigner {} not found", cosigner)))
}
}
serde_json::Value::Object(map) if map.contains_key("all") => {
let mut all = NativeScripts::new();

if let serde_json::Value::Array(array) = map.get("all").unwrap() {
for val in array {
all.add(&encode_template_to_native_script(val, cosigners)?);
}
} else {
return Err(JsError::from_str("all must be an array"));
}

Ok(NativeScript::new_script_all(&ScriptAll::new(&all)))
}
serde_json::Value::Object(map) if map.contains_key("any") => {
let mut any = NativeScripts::new();

if let serde_json::Value::Array(array) = map.get("any").unwrap() {
for val in array {
any.add(&encode_template_to_native_script(val, cosigners)?);
}
} else {
return Err(JsError::from_str("any must be an array"));
}

Ok(NativeScript::new_script_any(&ScriptAny::new(&any)))
}
serde_json::Value::Object(map) if map.contains_key("some") => {
if let serde_json::Value::Object(some) = map.get("some").unwrap() {
if some.contains_key("at_least") && some.contains_key("from") {
let n = if let serde_json::Value::Number(at_least) =
some.get("at_least").unwrap()
{
if let Some(n) = at_least.as_u64() {
n as u32
} else {
return Err(JsError::from_str("at_least must be an integer"));
}
} else {
return Err(JsError::from_str("at_least must be an integer"));
};

let mut from_scripts = NativeScripts::new();

if let serde_json::Value::Array(array) = some.get("from").unwrap() {
for val in array {
from_scripts
.add(&encode_template_to_native_script(val, cosigners)?);
}
} else {
return Err(JsError::from_str("from must be an array"));
}

Ok(NativeScript::new_script_n_of_k(&ScriptNOfK::new(
n,
&from_scripts,
)))
} else {
Err(JsError::from_str("some must contain at_least and from"))
}
} else {
Err(JsError::from_str("some must be an object"))
}
}
serde_json::Value::Object(map) if map.contains_key("active_from") => {
if let serde_json::Value::Number(active_from) = map.get("active_from").unwrap() {
if let Some(n) = active_from.as_u64() {
let slot: u32 = n as u32;

let time_lock_start = TimelockStart::new(slot);

Ok(NativeScript::new_timelock_start(&time_lock_start))
} else {
Err(JsError::from_str(
"active_from slot must be an integer greater than or equal to 0",
))
}
} else {
Err(JsError::from_str("active_from slot must be a number"))
}
}
serde_json::Value::Object(map) if map.contains_key("active_until") => {
if let serde_json::Value::Number(active_until) = map.get("active_until").unwrap() {
if let Some(n) = active_until.as_u64() {
let slot: u32 = n as u32;

let time_lock_expiry = TimelockExpiry::new(slot);

Ok(NativeScript::new_timelock_expiry(&time_lock_expiry))
} else {
Err(JsError::from_str(
"active_until slot must be an integer greater than or equal to 0",
))
}
} else {
Err(JsError::from_str("active_until slot must be a number"))
}
}
_ => Err(JsError::from_str("invalid template format")),
}
}

#[cfg(test)]
mod tests {
use hex::FromHex;
Expand Down Expand Up @@ -1048,6 +1238,73 @@ mod tests {
}


#[test]
fn native_scripts_from_wallet_json() {
let cosigner0_hex = "1423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db11423856bc91c49e928f6f30f4e8d665d53eb4ab6028bd0ac971809d514c92db1";
let cosigner1_hex = "a48d97f57ce49433f347d44ee07e54a100229b4f8e125d25f7bca9ad66d9707a25cd1331f46f7d6e279451637ca20802a25c441ba9436abf644fe5410d1080e3";
let self_key_hex = "6ce83a12e9d4c783f54c0bb511303b37160a6e4f3f96b8e878a7c1f7751e18c4ccde3fb916d330d07f7bd51fb6bd99aa831d925008d3f7795033f48abd6df7f6";
let native_script = encode_json_str_to_native_script(
&format!(r#"
{{
"cosigners": {{
"cosigner#0": "{}",
"cosigner#1": "{}",
"cosigner#2": "self"
}},
"template": {{
"some": {{
"at_least": 2,
"from": [
{{
"all": [
"cosigner#0",
{{ "active_from": 120 }}
]
}},
{{
"any": [
"cosigner#1",
{{ "active_until": 1000 }}
]
}},
"cosigner#2"
]
}}
}}
}}"#, cosigner0_hex, cosigner1_hex),
self_key_hex,
ScriptSchema::Wallet,
);

let n_of_k = native_script.unwrap().as_script_n_of_k().unwrap();
let from = n_of_k.native_scripts();
assert_eq!(n_of_k.n(), 2);
assert_eq!(from.len(), 3);
let all = from.get(0).as_script_all().unwrap().native_scripts();
assert_eq!(all.len(), 2);
let all_0 = all.get(0).as_script_pubkey().unwrap();
assert_eq!(
all_0.addr_keyhash(),
Bip32PublicKey::from_bytes(&hex::decode(cosigner0_hex).unwrap()).unwrap().to_raw_key().hash()
);
let all_1 = all.get(1).as_timelock_start().unwrap();
assert_eq!(all_1.slot(), 120);
let any = from.get(1).as_script_any().unwrap().native_scripts();
assert_eq!(all.len(), 2);
let any_0 = any.get(0).as_script_pubkey().unwrap();
assert_eq!(
any_0.addr_keyhash(),
Bip32PublicKey::from_bytes(&hex::decode(cosigner1_hex).unwrap()).unwrap().to_raw_key().hash()
);
let any_1 = any.get(1).as_timelock_expiry().unwrap();
assert_eq!(any_1.slot(), 1000);
let self_key = from.get(2).as_script_pubkey().unwrap();
assert_eq!(
self_key.addr_keyhash(),
Bip32PublicKey::from_bytes(&hex::decode(self_key_hex).unwrap()).unwrap().to_raw_key().hash()
);
}

#[test]
fn no_token_minimum() {

Expand Down