Skip to content

Commit

Permalink
Add soroban-spec-tools
Browse files Browse the repository at this point in the history
  • Loading branch information
leighmcculloch committed Feb 26, 2024
1 parent ae5446f commit 9e35de6
Show file tree
Hide file tree
Showing 7 changed files with 2,087 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ jobs:

uses: stellar/actions/.github/workflows/rust-publish-dry-run-v2.yml@3b400304af2619a33730493eef07fa1071aa8485
with:
crates: soroban-spec-json soroban-spec-typescript soroban-test soroban-cli
crates: soroban-spec-tools soroban-spec-json soroban-spec-typescript soroban-test soroban-cli
runs-on: ${{ matrix.os }}
target: ${{ matrix.target }}
cargo-hack-feature-options: ${{ matrix.cargo-hack-feature-options }}
10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ members = [
"cmd/crates/soroban-test/tests/fixtures/test-wasms/*",
"cmd/crates/soroban-test/tests/fixtures/hello",
]
default-members = ["cmd/soroban-cli", "cmd/crates/soroban-test"]
default-members = ["cmd/soroban-cli", "cmd/soroban-spec-tools", "cmd/crates/soroban-test"]
exclude = ["cmd/crates/soroban-test/tests/fixtures/hello"]

[workspace.package]
Expand Down Expand Up @@ -45,6 +45,10 @@ path = "./cmd/crates/soroban-spec-json"
version = "20.3.1"
path = "./cmd/crates/soroban-spec-typescript"

[workspace.dependencies.soroban-spec-tools]
version = "20.3.1"
path = "./cmd/crates/soroban-spec-tools"

[workspace.dependencies.soroban-sdk]
version = "=20.3.1"
# git = "https://github.com/stellar/rs-soroban-sdk"
Expand All @@ -68,10 +72,6 @@ path = "cmd/soroban-cli"
version = "=20.3.3"
# git = "https://github.com/stellar/soroban-rpc"

[workspace.dependencies.soroban-spec-tools]
version = "=20.3.3"
# git = "https://github.com/stellar/soroban-rpc"

[workspace.dependencies.stellar-xdr]
version = "=20.1.0"
default-features = true
Expand Down
39 changes: 39 additions & 0 deletions cmd/crates/soroban-spec-tools/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "soroban-spec-tools"
description = "Tools for using a contract's XDR spec"
homepage = "https://github.com/stellar/soroban-tools"
repository = "https://github.com/stellar/soroban-tools"
authors = ["Stellar Development Foundation <[email protected]>"]
license = "Apache-2.0"
readme = "README.md"
version.workspace = true
edition = "2021"
rust-version.workspace = true
autobins = false


[lib]
crate-type = ["rlib"]


[dependencies]
soroban-spec = { workspace = true }
stellar-strkey = { workspace = true }
stellar-xdr = { workspace = true, features = ["curr", "std", "serde"] }
soroban-env-host = { workspace = true }

serde_json = { workspace = true }
itertools = { workspace = true }
ethnum = { workspace = true }
hex = { workspace = true }
wasmparser = { workspace = true }
base64 = { workspace = true }
thiserror = "1.0.31"
# soroban-ledger-snapshot = { workspace = true }
# soroban-sdk = { workspace = true }
# sep5 = { workspace = true }


[dev-dependencies]
which = { workspace = true }
tokio = "1.28.1"
3 changes: 3 additions & 0 deletions cmd/crates/soroban-spec-tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# soroban-spec-tools

Tools and utilities for soroban specification / interface.
276 changes: 276 additions & 0 deletions cmd/crates/soroban-spec-tools/src/contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
use base64::{engine::general_purpose::STANDARD as base64, Engine as _};
use std::{
fmt::Display,
io::{self, Cursor},
};

use soroban_env_host::xdr::{
self, Limited, Limits, ReadXdr, ScEnvMetaEntry, ScMetaEntry, ScMetaV0, ScSpecEntry,
ScSpecFunctionV0, ScSpecUdtEnumV0, ScSpecUdtErrorEnumV0, ScSpecUdtStructV0, ScSpecUdtUnionV0,
StringM, WriteXdr,
};

