From 430f47adc33f860003613f8437ab874ca4456ade Mon Sep 17 00:00:00 2001 From: acheron Date: Sat, 25 May 2024 09:57:33 +0200 Subject: [PATCH 1/2] idl: Add ability to convert legacy IDLs --- Cargo.lock | 2 + idl/Cargo.toml | 5 + idl/src/convert.rs | 563 +++++++++++++++++++++++++++++++++++++++++++++ idl/src/lib.rs | 3 + 4 files changed, 573 insertions(+) create mode 100644 idl/src/convert.rs diff --git a/Cargo.lock b/Cargo.lock index 3c51974cb9..73ae58d5f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,9 +293,11 @@ version = "0.1.0" dependencies = [ "anchor-syn", "anyhow", + "heck 0.3.3", "regex", "serde", "serde_json", + "sha2 0.10.8", ] [[package]] diff --git a/idl/Cargo.toml b/idl/Cargo.toml index 8c2d28db6f..e286915a7a 100644 --- a/idl/Cargo.toml +++ b/idl/Cargo.toml @@ -14,6 +14,7 @@ rustdoc-args = ["--cfg", "docsrs"] [features] build = ["anchor-syn", "regex"] +convert = ["heck", "sha2"] [dependencies] anyhow = "1" @@ -23,3 +24,7 @@ serde_json = "1" # `build` feature only anchor-syn = { path = "../lang/syn", version = "0.30.0", optional = true } regex = { version = "1", optional = true } + +# `convert` feature only +heck = { version = "0.3", optional = true } +sha2 = { version = "0.10", optional = true } diff --git a/idl/src/convert.rs b/idl/src/convert.rs new file mode 100644 index 0000000000..e01700c3db --- /dev/null +++ b/idl/src/convert.rs @@ -0,0 +1,563 @@ +use anyhow::{anyhow, Result}; + +use crate::types::Idl; + +impl Idl { + /// Create an [`Idl`] value with additional support for older specs based on the + /// `idl.metadata.spec` field. + /// + /// If `spec` field is not specified, the conversion will fallback to the legacy IDL spec + /// (pre Anchor v0.30.0). + /// + /// **Note:** For legacy IDLs, `idl.metadata.address` field is required to be populated with + /// program's address otherwise an error will be returned. + pub fn from_slice_with_conversion(idl: &[u8]) -> Result { + let value = serde_json::from_slice::(idl)?; + let spec = value + .get("metadata") + .and_then(|m| m.get("spec")) + .and_then(|spec| spec.as_str()); + match spec { + // New standard + Some(spec) => match spec { + "0.1.0" => serde_json::from_value(value).map_err(Into::into), + _ => Err(anyhow!("IDL spec not supported: `{spec}`")), + }, + // Legacy + None => serde_json::from_value::(value).map(TryInto::try_into)?, + } + } +} + +/// Legacy IDL spec (pre Anchor v0.30.0) +mod legacy { + use crate::types as t; + use anyhow::{anyhow, Result}; + use heck::SnakeCase; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct Idl { + pub version: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub docs: Option>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub constants: Vec, + pub instructions: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub accounts: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub types: Vec, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub events: Option>, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub errors: Option>, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub metadata: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlConst { + pub name: String, + #[serde(rename = "type")] + pub ty: IdlType, + pub value: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlState { + #[serde(rename = "struct")] + pub strct: IdlTypeDefinition, + pub methods: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlInstruction { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub docs: Option>, + pub accounts: Vec, + pub args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub returns: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct IdlAccounts { + pub name: String, + pub accounts: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(untagged)] + pub enum IdlAccountItem { + IdlAccount(IdlAccount), + IdlAccounts(IdlAccounts), + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct IdlAccount { + pub name: String, + pub is_mut: bool, + pub is_signer: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub is_optional: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub docs: Option>, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub pda: Option, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub relations: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct IdlPda { + pub seeds: Vec, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub program_id: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase", tag = "kind")] + pub enum IdlSeed { + Const(IdlSeedConst), + Arg(IdlSeedArg), + Account(IdlSeedAccount), + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct IdlSeedAccount { + #[serde(rename = "type")] + pub ty: IdlType, + // account_ty points to the entry in the "accounts" section. + // Some only if the `Account` type is used. + #[serde(skip_serializing_if = "Option::is_none")] + pub account: Option, + pub path: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct IdlSeedArg { + #[serde(rename = "type")] + pub ty: IdlType, + pub path: String, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct IdlSeedConst { + #[serde(rename = "type")] + pub ty: IdlType, + pub value: serde_json::Value, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlField { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub docs: Option>, + #[serde(rename = "type")] + pub ty: IdlType, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlEvent { + pub name: String, + pub fields: Vec, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlEventField { + pub name: String, + #[serde(rename = "type")] + pub ty: IdlType, + pub index: bool, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlTypeDefinition { + /// - `idl-parse`: always the name of the type + /// - `idl-build`: full path if there is a name conflict, otherwise the name of the type + pub name: String, + /// Documentation comments + #[serde(skip_serializing_if = "Option::is_none")] + pub docs: Option>, + /// Generics, only supported with `idl-build` + #[serde(skip_serializing_if = "Option::is_none")] + pub generics: Option>, + /// Type definition, `struct` or `enum` + #[serde(rename = "type")] + pub ty: IdlTypeDefinitionTy, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "lowercase", tag = "kind")] + pub enum IdlTypeDefinitionTy { + Struct { fields: Vec }, + Enum { variants: Vec }, + Alias { value: IdlType }, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + pub struct IdlEnumVariant { + pub name: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub fields: Option, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(untagged)] + pub enum EnumFields { + Named(Vec), + Tuple(Vec), + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub enum IdlType { + Bool, + U8, + I8, + U16, + I16, + U32, + I32, + F32, + U64, + I64, + F64, + U128, + I128, + U256, + I256, + Bytes, + String, + PublicKey, + Defined(String), + Option(Box), + Vec(Box), + Array(Box, usize), + GenericLenArray(Box, String), + Generic(String), + DefinedWithTypeArgs { + name: String, + args: Vec, + }, + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + #[serde(rename_all = "camelCase")] + pub enum IdlDefinedTypeArg { + Generic(String), + Value(String), + Type(IdlType), + } + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] + pub struct IdlErrorCode { + pub code: u32, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub msg: Option, + } + + impl TryFrom for t::Idl { + type Error = anyhow::Error; + + fn try_from(idl: Idl) -> Result { + Ok(Self { + address: idl + .metadata + .as_ref() + .and_then(|m| m.get("address")) + .and_then(|a| a.as_str()) + .ok_or_else(|| anyhow!("Program id missing in `idl.metadata.address` field"))? + .into(), + metadata: t::IdlMetadata { + name: idl.name, + version: idl.version, + spec: t::IDL_SPEC.into(), + description: Default::default(), + repository: Default::default(), + dependencies: Default::default(), + contact: Default::default(), + deployments: Default::default(), + }, + docs: idl.docs.unwrap_or_default(), + instructions: idl.instructions.into_iter().map(Into::into).collect(), + accounts: idl.accounts.clone().into_iter().map(Into::into).collect(), + events: idl + .events + .clone() + .unwrap_or_default() + .into_iter() + .map(Into::into) + .collect(), + errors: idl + .errors + .unwrap_or_default() + .into_iter() + .map(Into::into) + .collect(), + types: idl + .types + .into_iter() + .map(Into::into) + .chain(idl.accounts.into_iter().map(Into::into)) + .chain(idl.events.unwrap_or_default().into_iter().map(Into::into)) + .collect(), + constants: idl.constants.into_iter().map(Into::into).collect(), + }) + } + } + + fn get_disc(prefix: &str, name: &str) -> Vec { + use sha2::{Digest, Sha256}; + let mut hasher = Sha256::new(); + hasher.update(prefix); + hasher.update(b":"); + hasher.update(name); + hasher.finalize()[..8].into() + } + + impl From for t::IdlInstruction { + fn from(value: IdlInstruction) -> Self { + let name = value.name.to_snake_case(); + Self { + discriminator: get_disc("global", &name), + name, + docs: value.docs.unwrap_or_default(), + accounts: value.accounts.into_iter().map(Into::into).collect(), + args: value.args.into_iter().map(Into::into).collect(), + returns: value.returns.map(|r| r.into()), + } + } + } + + impl From for t::IdlAccount { + fn from(value: IdlTypeDefinition) -> Self { + Self { + discriminator: get_disc("account", &value.name), + name: value.name, + } + } + } + + impl From for t::IdlEvent { + fn from(value: IdlEvent) -> Self { + Self { + discriminator: get_disc("event", &value.name), + name: value.name, + } + } + } + + impl From for t::IdlErrorCode { + fn from(value: IdlErrorCode) -> Self { + Self { + name: value.name, + code: value.code, + msg: value.msg, + } + } + } + + impl From for t::IdlConst { + fn from(value: IdlConst) -> Self { + Self { + name: value.name, + docs: Default::default(), + ty: value.ty.into(), + value: value.value, + } + } + } + + impl From for t::IdlGenericArg { + fn from(value: IdlDefinedTypeArg) -> Self { + match value { + IdlDefinedTypeArg::Type(ty) => Self::Type { ty: ty.into() }, + IdlDefinedTypeArg::Value(value) => Self::Const { value }, + IdlDefinedTypeArg::Generic(generic) => Self::Type { + ty: t::IdlType::Generic(generic), + }, + } + } + } + + impl From for t::IdlTypeDef { + fn from(value: IdlTypeDefinition) -> Self { + Self { + name: value.name, + docs: value.docs.unwrap_or_default(), + serialization: Default::default(), + repr: Default::default(), + generics: Default::default(), + ty: value.ty.into(), + } + } + } + + impl From for t::IdlTypeDef { + fn from(value: IdlEvent) -> Self { + Self { + name: value.name, + docs: Default::default(), + serialization: Default::default(), + repr: Default::default(), + generics: Default::default(), + ty: t::IdlTypeDefTy::Struct { + fields: Some(t::IdlDefinedFields::Named( + value + .fields + .into_iter() + .map(|f| t::IdlField { + name: f.name.to_snake_case(), + docs: Default::default(), + ty: f.ty.into(), + }) + .collect(), + )), + }, + } + } + } + + impl From for t::IdlTypeDefTy { + fn from(value: IdlTypeDefinitionTy) -> Self { + match value { + IdlTypeDefinitionTy::Struct { fields } => Self::Struct { + fields: fields + .is_empty() + .then(|| None) + .unwrap_or_else(|| Some(fields.into())), + }, + IdlTypeDefinitionTy::Enum { variants } => Self::Enum { + variants: variants + .into_iter() + .map(|variant| t::IdlEnumVariant { + name: variant.name, + fields: variant.fields.map(|fields| match fields { + EnumFields::Named(fields) => fields.into(), + EnumFields::Tuple(tys) => t::IdlDefinedFields::Tuple( + tys.into_iter().map(Into::into).collect(), + ), + }), + }) + .collect(), + }, + IdlTypeDefinitionTy::Alias { value } => Self::Type { + alias: value.into(), + }, + } + } + } + + impl From for t::IdlField { + fn from(value: IdlField) -> Self { + Self { + name: value.name.to_snake_case(), + docs: value.docs.unwrap_or_default(), + ty: value.ty.into(), + } + } + } + + impl From> for t::IdlDefinedFields { + fn from(value: Vec) -> Self { + Self::Named(value.into_iter().map(Into::into).collect()) + } + } + + impl From for t::IdlType { + fn from(value: IdlType) -> Self { + match value { + IdlType::PublicKey => t::IdlType::Pubkey, + IdlType::Defined(name) => t::IdlType::Defined { + name, + generics: Default::default(), + }, + IdlType::DefinedWithTypeArgs { name, args } => t::IdlType::Defined { + name, + generics: args.into_iter().map(Into::into).collect(), + }, + IdlType::Option(ty) => t::IdlType::Option(ty.into()), + IdlType::Vec(ty) => t::IdlType::Vec(ty.into()), + IdlType::Array(ty, len) => t::IdlType::Array(ty.into(), t::IdlArrayLen::Value(len)), + IdlType::GenericLenArray(ty, generic) => { + t::IdlType::Array(ty.into(), t::IdlArrayLen::Generic(generic)) + } + _ => serde_json::to_value(value) + .and_then(serde_json::from_value) + .unwrap(), + } + } + } + + impl From> for Box { + fn from(value: Box) -> Self { + Box::new((*value).into()) + } + } + + impl From for t::IdlInstructionAccountItem { + fn from(value: IdlAccountItem) -> Self { + match value { + IdlAccountItem::IdlAccount(acc) => Self::Single(t::IdlInstructionAccount { + name: acc.name.to_snake_case(), + docs: acc.docs.unwrap_or_default(), + writable: acc.is_mut, + signer: acc.is_signer, + optional: acc.is_optional.unwrap_or_default(), + address: Default::default(), + pda: acc + .pda + .map(|pda| -> Result { + Ok(t::IdlPda { + seeds: pda + .seeds + .into_iter() + .map(TryInto::try_into) + .collect::>()?, + program: pda.program_id.map(TryInto::try_into).transpose()?, + }) + }) + .transpose() + .unwrap_or_default(), + relations: acc.relations, + }), + IdlAccountItem::IdlAccounts(accs) => Self::Composite(t::IdlInstructionAccounts { + name: accs.name.to_snake_case(), + accounts: accs.accounts.into_iter().map(Into::into).collect(), + }), + } + } + } + + impl TryFrom for t::IdlSeed { + type Error = anyhow::Error; + + fn try_from(value: IdlSeed) -> Result { + let seed = match value { + IdlSeed::Account(seed) => Self::Account(t::IdlSeedAccount { + account: seed.account, + path: seed.path, + }), + IdlSeed::Arg(seed) => Self::Arg(t::IdlSeedArg { path: seed.path }), + IdlSeed::Const(seed) => Self::Const(t::IdlSeedConst { + value: match seed.ty { + IdlType::String => seed.value.to_string().as_bytes().into(), + _ => return Err(anyhow!("Const seed conversion not supported")), + }, + }), + }; + Ok(seed) + } + } +} diff --git a/idl/src/lib.rs b/idl/src/lib.rs index 0ba2ddf4cf..d49038a2d3 100644 --- a/idl/src/lib.rs +++ b/idl/src/lib.rs @@ -5,5 +5,8 @@ pub mod types; #[cfg(feature = "build")] pub mod build; +#[cfg(feature = "convert")] +pub mod convert; + #[cfg(feature = "build")] pub use serde_json; From 77a20c2a7a32281b6efcb261d84526fbec0b6861 Mon Sep 17 00:00:00 2001 From: acheron Date: Sat, 25 May 2024 10:23:24 +0200 Subject: [PATCH 2/2] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7c1155dac..3126542df9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The minor version will be incremented upon a breaking change and the patch versi - idl, ts: Add accounts resolution for associated token accounts ([#2927](https://github.com/coral-xyz/anchor/pull/2927)). - cli: Add `--no-install` option to the `init` command ([#2945](https://github.com/coral-xyz/anchor/pull/2945)). - lang: Implement `TryFromIntError` for `Error` to be able to propagate integer conversion errors ([#2950](https://github.com/coral-xyz/anchor/pull/2950)). +- idl: Add ability to convert legacy IDLs ([#2986](https://github.com/coral-xyz/anchor/pull/2986)). ### Fixes