diff --git a/examples/abi/res/abi.json b/examples/abi/res/abi.json index d19613f36..1728ccba9 100644 --- a/examples/abi/res/abi.json +++ b/examples/abi/res/abi.json @@ -15,24 +15,74 @@ "params": [ { "name": "a", + "serialization_type": "json", "type_schema": { "$ref": "#/definitions/Pair" - }, - "serialization_type": "json" + } }, { "name": "b", + "serialization_type": "json", "type_schema": { "$ref": "#/definitions/Pair" - }, - "serialization_type": "json" + } } ], "result": { + "serialization_type": "json", "type_schema": { "$ref": "#/definitions/Pair" + } + } + }, + { + "name": "add_borsh", + "is_view": true, + "params": [ + { + "name": "a", + "serialization_type": "borsh", + "type_schema": { + "declaration": "Pair", + "definitions": { + "Pair": { + "Struct": [ + "u32", + "u32" + ] + } + } + } }, - "serialization_type": "json" + { + "name": "b", + "serialization_type": "borsh", + "type_schema": { + "declaration": "Pair", + "definitions": { + "Pair": { + "Struct": [ + "u32", + "u32" + ] + } + } + } + } + ], + "result": { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Pair", + "definitions": { + "Pair": { + "Struct": [ + "u32", + "u32" + ] + } + } + } } }, { @@ -40,32 +90,29 @@ "is_view": true, "callbacks": [ { + "serialization_type": "json", "type_schema": { "$ref": "#/definitions/DoublePair" - }, - "serialization_type": "json" + } }, { + "serialization_type": "json", "type_schema": { "$ref": "#/definitions/DoublePair" - }, - "serialization_type": "json" + } } ], "callbacks_vec": { + "serialization_type": "json", "type_schema": { - "type": "array", - "items": { - "$ref": "#/definitions/DoublePair" - } - }, - "serialization_type": "json" + "$ref": "#/definitions/DoublePair" + } }, "result": { + "serialization_type": "json", "type_schema": { "$ref": "#/definitions/DoublePair" - }, - "serialization_type": "json" + } } } ], diff --git a/examples/abi/res/abi.wasm b/examples/abi/res/abi.wasm index ee88390ba..55305694e 100755 Binary files a/examples/abi/res/abi.wasm and b/examples/abi/res/abi.wasm differ diff --git a/examples/abi/src/lib.rs b/examples/abi/src/lib.rs index 4041151ba..4791f5797 100644 --- a/examples/abi/src/lib.rs +++ b/examples/abi/src/lib.rs @@ -1,9 +1,9 @@ use near_sdk::__private::schemars::JsonSchema; -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSchema, BorshSerialize}; use near_sdk::near_bindgen; use near_sdk::serde::{Deserialize, Serialize}; -#[derive(JsonSchema, Serialize, Deserialize)] +#[derive(JsonSchema, Serialize, Deserialize, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct Pair(u32, u32); #[derive(JsonSchema, Serialize, Deserialize)] @@ -22,6 +22,11 @@ impl Adder { sum_pair(&a, &b) } + #[result_serializer(borsh)] + pub fn add_borsh(&self, #[serializer(borsh)] a: Pair, #[serializer(borsh)] b: Pair) -> Pair { + sum_pair(&a, &b) + } + pub fn add_callback( &self, #[callback_unwrap] a: DoublePair, diff --git a/examples/fungible-token/res/fungible_token.wasm b/examples/fungible-token/res/fungible_token.wasm index 5cba748fe..0cc1302c9 100755 Binary files a/examples/fungible-token/res/fungible_token.wasm and b/examples/fungible-token/res/fungible_token.wasm differ diff --git a/examples/non-fungible-token/res/non_fungible_token.wasm b/examples/non-fungible-token/res/non_fungible_token.wasm index 71acbd369..8f3e0588c 100755 Binary files a/examples/non-fungible-token/res/non_fungible_token.wasm and b/examples/non-fungible-token/res/non_fungible_token.wasm differ diff --git a/near-sdk-macros/src/core_impl/abi/abi_generator.rs b/near-sdk-macros/src/core_impl/abi/abi_generator.rs index 2e1135208..c547c909a 100644 --- a/near-sdk-macros/src/core_impl/abi/abi_generator.rs +++ b/near-sdk-macros/src/core_impl/abi/abi_generator.rs @@ -4,7 +4,7 @@ use crate::{ImplItemMethodInfo, MethodType}; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::quote; use syn::spanned::Spanned; -use syn::ReturnType; +use syn::{ReturnType, Type}; impl ImplItemMethodInfo { /// Generates ABI struct for this function. @@ -54,25 +54,19 @@ impl ImplItemMethodInfo { let mut callback_vec: Option = None; for arg in &self.attr_signature_info.args { let typ = &arg.ty; - let serialization_type = abi_serialization_type(&arg.serializer_ty); let arg_name = arg.ident.to_string(); match arg.bindgen_ty { BindgenArgType::Regular => { + let abi_type = generate_abi_type(typ, &arg.serializer_ty); params.push(quote! { near_sdk::__private::AbiParameter { name: #arg_name.to_string(), - type_schema: gen.subschema_for::<#typ>(), - serialization_type: #serialization_type, + typ: #abi_type } }); } BindgenArgType::CallbackArg => { - callbacks.push(quote! { - near_sdk::__private::AbiType { - type_schema: gen.subschema_for::<#typ>(), - serialization_type: #serialization_type, - } - }); + callbacks.push(generate_abi_type(typ, &arg.serializer_ty)); } BindgenArgType::CallbackResultArg => { let typ = if let Some(ok_type) = utils::extract_ok_type(typ) { @@ -85,12 +79,7 @@ impl ImplItemMethodInfo { ) .into_compile_error(); }; - callbacks.push(quote! { - near_sdk::__private::AbiType { - type_schema: gen.subschema_for::<#typ>(), - serialization_type: #serialization_type, - } - }); + callbacks.push(generate_abi_type(typ, &arg.serializer_ty)); } BindgenArgType::CallbackArgVec => { if callback_vec.is_none() { @@ -103,14 +92,10 @@ impl ImplItemMethodInfo { ) .into_compile_error(); }; - callback_vec = Some(quote! { - Some( - near_sdk::__private::AbiType { - type_schema: gen.subschema_for::<#typ>(), - serialization_type: #serialization_type, - } - ) - }) + + let abi_type = + generate_abi_type(typ, &self.attr_signature_info.result_serializer); + callback_vec = Some(quote! { Some(#abi_type) }) } else { return syn::Error::new( Span::call_site(), @@ -146,16 +131,9 @@ impl ImplItemMethodInfo { ) .into_compile_error(); }; - let serialization_type = - abi_serialization_type(&self.attr_signature_info.result_serializer); - quote! { - Some( - near_sdk::__private::AbiType { - type_schema: gen.subschema_for::<#ty>(), - serialization_type: #serialization_type, - } - ) - } + let abi_type = + generate_abi_type(ty, &self.attr_signature_info.result_serializer); + quote! { Some(#abi_type) } } ReturnType::Type(_, ty) if is_handles_result => { return syn::Error::new( @@ -165,16 +143,9 @@ impl ImplItemMethodInfo { .to_compile_error(); } ReturnType::Type(_, ty) => { - let serialization_type = - abi_serialization_type(&self.attr_signature_info.result_serializer); - quote! { - Some( - near_sdk::__private::AbiType { - type_schema: gen.subschema_for::<#ty>(), - serialization_type: #serialization_type, - } - ) - } + let abi_type = + generate_abi_type(ty, &self.attr_signature_info.result_serializer); + quote! { Some(#abi_type) } } }, }; @@ -195,13 +166,17 @@ impl ImplItemMethodInfo { } } -fn abi_serialization_type(serializer_type: &SerializerType) -> TokenStream2 { +fn generate_abi_type(ty: &Type, serializer_type: &SerializerType) -> TokenStream2 { match serializer_type { SerializerType::JSON => quote! { - near_sdk::__private::AbiSerializationType::Json + near_sdk::__private::AbiType::Json { + type_schema: gen.subschema_for::<#ty>(), + } }, SerializerType::Borsh => quote! { - near_sdk::__private::AbiSerializationType::Borsh + near_sdk::__private::AbiType::Borsh { + type_schema: #ty::schema_container(), + } }, } } diff --git a/near-sdk/src/private/abi.rs b/near-sdk/src/private/abi.rs index 0414b84ea..d9704aa03 100644 --- a/near-sdk/src/private/abi.rs +++ b/near-sdk/src/private/abi.rs @@ -1,3 +1,4 @@ +use borsh::schema::{BorshSchemaContainer, Declaration, Definition, Fields, VariantName}; use schemars::schema::{RootSchema, Schema}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -141,30 +142,474 @@ pub struct AbiFunction { pub struct AbiParameter { /// Parameter name (e.g. `p1` in `fn foo(p1: u32) {}`). pub name: String, - /// JSON Subschema representing the type of the parameter. - pub type_schema: Schema, - /// How the parameter is serialized (either JSON or Borsh). - pub serialization_type: AbiSerializationType, + /// Parameter type along with its serialization type (e.g. `u32` and `borsh` in `fn foo(#[serializer(borsh)] p1: u32) {}`). + #[serde(flatten)] + pub typ: AbiType, } /// Information about a single type (e.g. return type). -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -pub struct AbiType { - /// JSON Subschema that represents this type. - pub type_schema: Schema, - /// How the type instance is serialized (either JSON or Borsh). - pub serialization_type: AbiSerializationType, +#[derive(Serialize, Deserialize, Debug, PartialEq)] +#[serde(tag = "serialization_type")] +#[serde(rename_all = "lowercase")] +pub enum AbiType { + Json { + /// JSON Subschema that represents this type (can be an inline primitive, a reference to the root schema and a few other corner-case things). + type_schema: Schema, + }, + Borsh { + /// Inline Borsh schema that represents this type. + #[serde(with = "BorshSchemaContainerDef")] + type_schema: BorshSchemaContainer, + }, } -/// Represents how instances of a certain type are serialized in a certain context. Same type -/// can have different serialization types associated with it depending on where they occur. -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum AbiSerializationType { - Json, - Borsh, +// TODO: Maybe implement `Clone` for `BorshSchemaContainer` in borsh upstream? +impl Clone for AbiType { + fn clone(&self) -> Self { + match self { + Self::Json { type_schema } => Self::Json { type_schema: type_schema.clone() }, + Self::Borsh { type_schema } => { + let type_schema = BorshSchemaContainer { + declaration: type_schema.declaration.clone(), + definitions: type_schema + .definitions + .iter() + .map(|(k, v)| (k.clone(), borsh_clone::clone_definition(v))) + .collect(), + }; + Self::Borsh { type_schema } + } + } + } +} + +#[derive(Serialize, Deserialize)] +#[serde(remote = "BorshSchemaContainer")] +struct BorshSchemaContainerDef { + declaration: Declaration, + #[serde(with = "borsh_serde")] + definitions: HashMap, +} + +/// Cloning functions for borsh types. +mod borsh_clone { + use borsh::schema::{Definition, Fields}; + + pub fn clone_fields(fields: &Fields) -> Fields { + match fields { + Fields::Empty => Fields::Empty, + Fields::NamedFields(f) => Fields::NamedFields(f.clone()), + Fields::UnnamedFields(f) => Fields::UnnamedFields(f.clone()), + } + } + + pub fn clone_definition(definition: &Definition) -> Definition { + match definition { + Definition::Array { length, elements } => { + Definition::Array { length: length.clone(), elements: elements.clone() } + } + Definition::Sequence { elements } => { + Definition::Sequence { elements: elements.clone() } + } + Definition::Tuple { elements } => Definition::Tuple { elements: elements.clone() }, + Definition::Enum { variants } => Definition::Enum { variants: variants.clone() }, + Definition::Struct { fields } => Definition::Struct { fields: clone_fields(fields) }, + } + } +} + +/// This submodules follows https://serde.rs/remote-derive.html to derive Serialize/Deserialize for +/// `BorshSchemaContainer` parameters. The top-level serialization type is `HashMap` +/// for the sake of being easily plugged into `BorshSchemaContainerDef` (see its parameters). +mod borsh_serde { + use super::*; + use serde::ser::SerializeMap; + use serde::{Deserializer, Serializer}; + + #[derive(Serialize, Deserialize)] + #[serde(remote = "Definition")] + enum DefinitionDef { + Array { + length: u32, + elements: Declaration, + }, + #[serde(with = "transparent")] + Sequence { + elements: Declaration, + }, + #[serde(with = "transparent")] + Tuple { + elements: Vec, + }, + #[serde(with = "transparent")] + Enum { + variants: Vec<(VariantName, Declaration)>, + }, + #[serde(with = "transparent_fields")] + Struct { + fields: Fields, + }, + } + + #[derive(Serialize, Deserialize)] + struct HelperDefinition(#[serde(with = "DefinitionDef")] Definition); + + /// #[serde(transparent)] does not support enum variants, so we have to use a custom ser/de impls for now. + /// See https://github.com/serde-rs/serde/issues/2092. + mod transparent { + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize(field: &T, serializer: S) -> Result + where + T: Serialize, + S: Serializer, + { + serializer.serialize_some(&field) + } + + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: Deserialize<'de>, + D: Deserializer<'de>, + { + T::deserialize(deserializer) + } + } + + /// Since `Fields` itself does not implement `Serialization`/`Deserialization`, we can't use + /// `transparent` in combination with `#[serde(with = "...")]. Instead we have do it in this + /// roundabout way. + mod transparent_fields { + use super::borsh_clone; + use borsh::schema::{Declaration, FieldName, Fields}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Serialize, Deserialize)] + #[serde(remote = "Fields", untagged)] + enum FieldsDef { + NamedFields(Vec<(FieldName, Declaration)>), + UnnamedFields(Vec), + Empty, + } + + #[derive(Serialize, Deserialize)] + struct HelperFields(#[serde(with = "FieldsDef")] Fields); + + pub fn serialize(fields: &Fields, serializer: S) -> Result + where + S: Serializer, + { + HelperFields(borsh_clone::clone_fields(fields)).serialize(serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(HelperFields::deserialize(deserializer)?.0) + } + } + + pub fn serialize( + map: &HashMap, + serializer: S, + ) -> Result + where + S: Serializer, + { + let mut map_ser = serializer.serialize_map(Some(map.len()))?; + for (k, v) in map { + map_ser.serialize_entry(k, &HelperDefinition(borsh_clone::clone_definition(v)))?; + } + map_ser.end() + } + + pub fn deserialize<'de, D>( + deserializer: D, + ) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let map = HashMap::::deserialize(deserializer)?; + Ok(map.into_iter().map(|(k, HelperDefinition(v))| (k, v)).collect()) + } } fn is_false(b: &bool) -> bool { !b } + +#[cfg(test)] +mod tests { + use super::*; + use borsh::BorshSchema; + use serde_json::Value; + + #[test] + fn test_serde_array() { + let abi_type = AbiType::Borsh { type_schema: <[u32; 2]>::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Array", + "definitions": { + "Array": { + "Array": { + "length": 2, + "elements": "u32" + } + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Array".to_string()); + assert_eq!(type_schema.definitions.len(), 1); + assert_eq!( + type_schema.definitions.get("Array").unwrap(), + &Definition::Array { length: 2, elements: "u32".to_string() } + ); + } else { + panic!("Unexpected serialization type") + } + } + + #[test] + fn test_serde_sequence() { + let abi_type = AbiType::Borsh { type_schema: >::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Vec", + "definitions": { + "Vec": { + "Sequence": "u32" + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Vec".to_string()); + assert_eq!(type_schema.definitions.len(), 1); + assert_eq!( + type_schema.definitions.get("Vec").unwrap(), + &Definition::Sequence { elements: "u32".to_string() } + ); + } else { + panic!("Unexpected serialization type") + } + } + + #[test] + fn test_serde_tuple() { + let abi_type = AbiType::Borsh { type_schema: <(u32, u32)>::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Tuple", + "definitions": { + "Tuple": { + "Tuple": ["u32", "u32"] + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Tuple".to_string()); + assert_eq!(type_schema.definitions.len(), 1); + assert_eq!( + type_schema.definitions.get("Tuple").unwrap(), + &Definition::Tuple { elements: vec!["u32".to_string(), "u32".to_string()] } + ); + } else { + panic!("Unexpected serialization type") + } + } + + #[test] + fn test_deser_enum() { + #[derive(BorshSchema)] + enum Either { + _Left(u32), + _Right(u32), + } + let abi_type = AbiType::Borsh { type_schema: ::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Either", + "definitions": { + "Either": { + "Enum": [ + ["_Left", "Either_Left"], + ["_Right", "Either_Right"] + ] + }, + "Either_Left": { + "Struct": ["u32"] + }, + "Either_Right": { + "Struct": ["u32"] + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Either".to_string()); + assert_eq!(type_schema.definitions.len(), 3); + assert_eq!( + type_schema.definitions.get("Either").unwrap(), + &Definition::Enum { + variants: vec![ + ("_Left".to_string(), "Either_Left".to_string()), + ("_Right".to_string(), "Either_Right".to_string()) + ] + } + ); + } else { + panic!("Unexpected serialization type") + } + } + + #[test] + fn test_deser_struct_named() { + #[derive(BorshSchema)] + struct Pair { + _first: u32, + _second: u32, + } + let abi_type = AbiType::Borsh { type_schema: ::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Pair", + "definitions": { + "Pair": { + "Struct": [ + ["_first", "u32"], + ["_second", "u32"] + ] + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Pair".to_string()); + assert_eq!(type_schema.definitions.len(), 1); + assert_eq!( + type_schema.definitions.get("Pair").unwrap(), + &Definition::Struct { + fields: Fields::NamedFields(vec![ + ("_first".to_string(), "u32".to_string()), + ("_second".to_string(), "u32".to_string()) + ]) + } + ); + } else { + panic!("Unexpected serialization type") + } + } + + #[test] + fn test_deser_struct_unnamed() { + #[derive(BorshSchema)] + struct Pair(u32, u32); + let abi_type = AbiType::Borsh { type_schema: ::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Pair", + "definitions": { + "Pair": { + "Struct": [ + "u32", + "u32" + ] + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Pair".to_string()); + assert_eq!(type_schema.definitions.len(), 1); + assert_eq!( + type_schema.definitions.get("Pair").unwrap(), + &Definition::Struct { + fields: Fields::UnnamedFields(vec!["u32".to_string(), "u32".to_string()]) + } + ); + } else { + panic!("Unexpected serialization type") + } + } + + #[test] + fn test_deser_struct_empty() { + #[derive(BorshSchema)] + struct Unit; + let abi_type = AbiType::Borsh { type_schema: ::schema_container() }; + let value = serde_json::to_value(&abi_type).unwrap(); + let expected_json = r#" + { + "serialization_type": "borsh", + "type_schema": { + "declaration": "Unit", + "definitions": { + "Unit": { + "Struct": null + } + } + } + } + "#; + let expected_value: Value = serde_json::from_str(expected_json).unwrap(); + assert_eq!(value, expected_value); + + if let AbiType::Borsh { type_schema } = serde_json::from_str(expected_json).unwrap() { + assert_eq!(type_schema.declaration, "Unit".to_string()); + assert_eq!(type_schema.definitions.len(), 1); + assert_eq!( + type_schema.definitions.get("Unit").unwrap(), + &Definition::Struct { fields: Fields::Empty } + ); + } else { + panic!("Unexpected serialization type") + } + } +} diff --git a/near-sdk/src/private/mod.rs b/near-sdk/src/private/mod.rs index 102d8cccf..5767a7493 100644 --- a/near-sdk/src/private/mod.rs +++ b/near-sdk/src/private/mod.rs @@ -2,9 +2,7 @@ mod abi; #[cfg(feature = "abi")] -pub use abi::{ - Abi, AbiFunction, AbiMetadata, AbiParameter, AbiRoot, AbiSerializationType, AbiType, -}; +pub use abi::{Abi, AbiFunction, AbiMetadata, AbiParameter, AbiRoot, AbiType}; #[cfg(feature = "abi")] pub use schemars;