pub struct Spec {
pub env_meta_base64: Option<String>,
pub env_meta: Vec<ScEnvMetaEntry>,
pub meta_base64: Option<String>,
pub meta: Vec<ScMetaEntry>,
pub spec_base64: Option<String>,
pub spec: Vec<ScSpecEntry>,
}

#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("reading file {filepath}: {error}")]
CannotReadContractFile {
filepath: std::path::PathBuf,
error: io::Error,
},
#[error("cannot parse wasm file {file}: {error}")]
CannotParseWasm {
file: std::path::PathBuf,
error: wasmparser::BinaryReaderError,
},
#[error("xdr processing error: {0}")]
Xdr(#[from] xdr::Error),

#[error(transparent)]
Parser(#[from] wasmparser::BinaryReaderError),
}

impl Spec {
pub fn new(bytes: &[u8]) -> Result<Self, Error> {
let mut env_meta: Option<&[u8]> = None;
let mut meta: Option<&[u8]> = None;
let mut spec: Option<&[u8]> = None;
for payload in wasmparser::Parser::new(0).parse_all(bytes) {
let payload = payload?;
if let wasmparser::Payload::CustomSection(section) = payload {
let out = match section.name() {
"contractenvmetav0" => &mut env_meta,
"contractmetav0" => &mut meta,
"contractspecv0" => &mut spec,
_ => continue,
};
*out = Some(section.data());
};
}

let mut env_meta_base64 = None;
let env_meta = if let Some(env_meta) = env_meta {
env_meta_base64 = Some(base64.encode(env_meta));
let cursor = Cursor::new(env_meta);
let mut read = Limited::new(cursor, Limits::none());
ScEnvMetaEntry::read_xdr_iter(&mut read).collect::<Result<Vec<_>, xdr::Error>>()?
} else {
vec![]
};

let mut meta_base64 = None;
let meta = if let Some(meta) = meta {
meta_base64 = Some(base64.encode(meta));
let cursor = Cursor::new(meta);
let mut depth_limit_read = Limited::new(cursor, Limits::none());
ScMetaEntry::read_xdr_iter(&mut depth_limit_read)
.collect::<Result<Vec<_>, xdr::Error>>()?
} else {
vec![]
};

let mut spec_base64 = None;
let spec = if let Some(spec) = spec {
spec_base64 = Some(base64.encode(spec));
let cursor = Cursor::new(spec);
let mut read = Limited::new(cursor, Limits::none());
ScSpecEntry::read_xdr_iter(&mut read).collect::<Result<Vec<_>, xdr::Error>>()?
} else {
vec![]
};

Ok(Spec {
env_meta_base64,
env_meta,
meta_base64,
meta,
spec_base64,
spec,
})
}

pub fn spec_as_json_array(&self) -> Result<String, Error> {
let spec = self
.spec
.iter()
.map(|e| Ok(format!("\"{}\"", e.to_xdr_base64(Limits::none())?)))
.collect::<Result<Vec<_>, Error>>()?
.join(",\n");
Ok(format!("[{spec}]"))
}
}

impl Display for Spec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(env_meta) = &self.env_meta_base64 {
writeln!(f, "Env Meta: {env_meta}")?;
for env_meta_entry in &self.env_meta {
match env_meta_entry {
ScEnvMetaEntry::ScEnvMetaKindInterfaceVersion(v) => {
writeln!(f, " • Interface Version: {v}")?;
}
}
}
writeln!(f)?;
} else {
writeln!(f, "Env Meta: None\n")?;
}

if let Some(_meta) = &self.meta_base64 {
writeln!(f, "Contract Meta:")?;
for meta_entry in &self.meta {
match meta_entry {
ScMetaEntry::ScMetaV0(ScMetaV0 { key, val }) => {
writeln!(f, " • {key}: {val}")?;
}
}
}
writeln!(f)?;
} else {
writeln!(f, "Contract Meta: None\n")?;
}

if let Some(_spec_base64) = &self.spec_base64 {
writeln!(f, "Contract Spec:")?;
for spec_entry in &self.spec {
match spec_entry {
ScSpecEntry::FunctionV0(func) => write_func(f, func)?,
ScSpecEntry::UdtUnionV0(udt) => write_union(f, udt)?,
ScSpecEntry::UdtStructV0(udt) => write_struct(f, udt)?,
ScSpecEntry::UdtEnumV0(udt) => write_enum(f, udt)?,
ScSpecEntry::UdtErrorEnumV0(udt) => write_error(f, udt)?,
}
}
} else {
writeln!(f, "Contract Spec: None")?;
}
Ok(())
}
}

fn write_func(f: &mut std::fmt::Formatter<'_>, func: &ScSpecFunctionV0) -> std::fmt::Result {
writeln!(f, " • Function: {}", func.name.to_utf8_string_lossy())?;
if func.doc.len() > 0 {
writeln!(
f,
" Docs: {}",
&indent(&func.doc.to_utf8_string_lossy(), 11).trim()
)?;
}
writeln!(
f,
" Inputs: {}",
indent(&format!("{:#?}", func.inputs), 5).trim()
)?;
writeln!(
f,
" Output: {}",
indent(&format!("{:#?}", func.outputs), 5).trim()
)?;
writeln!(f)?;
Ok(())
}

fn write_union(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtUnionV0) -> std::fmt::Result {
writeln!(f, " • Union: {}", format_name(&udt.lib, &udt.name))?;
if udt.doc.len() > 0 {
writeln!(
f,
" Docs: {}",
indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
)?;
}
writeln!(f, " Cases:")?;
for case in udt.cases.iter() {
writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
}
writeln!(f)?;
Ok(())
}

fn write_struct(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtStructV0) -> std::fmt::Result {
writeln!(f, " • Struct: {}", format_name(&udt.lib, &udt.name))?;
if udt.doc.len() > 0 {
writeln!(
f,
" Docs: {}",
indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
)?;
}
writeln!(f, " Fields:")?;
for field in udt.fields.iter() {
writeln!(
f,
" • {}: {}",
field.name.to_utf8_string_lossy(),
indent(&format!("{:#?}", field.type_), 8).trim()
)?;
if field.doc.len() > 0 {
writeln!(f, "{}", indent(&format!("{:#?}", field.doc), 8))?;
}
}
writeln!(f)?;
Ok(())
}

fn write_enum(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtEnumV0) -> std::fmt::Result {
writeln!(f, " • Enum: {}", format_name(&udt.lib, &udt.name))?;
if udt.doc.len() > 0 {
writeln!(
f,
" Docs: {}",
indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
)?;
}
writeln!(f, " Cases:")?;
for case in udt.cases.iter() {
writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
}
writeln!(f)?;
Ok(())
}

fn write_error(f: &mut std::fmt::Formatter<'_>, udt: &ScSpecUdtErrorEnumV0) -> std::fmt::Result {
writeln!(f, " • Error: {}", format_name(&udt.lib, &udt.name))?;
if udt.doc.len() > 0 {
writeln!(
f,
" Docs: {}",
indent(&udt.doc.to_utf8_string_lossy(), 10).trim()
)?;
}
writeln!(f, " Cases:")?;
for case in udt.cases.iter() {
writeln!(f, " • {}", indent(&format!("{case:#?}"), 8).trim())?;
}
writeln!(f)?;
Ok(())
}

fn indent(s: &str, n: usize) -> String {
let pad = " ".repeat(n);
s.lines()
.map(|line| format!("{pad}{line}"))
.collect::<Vec<_>>()
.join("\n")
}

fn format_name(lib: &StringM<80>, name: &StringM<60>) -> String {
if lib.len() > 0 {
format!(
"{}::{}",
lib.to_utf8_string_lossy(),
name.to_utf8_string_lossy()
)
} else {
name.to_utf8_string_lossy()
}
}
Loading

0 comments on commit 9e35de6

Please sign in to comment.